Merge pull request #48 from Discours/auth-debug

Auth debug
This commit is contained in:
Igor Lobanov 2022-11-24 18:57:55 +01:00 committed by GitHub
commit 93bf7be464
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 217 additions and 146 deletions

View File

@ -2,48 +2,14 @@ from functools import wraps
from typing import Optional, Tuple
from graphql.type 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.jwtcodec import JWTCodec
from auth.tokenstorage import TokenStorage
from base.exceptions import ExpiredToken, InvalidToken
from services.auth.users import UserStorage
from settings import SESSION_TOKEN_HEADER
class SessionToken:
@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:
print('[auth.authenticate] session token verify')
payload = JWTCodec.decode(token)
except ExpiredSignatureError:
payload = JWTCodec.decode(token, verify_exp=False)
if not await cls.get(payload.user_id, token):
raise ExpiredToken("Token signature has expired, please try again")
except DecodeError as e:
raise InvalidToken("token format error") from e
else:
if not await cls.get(payload.user_id, token):
raise ExpiredToken("Session token has expired, please login again")
return payload
@classmethod
async def get(cls, uid, token):
return await TokenStorage.get(f"{uid}-{token}")
from auth.tokenstorage import SessionToken
from base.exceptions import InvalidToken
class JWTAuthenticate(AuthenticationBackend):
@ -54,10 +20,18 @@ class JWTAuthenticate(AuthenticationBackend):
if SESSION_TOKEN_HEADER not in request.headers:
return AuthCredentials(scopes=[]), AuthUser(user_id=None)
token = request.headers.get(SESSION_TOKEN_HEADER, "")
token = request.headers.get(SESSION_TOKEN_HEADER)
if not token:
print("[auth.authenticate] no token in header %s" % SESSION_TOKEN_HEADER)
return AuthCredentials(scopes=[], error_message=str("no token")), AuthUser(
user_id=None
)
try:
payload = await SessionToken.verify(token)
if len(token.split('.')) > 1:
payload = await SessionToken.verify(token)
else:
InvalidToken("please try again")
except Exception as exc:
print("[auth.authenticate] session token verify error")
print(exc)
@ -84,8 +58,25 @@ def login_required(func):
async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs):
# print('[auth.authenticate] login required for %r with info %r' % (func, info)) # debug only
auth: AuthCredentials = info.context["request"].auth
if auth and auth.user_id:
print(auth) # debug only
if not auth.logged_in:
return {"error": auth.error_message or "Please login"}
return await func(parent, info, *args, **kwargs)
return wrap
def permission_required(resource, operation, func):
@wraps(func)
async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs):
print('[auth.authenticate] permission_required for %r with info %r' % (func, info)) # debug only
auth: AuthCredentials = info.context["request"].auth
if not auth.logged_in:
return {"error": auth.error_message or "Please login"}
# TODO: add check permission logix
return await func(parent, info, *args, **kwargs)
return wrap

View File

@ -20,7 +20,7 @@ class AuthCredentials(BaseModel):
return True
async def permissions(self) -> List[Permission]:
if self.user_id is not None:
if self.user_id is None:
raise OperationNotAllowed("Please login first")
return NotImplemented()

View File

@ -1,4 +1,4 @@
from datetime import datetime
from datetime import datetime, timezone
import jwt
from base.exceptions import ExpiredToken, InvalidToken
from validations.auth import TokenPayload, AuthInput
@ -8,14 +8,11 @@ from settings import JWT_ALGORITHM, JWT_SECRET_KEY
class JWTCodec:
@staticmethod
def encode(user: AuthInput, exp: datetime) -> str:
expires = int(exp.timestamp() * 1000)
issued = int(datetime.now().timestamp() * 1000)
payload = {
"user_id": user.id,
"username": user.email or user.phone,
# "device": device, # no use cases
"exp": expires,
"iat": issued,
"exp": exp,
"iat": datetime.now(tz=timezone.utc),
"iss": "discours"
}
try:

View File

@ -9,13 +9,34 @@ from settings import SESSION_TOKEN_LIFE_SPAN, ONETIME_TOKEN_LIFE_SPAN
async def save(token_key, life_span, auto_delete=True):
await redis.execute("SET", token_key, "True")
if auto_delete:
expire_at = (datetime.now() + timedelta(seconds=life_span)).timestamp()
expire_at = (datetime.now(tz=timezone.utc) + timedelta(seconds=life_span)).timestamp()
await redis.execute("EXPIREAT", token_key, int(expire_at))
class SessionToken:
@classmethod
async def verify(cls, token: str):
"""
Rules for a token to be valid.
- token format is legal
- token exists in redis database
- token is not expired
"""
try:
return JWTCodec.decode(token)
except Exception as e:
raise e
@classmethod
async def get(cls, uid, token):
return await TokenStorage.get(f"{uid}-{token}")
class TokenStorage:
@staticmethod
async def get(token_key):
print('[tokenstorage.get] ' + token_key)
# 2041-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoyMDQxLCJ1c2VybmFtZSI6ImFudG9uLnJld2luK3Rlc3QtbG9hZGNoYXRAZ21haWwuY29tIiwiZXhwIjoxNjcxNzgwNjE2LCJpYXQiOjE2NjkxODg2MTYsImlzcyI6ImRpc2NvdXJzIn0.Nml4oV6iMjMmc6xwM7lTKEZJKBXvJFEIZ-Up1C1rITQ
return await redis.execute("GET", token_key)
@staticmethod

View File

@ -4,7 +4,7 @@ import json
import os
import subprocess
import sys
from datetime import datetime
from datetime import datetime, timezone
import bs4
from migration.tables.comments import migrate as migrateComment
@ -21,7 +21,7 @@ from orm import init_tables
# from export import export_email_subscriptions
from .export import export_mdx, export_slug
TODAY = datetime.strftime(datetime.now(), "%Y%m%d")
TODAY = datetime.strftime(datetime.now(tz=timezone.utc), "%Y%m%d")
OLD_DATE = "2016-03-05 22:22:00.350000"

View File

@ -1,6 +1,6 @@
import json
import os
from datetime import datetime
from datetime import datetime, timezone
import frontmatter
@ -11,7 +11,7 @@ OLD_DATE = "2016-03-05 22:22:00.350000"
EXPORT_DEST = "../discoursio-web/data/"
parentDir = "/".join(os.getcwd().split("/")[:-1])
contentDir = parentDir + "/discoursio-web/content/"
ts = datetime.now()
ts = datetime.now(tz=timezone.utc)
def get_metadata(r):

View File

@ -1,4 +1,4 @@
from datetime import datetime
from datetime import datetime, timezone
from dateutil.parser import parse as date_parse
@ -10,7 +10,7 @@ from orm.topic import TopicFollower
from orm.user import User
from services.stat.reacted import ReactedStorage
ts = datetime.now()
ts = datetime.now(tz=timezone.utc)
async def migrate(entry, storage):

View File

@ -1,4 +1,4 @@
from datetime import datetime
from datetime import datetime, timezone
import json
from dateutil.parser import parse as date_parse
from sqlalchemy.exc import IntegrityError
@ -13,7 +13,7 @@ from services.stat.reacted import ReactedStorage
from services.stat.viewed import ViewedStorage
OLD_DATE = "2016-03-05 22:22:00.350000"
ts = datetime.now()
ts = datetime.now(tz=timezone.utc)
type2layout = {
"Article": "article",
"Literature": "literature",

View File

@ -420,6 +420,7 @@
"marketing": "marketing",
"marksizm": "marxism",
"marsel-dyushan": "marchel-duchamp",
"marsel-prust": "marcel-proust",
"martin-haydegger": "martin-hidegger",
"matematika": "maths",
"mayakovskiy": "vladimir-mayakovsky",

View File

@ -32,8 +32,8 @@ def init_tables():
Resource.init_table()
User.init_table()
Community.init_table()
Role.init_table()
UserRating.init_table()
Shout.init_table()
Role.init_table()
ViewedEntry.init_table()
print("[orm] tables initialized")

View File

@ -1,7 +1,6 @@
from datetime import datetime
from sqlalchemy import Column, String, ForeignKey, DateTime, Boolean
from sqlalchemy import Column, String, ForeignKey, DateTime
from base.orm import Base, local_session
@ -11,10 +10,10 @@ class CommunityFollower(Base):
id = None # type: ignore
follower = Column(ForeignKey("user.slug"), primary_key=True)
community = Column(ForeignKey("community.slug"), primary_key=True)
createdAt = Column(
joinedAt = Column(
DateTime, nullable=False, default=datetime.now, comment="Created at"
)
auto = Column(Boolean, nullable=False, default=False)
# role = Column(ForeignKey(Role.id), nullable=False, comment="Role for member")
class Community(Base):
@ -27,7 +26,6 @@ class Community(Base):
createdAt = Column(
DateTime, nullable=False, default=datetime.now, comment="Created at"
)
createdBy = Column(ForeignKey("user.slug"), nullable=False, comment="Author")
@staticmethod
def init_table():
@ -36,9 +34,7 @@ class Community(Base):
session.query(Community).filter(Community.slug == "discours").first()
)
if not d:
d = Community.create(
name="Дискурс", slug="discours", createdBy="anonymous"
)
d = Community.create(name="Дискурс", slug="discours")
session.add(d)
session.commit()
Community.default_community = d

View File

@ -7,6 +7,8 @@ from base.orm import Base, REGISTRY, engine, local_session
from orm.community import Community
# Role Based Access Control #
class ClassType(TypeDecorator):
impl = String
@ -42,18 +44,44 @@ class Role(Base):
@staticmethod
def init_table():
with local_session() as session:
default = session.query(Role).filter(Role.name == "author").first()
if default:
Role.default_role = default
return
r = session.query(Role).filter(Role.name == "author").first()
if r:
Role.default_role = r
return
default = Role.create(
r1 = Role.create(
name="author",
desc="Role for author",
desc="Role for an author",
community=1,
)
Role.default_role = default
session.add(r1)
Role.default_role = r1
r2 = Role.create(
name="reader",
desc="Role for a reader",
community=1,
)
session.add(r2)
r3 = Role.create(
name="expert",
desc="Role for an expert",
community=1,
)
session.add(r3)
r4 = Role.create(
name="editor",
desc="Role for an editor",
community=1,
)
session.add(r4)
class Operation(Base):
@ -63,10 +91,33 @@ class Operation(Base):
@staticmethod
def init_table():
with local_session() as session:
edit_op = session.query(Operation).filter(Operation.name == "edit").first()
if not edit_op:
edit_op = Operation.create(name="edit")
Operation.edit_id = edit_op.id # type: ignore
for name in ["create", "update", "delete", "load"]:
"""
* everyone can:
- load shouts
- load topics
- load reactions
- create an account to become a READER
* readers can:
- update and delete their account
- load chats
- load messages
- create reaction of some shout's author allowed kinds
- create shout to become an AUTHOR
* authors can:
- update and delete their shout
- invite other authors to edit shout and chat
- manage allowed reactions for their shout
* pros can:
- create/update/delete their community
- create/update/delete topics for their community
"""
op = session.query(Operation).filter(Operation.name == name).first()
if not op:
op = Operation.create(name=name)
session.add(op)
session.commit()
class Resource(Base):
@ -75,14 +126,17 @@ class Resource(Base):
String, nullable=False, unique=True, comment="Resource class"
)
name = Column(String, nullable=False, unique=True, comment="Resource name")
# TODO: community = Column(ForeignKey())
@staticmethod
def init_table():
with local_session() as session:
shout_res = session.query(Resource).filter(Resource.name == "shout").first()
if not shout_res:
shout_res = Resource.create(name="shout", resource_class="shout")
Resource.shout_id = shout_res.id # type: ignore
for res in ["shout", "topic", "reaction", "chat", "message", "invite", "community", "user"]:
r = session.query(Resource).filter(Resource.name == res).first()
if not r:
r = Resource.create(name=res, resource_class=res)
session.add(r)
session.commit()
class Permission(Base):

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from datetime import datetime, timezone
from urllib.parse import quote_plus
from graphql.type import GraphQLResolveInfo
@ -21,21 +21,23 @@ from resolvers.zine.profile import user_subscriptions
from settings import SESSION_TOKEN_HEADER
@mutation.field("refreshSession")
@mutation.field("getSession")
@login_required
async def get_current_user(_, info):
print('[resolvers.auth] get current user %s' % str(info))
user = info.context["request"].user
user.lastSeen = datetime.now()
with local_session() as session:
session.add(user)
session.commit()
token = await TokenStorage.create_session(user)
return {
"token": token,
"user": user,
"news": await user_subscriptions(user.slug),
}
token = info.context["request"].headers.get("Authorization")
if user and token:
user.lastSeen = datetime.now(tz=timezone.utc)
with local_session() as session:
session.add(user)
session.commit()
return {
"token": token,
"user": user,
"news": await user_subscriptions(user.slug),
}
else:
raise OperationNotAllowed("No session token present in request, try to login")
@mutation.field("confirmEmail")
@ -50,7 +52,7 @@ async def confirm_email(_, info, token):
user = session.query(User).where(User.id == user_id).first()
session_token = await TokenStorage.create_session(user)
user.emailConfirmed = True
user.lastSeen = datetime.now()
user.lastSeen = datetime.now(tz=timezone.utc)
session.add(user)
session.commit()
return {
@ -80,8 +82,8 @@ async def confirm_email_handler(request):
def create_user(user_dict):
user = User(**user_dict)
user.roles.append(Role.default_role)
with local_session() as session:
user.roles.append(session.query(Role).first())
session.add(user)
session.commit()
return user

View File

@ -1,4 +1,4 @@
from datetime import datetime
from datetime import datetime, timezone
from auth.authenticate import login_required
from base.orm import local_session
@ -37,7 +37,7 @@ async def invite_author(_, info, author, shout):
if author.id in authors:
return {"error": "already added"}
shout.authors.append(author)
shout.updated_at = datetime.now()
shout.updated_at = datetime.now(tz=timezone.utc)
session.add(shout)
session.commit()
@ -63,7 +63,7 @@ async def remove_author(_, info, author, shout):
if author.id not in authors:
return {"error": "not in authors"}
shout.authors.remove(author)
shout.updated_at = datetime.now()
shout.updated_at = datetime.now(tz=timezone.utc)
session.add(shout)
session.commit()

View File

@ -1,4 +1,4 @@
from datetime import datetime
from datetime import datetime, timezone
from auth.authenticate import login_required
from base.orm import local_session
@ -71,7 +71,7 @@ async def update_shout(_, info, inp):
return {"error": "access denied"}
else:
shout.update(inp)
shout.updatedAt = datetime.now()
shout.updatedAt = datetime.now(tz=timezone.utc)
session.add(shout)
if inp.get("topics"):
# remove old links
@ -103,7 +103,7 @@ async def delete_shout(_, info, slug):
return {"error": "access denied"}
for a in authors:
reactions_unfollow(a.slug, slug, True)
shout.deletedAt = datetime.now()
shout.deletedAt = datetime.now(tz=timezone.utc)
session.add(shout)
session.commit()

View File

@ -1,6 +1,6 @@
import json
import uuid
from datetime import datetime
from datetime import datetime, timezone
from auth.authenticate import login_required
from base.redis import redis
@ -67,7 +67,7 @@ async def update_chat(_, info, chat_new: dict):
chat.update({
"title": chat_new.get("title", chat["title"]),
"description": chat_new.get("description", chat["description"]),
"updatedAt": int(datetime.now().timestamp()),
"updatedAt": int(datetime.now(tz=timezone.utc).timestamp()),
"admins": chat_new.get("admins", chat["admins"]),
"users": chat_new.get("users", chat["users"])
})
@ -90,8 +90,8 @@ async def create_chat(_, info, title="", members=[]):
members.append(user.slug)
chat = {
"title": title,
"createdAt": int(datetime.now().timestamp()),
"updatedAt": int(datetime.now().timestamp()),
"createdAt": int(datetime.now(tz=timezone.utc).timestamp()),
"updatedAt": int(datetime.now(tz=timezone.utc).timestamp()),
"createdBy": user.slug,
"id": chat_id,
"users": members,

View File

@ -1,5 +1,5 @@
import json
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from auth.authenticate import login_required
from base.redis import redis
@ -83,7 +83,7 @@ async def load_messages_by(_, info, by, limit: int = 50, offset: int = 0):
days = by.get("days")
if days:
messages = filter(
lambda m: datetime.now() - int(m["createdAt"]) < timedelta(days=by.get("days")),
lambda m: datetime.now(tz=timezone.utc) - int(m["createdAt"]) < timedelta(days=by.get("days")),
messages
)
return {

View File

@ -1,6 +1,6 @@
import asyncio
import json
from datetime import datetime
from datetime import datetime, timezone
from auth.authenticate import login_required
from base.redis import redis
@ -28,7 +28,7 @@ async def create_message(_, info, chat: str, body: str, replyTo=None):
"author": user.slug,
"body": body,
"replyTo": replyTo,
"createdAt": int(datetime.now().timestamp()),
"createdAt": int(datetime.now(tz=timezone.utc).timestamp()),
}
await redis.execute(
"SET", f"chats/{chat['id']}/messages/{message_id}", json.dumps(new_message)
@ -70,7 +70,7 @@ async def update_message(_, info, chat_id: str, message_id: int, body: str):
return {"error": "access denied"}
message["body"] = body
message["updatedAt"] = int(datetime.now().timestamp())
message["updatedAt"] = int(datetime.now(tz=timezone.utc).timestamp())
await redis.execute("SET", f"chats/{chat_id}/messages/{message_id}", json.dumps(message))

View File

@ -1,4 +1,4 @@
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
import sqlalchemy as sa
from sqlalchemy.orm import joinedload
from sqlalchemy.sql.expression import desc, asc, select, case
@ -27,7 +27,7 @@ def apply_filters(q, filters, user=None):
if filters.get("body"):
q = q.filter(Shout.body.ilike(f'%{filters.get("body")}%s'))
if filters.get("days"):
before = datetime.now() - timedelta(days=int(filters.get("days")) or 30)
before = datetime.now(tz=timezone.utc) - timedelta(days=int(filters.get("days")) or 30)
q = q.filter(Shout.createdAt > before)
return q

View File

@ -1,5 +1,5 @@
from typing import List
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from sqlalchemy import and_, func
from sqlalchemy.orm import selectinload
@ -185,8 +185,7 @@ async def get_authors_all(_, _info):
async def get_author(_, _info, slug):
with local_session() as session:
author = session.query(User).join(ShoutAuthor).where(User.slug == slug).first()
for author in author:
author.stat = await get_author_stat(author.slug)
author.stat = await get_author_stat(author.slug)
return author
@ -203,10 +202,10 @@ async def load_authors_by(_, info, by, limit, offset):
aaa = list(map(lambda a: a.slug, TopicStat.authors_by_topic.get(by["topic"])))
aq = aq.filter(User.name._in(aaa))
if by.get("lastSeen"): # in days
days_before = datetime.now() - timedelta(days=by["lastSeen"])
days_before = datetime.now(tz=timezone.utc) - timedelta(days=by["lastSeen"])
aq = aq.filter(User.lastSeen > days_before)
elif by.get("createdAt"): # in days
days_before = datetime.now() - timedelta(days=by["createdAt"])
days_before = datetime.now(tz=timezone.utc) - timedelta(days=by["createdAt"])
aq = aq.filter(User.createdAt > days_before)
aq = aq.group_by(
User.id

View File

@ -1,4 +1,4 @@
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from sqlalchemy import and_, asc, desc, select, text, func
from sqlalchemy.orm import aliased
@ -109,7 +109,7 @@ def check_to_hide(session, user, reaction):
def set_published(session, slug, publisher):
s = session.query(Shout).where(Shout.slug == slug).first()
s.publishedAt = datetime.now()
s.publishedAt = datetime.now(tz=timezone.utc)
s.publishedBy = publisher
s.visibility = text('public')
session.add(s)
@ -166,7 +166,7 @@ async def update_reaction(_, info, inp):
if reaction.createdBy != user.slug:
return {"error": "access denied"}
reaction.body = inp["body"]
reaction.updatedAt = datetime.now()
reaction.updatedAt = datetime.now(tz=timezone.utc)
if reaction.kind != inp["kind"]:
# NOTE: change mind detection can be here
pass
@ -191,7 +191,7 @@ async def delete_reaction(_, info, rid):
return {"error": "invalid reaction id"}
if reaction.createdBy != user.slug:
return {"error": "access denied"}
reaction.deletedAt = datetime.now()
reaction.deletedAt = datetime.now(tz=timezone.utc)
session.commit()
return {}
@ -240,7 +240,7 @@ async def load_reactions_by(_, _info, by, limit=50, offset=0):
if by.get('search', 0) > 2:
q = q.filter(Reaction.body.ilike(f'%{by["body"]}%'))
if by.get("days"):
after = datetime.now() - timedelta(days=int(by["days"]) or 30)
after = datetime.now(tz=timezone.utc) - timedelta(days=int(by["days"]) or 30)
q = q.filter(Reaction.createdAt > after)
order_way = asc if by.get("sort", "").startswith("-") else desc
order_field = by.get("sort") or Reaction.createdAt

View File

@ -159,7 +159,7 @@ type Mutation {
markAsRead(chatId: String!, ids: [Int]!): Result!
# auth
refreshSession: AuthResult!
getSession: AuthResult!
registerUser(email: String!, password: String, name: String): AuthResult!
sendLink(email: String!, lang: String): Result!
confirmEmail(token: String!): AuthResult!
@ -478,7 +478,7 @@ type TopicStat {
authors: Int!
# viewed: Int
# reacted: Int!
#commented: Int
# commented: Int
# rating: Int
}

View File

@ -48,37 +48,45 @@ log_settings = {
}
}
local_headers = [
("Access-Control-Allow-Methods", "GET, POST, OPTIONS, HEAD"),
("Access-Control-Allow-Origin", "http://localhost:3000"),
(
"Access-Control-Allow-Headers",
"DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization",
),
("Access-Control-Expose-Headers", "Content-Length,Content-Range"),
("Access-Control-Allow-Credentials", "true"),
]
if __name__ == "__main__":
x = ""
if len(sys.argv) > 1:
x = sys.argv[1]
if x == "dev":
print("DEV MODE")
if os.path.exists(DEV_SERVER_STATUS_FILE_NAME):
os.remove(DEV_SERVER_STATUS_FILE_NAME)
headers = [
("Access-Control-Allow-Methods", "GET, POST, OPTIONS, HEAD"),
("Access-Control-Allow-Origin", "http://localhost:3000"),
(
"Access-Control-Allow-Headers",
"DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization",
),
("Access-Control-Expose-Headers", "Content-Length,Content-Range"),
("Access-Control-Allow-Credentials", "true"),
]
want_reload = False
if "reload" in sys.argv:
print("MODE: DEV + RELOAD")
want_reload = True
else:
print("MODE: DEV")
uvicorn.run(
"main:dev_app",
host="localhost",
port=8080,
headers=headers,
headers=local_headers,
# log_config=LOGGING_CONFIG,
log_level=None,
access_log=False,
reload=True
reload=want_reload
) # , ssl_keyfile="discours.key", ssl_certfile="discours.crt")
elif x == "migrate":
from migration import migrate
print("MODE: MIGRATE")
migrate()
else:

View File

@ -1,5 +1,5 @@
import asyncio
from sqlalchemy.orm import selectinload
from sqlalchemy.orm import selectinload, exc
from orm.user import User
from base.orm import local_session
@ -22,16 +22,18 @@ class UserStorage:
@staticmethod
async def get_user(id):
with local_session() as session:
user = (
session.query(User).options(
selectinload(User.roles),
selectinload(User.ratings)
).filter(
User.id == id
).one()
)
return user
try:
user = (
session.query(User).options(
selectinload(User.roles),
selectinload(User.ratings)
).filter(
User.id == id
).one()
)
return user
except exc.NoResultFound:
return None
@staticmethod
async def get_all_users():