wip: redis, sqlalchemy, structured, etc

This commit is contained in:
Untone 2021-06-28 12:08:09 +03:00
parent 133e1cd490
commit 9f01572557
37 changed files with 1297 additions and 62 deletions

131
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,5 @@
from redis.client import Redis
redis = Redis()
__all__ = ['redis']

51
redis/client.py Normal file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,6 @@
from ariadne import MutationType, QueryType
query = QueryType()
mutation = MutationType()
resolvers = [query, mutation]

1
resolvers/editor.py Normal file
View File

@ -0,0 +1 @@
# TODO: implement me

27
resolvers/inbox.py Normal file
View 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
View 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!
}

View File

@ -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
View 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
View 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
View 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
View 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"