commit
93bf7be464
|
@ -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:
|
||||||
payload = await SessionToken.verify(token)
|
if len(token.split('.')) > 1:
|
||||||
|
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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
84
orm/rbac.py
84
orm/rbac.py
|
@ -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):
|
||||||
|
|
|
@ -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")
|
||||||
with local_session() as session:
|
if user and token:
|
||||||
session.add(user)
|
user.lastSeen = datetime.now(tz=timezone.utc)
|
||||||
session.commit()
|
with local_session() as session:
|
||||||
token = await TokenStorage.create_session(user)
|
session.add(user)
|
||||||
return {
|
session.commit()
|
||||||
"token": token,
|
return {
|
||||||
"user": user,
|
"token": token,
|
||||||
"news": await user_subscriptions(user.slug),
|
"user": user,
|
||||||
}
|
"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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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))
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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,8 +185,7 @@ 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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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!
|
||||||
|
@ -478,7 +478,7 @@ type TopicStat {
|
||||||
authors: Int!
|
authors: Int!
|
||||||
# viewed: Int
|
# viewed: Int
|
||||||
# reacted: Int!
|
# reacted: Int!
|
||||||
#commented: Int
|
# commented: Int
|
||||||
# rating: Int
|
# rating: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
34
server.py
34
server.py
|
@ -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__":
|
if __name__ == "__main__":
|
||||||
x = ""
|
x = ""
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
x = sys.argv[1]
|
x = sys.argv[1]
|
||||||
if x == "dev":
|
if x == "dev":
|
||||||
print("DEV MODE")
|
|
||||||
if os.path.exists(DEV_SERVER_STATUS_FILE_NAME):
|
if os.path.exists(DEV_SERVER_STATUS_FILE_NAME):
|
||||||
os.remove(DEV_SERVER_STATUS_FILE_NAME)
|
os.remove(DEV_SERVER_STATUS_FILE_NAME)
|
||||||
|
|
||||||
headers = [
|
want_reload = False
|
||||||
("Access-Control-Allow-Methods", "GET, POST, OPTIONS, HEAD"),
|
if "reload" in sys.argv:
|
||||||
("Access-Control-Allow-Origin", "http://localhost:3000"),
|
print("MODE: DEV + RELOAD")
|
||||||
(
|
want_reload = True
|
||||||
"Access-Control-Allow-Headers",
|
else:
|
||||||
"DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization",
|
print("MODE: DEV")
|
||||||
),
|
|
||||||
("Access-Control-Expose-Headers", "Content-Length,Content-Range"),
|
|
||||||
("Access-Control-Allow-Credentials", "true"),
|
|
||||||
]
|
|
||||||
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:
|
||||||
|
|
|
@ -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,16 +22,18 @@ class UserStorage:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_user(id):
|
async def get_user(id):
|
||||||
with local_session() as session:
|
with local_session() as session:
|
||||||
user = (
|
try:
|
||||||
session.query(User).options(
|
user = (
|
||||||
selectinload(User.roles),
|
session.query(User).options(
|
||||||
selectinload(User.ratings)
|
selectinload(User.roles),
|
||||||
).filter(
|
selectinload(User.ratings)
|
||||||
User.id == id
|
).filter(
|
||||||
).one()
|
User.id == id
|
||||||
)
|
).one()
|
||||||
|
)
|
||||||
return user
|
return user
|
||||||
|
except exc.NoResultFound:
|
||||||
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_all_users():
|
async def get_all_users():
|
||||||
|
|
Loading…
Reference in New Issue
Block a user