diff --git a/royalnet/backpack/stars/__init__.py b/royalnet/backpack/stars/__init__.py index 129ed92b..4ca5f3fa 100644 --- a/royalnet/backpack/stars/__init__.py +++ b/royalnet/backpack/stars/__init__.py @@ -1,10 +1,12 @@ # Imports go here! from .api_royalnet_version import ApiRoyalnetVersionStar +from .api_royalnet_login import ApiRoyalnetLoginStar # Enter the PageStars of your Pack here! available_page_stars = [ ApiRoyalnetVersionStar, + ApiRoyalnetLoginStar, ] # Don't change this, it should automatically generate __all__ diff --git a/royalnet/backpack/stars/api_royalnet_login.py b/royalnet/backpack/stars/api_royalnet_login.py new file mode 100644 index 00000000..c20d14b3 --- /dev/null +++ b/royalnet/backpack/stars/api_royalnet_login.py @@ -0,0 +1,32 @@ +import datetime +import royalnet.utils as ru +from royalnet.constellation.api import * +from ..tables.users import User +from ..tables.aliases import Alias +from ..tables.tokens import Token + + +class ApiRoyalnetLoginStar(ApiStar): + path = "/api/royalnet/login/v1" + + async def api(self, data: ApiDataDict) -> dict: + TokenT = self.alchemy.get(Token) + UserT = self.alchemy.get(User) + AliasT = self.alchemy.get(Alias) + + username = data["username"] + password = data["password"] + + async with self.session_acm() as session: + user: User = await ru.asyncify(session.query(UserT).filter_by(username=username).one_or_none) + if user is None: + raise NotFoundException("User not found") + pswd_check = user.test_password(password) + if not pswd_check: + raise ApiError("Invalid password") + token: Token = TokenT.generate(user=user, expiration_delta=datetime.timedelta(days=7)) + session.add(token) + await ru.asyncify(session.commit) + response = token.json() + + return response diff --git a/royalnet/backpack/stars/api_royalnet_version.py b/royalnet/backpack/stars/api_royalnet_version.py index a842ddfe..de77030a 100644 --- a/royalnet/backpack/stars/api_royalnet_version.py +++ b/royalnet/backpack/stars/api_royalnet_version.py @@ -1,11 +1,11 @@ import royalnet.version as rv -from royalnet.constellation import ApiStar +from royalnet.constellation.api import * class ApiRoyalnetVersionStar(ApiStar): - path = "/api/royalnet/version" + path = "/api/royalnet/version/v1" - async def api(self, data: dict) -> dict: + async def api(self, data: ApiDataDict) -> dict: return { "semantic": rv.semantic } diff --git a/royalnet/backpack/tables/__init__.py b/royalnet/backpack/tables/__init__.py index d4c750e9..3a18b96f 100644 --- a/royalnet/backpack/tables/__init__.py +++ b/royalnet/backpack/tables/__init__.py @@ -4,6 +4,7 @@ from .telegram import Telegram from .discord import Discord from .matrix import Matrix from .aliases import Alias +from .tokens import Token # Enter the tables of your Pack here! available_tables = { @@ -11,7 +12,8 @@ available_tables = { Telegram, Discord, Matrix, - Alias + Alias, + Token, } # Don't change this, it should automatically generate __all__ diff --git a/royalnet/backpack/tables/tokens.py b/royalnet/backpack/tables/tokens.py index 3d87e1ef..7b332f84 100644 --- a/royalnet/backpack/tables/tokens.py +++ b/royalnet/backpack/tables/tokens.py @@ -1,4 +1,5 @@ import datetime +import secrets from sqlalchemy import * from sqlalchemy.orm import * from sqlalchemy.ext.declarative import declared_attr @@ -26,3 +27,16 @@ class Token: @property def expired(self): return datetime.datetime.now() > self.expiration + + @classmethod + def generate(cls, user, expiration_delta: datetime.timedelta): + # noinspection PyArgumentList + token = cls(user=user, expiration=datetime.datetime.now() + expiration_delta, token=secrets.token_urlsafe()) + return token + + def json(self) -> dict: + return { + "user": self.user.json(), + "token": self.token, + "expiration": self.expiration.isoformat() + } diff --git a/royalnet/constellation/__init__.py b/royalnet/constellation/__init__.py index d9438019..c1e373a5 100644 --- a/royalnet/constellation/__init__.py +++ b/royalnet/constellation/__init__.py @@ -17,15 +17,9 @@ You can install them with: :: from .constellation import Constellation from .star import Star from .pagestar import PageStar -from .apistar import ApiStar -from .jsonapi import api_response, api_success, api_error __all__ = [ "Constellation", "Star", "PageStar", - "ApiStar", - "api_response", - "api_success", - "api_error", ] diff --git a/royalnet/constellation/api/__init__.py b/royalnet/constellation/api/__init__.py new file mode 100644 index 00000000..a10f1f7d --- /dev/null +++ b/royalnet/constellation/api/__init__.py @@ -0,0 +1,16 @@ +from .apistar import ApiStar +from .jsonapi import api_response, api_success, api_error +from .apidatadict import ApiDataDict +from .apierrors import ApiError, MissingParameterException, NotFoundException, UnauthorizedException + + +__all__ = [ + "ApiStar", + "api_response", + "api_success", + "api_error", + "ApiDataDict", + "ApiError", + "MissingParameterException", + "NotFoundException", +] diff --git a/royalnet/constellation/api/apidatadict.py b/royalnet/constellation/api/apidatadict.py new file mode 100644 index 00000000..f9c7d217 --- /dev/null +++ b/royalnet/constellation/api/apidatadict.py @@ -0,0 +1,6 @@ +from .apierrors import MissingParameterException + + +class ApiDataDict(dict): + def __missing__(self, key): + raise MissingParameterException(f"Missing '{key}'") diff --git a/royalnet/constellation/api/apierrors.py b/royalnet/constellation/api/apierrors.py new file mode 100644 index 00000000..040ccc7b --- /dev/null +++ b/royalnet/constellation/api/apierrors.py @@ -0,0 +1,14 @@ +class ApiError(Exception): + pass + + +class NotFoundException(ApiError): + pass + + +class UnauthorizedException(ApiError): + pass + + +class MissingParameterException(ApiError): + pass diff --git a/royalnet/constellation/apistar.py b/royalnet/constellation/api/apistar.py similarity index 64% rename from royalnet/constellation/apistar.py rename to royalnet/constellation/api/apistar.py index 372af97e..1d7b33e2 100644 --- a/royalnet/constellation/apistar.py +++ b/royalnet/constellation/api/apistar.py @@ -3,8 +3,10 @@ from json import JSONDecodeError from abc import * from starlette.requests import Request from starlette.responses import JSONResponse -from .pagestar import PageStar +from ..pagestar import PageStar from .jsonapi import api_error, api_success +from .apidatadict import ApiDataDict +from .apierrors import ApiError, NotFoundException class ApiStar(PageStar, ABC): @@ -17,9 +19,13 @@ class ApiStar(PageStar, ABC): except JSONDecodeError: data = {} try: - response = await self.api(data) + response = await self.api(ApiDataDict(data)) + except NotFoundException as e: + return api_error(e, code=404) + except ApiError as e: + return api_error(e, code=400) except Exception as e: - return api_error(e) + return api_error(e, code=500) return api_success(response) async def api(self, data: dict) -> dict: diff --git a/royalnet/constellation/jsonapi.py b/royalnet/constellation/api/jsonapi.py similarity index 100% rename from royalnet/constellation/jsonapi.py rename to royalnet/constellation/api/jsonapi.py