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!
|
||||||
|
}
|
159
schema.graphql
159
schema.graphql
|
@ -1,29 +1,5 @@
|
||||||
scalar DateTime
|
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 {
|
type createMessagePayload {
|
||||||
status: Boolean!
|
status: Boolean!
|
||||||
error: String
|
error: String
|
||||||
|
@ -45,18 +21,143 @@ input updateMessageInput {
|
||||||
body: String!
|
body: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Message {
|
||||||
getMessages(count: Int = 100, page: Int = 1): [Message!]!
|
author: Int!
|
||||||
|
visibleForUsers: [Int]
|
||||||
|
body: String!
|
||||||
|
createdAt: DateTime!
|
||||||
|
id: Int!
|
||||||
|
replyTo: Int
|
||||||
|
updatedAt: DateTime!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
|
# message
|
||||||
createMessage(input: MessageInput!): createMessagePayload!
|
createMessage(input: MessageInput!): createMessagePayload!
|
||||||
updateMessage(input: updateMessageInput!): createMessagePayload!
|
updateMessage(input: updateMessageInput!): createMessagePayload!
|
||||||
deleteMessage(messageId: Int!): deleteMessagePayload!
|
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 {
|
type Subscription {
|
||||||
messageCreated: Message!
|
profileUpdate(user_id: Int!): User!
|
||||||
messageUpdated: Message!
|
chatUpdate(user_id: Int!): Message!
|
||||||
messageDeleted: 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