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 typing import Optional, Tuple
from graphql.type import GraphQLResolveInfo from graphql.type import GraphQLResolveInfo
from jwt import DecodeError, ExpiredSignatureError
from starlette.authentication import AuthenticationBackend from starlette.authentication import AuthenticationBackend
from starlette.requests import HTTPConnection from starlette.requests import HTTPConnection
from auth.credentials import AuthCredentials, AuthUser 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 services.auth.users import UserStorage
from settings import SESSION_TOKEN_HEADER from settings import SESSION_TOKEN_HEADER
from auth.tokenstorage import SessionToken
from base.exceptions import InvalidToken
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}")
class JWTAuthenticate(AuthenticationBackend): class JWTAuthenticate(AuthenticationBackend):
@ -54,10 +20,18 @@ class JWTAuthenticate(AuthenticationBackend):
if SESSION_TOKEN_HEADER not in request.headers: if SESSION_TOKEN_HEADER not in request.headers:
return AuthCredentials(scopes=[]), AuthUser(user_id=None) 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: try:
if len(token.split('.')) > 1:
payload = await SessionToken.verify(token) payload = await SessionToken.verify(token)
else:
InvalidToken("please try again")
except Exception as exc: except Exception as exc:
print("[auth.authenticate] session token verify error") print("[auth.authenticate] session token verify error")
print(exc) print(exc)
@ -84,8 +58,25 @@ def login_required(func):
async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs): async def wrap(parent, info: GraphQLResolveInfo, *args, **kwargs):
# print('[auth.authenticate] login required for %r with info %r' % (func, info)) # debug only # print('[auth.authenticate] login required for %r with info %r' % (func, info)) # debug only
auth: AuthCredentials = info.context["request"].auth auth: AuthCredentials = info.context["request"].auth
if auth and auth.user_id:
print(auth) # debug only
if not auth.logged_in: if not auth.logged_in:
return {"error": auth.error_message or "Please login"} return {"error": auth.error_message or "Please login"}
return await func(parent, info, *args, **kwargs) return await func(parent, info, *args, **kwargs)
return wrap 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 return True
async def permissions(self) -> List[Permission]: async def permissions(self) -> List[Permission]:
if self.user_id is not None: if self.user_id is None:
raise OperationNotAllowed("Please login first") raise OperationNotAllowed("Please login first")
return NotImplemented() return NotImplemented()

View File

@ -1,4 +1,4 @@
from datetime import datetime from datetime import datetime, timezone
import jwt import jwt
from base.exceptions import ExpiredToken, InvalidToken from base.exceptions import ExpiredToken, InvalidToken
from validations.auth import TokenPayload, AuthInput from validations.auth import TokenPayload, AuthInput
@ -8,14 +8,11 @@ from settings import JWT_ALGORITHM, JWT_SECRET_KEY
class JWTCodec: class JWTCodec:
@staticmethod @staticmethod
def encode(user: AuthInput, exp: datetime) -> str: def encode(user: AuthInput, exp: datetime) -> str:
expires = int(exp.timestamp() * 1000)
issued = int(datetime.now().timestamp() * 1000)
payload = { payload = {
"user_id": user.id, "user_id": user.id,
"username": user.email or user.phone, "username": user.email or user.phone,
# "device": device, # no use cases "exp": exp,
"exp": expires, "iat": datetime.now(tz=timezone.utc),
"iat": issued,
"iss": "discours" "iss": "discours"
} }
try: 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): async def save(token_key, life_span, auto_delete=True):
await redis.execute("SET", token_key, "True") await redis.execute("SET", token_key, "True")
if auto_delete: 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)) 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: class TokenStorage:
@staticmethod @staticmethod
async def get(token_key): async def get(token_key):
print('[tokenstorage.get] ' + token_key)
# 2041-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoyMDQxLCJ1c2VybmFtZSI6ImFudG9uLnJld2luK3Rlc3QtbG9hZGNoYXRAZ21haWwuY29tIiwiZXhwIjoxNjcxNzgwNjE2LCJpYXQiOjE2NjkxODg2MTYsImlzcyI6ImRpc2NvdXJzIn0.Nml4oV6iMjMmc6xwM7lTKEZJKBXvJFEIZ-Up1C1rITQ
return await redis.execute("GET", token_key) return await redis.execute("GET", token_key)
@staticmethod @staticmethod

View File

@ -4,7 +4,7 @@ import json
import os import os
import subprocess import subprocess
import sys import sys
from datetime import datetime from datetime import datetime, timezone
import bs4 import bs4
from migration.tables.comments import migrate as migrateComment 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_email_subscriptions
from .export import export_mdx, export_slug 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" OLD_DATE = "2016-03-05 22:22:00.350000"

View File

@ -1,6 +1,6 @@
import json import json
import os import os
from datetime import datetime from datetime import datetime, timezone
import frontmatter import frontmatter
@ -11,7 +11,7 @@ OLD_DATE = "2016-03-05 22:22:00.350000"
EXPORT_DEST = "../discoursio-web/data/" EXPORT_DEST = "../discoursio-web/data/"
parentDir = "/".join(os.getcwd().split("/")[:-1]) parentDir = "/".join(os.getcwd().split("/")[:-1])
contentDir = parentDir + "/discoursio-web/content/" contentDir = parentDir + "/discoursio-web/content/"
ts = datetime.now() ts = datetime.now(tz=timezone.utc)
def get_metadata(r): 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 from dateutil.parser import parse as date_parse
@ -10,7 +10,7 @@ from orm.topic import TopicFollower
from orm.user import User from orm.user import User
from services.stat.reacted import ReactedStorage from services.stat.reacted import ReactedStorage
ts = datetime.now() ts = datetime.now(tz=timezone.utc)
async def migrate(entry, storage): async def migrate(entry, storage):

View File

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

View File

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

View File

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

View File

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

View File

@ -7,6 +7,8 @@ from base.orm import Base, REGISTRY, engine, local_session
from orm.community import Community from orm.community import Community
# Role Based Access Control #
class ClassType(TypeDecorator): class ClassType(TypeDecorator):
impl = String impl = String
@ -42,18 +44,44 @@ class Role(Base):
@staticmethod @staticmethod
def init_table(): def init_table():
with local_session() as session: with local_session() as session:
default = session.query(Role).filter(Role.name == "author").first() r = session.query(Role).filter(Role.name == "author").first()
if default: if r:
Role.default_role = default Role.default_role = r
return return
default = Role.create( r1 = Role.create(
name="author", name="author",
desc="Role for author", desc="Role for an author",
community=1, 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): class Operation(Base):
@ -63,10 +91,33 @@ class Operation(Base):
@staticmethod @staticmethod
def init_table(): def init_table():
with local_session() as session: with local_session() as session:
edit_op = session.query(Operation).filter(Operation.name == "edit").first() for name in ["create", "update", "delete", "load"]:
if not edit_op: """
edit_op = Operation.create(name="edit") * everyone can:
Operation.edit_id = edit_op.id # type: ignore - 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): class Resource(Base):
@ -75,14 +126,17 @@ class Resource(Base):
String, nullable=False, unique=True, comment="Resource class" String, nullable=False, unique=True, comment="Resource class"
) )
name = Column(String, nullable=False, unique=True, comment="Resource name") name = Column(String, nullable=False, unique=True, comment="Resource name")
# TODO: community = Column(ForeignKey())
@staticmethod @staticmethod
def init_table(): def init_table():
with local_session() as session: with local_session() as session:
shout_res = session.query(Resource).filter(Resource.name == "shout").first() for res in ["shout", "topic", "reaction", "chat", "message", "invite", "community", "user"]:
if not shout_res: r = session.query(Resource).filter(Resource.name == res).first()
shout_res = Resource.create(name="shout", resource_class="shout") if not r:
Resource.shout_id = shout_res.id # type: ignore r = Resource.create(name=res, resource_class=res)
session.add(r)
session.commit()
class Permission(Base): class Permission(Base):

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import json import json
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from auth.authenticate import login_required from auth.authenticate import login_required
from base.redis import redis 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") days = by.get("days")
if days: if days:
messages = filter( 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 messages
) )
return { return {

View File

@ -1,6 +1,6 @@
import asyncio import asyncio
import json import json
from datetime import datetime from datetime import datetime, timezone
from auth.authenticate import login_required from auth.authenticate import login_required
from base.redis import redis from base.redis import redis
@ -28,7 +28,7 @@ async def create_message(_, info, chat: str, body: str, replyTo=None):
"author": user.slug, "author": user.slug,
"body": body, "body": body,
"replyTo": replyTo, "replyTo": replyTo,
"createdAt": int(datetime.now().timestamp()), "createdAt": int(datetime.now(tz=timezone.utc).timestamp()),
} }
await redis.execute( await redis.execute(
"SET", f"chats/{chat['id']}/messages/{message_id}", json.dumps(new_message) "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"} return {"error": "access denied"}
message["body"] = body 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)) 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 import sqlalchemy as sa
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from sqlalchemy.sql.expression import desc, asc, select, case from sqlalchemy.sql.expression import desc, asc, select, case
@ -27,7 +27,7 @@ def apply_filters(q, filters, user=None):
if filters.get("body"): if filters.get("body"):
q = q.filter(Shout.body.ilike(f'%{filters.get("body")}%s')) q = q.filter(Shout.body.ilike(f'%{filters.get("body")}%s'))
if filters.get("days"): 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) q = q.filter(Shout.createdAt > before)
return q return q

View File

@ -1,5 +1,5 @@
from typing import List from typing import List
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from sqlalchemy import and_, func from sqlalchemy import and_, func
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@ -185,7 +185,6 @@ async def get_authors_all(_, _info):
async def get_author(_, _info, slug): async def get_author(_, _info, slug):
with local_session() as session: with local_session() as session:
author = session.query(User).join(ShoutAuthor).where(User.slug == slug).first() 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 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"]))) aaa = list(map(lambda a: a.slug, TopicStat.authors_by_topic.get(by["topic"])))
aq = aq.filter(User.name._in(aaa)) aq = aq.filter(User.name._in(aaa))
if by.get("lastSeen"): # in days 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) aq = aq.filter(User.lastSeen > days_before)
elif by.get("createdAt"): # in days 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.filter(User.createdAt > days_before)
aq = aq.group_by( aq = aq.group_by(
User.id 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 import and_, asc, desc, select, text, func
from sqlalchemy.orm import aliased from sqlalchemy.orm import aliased
@ -109,7 +109,7 @@ def check_to_hide(session, user, reaction):
def set_published(session, slug, publisher): def set_published(session, slug, publisher):
s = session.query(Shout).where(Shout.slug == slug).first() s = session.query(Shout).where(Shout.slug == slug).first()
s.publishedAt = datetime.now() s.publishedAt = datetime.now(tz=timezone.utc)
s.publishedBy = publisher s.publishedBy = publisher
s.visibility = text('public') s.visibility = text('public')
session.add(s) session.add(s)
@ -166,7 +166,7 @@ async def update_reaction(_, info, inp):
if reaction.createdBy != user.slug: if reaction.createdBy != user.slug:
return {"error": "access denied"} return {"error": "access denied"}
reaction.body = inp["body"] reaction.body = inp["body"]
reaction.updatedAt = datetime.now() reaction.updatedAt = datetime.now(tz=timezone.utc)
if reaction.kind != inp["kind"]: if reaction.kind != inp["kind"]:
# NOTE: change mind detection can be here # NOTE: change mind detection can be here
pass pass
@ -191,7 +191,7 @@ async def delete_reaction(_, info, rid):
return {"error": "invalid reaction id"} return {"error": "invalid reaction id"}
if reaction.createdBy != user.slug: if reaction.createdBy != user.slug:
return {"error": "access denied"} return {"error": "access denied"}
reaction.deletedAt = datetime.now() reaction.deletedAt = datetime.now(tz=timezone.utc)
session.commit() session.commit()
return {} return {}
@ -240,7 +240,7 @@ async def load_reactions_by(_, _info, by, limit=50, offset=0):
if by.get('search', 0) > 2: if by.get('search', 0) > 2:
q = q.filter(Reaction.body.ilike(f'%{by["body"]}%')) q = q.filter(Reaction.body.ilike(f'%{by["body"]}%'))
if by.get("days"): 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) q = q.filter(Reaction.createdAt > after)
order_way = asc if by.get("sort", "").startswith("-") else desc order_way = asc if by.get("sort", "").startswith("-") else desc
order_field = by.get("sort") or Reaction.createdAt order_field = by.get("sort") or Reaction.createdAt

View File

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

View File

@ -48,16 +48,7 @@ log_settings = {
} }
} }
if __name__ == "__main__": local_headers = [
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-Methods", "GET, POST, OPTIONS, HEAD"),
("Access-Control-Allow-Origin", "http://localhost:3000"), ("Access-Control-Allow-Origin", "http://localhost:3000"),
( (
@ -67,18 +58,35 @@ if __name__ == "__main__":
("Access-Control-Expose-Headers", "Content-Length,Content-Range"), ("Access-Control-Expose-Headers", "Content-Length,Content-Range"),
("Access-Control-Allow-Credentials", "true"), ("Access-Control-Allow-Credentials", "true"),
] ]
if __name__ == "__main__":
x = ""
if len(sys.argv) > 1:
x = sys.argv[1]
if x == "dev":
if os.path.exists(DEV_SERVER_STATUS_FILE_NAME):
os.remove(DEV_SERVER_STATUS_FILE_NAME)
want_reload = False
if "reload" in sys.argv:
print("MODE: DEV + RELOAD")
want_reload = True
else:
print("MODE: DEV")
uvicorn.run( uvicorn.run(
"main:dev_app", "main:dev_app",
host="localhost", host="localhost",
port=8080, port=8080,
headers=headers, headers=local_headers,
# log_config=LOGGING_CONFIG, # log_config=LOGGING_CONFIG,
log_level=None, log_level=None,
access_log=False, access_log=False,
reload=True reload=want_reload
) # , ssl_keyfile="discours.key", ssl_certfile="discours.crt") ) # , ssl_keyfile="discours.key", ssl_certfile="discours.crt")
elif x == "migrate": elif x == "migrate":
from migration import migrate from migration import migrate
print("MODE: MIGRATE")
migrate() migrate()
else: else:

View File

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