wip: redis, sqlalchemy, structured, etc
This commit is contained in:
parent
133e1cd490
commit
9f01572557
131
.gitignore
vendored
Normal file
131
.gitignore
vendored
Normal file
|
@ -0,0 +1,131 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
.idea
|
||||
temp.*
|
44
.pre-commit-config.yaml
Normal file
44
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,44 @@
|
|||
exclude: |
|
||||
(?x)(
|
||||
^tests/unit_tests/resource|
|
||||
_grpc.py|
|
||||
_pb2.py
|
||||
)
|
||||
|
||||
default_language_version:
|
||||
python: python3.8
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v3.2.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-case-conflict
|
||||
- id: check-docstring-first
|
||||
- id: check-json
|
||||
- id: check-merge-conflict
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
|
||||
- repo: https://github.com/timothycrosley/isort
|
||||
rev: 5.5.3
|
||||
hooks:
|
||||
- id: isort
|
||||
|
||||
- repo: https://github.com/ambv/black
|
||||
rev: 20.8b1
|
||||
hooks:
|
||||
- id: black
|
||||
args:
|
||||
- --line-length=100
|
||||
- --skip-string-normalization
|
||||
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 3.8.3
|
||||
hooks:
|
||||
- id: flake8
|
||||
args:
|
||||
- --max-line-length=100
|
||||
- --disable=protected-access
|
9
Dockerfile
Normal file
9
Dockerfile
Normal file
|
@ -0,0 +1,9 @@
|
|||
FROM python:3.9
|
||||
|
||||
WORKDIR /home/ruicore/auth
|
||||
|
||||
COPY . /home/ruicore/auth
|
||||
|
||||
RUN pip3 install --upgrade pip && pip3 install -r requirements.txt
|
||||
|
||||
LABEL ruicore="hrui835@gmail.com" version="v.0.0.1"
|
19
Makefile
Normal file
19
Makefile
Normal file
|
@ -0,0 +1,19 @@
|
|||
up-pip:
|
||||
pip install --upgrade pip
|
||||
|
||||
freeze:
|
||||
pip freeze>requirements.txt
|
||||
|
||||
install-pkg:
|
||||
pip install -r requirements.txt
|
||||
|
||||
uninstall-pkg:
|
||||
pip freeze | xargs pip uninstall -y
|
||||
|
||||
server-up:
|
||||
docker-compose up -d --remove-orphans
|
||||
docker-compose ps
|
||||
|
||||
server-down:
|
||||
docker-compose down
|
||||
docker-compose ps
|
9
auth/README.md
Normal file
9
auth/README.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
## Based on
|
||||
|
||||
* pyjwt
|
||||
* [ariadne](https://github.com/mirumee/ariadne)
|
||||
* [aioredis](https://github.com/aio-libs/aioredis)
|
||||
* [starlette](https://github.com/encode/starlette)、
|
||||
* sqlalchmy ORM
|
||||
|
||||
token is valid for one day, user can choose to logout, logout is revoke token
|
78
auth/authenticate.py
Normal file
78
auth/authenticate.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
from functools import wraps
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from graphql import GraphQLResolveInfo
|
||||
from jwt import DecodeError, ExpiredSignatureError
|
||||
from starlette.authentication import AuthenticationBackend
|
||||
from starlette.requests import HTTPConnection
|
||||
|
||||
from auth.credentials import AuthCredentials, AuthUser
|
||||
from auth.token import Token
|
||||
from exceptions import InvalidToken, OperationNotAllowed
|
||||
from orm import User
|
||||
from redis import redis
|
||||
from settings import JWT_AUTH_HEADER
|
||||
|
||||
|
||||
class _Authenticate:
|
||||
@classmethod
|
||||
async def verify(cls, token: str):
|
||||
"""
|
||||
Rules for a token to be valid.
|
||||
1. token format is legal &&
|
||||
token exists in redis database &&
|
||||
token is not expired
|
||||
2. token format is legal &&
|
||||
token exists in redis database &&
|
||||
token is expired &&
|
||||
token is of specified type
|
||||
"""
|
||||
try:
|
||||
payload = Token.decode(token)
|
||||
except ExpiredSignatureError:
|
||||
payload = Token.decode(token, verify_exp=False)
|
||||
if not await cls.exists(payload.user_id, token):
|
||||
raise InvalidToken("Login expired, please login again")
|
||||
if payload.device == "mobile": # noqa
|
||||
"we cat set mobile token to be valid forever"
|
||||
return payload
|
||||
except DecodeError as e:
|
||||
raise InvalidToken("token format error") from e
|
||||
else:
|
||||
if not await cls.exists(payload.user_id, token):
|
||||
raise InvalidToken("Login expired, please login again")
|
||||
return payload
|
||||
|
||||
@classmethod
|
||||
async def exists(cls, user_id, token):
|
||||
token = await redis.execute("GET", f"{user_id}-{token}")
|
||||
return token is not None
|
||||
|
||||
|
||||
class JWTAuthenticate(AuthenticationBackend):
|
||||
async def authenticate(
|
||||
self, request: HTTPConnection
|
||||
) -> Optional[Tuple[AuthCredentials, AuthUser]]:
|
||||
if JWT_AUTH_HEADER not in request.headers:
|
||||
return AuthCredentials(scopes=[]), AuthUser(user_id=None)
|
||||
|
||||
auth = request.headers[JWT_AUTH_HEADER]
|
||||
try:
|
||||
scheme, token = auth.split()
|
||||
payload = await _Authenticate.verify(token)
|
||||
except Exception as exc:
|
||||
return AuthCredentials(scopes=[], error_message=str(exc)), AuthUser(user_id=None)
|
||||
|
||||
scopes = User.get_permission(user_id=payload.user_id)
|
||||
return AuthCredentials(scopes=scopes, logged_in=True), AuthUser(user_id=payload.user_id)
|
||||
|
||||
|
||||
def login_required(func):
|
||||
@wraps(func)
|
||||
async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs):
|
||||
auth: AuthCredentials = info.context["request"].auth
|
||||
if not auth.logged_in:
|
||||
raise OperationNotAllowed(auth.error_message or "Please login")
|
||||
return await func(parent, info, *args, **kwargs)
|
||||
|
||||
return wrap
|
39
auth/authorize.py
Normal file
39
auth/authorize.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
from datetime import datetime, timedelta
|
||||
|
||||
from auth.token import Token
|
||||
from redis import redis
|
||||
from settings import JWT_LIFE_SPAN
|
||||
from validations import User
|
||||
|
||||
|
||||
class Authorize:
|
||||
@staticmethod
|
||||
async def authorize(user: User, device: str = "pc", auto_delete=True) -> str:
|
||||
"""
|
||||
:param user:
|
||||
:param device:
|
||||
:param auto_delete: Whether the expiration is automatically deleted, the default is True
|
||||
:return:
|
||||
"""
|
||||
exp = datetime.utcnow() + timedelta(seconds=JWT_LIFE_SPAN)
|
||||
token = Token.encode(user, exp=exp, device=device)
|
||||
await redis.execute("SET", f"{user.id}-{token}", "True")
|
||||
if auto_delete:
|
||||
expire_at = (exp + timedelta(seconds=JWT_LIFE_SPAN)).timestamp()
|
||||
await redis.execute("EXPIREAT", f"{user.id}-{token}", int(expire_at))
|
||||
return token
|
||||
|
||||
@staticmethod
|
||||
async def revoke(token: str) -> bool:
|
||||
try:
|
||||
payload = Token.decode(token)
|
||||
except: # noqa
|
||||
pass
|
||||
else:
|
||||
await redis.execute("DEL", f"{payload.id}-{token}")
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def revoke_all(user: User):
|
||||
tokens = await redis.execute("KEYS", f"{user.id}-*")
|
||||
await redis.execute("DEL", *tokens)
|
34
auth/credentials.py
Normal file
34
auth/credentials.py
Normal file
|
@ -0,0 +1,34 @@
|
|||
from typing import List, Optional, Text
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Permission(BaseModel):
|
||||
name: Text
|
||||
|
||||
|
||||
class AuthCredentials(BaseModel):
|
||||
user_id: Optional[int] = None
|
||||
scopes: Optional[set] = {}
|
||||
logged_in: bool = False
|
||||
error_message: str = ""
|
||||
|
||||
@property
|
||||
def is_admin(self):
|
||||
return True
|
||||
|
||||
async def permissions(self) -> List[Permission]:
|
||||
assert self.user_id is not None, "Please login first"
|
||||
return NotImplemented()
|
||||
|
||||
|
||||
class AuthUser(BaseModel):
|
||||
user_id: Optional[int]
|
||||
|
||||
@property
|
||||
def is_authenticated(self) -> bool:
|
||||
return self.user_id is not None
|
||||
|
||||
@property
|
||||
def display_id(self) -> int:
|
||||
return self.user_id
|
17
auth/identity.py
Normal file
17
auth/identity.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
from auth.password import Password
|
||||
from exceptions import InvalidPassword, ObjectNotExist
|
||||
from orm import User as OrmUser
|
||||
from orm.base import global_session
|
||||
from validations import User
|
||||
|
||||
|
||||
class Identity:
|
||||
@staticmethod
|
||||
def identity(user_id: int, password: str) -> User:
|
||||
user = global_session.query(OrmUser).filter_by(id=user_id).first()
|
||||
if not user:
|
||||
raise ObjectNotExist("User does not exist")
|
||||
user = User(**user.dict())
|
||||
if not Password.verify(password, user.password):
|
||||
raise InvalidPassword("Wrong user password")
|
||||
return user
|
11
auth/password.py
Normal file
11
auth/password.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from passlib.hash import pbkdf2_sha256
|
||||
|
||||
|
||||
class Password:
|
||||
@staticmethod
|
||||
def encode(password: str) -> str:
|
||||
return pbkdf2_sha256.hash(password)
|
||||
|
||||
@staticmethod
|
||||
def verify(password: str, other: str) -> bool:
|
||||
return pbkdf2_sha256.verify(password, other)
|
23
auth/token.py
Normal file
23
auth/token.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
from datetime import datetime
|
||||
|
||||
import jwt
|
||||
|
||||
from settings import JWT_ALGORITHM, JWT_SECRET_KEY
|
||||
from validations import PayLoad, User
|
||||
|
||||
|
||||
class Token:
|
||||
@staticmethod
|
||||
def encode(user: User, exp: datetime, device: str = "pc") -> str:
|
||||
payload = {"user_id": user.id, "device": device, "exp": exp, "iat": datetime.utcnow()}
|
||||
return jwt.encode(payload, JWT_SECRET_KEY, JWT_ALGORITHM).decode("UTF-8")
|
||||
|
||||
@staticmethod
|
||||
def decode(token: str, verify_exp: bool = True) -> PayLoad:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
key=JWT_SECRET_KEY,
|
||||
options={"verify_exp": verify_exp},
|
||||
algorithms=[JWT_ALGORITHM],
|
||||
)
|
||||
return PayLoad(**payload)
|
28
auth/validations.py
Normal file
28
auth/validations.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
from datetime import datetime
|
||||
from typing import Optional, Text
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
id: Optional[int]
|
||||
# age: Optional[int]
|
||||
username: Optional[Text]
|
||||
# phone: Optional[Text]
|
||||
password: Optional[Text]
|
||||
|
||||
|
||||
class PayLoad(BaseModel):
|
||||
user_id: int
|
||||
device: Text
|
||||
exp: datetime
|
||||
iat: datetime
|
||||
|
||||
|
||||
class CreateUser(BaseModel):
|
||||
username: Text
|
||||
# age: Optional[int]
|
||||
# phone: Optional[Text]
|
||||
password: Optional[Text]
|
||||
|
||||
# TODO: update validations
|
15
docker-compose.yml
Normal file
15
docker-compose.yml
Normal file
|
@ -0,0 +1,15 @@
|
|||
version: '3'
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:5.0.3-alpine
|
||||
container_name: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
server:
|
||||
image: discoursio/api:v0.0.1
|
||||
container_name: api
|
||||
ports:
|
||||
- 8002:24579
|
||||
command: ["python", "server.py"]
|
26
exceptions.py
Normal file
26
exceptions.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
from graphql import GraphQLError
|
||||
|
||||
|
||||
class BaseHttpException(GraphQLError):
|
||||
code = 500
|
||||
message = "500 Server error"
|
||||
|
||||
|
||||
class InvalidToken(BaseHttpException):
|
||||
code = 403
|
||||
message = "403 Invalid Token"
|
||||
|
||||
|
||||
class ObjectNotExist(BaseHttpException):
|
||||
code = 404
|
||||
message = "404 Object Does Not Exist"
|
||||
|
||||
|
||||
class OperationNotAllowed(BaseHttpException):
|
||||
code = 403
|
||||
message = "403 Operation is not allowed"
|
||||
|
||||
|
||||
class InvalidPassword(BaseHttpException):
|
||||
code = 401
|
||||
message = "401 Invalid Password"
|
28
main.py
Normal file
28
main.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
from importlib import import_module
|
||||
|
||||
from ariadne import load_schema_from_path, make_executable_schema
|
||||
from ariadne.asgi import GraphQL
|
||||
from starlette.applications import Starlette
|
||||
from starlette.middleware import Middleware
|
||||
from starlette.middleware.authentication import AuthenticationMiddleware
|
||||
|
||||
from authority.authenticate import JWTAuthenticate
|
||||
from redis import redis
|
||||
from resolvers.base import resolvers
|
||||
|
||||
import_module('resolvers')
|
||||
schema = make_executable_schema(load_schema_from_path("schema.graphql"), resolvers)
|
||||
|
||||
middleware = [Middleware(AuthenticationMiddleware, backend=JWTAuthenticate())]
|
||||
|
||||
|
||||
async def start_up():
|
||||
await redis.connect()
|
||||
|
||||
|
||||
async def shutdown():
|
||||
await redis.disconnect()
|
||||
|
||||
|
||||
app = Starlette(debug=True, on_startup=[start_up], on_shutdown=[shutdown], middleware=middleware)
|
||||
app.mount("/", GraphQL(schema, debug=True))
|
4
orm/__init__.py
Normal file
4
orm/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
from orm.rbac import Operation, Permission, Role
|
||||
from orm.user import User
|
||||
|
||||
__all__ = ["User", "Role", "Operation", "Permission"]
|
45
orm/base.py
Normal file
45
orm/base.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
from typing import TypeVar, Any, Dict, Generic, Callable
|
||||
|
||||
from sqlalchemy import create_engine, Column, Integer
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.sql.schema import Table
|
||||
|
||||
from settings import SQLITE_URI
|
||||
|
||||
engine = create_engine(f'sqlite:///{SQLITE_URI}', convert_unicode=True, echo=False)
|
||||
Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
global_session = Session()
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
REGISTRY: Dict[str, type] = {}
|
||||
|
||||
|
||||
class Base(declarative_base()):
|
||||
__table__: Table
|
||||
__tablename__: str
|
||||
__new__: Callable
|
||||
__init__: Callable
|
||||
|
||||
__abstract__: bool = True
|
||||
__table_args__ = {"extend_existing": True}
|
||||
id: int = Column(Integer, primary_key=True)
|
||||
session = global_session
|
||||
|
||||
def __init_subclass__(cls, **kwargs):
|
||||
REGISTRY[cls.__name__] = cls
|
||||
|
||||
@classmethod
|
||||
def create(cls: Generic[T], **kwargs) -> Generic[T]:
|
||||
instance = cls(**kwargs)
|
||||
return instance.save()
|
||||
|
||||
def save(self) -> Generic[T]:
|
||||
self.session.add(self)
|
||||
self.session.commit()
|
||||
return self
|
||||
|
||||
def dict(self) -> Dict[str, Any]:
|
||||
column_names = self.__table__.columns.keys()
|
||||
return {c: getattr(self, c) for c in column_names}
|
17
orm/like.py
Normal file
17
orm/like.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
from typing import List
|
||||
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, Datetime
|
||||
|
||||
from orm import Permission
|
||||
from orm.base import Base
|
||||
|
||||
|
||||
class Like(Base):
|
||||
__tablename__ = 'like'
|
||||
|
||||
author_id: str = Column(ForeignKey("user.id"), nullable=False, comment="Author")
|
||||
value: str = Column(String, nullable=False, comment="Value")
|
||||
shout: str = Column(ForeignKey("shout.id"), nullable=True, comment="Liked shout")
|
||||
user: str = Column(ForeignKey("user.id"), nullable=True, comment="Liked user")
|
||||
|
||||
# TODO: add resolvers, debug, etc.
|
18
orm/message.py
Normal file
18
orm/message.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from typing import List
|
||||
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, Datetime
|
||||
|
||||
from orm import Permission
|
||||
from orm.base import Base
|
||||
|
||||
|
||||
class Message(Base):
|
||||
__tablename__ = 'message'
|
||||
|
||||
sender: str = Column(ForeignKey("user.id"), nullable=False, comment="Sender")
|
||||
body: str = Column(String, nullable=False, comment="Body")
|
||||
createdAt: str = Column(Datetime, nullable=False, comment="Created at")
|
||||
updatedAt: str = Column(Datetime, nullable=True, comment="Updated at")
|
||||
replyTo: str = Column(ForeignKey("message.id", nullable=True, comment="Reply to"))
|
||||
|
||||
# TODO: work in progress, udpate this code
|
18
orm/proposal.py
Normal file
18
orm/proposal.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
from typing import List
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, Datetime
|
||||
|
||||
from orm import Permission
|
||||
from orm.base import Base
|
||||
|
||||
|
||||
class Proposal(Base):
|
||||
__tablename__ = 'proposal'
|
||||
|
||||
author_id: str = Column(ForeignKey("user.id"), nullable=False, comment="Author")
|
||||
body: str = Column(String, nullable=False, comment="Body")
|
||||
createdAt: str = Column(datetime, nullable=False, comment="Created at")
|
||||
shout: str = Column(ForeignKey("shout.id"), nullable=False, comment="Updated at")
|
||||
range: str = Column(String, nullable=True, comment="Range in format <start index>:<end>")
|
||||
|
||||
# TODO: debug, logix
|
65
orm/rbac.py
Normal file
65
orm/rbac.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
import warnings
|
||||
|
||||
from typing import Type
|
||||
|
||||
from sqlalchemy import String, Column, ForeignKey, types, UniqueConstraint
|
||||
|
||||
from orm.base import Base, REGISTRY, engine, global_session
|
||||
|
||||
|
||||
class ClassType(types.TypeDecorator):
|
||||
impl = types.String
|
||||
|
||||
@property
|
||||
def python_type(self):
|
||||
return NotImplemented
|
||||
|
||||
def process_literal_param(self, value, dialect):
|
||||
return NotImplemented
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
return value.__name__ if isinstance(value, type) else str(value)
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
class_ = REGISTRY.get(value)
|
||||
if class_ is None:
|
||||
warnings.warn(f"Can't find class <{value}>,find it yourself 😊", stacklevel=2)
|
||||
return class_
|
||||
|
||||
|
||||
class Role(Base):
|
||||
__tablename__ = 'role'
|
||||
name: str = Column(String, nullable=False, unique=True, comment="Role Name")
|
||||
|
||||
|
||||
class Operation(Base):
|
||||
__tablename__ = 'operation'
|
||||
name: str = Column(String, nullable=False, unique=True, comment="Operation Name")
|
||||
|
||||
|
||||
class Resource(Base):
|
||||
__tablename__ = "resource"
|
||||
resource_class: Type[Base] = Column(ClassType, nullable=False, unique=True, comment="Resource class")
|
||||
name: str = Column(String, nullable=False, unique=True, comment="Resource name")
|
||||
|
||||
|
||||
class Permission(Base):
|
||||
__tablename__ = "permission"
|
||||
__table_args__ = (UniqueConstraint("role_id", "operation_id", "resource_id"), {"extend_existing": True})
|
||||
|
||||
role_id: int = Column(ForeignKey("role.id", ondelete="CASCADE"), nullable=False, comment="Role")
|
||||
operation_id: int = Column(ForeignKey("operation.id", ondelete="CASCADE"), nullable=False, comment="Operation")
|
||||
resource_id: int = Column(ForeignKey("operation.id", ondelete="CASCADE"), nullable=False, comment="Resource")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
Base.metadata.create_all(engine)
|
||||
ops = [
|
||||
Permission(role_id=1, operation_id=1, resource_id=1),
|
||||
Permission(role_id=1, operation_id=2, resource_id=1),
|
||||
Permission(role_id=1, operation_id=3, resource_id=1),
|
||||
Permission(role_id=1, operation_id=4, resource_id=1),
|
||||
Permission(role_id=2, operation_id=4, resource_id=1)
|
||||
]
|
||||
global_session.add_all(ops)
|
||||
global_session.commit()
|
17
orm/shout.py
Normal file
17
orm/shout.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
from typing import List
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey, Datetime
|
||||
|
||||
from orm import Permission
|
||||
from orm.base import Base
|
||||
|
||||
|
||||
class Shout(Base):
|
||||
__tablename__ = 'shout'
|
||||
|
||||
author_id: str = Column(ForeignKey("user.id"), nullable=False, comment="Author")
|
||||
body: str = Column(String, nullable=False, comment="Body")
|
||||
createdAt: str = Column(datetime, nullable=False, comment="Created at")
|
||||
updatedAt: str = Column(datetime, nullable=False, comment="Updated at")
|
||||
|
||||
# TODO: add all the fields
|
26
orm/user.py
Normal file
26
orm/user.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
from typing import List
|
||||
|
||||
from sqlalchemy import Column, Integer, String, ForeignKey
|
||||
|
||||
from orm import Permission
|
||||
from orm.base import Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = 'user'
|
||||
|
||||
name: str = Column(String, nullable=False, comment="Name")
|
||||
password: str = Column(String, nullable=False, comment="Password")
|
||||
# phone: str = Column(String, comment="Phone")
|
||||
# age: int = Column(Integer, comment="Age")
|
||||
role_id: int = Column(ForeignKey("role.id"), nullable=False, comment="Role")
|
||||
|
||||
@classmethod
|
||||
def get_permission(cls, user_id):
|
||||
perms: List[Permission] = cls.session.query(Permission).join(User, User.role_id == Permission.role_id).filter(
|
||||
User.id == user_id).all()
|
||||
return {f"{p.operation_id}-{p.resource_id}" for p in perms}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(User.get_permission(user_id=1))
|
5
redis/__init__.py
Normal file
5
redis/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from redis.client import Redis
|
||||
|
||||
redis = Redis()
|
||||
|
||||
__all__ = ['redis']
|
51
redis/client.py
Normal file
51
redis/client.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
from typing import Optional
|
||||
|
||||
import aioredis
|
||||
from aioredis import ConnectionsPool
|
||||
|
||||
from settings import REDIS_URL
|
||||
|
||||
|
||||
class Redis:
|
||||
def __init__(self, uri=REDIS_URL):
|
||||
self._uri: str = uri
|
||||
self._pool: Optional[ConnectionsPool] = None
|
||||
|
||||
async def connect(self):
|
||||
if self._pool is not None:
|
||||
return
|
||||
pool = await aioredis.create_pool(self._uri)
|
||||
self._pool = pool
|
||||
|
||||
async def disconnect(self):
|
||||
if self._pool is None:
|
||||
return
|
||||
self._pool.close()
|
||||
await self._pool.wait_closed()
|
||||
self._pool = None
|
||||
|
||||
async def execute(self, command, *args, **kwargs):
|
||||
return await self._pool.execute(command, *args, **kwargs, encoding="UTF-8")
|
||||
|
||||
|
||||
async def test():
|
||||
redis = Redis()
|
||||
from datetime import datetime
|
||||
|
||||
await redis.connect()
|
||||
await redis.execute("SET", "1-KEY1", 1)
|
||||
await redis.execute("SET", "1-KEY2", 1)
|
||||
await redis.execute("SET", "1-KEY3", 1)
|
||||
await redis.execute("SET", "1-KEY4", 1)
|
||||
await redis.execute("EXPIREAT", "1-KEY4", int(datetime.utcnow().timestamp()))
|
||||
v = await redis.execute("KEYS", "1-*")
|
||||
print(v)
|
||||
await redis.execute("DEL", *v)
|
||||
v = await redis.execute("KEYS", "1-*")
|
||||
print(v)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import asyncio
|
||||
|
||||
asyncio.run(test())
|
29
requirements.txt
Normal file
29
requirements.txt
Normal file
|
@ -0,0 +1,29 @@
|
|||
aioredis==1.3.1
|
||||
appdirs==1.4.4
|
||||
ariadne==0.12.0
|
||||
async-timeout==3.0.1
|
||||
cffi==1.14.3
|
||||
cfgv==3.2.0
|
||||
click==7.1.2
|
||||
cryptography==3.2
|
||||
distlib==0.3.1
|
||||
filelock==3.0.12
|
||||
graphql-core==3.0.5
|
||||
h11==0.10.0
|
||||
hiredis==1.1.0
|
||||
identify==1.5.5
|
||||
jwt==1.0.0
|
||||
nodeenv==1.5.0
|
||||
passlib==1.7.2
|
||||
pre-commit==2.7.1
|
||||
pycparser==2.20
|
||||
pydantic==1.6.1
|
||||
PyJWT==1.7.1
|
||||
PyYAML==5.3.1
|
||||
six==1.15.0
|
||||
SQLAlchemy==1.3.19
|
||||
starlette==0.13.8
|
||||
toml==0.10.1
|
||||
typing-extensions==3.7.4.3
|
||||
uvicorn==0.12.1
|
||||
virtualenv==20.0.33
|
3
resolvers/__init__.py
Normal file
3
resolvers/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from resolvers.login import get_user, login, logout, register
|
||||
|
||||
__all__ = ["get_user", "login", "logout", "register"]
|
46
resolvers/auth.py
Normal file
46
resolvers/auth.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
from graphql import GraphQLResolveInfo
|
||||
|
||||
from auth.authenticate import login_required
|
||||
from auth.authorize import Authorize
|
||||
from auth.identity import Identity
|
||||
from auth.password import Password
|
||||
from auth.validations import CreateUser
|
||||
from orm import User
|
||||
from orm.base import global_session
|
||||
from resolvers.base import mutation, query
|
||||
|
||||
from settings import JWT_AUTH_HEADER
|
||||
|
||||
@mutation.field("SignUp")
|
||||
async def register(*_, create: dict = None) -> User:
|
||||
create_user = CreateUser(**create)
|
||||
create_user.password = Password.encode(create_user.password)
|
||||
return User.create(**create_user.dict())
|
||||
|
||||
|
||||
@query.field("SignIn")
|
||||
async def login(_, info: GraphQLResolveInfo, id: int, password: str) -> str:
|
||||
try:
|
||||
device = info.context["request"].headers['device']
|
||||
except KeyError:
|
||||
device = "pc"
|
||||
auto_delete = False if device == "mobile" else True
|
||||
user = Identity.identity(user_id=id, password=password)
|
||||
return await Authorize.authorize(user, device=device, auto_delete=auto_delete)
|
||||
|
||||
|
||||
# TODO: implement some queries, ex. @query.field("isUsernameFree")
|
||||
|
||||
@query.field("logout")
|
||||
@login_required
|
||||
async def logout(_, info: GraphQLResolveInfo, id: int) -> bool:
|
||||
token = info.context["request"].headers[JWT_AUTH_HEADER]
|
||||
return await Authorize.revoke(token)
|
||||
|
||||
|
||||
@query.field("getUser")
|
||||
@login_required
|
||||
async def get_user(*_, id: int):
|
||||
return global_session.query(User).filter(User.id == id).first()
|
||||
|
||||
|
6
resolvers/base.py
Normal file
6
resolvers/base.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from ariadne import MutationType, QueryType
|
||||
|
||||
query = QueryType()
|
||||
mutation = MutationType()
|
||||
|
||||
resolvers = [query, mutation]
|
1
resolvers/editor.py
Normal file
1
resolvers/editor.py
Normal file
|
@ -0,0 +1 @@
|
|||
# TODO: implement me
|
27
resolvers/inbox.py
Normal file
27
resolvers/inbox.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
from orm import message, user
|
||||
|
||||
from ariadne import ObjectType, convert_kwargs_to_snake_case
|
||||
|
||||
query = ObjectType("Query")
|
||||
|
||||
|
||||
@query.field("messages")
|
||||
@convert_kwargs_to_snake_case
|
||||
async def resolve_messages(obj, info, user_id):
|
||||
def filter_by_userid(message):
|
||||
return message["sender_id"] == user_id or \
|
||||
message["recipient_id"] == user_id
|
||||
|
||||
user_messages = filter(filter_by_userid, messages)
|
||||
return {
|
||||
"success": True,
|
||||
"messages": user_messages
|
||||
}
|
||||
|
||||
|
||||
@query.field("userId")
|
||||
@convert_kwargs_to_snake_case
|
||||
async def resolve_user_id(obj, info, username):
|
||||
user = users.get(username)
|
||||
if user:
|
||||
return user["user_id"]
|
24
schema.auth.graphql
Normal file
24
schema.auth.graphql
Normal file
|
@ -0,0 +1,24 @@
|
|||
type User{
|
||||
id: ID!
|
||||
name: String!
|
||||
phone: String
|
||||
age: Int
|
||||
}
|
||||
|
||||
input CreateUserInput{
|
||||
password: String!
|
||||
name: String!
|
||||
phone: String
|
||||
age: Int
|
||||
}
|
||||
|
||||
|
||||
type Query{
|
||||
user(id: ID!): User!
|
||||
login(id: ID!,password: String!): String!
|
||||
logout(id: ID!): Boolean!
|
||||
}
|
||||
|
||||
type Mutation{
|
||||
register(create: CraeteUserInput): User!
|
||||
}
|
225
schema.graphql
225
schema.graphql
|
@ -1,62 +1,163 @@
|
|||
scalar DateTime
|
||||
|
||||
type User {
|
||||
createdAt: DateTime!
|
||||
email: String
|
||||
emailConfirmed: Boolean
|
||||
id: Int!
|
||||
muted: Boolean
|
||||
rating: Int
|
||||
updatedAt: DateTime!
|
||||
username: String
|
||||
userpic: String
|
||||
userpicId: String
|
||||
wasOnlineAt: DateTime
|
||||
}
|
||||
|
||||
type Message {
|
||||
author: Int!
|
||||
body: String!
|
||||
createdAt: DateTime!
|
||||
id: Int!
|
||||
replyTo: Int
|
||||
updatedAt: DateTime!
|
||||
visibleForUsers: [Int]
|
||||
}
|
||||
|
||||
type createMessagePayload {
|
||||
status: Boolean!
|
||||
error: String
|
||||
message: Message
|
||||
}
|
||||
|
||||
type deleteMessagePayload {
|
||||
status: Boolean!
|
||||
error: String
|
||||
}
|
||||
|
||||
input MessageInput {
|
||||
body: String!
|
||||
replyTo: Int
|
||||
}
|
||||
|
||||
input updateMessageInput {
|
||||
id: Int!
|
||||
body: String!
|
||||
}
|
||||
|
||||
type Query {
|
||||
getMessages(count: Int = 100, page: Int = 1): [Message!]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createMessage(input: MessageInput!): createMessagePayload!
|
||||
updateMessage(input: updateMessageInput!): createMessagePayload!
|
||||
deleteMessage(messageId: Int!): deleteMessagePayload!
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
messageCreated: Message!
|
||||
messageUpdated: Message!
|
||||
messageDeleted: Message!
|
||||
}
|
||||
scalar DateTime
|
||||
|
||||
type createMessagePayload {
|
||||
status: Boolean!
|
||||
error: String
|
||||
message: Message
|
||||
}
|
||||
|
||||
type deleteMessagePayload {
|
||||
status: Boolean!
|
||||
error: String
|
||||
}
|
||||
|
||||
input MessageInput {
|
||||
body: String!
|
||||
replyTo: Int
|
||||
}
|
||||
|
||||
input updateMessageInput {
|
||||
id: Int!
|
||||
body: String!
|
||||
}
|
||||
|
||||
type Message {
|
||||
author: Int!
|
||||
visibleForUsers: [Int]
|
||||
body: String!
|
||||
createdAt: DateTime!
|
||||
id: Int!
|
||||
replyTo: Int
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
# message
|
||||
createMessage(input: MessageInput!): createMessagePayload!
|
||||
updateMessage(input: updateMessageInput!): createMessagePayload!
|
||||
deleteMessage(messageId: Int!): deleteMessagePayload!
|
||||
|
||||
# auth
|
||||
confirmEmail(token: String!): Token!
|
||||
invalidateAllTokens: Boolean!
|
||||
invalidateTokenById(id: Int!): Boolean!
|
||||
requestEmailConfirmation: User!
|
||||
requestPasswordReset(email: String!): Boolean!
|
||||
resetPassword(password: String!, token: String!): Token!
|
||||
signIn(email: String!, password: String!): Token! # login
|
||||
signUp(email: String!, password: String!, username: String): User! # register
|
||||
|
||||
# shout
|
||||
createShout(body: String!, replyTo: [Int], title: String, versionOf: [Int], visibleForRoles: [Int], visibleForUsers: [Int]): Message!
|
||||
deleteShout(shoutId: Int!): Message!
|
||||
rateShout(value: Int!): Boolean!
|
||||
|
||||
# profile
|
||||
rateUser(value: Int!): Boolean!
|
||||
updateOnlineStatus: Boolean!
|
||||
updateUsername(username: String!): User!
|
||||
|
||||
# proposal
|
||||
createProposal(shout: Int!, range: String!): Boolean!
|
||||
updateProposal(proposal: Int!, body: String!): Boolean!
|
||||
removeProposal(proposal: Int!)
|
||||
approveProposal(proposal: Int!): Boolean!
|
||||
}
|
||||
|
||||
type Query {
|
||||
# auth
|
||||
getCurrentUser: User!
|
||||
logout: [Boolean!]
|
||||
getTokens: [Token!]!
|
||||
isUsernameFree(username: String!): Boolean!
|
||||
|
||||
# profile
|
||||
getUserById(id: Int!): User!
|
||||
getUserRating(shout: Int): Int!
|
||||
getOnline: [User!]!
|
||||
|
||||
# message
|
||||
getMessages(count: Int = 100, page: Int = 1): [Message!]!
|
||||
|
||||
# shout
|
||||
getShoutRating(shout: Int): Int!
|
||||
shoutsByAuthor(author: Int): [Shout]!
|
||||
shoutsByReplyTo(shout: Int): [Shout]!
|
||||
shoutsByTags(tags: [String]): [Shout]!
|
||||
shoutsByTime(time: DateTime): [Shout]!
|
||||
topAuthors: [User]!
|
||||
topShouts: [Shout]!
|
||||
|
||||
# proposal
|
||||
getShoutProposals(shout: Int): [Proposal]!
|
||||
}
|
||||
|
||||
type Role {
|
||||
id: Int!
|
||||
name: String!
|
||||
}
|
||||
|
||||
type Shout {
|
||||
author: Int!
|
||||
body: String!
|
||||
createdAt: DateTime!
|
||||
deletedAt: DateTime
|
||||
deletedBy: Int
|
||||
id: Int!
|
||||
rating: Int
|
||||
published: DateTime! # if there is no published field - it is not published
|
||||
replyTo: Int # another shout
|
||||
tags: [String]
|
||||
title: String
|
||||
updatedAt: DateTime!
|
||||
versionOf: Int
|
||||
visibleForRoles: [Role]!
|
||||
visibleForUsers: [Int]
|
||||
}
|
||||
|
||||
type Proposal {
|
||||
body: String!
|
||||
shout: Int!
|
||||
range: String # full / 0:2340
|
||||
author: Int!
|
||||
createdAt: DateTime!
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
profileUpdate(user_id: Int!): User!
|
||||
chatUpdate(user_id: Int!): Message!
|
||||
onlineUpdate: [User!]! # userlist
|
||||
shoutUpdate(shout_id: Int!): Shout!
|
||||
}
|
||||
|
||||
type Token {
|
||||
createdAt: DateTime!
|
||||
expiresAt: DateTime
|
||||
id: Int!
|
||||
ownerId: Int!
|
||||
usedAt: DateTime
|
||||
value: String!
|
||||
}
|
||||
|
||||
type User {
|
||||
createdAt: DateTime!
|
||||
email: String
|
||||
emailConfirmed: Boolean
|
||||
id: Int!
|
||||
muted: Boolean
|
||||
rating: Int
|
||||
roles: [Role!]!
|
||||
updatedAt: DateTime!
|
||||
username: String
|
||||
userpic: String
|
||||
userpicId: String
|
||||
wasOnlineAt: DateTime
|
||||
}
|
||||
|
||||
type Like {
|
||||
author: Int!
|
||||
id: Int!
|
||||
shout: Int
|
||||
user: Int
|
||||
value: Int!
|
||||
}
|
62
schema.messages.graphql
Normal file
62
schema.messages.graphql
Normal file
|
@ -0,0 +1,62 @@
|
|||
scalar DateTime
|
||||
|
||||
type User {
|
||||
createdAt: DateTime!
|
||||
email: String
|
||||
emailConfirmed: Boolean
|
||||
id: Int!
|
||||
muted: Boolean
|
||||
rating: Int
|
||||
updatedAt: DateTime!
|
||||
username: String
|
||||
userpic: String
|
||||
userpicId: String
|
||||
wasOnlineAt: DateTime
|
||||
}
|
||||
|
||||
type Message {
|
||||
author: Int!
|
||||
body: String!
|
||||
createdAt: DateTime!
|
||||
id: Int!
|
||||
replyTo: Int
|
||||
updatedAt: DateTime!
|
||||
visibleForUsers: [Int]
|
||||
}
|
||||
|
||||
type createMessagePayload {
|
||||
status: Boolean!
|
||||
error: String
|
||||
message: Message
|
||||
}
|
||||
|
||||
type deleteMessagePayload {
|
||||
status: Boolean!
|
||||
error: String
|
||||
}
|
||||
|
||||
input MessageInput {
|
||||
body: String!
|
||||
replyTo: Int
|
||||
}
|
||||
|
||||
input updateMessageInput {
|
||||
id: Int!
|
||||
body: String!
|
||||
}
|
||||
|
||||
type Query {
|
||||
getMessages(count: Int = 100, page: Int = 1): [Message!]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
createMessage(input: MessageInput!): createMessagePayload!
|
||||
updateMessage(input: updateMessageInput!): createMessagePayload!
|
||||
deleteMessage(messageId: Int!): deleteMessagePayload!
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
messageCreated: Message!
|
||||
messageUpdated: Message!
|
||||
messageDeleted: Message!
|
||||
}
|
147
schema.prototype.graphql
Normal file
147
schema.prototype.graphql
Normal file
|
@ -0,0 +1,147 @@
|
|||
scalar DateTime
|
||||
|
||||
type Like {
|
||||
author: Int!
|
||||
id: Int!
|
||||
shout: Int
|
||||
user: Int
|
||||
value: Int!
|
||||
}
|
||||
|
||||
type createMessagePayload {
|
||||
status: Boolean!
|
||||
error: String
|
||||
message: Message
|
||||
}
|
||||
|
||||
type deleteMessagePayload {
|
||||
status: Boolean!
|
||||
error: String
|
||||
}
|
||||
|
||||
input MessageInput {
|
||||
body: String!
|
||||
replyTo: Int
|
||||
}
|
||||
|
||||
input updateMessageInput {
|
||||
id: Int!
|
||||
body: String!
|
||||
}
|
||||
|
||||
type Message {
|
||||
author: Int!
|
||||
body: String!
|
||||
createdAt: DateTime!
|
||||
id: Int!
|
||||
replyTo: Int
|
||||
updatedAt: DateTime!
|
||||
visibleForUsers: [Int]
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
# message
|
||||
createMessage(input: MessageInput!): createMessagePayload!
|
||||
updateMessage(input: updateMessageInput!): createMessagePayload!
|
||||
deleteMessage(messageId: Int!): deleteMessagePayload!
|
||||
|
||||
# auth
|
||||
confirmEmail(token: String!): Token!
|
||||
invalidateAllTokens: Boolean!
|
||||
invalidateTokenById(id: Int!): Boolean!
|
||||
requestEmailConfirmation: User!
|
||||
requestPasswordReset(email: String!): Boolean!
|
||||
resetPassword(password: String!, token: String!): Token!
|
||||
signIn(email: String!, password: String!): Token!
|
||||
signUp(email: String!, password: String!, username: String): User!
|
||||
|
||||
# shout
|
||||
createShout(body: String!, replyTo: [Int], title: String, versionOf: [Int], visibleForRoles: [Int], visibleForUsers: [Int]): Message!
|
||||
deleteShout(shoutId: Int!): Message!
|
||||
rateShout(value: Int!): Boolean!
|
||||
|
||||
# profile
|
||||
rateUser(value: Int!): Boolean!
|
||||
updateOnlineStatus: Boolean!
|
||||
updateUsername(username: String!): User!
|
||||
}
|
||||
|
||||
type Query {
|
||||
getCurrentUser: User!
|
||||
getMessages(count: Int = 100, page: Int = 1): [Message!]!
|
||||
getOnline: [User!]!
|
||||
getShoutRating(shout: Int): Int!
|
||||
getTokens: [Token!]!
|
||||
getUserById(id: Int!): User!
|
||||
getUserRating(shout: Int): Int!
|
||||
isUsernameFree(username: String!): Boolean!
|
||||
shoutsByAuthor(author: Int): [Shout]!
|
||||
shoutsByReplyTo(shout: Int): [Shout]!
|
||||
shoutsByTags(tags: [String]): [Shout]!
|
||||
shoutsByTime(time: DateTime): [Shout]!
|
||||
topAuthors: [User]!
|
||||
topShouts: [Shout]!
|
||||
}
|
||||
|
||||
type Role {
|
||||
id: Int!
|
||||
name: String!
|
||||
}
|
||||
|
||||
type Shout {
|
||||
author: Int!
|
||||
body: String!
|
||||
createdAt: DateTime!
|
||||
deletedAt: DateTime
|
||||
deletedBy: Int
|
||||
id: Int!
|
||||
rating: Int
|
||||
published: DateTime! # if there is no published field - it is not published
|
||||
replyTo: Int # another shout
|
||||
tags: [String]
|
||||
title: String
|
||||
updatedAt: DateTime!
|
||||
versionOf: Int
|
||||
visibleForRoles: [Role]!
|
||||
visibleForUsers: [Int]
|
||||
}
|
||||
|
||||
type Proposal {
|
||||
body: String!
|
||||
shout: Int!
|
||||
range: String # full / 0:2340
|
||||
author: Int!
|
||||
createdAt: DateTime!
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
messageCreated: Message!
|
||||
messageDeleted: Message!
|
||||
onlineUpdated: [User!]!
|
||||
shoutUpdated: Shout!
|
||||
userUpdated: User!
|
||||
}
|
||||
|
||||
type Token {
|
||||
createdAt: DateTime!
|
||||
expiresAt: DateTime
|
||||
id: Int!
|
||||
ownerId: Int!
|
||||
usedAt: DateTime
|
||||
value: String!
|
||||
}
|
||||
|
||||
type User {
|
||||
createdAt: DateTime!
|
||||
email: String
|
||||
emailConfirmed: Boolean
|
||||
id: Int!
|
||||
muted: Boolean
|
||||
rating: Int
|
||||
roles: [Role!]!
|
||||
updatedAt: DateTime!
|
||||
username: String
|
||||
userpic: String
|
||||
userpicId: String
|
||||
wasOnlineAt: DateTime
|
||||
}
|
4
server.py
Normal file
4
server.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
import uvicorn
|
||||
|
||||
if __name__ == '__main__':
|
||||
uvicorn.run("main:app", host="0.0.0.0", port=24579, reload=True)
|
8
settings.py
Normal file
8
settings.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from pathlib import Path
|
||||
|
||||
SQLITE_URI = Path(__file__).parent / "database.sqlite3"
|
||||
JWT_ALGORITHM = "HS256"
|
||||
JWT_SECRET_KEY = "8f1bd7696ffb482d8486dfbc6e7d16dd-secret-key"
|
||||
JWT_LIFE_SPAN = 24 * 60 * 60 # seconds
|
||||
JWT_AUTH_HEADER = "Auth"
|
||||
REDIS_URL = "redis://redis"
|
Loading…
Reference in New Issue
Block a user