mirror of
https://github.com/RYGhub/royalnet.git
synced 2024-11-27 13:34:28 +00:00
Do some token things
This commit is contained in:
parent
bb3aa1fd85
commit
56cbd5b5c6
11 changed files with 138 additions and 18 deletions
|
@ -1,4 +1,4 @@
|
||||||
from typing import Set, Dict, Union
|
from typing import *
|
||||||
from contextlib import contextmanager, asynccontextmanager
|
from contextlib import contextmanager, asynccontextmanager
|
||||||
from royalnet.utils import asyncify
|
from royalnet.utils import asyncify
|
||||||
from royalnet.alchemy.errors import TableNotFoundError
|
from royalnet.alchemy.errors import TableNotFoundError
|
||||||
|
@ -6,7 +6,7 @@ from sqlalchemy import create_engine
|
||||||
from sqlalchemy.engine import Engine
|
from sqlalchemy.engine import Engine
|
||||||
from sqlalchemy.schema import Table
|
from sqlalchemy.schema import Table
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.ext.declarative.api import DeclarativeMeta
|
from sqlalchemy.ext.declarative.api import DeclarativeMeta, AbstractConcreteBase
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ class Alchemy:
|
||||||
raise NotImplementedError("sqlite databases aren't supported, as they can't be used in multithreaded"
|
raise NotImplementedError("sqlite databases aren't supported, as they can't be used in multithreaded"
|
||||||
" applications")
|
" applications")
|
||||||
self._engine: Engine = create_engine(database_uri)
|
self._engine: Engine = create_engine(database_uri)
|
||||||
self._Base: DeclarativeMeta = declarative_base(bind=self._engine)
|
self._Base = declarative_base(bind=self._engine)
|
||||||
self.Session: sessionmaker = sessionmaker(bind=self._engine)
|
self.Session: sessionmaker = sessionmaker(bind=self._engine)
|
||||||
self._tables: Dict[str, Table] = {}
|
self._tables: Dict[str, Table] = {}
|
||||||
for table in tables:
|
for table in tables:
|
||||||
|
@ -38,7 +38,7 @@ class Alchemy:
|
||||||
self._tables[name] = bound_table
|
self._tables[name] = bound_table
|
||||||
self._Base.metadata.create_all()
|
self._Base.metadata.create_all()
|
||||||
|
|
||||||
def get(self, table: Union[str, type]) -> Table:
|
def get(self, table: Union[str, type]) -> DeclarativeMeta:
|
||||||
"""Get the table with a specified name or class.
|
"""Get the table with a specified name or class.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
|
@ -2,13 +2,16 @@
|
||||||
from .api_royalnet_version import ApiRoyalnetVersionStar
|
from .api_royalnet_version import ApiRoyalnetVersionStar
|
||||||
from .api_login_royalnet import ApiLoginRoyalnetStar
|
from .api_login_royalnet import ApiLoginRoyalnetStar
|
||||||
from .api_token_info import ApiTokenInfoStar
|
from .api_token_info import ApiTokenInfoStar
|
||||||
|
from .api_token_passwd import ApiTokenPasswdStar
|
||||||
|
from .api_token_create import ApiTokenCreateStar
|
||||||
|
|
||||||
# Enter the PageStars of your Pack here!
|
# Enter the PageStars of your Pack here!
|
||||||
available_page_stars = [
|
available_page_stars = [
|
||||||
ApiRoyalnetVersionStar,
|
ApiRoyalnetVersionStar,
|
||||||
ApiLoginRoyalnetStar,
|
ApiLoginRoyalnetStar,
|
||||||
ApiTokenInfoStar,
|
ApiTokenInfoStar,
|
||||||
|
ApiTokenPasswdStar,
|
||||||
|
ApiTokenCreateStar,
|
||||||
]
|
]
|
||||||
|
|
||||||
# Don't change this, it should automatically generate __all__
|
# Don't change this, it should automatically generate __all__
|
||||||
|
|
21
royalnet/backpack/stars/api_token_create.py
Normal file
21
royalnet/backpack/stars/api_token_create.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
from typing import *
|
||||||
|
import datetime
|
||||||
|
import royalnet.utils as ru
|
||||||
|
from royalnet.constellation.api import *
|
||||||
|
from ..tables.tokens import Token
|
||||||
|
from sqlalchemy import and_
|
||||||
|
|
||||||
|
|
||||||
|
class ApiTokenCreateStar(ApiStar):
|
||||||
|
path = "/api/token/create/v1"
|
||||||
|
|
||||||
|
async def api(self, data: ApiData) -> dict:
|
||||||
|
user = await data.user()
|
||||||
|
try:
|
||||||
|
duration = int(data["duration"])
|
||||||
|
except ValueError:
|
||||||
|
raise InvalidParameterError("Duration is not a valid integer")
|
||||||
|
new_token = Token.generate(self.alchemy, user, datetime.timedelta(seconds=duration))
|
||||||
|
data.session.add(new_token)
|
||||||
|
await data.session_commit()
|
||||||
|
return new_token.json()
|
33
royalnet/backpack/stars/api_token_passwd.py
Normal file
33
royalnet/backpack/stars/api_token_passwd.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
from typing import *
|
||||||
|
import datetime
|
||||||
|
import royalnet.utils as ru
|
||||||
|
from royalnet.constellation.api import *
|
||||||
|
from sqlalchemy import and_
|
||||||
|
from ..tables.tokens import Token
|
||||||
|
|
||||||
|
|
||||||
|
class ApiTokenPasswdStar(ApiStar):
|
||||||
|
path = "/api/token/passwd/v1"
|
||||||
|
|
||||||
|
async def api(self, data: ApiData) -> dict:
|
||||||
|
TokenT = self.alchemy.get(Token)
|
||||||
|
token = await data.token()
|
||||||
|
user = token.user
|
||||||
|
user.set_password(data["new_password"])
|
||||||
|
tokens: List[Token] = await ru.asyncify(
|
||||||
|
data.session
|
||||||
|
.query(self.alchemy.get(Token))
|
||||||
|
.filter(
|
||||||
|
and_(
|
||||||
|
TokenT.user == user,
|
||||||
|
TokenT.expiration >= datetime.datetime.now()
|
||||||
|
))
|
||||||
|
.all
|
||||||
|
)
|
||||||
|
for t in tokens:
|
||||||
|
if t.token != token.token:
|
||||||
|
t.expired = True
|
||||||
|
await data.session_commit()
|
||||||
|
return {
|
||||||
|
"revoked_tokens": len(tokens) - 1
|
||||||
|
}
|
|
@ -4,7 +4,6 @@ from sqlalchemy import *
|
||||||
from sqlalchemy.orm import *
|
from sqlalchemy.orm import *
|
||||||
from sqlalchemy.ext.declarative import declared_attr
|
from sqlalchemy.ext.declarative import declared_attr
|
||||||
import royalnet.utils as ru
|
import royalnet.utils as ru
|
||||||
from .users import User
|
|
||||||
|
|
||||||
|
|
||||||
class Token:
|
class Token:
|
||||||
|
@ -30,10 +29,18 @@ class Token:
|
||||||
def expired(self):
|
def expired(self):
|
||||||
return datetime.datetime.now() > self.expiration
|
return datetime.datetime.now() > self.expiration
|
||||||
|
|
||||||
|
@expired.setter
|
||||||
|
def expired(self, value):
|
||||||
|
if value is True:
|
||||||
|
self.expiration = datetime.datetime.fromtimestamp(0)
|
||||||
|
else:
|
||||||
|
raise ValueError("'expired' can only be set to True.")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate(cls, user, expiration_delta: datetime.timedelta):
|
def generate(cls, alchemy, user, expiration_delta: datetime.timedelta):
|
||||||
# noinspection PyArgumentList
|
# noinspection PyArgumentList
|
||||||
token = cls(user=user, expiration=datetime.datetime.now() + expiration_delta, token=secrets.token_urlsafe())
|
TokenT = alchemy.get(cls)
|
||||||
|
token = TokenT(user=user, expiration=datetime.datetime.now() + expiration_delta, token=secrets.token_urlsafe())
|
||||||
return token
|
return token
|
||||||
|
|
||||||
def json(self) -> dict:
|
def json(self) -> dict:
|
||||||
|
|
|
@ -26,6 +26,7 @@ class CommandData:
|
||||||
if self._session is None:
|
if self._session is None:
|
||||||
if self._interface.alchemy is None:
|
if self._interface.alchemy is None:
|
||||||
raise UnsupportedError("'alchemy' is not enabled on this Royalnet instance")
|
raise UnsupportedError("'alchemy' is not enabled on this Royalnet instance")
|
||||||
|
log.debug("Creating Session...")
|
||||||
self._session = self._interface.alchemy.Session()
|
self._session = self._interface.alchemy.Session()
|
||||||
return self._session
|
return self._session
|
||||||
|
|
||||||
|
@ -34,11 +35,13 @@ class CommandData:
|
||||||
if self._session:
|
if self._session:
|
||||||
log.warning("Session had to be created to be committed")
|
log.warning("Session had to be created to be committed")
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
|
log.debug("Committing Session...")
|
||||||
await ru.asyncify(self.session.commit)
|
await ru.asyncify(self.session.commit)
|
||||||
|
|
||||||
async def session_close(self):
|
async def session_close(self):
|
||||||
"""Asyncronously close the :attr:`.session` of this object."""
|
"""Asyncronously close the :attr:`.session` of this object."""
|
||||||
if self._session is not None:
|
if self._session is not None:
|
||||||
|
log.debug("Closing Session...")
|
||||||
await ru.asyncify(self._session.close)
|
await ru.asyncify(self._session.close)
|
||||||
|
|
||||||
async def reply(self, text: str) -> None:
|
async def reply(self, text: str) -> None:
|
||||||
|
@ -71,9 +74,7 @@ class CommandData:
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
alias: the Alias to search for."""
|
alias: the Alias to search for."""
|
||||||
return await ru.asyncify(
|
return await Alias.find_user(self._interface.alchemy, self.session, alias)
|
||||||
Alias.find_user(self._interface.alchemy, self.session, alias)
|
|
||||||
)
|
|
||||||
|
|
||||||
@contextlib.asynccontextmanager
|
@contextlib.asynccontextmanager
|
||||||
async def keyboard(self, text, keys: List["KeyboardKey"]):
|
async def keyboard(self, text, keys: List["KeyboardKey"]):
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
from .apistar import ApiStar
|
from .apistar import ApiStar
|
||||||
from .jsonapi import api_response, api_success, api_error
|
from .jsonapi import api_response, api_success, api_error
|
||||||
from .apidata import ApiData
|
from .apidata import ApiData
|
||||||
from .apierrors import ApiError, MissingParameterError, NotFoundError, ForbiddenError
|
from .apierrors import \
|
||||||
|
ApiError, \
|
||||||
|
NotFoundError, \
|
||||||
|
ForbiddenError, \
|
||||||
|
BadRequestError, \
|
||||||
|
ParameterError, \
|
||||||
|
MissingParameterError, \
|
||||||
|
InvalidParameterError, \
|
||||||
|
NotImplementedError, \
|
||||||
|
UnsupportedError
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
@ -13,4 +22,13 @@ __all__ = [
|
||||||
"ApiError",
|
"ApiError",
|
||||||
"MissingParameterError",
|
"MissingParameterError",
|
||||||
"NotFoundError",
|
"NotFoundError",
|
||||||
|
"ApiError",
|
||||||
|
"NotFoundError",
|
||||||
|
"ForbiddenError",
|
||||||
|
"BadRequestError",
|
||||||
|
"ParameterError",
|
||||||
|
"MissingParameterError",
|
||||||
|
"InvalidParameterError",
|
||||||
|
"NotImplementedError",
|
||||||
|
"UnsupportedError",
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
|
import logging
|
||||||
from .apierrors import MissingParameterError
|
from .apierrors import MissingParameterError
|
||||||
from royalnet.backpack.tables.tokens import Token
|
from royalnet.backpack.tables.tokens import Token
|
||||||
from royalnet.backpack.tables.users import User
|
from royalnet.backpack.tables.users import User
|
||||||
from .apierrors import *
|
from .apierrors import *
|
||||||
|
import royalnet.utils as ru
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ApiData(dict):
|
class ApiData(dict):
|
||||||
|
@ -19,7 +23,7 @@ class ApiData(dict):
|
||||||
raise ForbiddenError("'token' is invalid")
|
raise ForbiddenError("'token' is invalid")
|
||||||
return token
|
return token
|
||||||
|
|
||||||
async def user(self) -> Token:
|
async def user(self) -> User:
|
||||||
return (await self.token()).user
|
return (await self.token()).user
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -27,5 +31,20 @@ class ApiData(dict):
|
||||||
if self._session is None:
|
if self._session is None:
|
||||||
if self.star.alchemy is None:
|
if self.star.alchemy is None:
|
||||||
raise UnsupportedError("'alchemy' is not enabled on this Royalnet instance")
|
raise UnsupportedError("'alchemy' is not enabled on this Royalnet instance")
|
||||||
|
log.debug("Creating Session...")
|
||||||
self._session = self.star.alchemy.Session()
|
self._session = self.star.alchemy.Session()
|
||||||
return self._session
|
return self._session
|
||||||
|
|
||||||
|
async def session_commit(self):
|
||||||
|
"""Asyncronously commit the :attr:`.session` of this object."""
|
||||||
|
if self._session:
|
||||||
|
log.warning("Session had to be created to be committed")
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
log.debug("Committing Session...")
|
||||||
|
await ru.asyncify(self.session.commit)
|
||||||
|
|
||||||
|
async def session_close(self):
|
||||||
|
"""Asyncronously close the :attr:`.session` of this object."""
|
||||||
|
if self._session is not None:
|
||||||
|
log.debug("Closing Session...")
|
||||||
|
await ru.asyncify(self._session.close)
|
||||||
|
|
|
@ -10,7 +10,19 @@ class ForbiddenError(ApiError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class MissingParameterError(ApiError):
|
class BadRequestError(ApiError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ParameterError(BadRequestError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MissingParameterError(ParameterError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidParameterError(ParameterError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ from ..pagestar import PageStar
|
||||||
from .jsonapi import api_error, api_success
|
from .jsonapi import api_error, api_success
|
||||||
from .apidata import ApiData
|
from .apidata import ApiData
|
||||||
from .apierrors import *
|
from .apierrors import *
|
||||||
|
from royalnet.utils import sentry_exc
|
||||||
|
|
||||||
|
|
||||||
class ApiStar(PageStar, ABC):
|
class ApiStar(PageStar, ABC):
|
||||||
|
@ -18,19 +19,24 @@ class ApiStar(PageStar, ABC):
|
||||||
data = await request.json()
|
data = await request.json()
|
||||||
except JSONDecodeError:
|
except JSONDecodeError:
|
||||||
data = {}
|
data = {}
|
||||||
|
apidata = ApiData(data, self)
|
||||||
try:
|
try:
|
||||||
response = await self.api(ApiData(data, self))
|
response = await self.api(apidata)
|
||||||
except NotFoundError as e:
|
except NotFoundError as e:
|
||||||
return api_error(e, code=404)
|
return api_error(e, code=404)
|
||||||
except ForbiddenError as e:
|
except ForbiddenError as e:
|
||||||
return api_error(e, code=403)
|
return api_error(e, code=403)
|
||||||
except NotImplementedError as e:
|
except NotImplementedError as e:
|
||||||
return api_error(e, code=501)
|
return api_error(e, code=501)
|
||||||
except ApiError as e:
|
except BadRequestError as e:
|
||||||
return api_error(e, code=400)
|
return api_error(e, code=400)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
sentry_exc(e)
|
||||||
return api_error(e, code=500)
|
return api_error(e, code=500)
|
||||||
return api_success(response)
|
else:
|
||||||
|
return api_success(response)
|
||||||
|
finally:
|
||||||
|
await apidata.session_close()
|
||||||
|
|
||||||
async def api(self, data: dict) -> dict:
|
async def api(self, data: dict) -> dict:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
|
@ -47,7 +47,7 @@ class Serf:
|
||||||
}
|
}
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
log.error(f"{e.__class__.__name__} during the import of {pack_name}:\n"
|
log.error(f"{e.__class__.__name__} during the import of {pack_name}:\n"
|
||||||
f"{traceback.format_exception(*sys.exc_info())}")
|
f"{''.join(traceback.format_exception(*sys.exc_info()))}")
|
||||||
log.info(f"Packs: {len(packs)} imported")
|
log.info(f"Packs: {len(packs)} imported")
|
||||||
|
|
||||||
self.alchemy: Optional[ra.Alchemy] = None
|
self.alchemy: Optional[ra.Alchemy] = None
|
||||||
|
|
Loading…
Reference in a new issue