Skip to content

Commit

Permalink
feat: feed mutations
Browse files Browse the repository at this point in the history
  • Loading branch information
dartt0n committed Jul 22, 2024
1 parent 13c0657 commit 87e8f62
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 28 deletions.
16 changes: 16 additions & 0 deletions generate_jwt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import os

from dotenv import load_dotenv

from src.adapter.external.auth.telegram import TgOauthContainer, TgOauthDTO
from src.domain.model.scalar.object_id import ObjectID

load_dotenv()

jwt_secret = os.getenv("JWT_SECRET")
assert jwt_secret is not None


dto = TgOauthDTO(id=ObjectID("669d90fa55df5710d2d9624c"), telegram_id=9536)

print(TgOauthContainer.construct(dto, jwt_secret).to_string())
10 changes: 5 additions & 5 deletions src/adapter/external/auth/telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,12 @@ def to_string(self, *args, **kwargs) -> str:

class TelegramOauthAdapter(OauthProtocol):
__secret_token: str
__jwt_secret: str
_jwt_secret: str
__service: UserService

def __init__(self, secret_token: str, jwt_secret: str, service: UserService):
self.__secret_token = secret_token
self.__jwt_secret = jwt_secret
self._jwt_secret = jwt_secret
self.__service = service
self.__verify_keys = [
"auth_date",
Expand Down Expand Up @@ -145,7 +145,7 @@ async def register(self, data: Any) -> TgOauthContainer:
raise e

return TgOauthContainer.construct(
TgOauthDTO(id=user.id, telegram_id=user.telegram_id), self.__jwt_secret
TgOauthDTO(id=user.id, telegram_id=user.telegram_id), self._jwt_secret
)

async def login(self, data: Any) -> TgOauthContainer:
Expand Down Expand Up @@ -188,7 +188,7 @@ async def login(self, data: Any) -> TgOauthContainer:
raise e

return TgOauthContainer.construct(
TgOauthDTO(id=user.id, telegram_id=user.telegram_id), self.__jwt_secret
TgOauthDTO(id=user.id, telegram_id=user.telegram_id), self._jwt_secret
)

async def retrieve_user(self, data: TgOauthContainer) -> User:
Expand All @@ -202,7 +202,7 @@ async def retrieve_user(self, data: TgOauthContainer) -> User:

try:
log.debug("building dto from container")
dto = data.to_dto(self.__jwt_secret)
dto = data.to_dto(self._jwt_secret)
except Exception as e:
log.error("failed to build dto from container with exception: {}", e)
raise auth_exception.InvalidCredentialsException(
Expand Down
104 changes: 104 additions & 0 deletions src/adapter/external/graphql/operation/feed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from __future__ import annotations

import strawberry as sb

import src.protocol.internal.database.participant as proto
from src.adapter.external.graphql import scalar
from src.adapter.external.graphql.tool.context import Info
from src.adapter.external.graphql.tool.permission import DefaultPermissions
from src.adapter.external.graphql.type.participant import (
BaseParticipantType,
domain_to_participant,
)


@sb.type
class FeedMutation:
@sb.mutation(permission_classes=[DefaultPermissions])
async def mark_viewed(
root: FeedMutation, info: Info[FeedMutation], id: scalar.ObjectID
) -> BaseParticipantType:
participants = await info.context.participant.service.read_all()

current = None
for participant in participants:
if participant.user_id == info.context.user_id:
current = participant
break
if current is None:
raise Exception("User is not participant")

current = await info.context.participant.loader.load(current.id)
data = await info.context.participant.service.update(
proto.UpdateParticipant(
_id=current.id, viewed_ids=set(current.viewed_ids) | {id}
)
)
info.context.participant.loader.clear(current.id)
return domain_to_participant(data)

@sb.mutation(permission_classes=[DefaultPermissions])
async def unsubscribe(
root: FeedMutation, info: Info[FeedMutation], id: scalar.ObjectID
) -> BaseParticipantType:
participants = await info.context.participant.service.read_all()

current = None
for participant in participants:
if participant.user_id == info.context.user_id:
current = participant
break
if current is None:
raise Exception("User is not participant")

current = await info.context.participant.loader.load(current.id)
other = await info.context.participant.loader.load(id)

data_current = await info.context.participant.service.update(
proto.UpdateParticipant(
_id=current.id, subscription_ids=set(current.subscription_ids) - {id}
)
)

data_other = await info.context.participant.service.update(
proto.UpdateParticipant(
_id=other.id, subscribers_ids=set(other.subscribers_ids) - {current.id}
)
)
_ = data_other

info.context.participant.loader.clear_many([current.id, other.id])
return domain_to_participant(data_current)

@sb.mutation(permission_classes=[DefaultPermissions])
async def subscribe(
root: FeedMutation, info: Info[FeedMutation], id: scalar.ObjectID
) -> BaseParticipantType:
participants = await info.context.participant.service.read_all()

current = None
for participant in participants:
if participant.user_id == info.context.user_id:
current = participant
break
if current is None:
raise Exception("User is not participant")

current = await info.context.participant.loader.load(current.id)
other = await info.context.participant.loader.load(id)

data_current = await info.context.participant.service.update(
proto.UpdateParticipant(
_id=current.id, subscription_ids=set(current.subscription_ids) | {id}
)
)

data_other = await info.context.participant.service.update(
proto.UpdateParticipant(
_id=other.id, subscribers_ids=set(other.subscribers_ids) | {current.id}
)
)
_ = data_other

info.context.participant.loader.clear_many([current.id, other.id])
return domain_to_participant(data_current)
9 changes: 6 additions & 3 deletions src/adapter/external/graphql/operation/recommentaions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,27 @@
import src.domain.model as domain
from src.adapter.external.graphql import scalar
from src.adapter.external.graphql.tool.context import Info
from src.adapter.external.graphql.tool.permission import DefaultPermissions
from src.adapter.external.graphql.type.participant import BaseParticipantType


@sb.type
class RecommendationsQuery:
@sb.field
@sb.field(permission_classes=[DefaultPermissions])
async def recommendations(
root: RecommendationsQuery,
info: Info[RecommendationsQuery],
user_id: scalar.ObjectID,
allocation_id: scalar.ObjectID,
) -> list[BaseParticipantType]:
if info.context.user_id is None:
raise Exception("User is not authenticated")

participants = await info.context.participant.service.read_all()

selected = [
participant.id
for participant in participants
if participant.user_id != user_id
if participant.user_id != info.context.user_id
and participant.allocation_id == allocation_id
and participant.state == domain.ParticipantState.ACTIVE
]
Expand Down
7 changes: 7 additions & 0 deletions src/adapter/external/graphql/operation/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ async def user(
with log.activity(f"loading user {id}"):
return await info.context.user.loader.load(id)

@sb.field(permission_classes=[DefaultPermissions])
async def me(root: UserQuery, info: Info[UserQuery]) -> UserType:
with log.activity("loading user"):
if info.context.user_id is None:
raise Exception("User is not authenticated")
return await info.context.user.loader.load(info.context.user_id)


@sb.type
class UserMutation:
Expand Down
2 changes: 2 additions & 0 deletions src/adapter/external/graphql/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
AllocationMutation,
AllocationQuery,
)
from src.adapter.external.graphql.operation.feed import FeedMutation
from src.adapter.external.graphql.operation.form_field import (
AnswerMutation,
AnswerQuery,
Expand Down Expand Up @@ -45,6 +46,7 @@ class Mutation(
PreferenceMutation,
RoomMutation,
UserMutation,
FeedMutation,
): ...


Expand Down
5 changes: 5 additions & 0 deletions src/adapter/external/graphql/tool/context.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from dataclasses import dataclass

import aiohttp.web as web
import strawberry as sb
from strawberry.dataloader import DataLoader

Expand Down Expand Up @@ -27,6 +28,10 @@ class DataContext[LoaderType, ServiceType]:

@dataclass
class Context:
user_id: scalar.ObjectID | None
telegram_id: int | None
request: web.Request

answer: DataContext[AnswerType, answer.AnswerService] # type: ignore
allocation: DataContext[AllocationType, AllocationService] # type: ignore
form_field: DataContext[FormFieldType, FormFieldService] # type: ignore
Expand Down
64 changes: 44 additions & 20 deletions src/adapter/external/graphql/tool/permission.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,65 @@
from typing import Any
from typing import TYPE_CHECKING, Annotated, Any

import strawberry as sb

from src.utils.logger.logger import Logger

if TYPE_CHECKING:
from src.adapter.external.graphql.tool.context import Info

LazyInfo = Annotated[
"Info", # type: ignore
sb.lazy(module_path="src.adapter.external.graphql.tool.context"),
]

log = Logger("graphql-permission")


class AuthenticatedPermission(sb.BasePermission):
"""Denies access to unauthenticated users"""

def has_permission(self, source: Any, info: sb.Info, **kwargs):
# todo implement logic
return True
message = "User is not authenticated"
error_extensions = {"code": "UNAUTHORIZED"}

def has_permission(self, source: Any, info: LazyInfo, **kwargs):
log.debug(f"checking permission user_id: {info.context.user_id}")
log.debug(f"user authenticated: {info.context.user_id is not None}")
return info.context.user_id is not None


class OwnerPermission(sb.BasePermission):
"""Allows access to the owner of the object"""

def has_permission(self, source: Any, info: sb.Info, **kwargs):
# todo: implement logic
message = "User does not have access to object"
error_extensions = {"code": "FORBIDDEN"}

def has_permission(self, source: Any, info: LazyInfo, **kwargs):
return True


class SharedPermission(sb.BasePermission):
"""Allows access to the object, if it is shared with the user"""

def has_permission(self, source: Any, info: sb.Info, **kwargs):
message = "User does not have access to object"
error_extensions = {"code": "FORBIDDEN"}

def has_permission(self, source: Any, info: LazyInfo, **kwargs):
# todo: implement logic
return True


class SystemPermission(sb.BasePermission):
"""Allows access to the system"""

def has_permission(self, source: Any, info: sb.Info, **kwargs):
def has_permission(self, source: Any, info: LazyInfo, **kwargs):
# todo: implement logic
return True


class PublicPermissions(sb.BasePermission):
"""Allow any access"""

def has_permission(self, source: Any, info: sb.Info, **kwargs):
def has_permission(self, source: Any, info: LazyInfo, **kwargs):
return True


Expand All @@ -49,23 +70,26 @@ def __init__(self):
self.shared_permission = SharedPermission()
self.system_permission = SystemPermission()

def has_permission(self, source: Any, info: sb.Info, **kwargs):
def has_permission(self, source: Any, info: LazyInfo, **kwargs):
log.debug("checking permission for source {}", source)
auth = self.auth_permission.has_permission(source, info, **kwargs)
owner = self.owner_permission.has_permission(source, info, **kwargs)
shared = self.shared_permission.has_permission(source, info, **kwargs)
system = self.system_permission.has_permission(source, info, **kwargs)
# owner = self.owner_permission.has_permission(source, info, **kwargs)
# shared = self.shared_permission.has_permission(source, info, **kwargs)
# system = self.system_permission.has_permission(source, info, **kwargs)

return auth

# system can access any infomation
if system:
return True
# if system:
# return True

# not authenticated
if not auth:
return False
# if not auth:
# return False

# access explicitly allowed
if owner or shared:
return True
# if owner or shared:
# return True

# deny access
return False
# return False
Loading

0 comments on commit 87e8f62

Please sign in to comment.