From 94a31e29e5d2476f93f8f46bde8a204132993bc1 Mon Sep 17 00:00:00 2001 From: tonyrewin Date: Wed, 23 Nov 2022 17:09:35 +0300 Subject: [PATCH 1/5] try timezones --- auth/jwtcodec.py | 8 +++----- auth/tokenstorage.py | 2 +- migration/__init__.py | 4 ++-- migration/export.py | 4 ++-- migration/tables/comments.py | 4 ++-- migration/tables/content_items.py | 4 ++-- resolvers/auth.py | 6 +++--- resolvers/create/collab.py | 6 +++--- resolvers/create/editor.py | 6 +++--- resolvers/inbox/chats.py | 8 ++++---- resolvers/inbox/load.py | 4 ++-- resolvers/inbox/messages.py | 6 +++--- resolvers/zine/load.py | 4 ++-- resolvers/zine/profile.py | 6 +++--- resolvers/zine/reactions.py | 10 +++++----- schema.graphql | 2 +- 16 files changed, 41 insertions(+), 43 deletions(-) diff --git a/auth/jwtcodec.py b/auth/jwtcodec.py index 0c35dea5..87bd2b5a 100644 --- a/auth/jwtcodec.py +++ b/auth/jwtcodec.py @@ -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,13 +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) + issued = datetime.now(tz=timezone.utc) payload = { "user_id": user.id, "username": user.email or user.phone, - # "device": device, # no use cases - "exp": expires, + "exp": exp, "iat": issued, "iss": "discours" } diff --git a/auth/tokenstorage.py b/auth/tokenstorage.py index 0d7a30e9..ef6fa0d6 100644 --- a/auth/tokenstorage.py +++ b/auth/tokenstorage.py @@ -9,7 +9,7 @@ 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)) diff --git a/migration/__init__.py b/migration/__init__.py index a6aaa0ff..2d195e06 100644 --- a/migration/__init__.py +++ b/migration/__init__.py @@ -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" diff --git a/migration/export.py b/migration/export.py index 75d95160..988ab35d 100644 --- a/migration/export.py +++ b/migration/export.py @@ -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): diff --git a/migration/tables/comments.py b/migration/tables/comments.py index 567259b3..c93e3d63 100644 --- a/migration/tables/comments.py +++ b/migration/tables/comments.py @@ -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): diff --git a/migration/tables/content_items.py b/migration/tables/content_items.py index 1a798103..af5f99d5 100644 --- a/migration/tables/content_items.py +++ b/migration/tables/content_items.py @@ -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", diff --git a/resolvers/auth.py b/resolvers/auth.py index c167394b..8f9fe0e9 100644 --- a/resolvers/auth.py +++ b/resolvers/auth.py @@ -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 @@ -26,7 +26,7 @@ from settings import SESSION_TOKEN_HEADER async def get_current_user(_, info): print('[resolvers.auth] get current user %s' % str(info)) user = info.context["request"].user - user.lastSeen = datetime.now() + user.lastSeen = datetime.now(tz=timezone.utc) with local_session() as session: session.add(user) session.commit() @@ -50,7 +50,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 { diff --git a/resolvers/create/collab.py b/resolvers/create/collab.py index 7db4620e..058a90c2 100644 --- a/resolvers/create/collab.py +++ b/resolvers/create/collab.py @@ -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() diff --git a/resolvers/create/editor.py b/resolvers/create/editor.py index 5dbcf95c..220ff229 100644 --- a/resolvers/create/editor.py +++ b/resolvers/create/editor.py @@ -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() diff --git a/resolvers/inbox/chats.py b/resolvers/inbox/chats.py index 1b80c2ea..f4b1ca1e 100644 --- a/resolvers/inbox/chats.py +++ b/resolvers/inbox/chats.py @@ -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, diff --git a/resolvers/inbox/load.py b/resolvers/inbox/load.py index d3ca0ff1..cd38690b 100644 --- a/resolvers/inbox/load.py +++ b/resolvers/inbox/load.py @@ -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 { diff --git a/resolvers/inbox/messages.py b/resolvers/inbox/messages.py index 48f45b4a..84734a61 100644 --- a/resolvers/inbox/messages.py +++ b/resolvers/inbox/messages.py @@ -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)) diff --git a/resolvers/zine/load.py b/resolvers/zine/load.py index 4f37f9c2..81141ac6 100644 --- a/resolvers/zine/load.py +++ b/resolvers/zine/load.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import sqlalchemy as sa from sqlalchemy.orm import selectinload 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 diff --git a/resolvers/zine/profile.py b/resolvers/zine/profile.py index 911c6058..cbb5d4c3 100644 --- a/resolvers/zine/profile.py +++ b/resolvers/zine/profile.py @@ -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 @@ -203,10 +203,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 diff --git a/resolvers/zine/reactions.py b/resolvers/zine/reactions.py index 1f4d48c2..646de03a 100644 --- a/resolvers/zine/reactions.py +++ b/resolvers/zine/reactions.py @@ -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 diff --git a/schema.graphql b/schema.graphql index e42e61db..ba62a678 100644 --- a/schema.graphql +++ b/schema.graphql @@ -478,7 +478,7 @@ type TopicStat { authors: Int! # viewed: Int # reacted: Int! - #commented: Int + # commented: Int # rating: Int } From 786bd20275a4f83511f8ede73d8f94b8ed78b339 Mon Sep 17 00:00:00 2001 From: tonyrewin Date: Thu, 24 Nov 2022 11:27:01 +0300 Subject: [PATCH 2/5] init --- auth/authenticate.py | 15 ++++++ migration/tables/replacements.json | 1 + orm/__init__.py | 2 +- orm/community.py | 12 ++--- orm/rbac.py | 84 ++++++++++++++++++++++++------ resolvers/auth.py | 2 +- 6 files changed, 91 insertions(+), 25 deletions(-) diff --git a/auth/authenticate.py b/auth/authenticate.py index 425b5d1d..6a84214f 100644 --- a/auth/authenticate.py +++ b/auth/authenticate.py @@ -89,3 +89,18 @@ def login_required(func): 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] login 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 diff --git a/migration/tables/replacements.json b/migration/tables/replacements.json index 74a0cc68..0aa081d2 100644 --- a/migration/tables/replacements.json +++ b/migration/tables/replacements.json @@ -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", diff --git a/orm/__init__.py b/orm/__init__.py index dfe0a323..c8251256 100644 --- a/orm/__init__.py +++ b/orm/__init__.py @@ -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") diff --git a/orm/community.py b/orm/community.py index d789d0e3..8c339714 100644 --- a/orm/community.py +++ b/orm/community.py @@ -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 diff --git a/orm/rbac.py b/orm/rbac.py index fccfc5ae..ad288798 100644 --- a/orm/rbac.py +++ b/orm/rbac.py @@ -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): diff --git a/resolvers/auth.py b/resolvers/auth.py index 8f9fe0e9..54947f9a 100644 --- a/resolvers/auth.py +++ b/resolvers/auth.py @@ -80,8 +80,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 From 84600308ad21bce78dc6699a1afaf738e5469b4c Mon Sep 17 00:00:00 2001 From: tonyrewin Date: Thu, 24 Nov 2022 17:31:52 +0300 Subject: [PATCH 3/5] refresh-token --- auth/authenticate.py | 50 +++++++++++--------------------------------- auth/credentials.py | 2 +- auth/jwtcodec.py | 3 +-- auth/tokenstorage.py | 21 +++++++++++++++++++ resolvers/auth.py | 13 +++++++++++- server.py | 9 ++++++-- 6 files changed, 54 insertions(+), 44 deletions(-) diff --git a/auth/authenticate.py b/auth/authenticate.py index 6a84214f..e8104fae 100644 --- a/auth/authenticate.py +++ b/auth/authenticate.py @@ -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) diff --git a/auth/credentials.py b/auth/credentials.py index 5e2dfea9..401ae420 100644 --- a/auth/credentials.py +++ b/auth/credentials.py @@ -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() diff --git a/auth/jwtcodec.py b/auth/jwtcodec.py index 87bd2b5a..c2feacd3 100644 --- a/auth/jwtcodec.py +++ b/auth/jwtcodec.py @@ -8,12 +8,11 @@ from settings import JWT_ALGORITHM, JWT_SECRET_KEY class JWTCodec: @staticmethod def encode(user: AuthInput, exp: datetime) -> str: - issued = datetime.now(tz=timezone.utc) payload = { "user_id": user.id, "username": user.email or user.phone, "exp": exp, - "iat": issued, + "iat": datetime.now(tz=timezone.utc), "iss": "discours" } try: diff --git a/auth/tokenstorage.py b/auth/tokenstorage.py index ef6fa0d6..5c1b5b2d 100644 --- a/auth/tokenstorage.py +++ b/auth/tokenstorage.py @@ -13,9 +13,30 @@ async def save(token_key, life_span, auto_delete=True): 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 diff --git a/resolvers/auth.py b/resolvers/auth.py index 54947f9a..648253c8 100644 --- a/resolvers/auth.py +++ b/resolvers/auth.py @@ -24,13 +24,24 @@ from settings import SESSION_TOKEN_HEADER @mutation.field("refreshSession") @login_required async def get_current_user(_, info): - print('[resolvers.auth] get current user %s' % str(info)) user = info.context["request"].user + # print(info.context["request"].headers) + old_token = info.context["request"].headers.get("Authorization") user.lastSeen = datetime.now(tz=timezone.utc) with local_session() as session: session.add(user) session.commit() token = await TokenStorage.create_session(user) + print("[resolvers.auth] new session token created") + if old_token: + payload = await TokenStorage.get(str(user.id) + '-' + str(old_token)) + if payload: + print("[resolvers.auth] got session from old token: %r" % payload) + return { + "token": token, + "user": user, + "news": await user_subscriptions(user.slug), + } return { "token": token, "user": user, diff --git a/server.py b/server.py index 2cb70715..265bfed6 100644 --- a/server.py +++ b/server.py @@ -53,7 +53,6 @@ if __name__ == "__main__": 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) @@ -67,6 +66,12 @@ if __name__ == "__main__": ("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", @@ -75,7 +80,7 @@ if __name__ == "__main__": # 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 From b2b8cf747f507897a21eff6bfb21f3b2f0fd7c60 Mon Sep 17 00:00:00 2001 From: tonyrewin Date: Thu, 24 Nov 2022 18:19:58 +0300 Subject: [PATCH 4/5] fix getSession, fix getAuthor --- auth/authenticate.py | 2 +- resolvers/auth.py | 27 +++++++++------------------ resolvers/zine/profile.py | 3 +-- schema.graphql | 2 +- 4 files changed, 12 insertions(+), 22 deletions(-) diff --git a/auth/authenticate.py b/auth/authenticate.py index e8104fae..d12924dc 100644 --- a/auth/authenticate.py +++ b/auth/authenticate.py @@ -68,7 +68,7 @@ def login_required(func): def permission_required(resource, operation, func): @wraps(func) 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] 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"} diff --git a/resolvers/auth.py b/resolvers/auth.py index 648253c8..d6f1d40b 100644 --- a/resolvers/auth.py +++ b/resolvers/auth.py @@ -21,32 +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): user = info.context["request"].user - # print(info.context["request"].headers) - old_token = info.context["request"].headers.get("Authorization") - user.lastSeen = datetime.now(tz=timezone.utc) - with local_session() as session: - session.add(user) - session.commit() - token = await TokenStorage.create_session(user) - print("[resolvers.auth] new session token created") - if old_token: - payload = await TokenStorage.get(str(user.id) + '-' + str(old_token)) - if payload: - print("[resolvers.auth] got session from old token: %r" % payload) + 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), } - 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") diff --git a/resolvers/zine/profile.py b/resolvers/zine/profile.py index cbb5d4c3..d2db5ccb 100644 --- a/resolvers/zine/profile.py +++ b/resolvers/zine/profile.py @@ -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 diff --git a/schema.graphql b/schema.graphql index ba62a678..adce2273 100644 --- a/schema.graphql +++ b/schema.graphql @@ -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! From 59640df3bc45a3affb531b1ac53888b9e82a8a4e Mon Sep 17 00:00:00 2001 From: tonyrewin Date: Thu, 24 Nov 2022 18:41:57 +0300 Subject: [PATCH 5/5] some more fixes --- auth/authenticate.py | 1 + services/auth/users.py | 24 +++++++++++++----------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/auth/authenticate.py b/auth/authenticate.py index d12924dc..a361b699 100644 --- a/auth/authenticate.py +++ b/auth/authenticate.py @@ -58,6 +58,7 @@ 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 + print('[auth.authenticate] request auth data: %r' % auth) # debug only if not auth.logged_in: return {"error": auth.error_message or "Please login"} return await func(parent, info, *args, **kwargs) diff --git a/services/auth/users.py b/services/auth/users.py index df6a84ee..2ba636bc 100644 --- a/services/auth/users.py +++ b/services/auth/users.py @@ -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():