diff --git a/backend/capellacollab/alembic/versions/c9f30ccd4650_add_basic_auth_token.py b/backend/capellacollab/alembic/versions/c9f30ccd4650_add_basic_auth_token.py index 2e0ffbe55..60a2aa09b 100644 --- a/backend/capellacollab/alembic/versions/c9f30ccd4650_add_basic_auth_token.py +++ b/backend/capellacollab/alembic/versions/c9f30ccd4650_add_basic_auth_token.py @@ -4,7 +4,7 @@ """Add basic auth token Revision ID: c9f30ccd4650 -Revises: d8cf851562cd +Revises: 1a4208c18909 Create Date: 2023-09-06 14:42:53.016924 """ @@ -13,13 +13,12 @@ # revision identifiers, used by Alembic. revision = "c9f30ccd4650" -down_revision = "d8cf851562cd" +down_revision = "1a4208c18909" branch_labels = None depends_on = None def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### op.create_table( "basic_auth_token", sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), @@ -27,10 +26,8 @@ def upgrade(): sa.Column("hash", sa.String(), nullable=False), sa.Column("expiration_date", sa.Date(), nullable=False), sa.Column("description", sa.String(), nullable=False), - sa.ForeignKeyConstraint( - ["user_id"], - ["users.id"], - ), + sa.Column("source", sa.String(), nullable=False), + sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), sa.PrimaryKeyConstraint("id"), ) op.create_index( @@ -39,4 +36,3 @@ def upgrade(): ["id"], unique=False, ) - # ### end Alembic commands ### diff --git a/backend/capellacollab/core/authentication/basic_auth.py b/backend/capellacollab/core/authentication/basic_auth.py index 6a497e929..504db5531 100644 --- a/backend/capellacollab/core/authentication/basic_auth.py +++ b/backend/capellacollab/core/authentication/basic_auth.py @@ -17,54 +17,53 @@ class HTTPBasicAuth(security.HTTPBasic): async def __call__( # type: ignore self, request: fastapi.Request - ) -> tuple[str | None, fastapi.HTTPException | None]: + ) -> str | None: credentials: security.HTTPBasicCredentials | None = ( await super().__call__(request) ) if not credentials: - error = fastapi.HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Not authenticated", - headers={"WWW-Authenticate": "Bearer, Basic"}, - ) if self.auto_error: - raise error - return None, error + raise fastapi.HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers={"WWW-Authenticate": "Bearer, Basic"}, + ) + return None with database.SessionLocal() as session: user = user_crud.get_user_by_name(session, credentials.username) - token_data = None - if user: - token_data = token_crud.get_token( + db_token = ( + token_crud.get_token_by_token_and_user( session, credentials.password, user.id ) - if not token_data or not user or token_data.user_id != user.id: - logger.error("Token invalid for user %s", credentials.username) - error = fastapi.HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail={ - "err_code": "TOKEN_INVALID", - "reason": "The used token is not valid.", - }, - headers={"WWW-Authenticate": "Bearer, Basic"}, - ) + if user + else None + ) + if not db_token: + logger.info("Token invalid for user %s", credentials.username) if self.auto_error: - raise error - return None, error + raise fastapi.HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail={ + "err_code": "BASIC_TOKEN_INVALID", + "reason": "The used token is not valid.", + }, + headers={"WWW-Authenticate": "Bearer, Basic"}, + ) + return None - if token_data.expiration_date < datetime.date.today(): - logger.error("Token expired for user %s", credentials.username) - error = fastapi.HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail={ - "err_code": "token_exp", - "reason": "The Signature of the token is expired. Please request a new access token.", - }, - headers={"WWW-Authenticate": "Bearer, Basic"}, - ) + if db_token.expiration_date < datetime.date.today(): + logger.info("Token expired for user %s", credentials.username) if self.auto_error: - raise error - return None, error - return self.get_username(credentials), None + raise fastapi.HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail={ + "err_code": "token_exp", + "reason": "The Signature of the token is expired. Please request a new access token.", + }, + headers={"WWW-Authenticate": "Bearer, Basic"}, + ) + return None + return self.get_username(credentials) def get_username(self, credentials: security.HTTPBasicCredentials) -> str: - return credentials.model_dump()["username"] + return credentials.username diff --git a/backend/capellacollab/core/authentication/injectables.py b/backend/capellacollab/core/authentication/injectables.py index 2a754f816..c7c36c2cf 100644 --- a/backend/capellacollab/core/authentication/injectables.py +++ b/backend/capellacollab/core/authentication/injectables.py @@ -10,6 +10,8 @@ from fastapi import status from fastapi.openapi import models as openapi_models from fastapi.security import base as security_base +from fastapi.security import utils as security_utils +from sqlalchemy import orm from capellacollab.core import database from capellacollab.core.authentication import basic_auth, jwt_bearer @@ -70,28 +72,24 @@ async def get_username( request: fastapi.Request, _unused1=fastapi.Depends(OpenAPIPersonalAccessToken()), _unused2=fastapi.Depends(OpenAPIBearerToken()), -): - basic = await basic_auth.HTTPBasicAuth(auto_error=False)(request) - jwt = await jwt_bearer.JWTBearer(auto_error=False)(request) - - jwt_username, jwt_error = jwt - if jwt_username: - return jwt_username - - basic_username, basic_error = basic - if basic_username: - return basic_username - - if jwt_error: - raise jwt_error - if basic_error: - raise basic_error - - raise fastapi.HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Token is none and username cannot be derived", - headers={"WWW-Authenticate": "Bearer, Basic"}, - ) +) -> str: + authorization = request.headers.get("Authorization") + scheme, _ = security_utils.get_authorization_scheme_param(authorization) + username = None + match scheme.lower(): + case "basic": + username = await basic_auth.HTTPBasicAuth()(request) + case "bearer": + username = await jwt_bearer.JWTBearer()(request) + case _: + raise fastapi.HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token is none and username cannot be derived", + headers={"WWW-Authenticate": "Bearer, Basic"}, + ) + + assert username + return username class RoleVerification: @@ -101,8 +99,8 @@ def __init__(self, required_role: users_models.Role, verify: bool = True): def __call__( self, - username=fastapi.Depends(get_username), - db=fastapi.Depends(database.get_db), + username: str = fastapi.Depends(get_username), + db: orm.Session = fastapi.Depends(database.get_db), ) -> bool: if not (user := users_crud.get_user_by_name(db, username)): if self.verify: @@ -148,8 +146,8 @@ def __init__( def __call__( self, project_slug: str, - username=fastapi.Depends(get_username), - db=fastapi.Depends(database.get_db), + username: str = fastapi.Depends(get_username), + db: orm.Session = fastapi.Depends(database.get_db), ) -> bool: if not (user := users_crud.get_user_by_name(db, username)): if self.verify: diff --git a/backend/capellacollab/core/authentication/jwt_bearer.py b/backend/capellacollab/core/authentication/jwt_bearer.py index ad11b2267..d3c90fc5b 100644 --- a/backend/capellacollab/core/authentication/jwt_bearer.py +++ b/backend/capellacollab/core/authentication/jwt_bearer.py @@ -27,31 +27,29 @@ def __init__(self, auto_error: bool = True): async def __call__( # type: ignore self, request: fastapi.Request - ) -> tuple[str | None, fastapi.HTTPException | None]: + ) -> str | None: credentials: security.HTTPAuthorizationCredentials | None = ( await super().__call__(request) ) if not credentials or credentials.scheme != "Bearer": - error = fastapi.HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Not authenticated", - headers={"WWW-Authenticate": "Bearer, Basic"}, - ) if self.auto_error: - raise error - return None, error + raise fastapi.HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers={"WWW-Authenticate": "Bearer, Basic"}, + ) + return None if token_decoded := self.validate_token(credentials.credentials): self.initialize_user(token_decoded) - return self.get_username(token_decoded), None - error = fastapi.HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Not authenticated", - headers={"WWW-Authenticate": "Bearer, Basic"}, - ) + return self.get_username(token_decoded) if self.auto_error: - raise error - return None, error + raise fastapi.HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers={"WWW-Authenticate": "Bearer, Basic"}, + ) + return None def get_username(self, token_decoded: dict[str, str]) -> str: return token_decoded[ @@ -74,7 +72,7 @@ def validate_token(self, token: str) -> dict[str, t.Any] | None: raise fastapi.HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail={ - "err_code": "TOKEN_INVALID", + "err_code": "JWT_TOKEN_INVALID", "reason": "The used token is not valid.", }, ) from None diff --git a/backend/capellacollab/projects/routes.py b/backend/capellacollab/projects/routes.py index 101d8304c..9db9fe41b 100644 --- a/backend/capellacollab/projects/routes.py +++ b/backend/capellacollab/projects/routes.py @@ -43,7 +43,7 @@ def get_projects( users_injectables.get_own_user ), db: orm.Session = fastapi.Depends(database.get_db), - username=fastapi.Depends(auth_injectables.get_username), + username: str = fastapi.Depends(auth_injectables.get_username), log: logging.LoggerAdapter = fastapi.Depends( core_logging.get_request_logger ), diff --git a/backend/capellacollab/projects/toolmodels/backups/routes.py b/backend/capellacollab/projects/toolmodels/backups/routes.py index 9cae8db8c..3b6850158 100644 --- a/backend/capellacollab/projects/toolmodels/backups/routes.py +++ b/backend/capellacollab/projects/toolmodels/backups/routes.py @@ -72,7 +72,7 @@ def create_backup( toolmodels_injectables.get_existing_capella_model ), db: orm.Session = fastapi.Depends(database.get_db), - username=fastapi.Depends(auth_injectables.get_username), + username: str = fastapi.Depends(auth_injectables.get_username), ): git_model = git_injectables.get_existing_git_model( body.git_model_id, capella_model, db @@ -141,7 +141,7 @@ def delete_pipeline( injectables.get_existing_pipeline ), db: orm.Session = fastapi.Depends(database.get_db), - username=fastapi.Depends(auth_injectables.get_username), + username: str = fastapi.Depends(auth_injectables.get_username), force: bool = False, ): try: diff --git a/backend/capellacollab/projects/users/routes.py b/backend/capellacollab/projects/users/routes.py index 9a7854ce0..eb97ecebd 100644 --- a/backend/capellacollab/projects/users/routes.py +++ b/backend/capellacollab/projects/users/routes.py @@ -79,7 +79,7 @@ def get_current_user( projects_injectables.get_existing_project ), db: orm.Session = fastapi.Depends(database.get_db), - username=fastapi.Depends(auth_injectables.get_username), + username: str = fastapi.Depends(auth_injectables.get_username), ) -> models.ProjectUserAssociation | models.ProjectUser: if auth_injectables.RoleVerification( required_role=users_models.Role.ADMIN, verify=False diff --git a/backend/capellacollab/sessions/hooks/interface.py b/backend/capellacollab/sessions/hooks/interface.py index 1ac086379..a47d5716c 100644 --- a/backend/capellacollab/sessions/hooks/interface.py +++ b/backend/capellacollab/sessions/hooks/interface.py @@ -37,7 +37,6 @@ def configuration_hook( user: users_models.DatabaseUser, tool_version: tools_models.DatabaseVersion, tool: tools_models.DatabaseTool, - username: str, **kwargs, ) -> tuple[ dict[str, str], diff --git a/backend/capellacollab/sessions/hooks/jupyter.py b/backend/capellacollab/sessions/hooks/jupyter.py index 25194a28e..75dd3345f 100644 --- a/backend/capellacollab/sessions/hooks/jupyter.py +++ b/backend/capellacollab/sessions/hooks/jupyter.py @@ -46,7 +46,6 @@ def configuration_hook( # type: ignore[override] db: orm.Session, user: users_models.DatabaseUser, tool: tools_models.DatabaseTool, - username: str, **kwargs, ) -> tuple[ JupyterConfigEnvironment, @@ -63,7 +62,7 @@ def configuration_hook( # type: ignore[override] "CSP_ORIGIN_HOST": f"{self._general_conf.get('scheme')}://{self._general_conf.get('host')}:{self._general_conf.get('port')}", } - volumes = self._get_project_share_volume_mounts(db, username, tool) + volumes = self._get_project_share_volume_mounts(db, user.name, tool) return environment, volumes, [] # type: ignore[return-value] diff --git a/backend/capellacollab/sessions/injectables.py b/backend/capellacollab/sessions/injectables.py index c0259f9bb..3a78ddf70 100644 --- a/backend/capellacollab/sessions/injectables.py +++ b/backend/capellacollab/sessions/injectables.py @@ -15,7 +15,7 @@ def get_existing_session( session_id: str, db: orm.Session = fastapi.Depends(database.get_db), - username=fastapi.Depends(auth_injectables.get_username), + username: str = fastapi.Depends(auth_injectables.get_username), ) -> models.DatabaseSession: if not (session := crud.get_session_by_id(db, session_id)): raise fastapi.HTTPException( diff --git a/backend/capellacollab/sessions/routes.py b/backend/capellacollab/sessions/routes.py index 7e791809a..fd776179d 100644 --- a/backend/capellacollab/sessions/routes.py +++ b/backend/capellacollab/sessions/routes.py @@ -86,7 +86,7 @@ def get_current_sessions( users_injectables.get_own_user ), db: orm.Session = fastapi.Depends(database.get_db), - username=fastapi.Depends(auth_injectables.get_username), + username: str = fastapi.Depends(auth_injectables.get_username), ): if auth_injectables.RoleVerification( required_role=users_models.Role.ADMIN, verify=False @@ -283,7 +283,7 @@ def request_persistent_session( ), db: orm.Session = fastapi.Depends(database.get_db), operator: k8s.KubernetesOperator = fastapi.Depends(operators.get_operator), - username=fastapi.Depends(auth_injectables.get_username), + username: str = fastapi.Depends(auth_injectables.get_username), ): log.info("Starting persistent session for user %s", user.name) @@ -604,7 +604,7 @@ def get_sessions_for_user( users_injectables.get_own_user ), db: orm.Session = fastapi.Depends(database.get_db), - username=fastapi.Depends(auth_injectables.get_username), + username: str = fastapi.Depends(auth_injectables.get_username), ): if user != current_user and not auth_injectables.RoleVerification( required_role=users_models.Role.ADMIN, verify=False diff --git a/backend/capellacollab/users/injectables.py b/backend/capellacollab/users/injectables.py index 876bf861f..698dfc97f 100644 --- a/backend/capellacollab/users/injectables.py +++ b/backend/capellacollab/users/injectables.py @@ -14,7 +14,7 @@ def get_own_user( db: orm.Session = fastapi.Depends(database.get_db), - username=fastapi.Depends(auth_injectables.get_username), + username: str = fastapi.Depends(auth_injectables.get_username), ) -> models.DatabaseUser: if user := crud.get_user_by_name(db, username): return user @@ -25,7 +25,7 @@ def get_own_user( def get_existing_user( user_id: int | t.Literal["current"], db=fastapi.Depends(database.get_db), - username=fastapi.Depends(auth_injectables.get_username), + username: str = fastapi.Depends(auth_injectables.get_username), ) -> models.DatabaseUser: if user_id == "current": return get_own_user(db, username) diff --git a/backend/capellacollab/users/models.py b/backend/capellacollab/users/models.py index ddbd865c6..9257f06ae 100644 --- a/backend/capellacollab/users/models.py +++ b/backend/capellacollab/users/models.py @@ -16,6 +16,7 @@ from capellacollab.projects.users.models import ProjectUserAssociation from capellacollab.sessions.models import DatabaseSession from capellacollab.users.events.models import DatabaseUserHistoryEvent + from capellacollab.users.tokens.models import DatabaseUserToken class Role(enum.Enum): @@ -64,3 +65,7 @@ class DatabaseUser(database.Base): events: orm.Mapped[list[DatabaseUserHistoryEvent]] = orm.relationship( back_populates="user", foreign_keys="DatabaseUserHistoryEvent.user_id" ) + + tokens: orm.Mapped[list[DatabaseUserToken]] = orm.relationship( + back_populates="user", cascade="all, delete-orphan" + ) diff --git a/backend/capellacollab/users/routes.py b/backend/capellacollab/users/routes.py index d799b6e62..1839a76ee 100644 --- a/backend/capellacollab/users/routes.py +++ b/backend/capellacollab/users/routes.py @@ -136,4 +136,6 @@ def delete_user( router.include_router(session_routes.users_router, tags=["Users - Sessions"]) router.include_router(events_routes.router, tags=["Users - History"]) -router.include_router(tokens_routes.router, tags=["Users - Token"]) +router.include_router( + tokens_routes.router, prefix="/current/tokens", tags=["Users - Token"] +) diff --git a/backend/capellacollab/users/tokens/crud.py b/backend/capellacollab/users/tokens/crud.py index ad568f40e..555b25bdf 100644 --- a/backend/capellacollab/users/tokens/crud.py +++ b/backend/capellacollab/users/tokens/crud.py @@ -14,43 +14,49 @@ def create_token( - db: orm.Session, user_id: int, description: str -) -> tuple[models.DatabaseUserTokenModel, str]: - password = credentials.generate_password(32) + db: orm.Session, + user_id: int, + description: str, + expiration_date: datetime.date | None, + source: str | None, +) -> tuple[models.DatabaseUserToken, str]: + password = "collabmanager_" + credentials.generate_password(32) ph = argon2.PasswordHasher() - token_data = models.DatabaseUserTokenModel( + if not expiration_date: + expiration_date = datetime.date.today() + datetime.timedelta(days=30) + db_token = models.DatabaseUserToken( user_id=user_id, hash=ph.hash(password), - expiration_date=datetime.datetime.now() + datetime.timedelta(days=30), + expiration_date=expiration_date, description=description, + source=source, ) - db.add(token_data) + db.add(db_token) db.commit() - return token_data, password + return db_token, password -def get_token( +def get_token_by_token_and_user( db: orm.Session, password: str, user_id: int -) -> models.DatabaseUserTokenModel | None: +) -> models.DatabaseUserToken | None: ph = argon2.PasswordHasher() - token_list = get_token_by_user(db, user_id) - if token_list: - for token in token_list: - try: - ph.verify(token.hash, password) - return token - except argon2.exceptions.VerifyMismatchError: - pass + + for token in get_token_by_user(db, user_id): + try: + ph.verify(token.hash, password) + return token + except argon2.exceptions.VerifyMismatchError: + pass return None def get_token_by_user( db: orm.Session, user_id: int -) -> abc.Sequence[models.DatabaseUserTokenModel] | None: +) -> abc.Sequence[models.DatabaseUserToken]: return ( db.execute( - sa.select(models.DatabaseUserTokenModel).where( - models.DatabaseUserTokenModel.user_id == user_id + sa.select(models.DatabaseUserToken).where( + models.DatabaseUserToken.user_id == user_id ) ) .scalars() @@ -58,8 +64,18 @@ def get_token_by_user( ) +def get_token_by_user_and_id( + db: orm.Session, user_id: int, token_id: int +) -> models.DatabaseUserToken | None: + return db.execute( + sa.select(models.DatabaseUserToken) + .where(models.DatabaseUserToken.user_id == user_id) + .where(models.DatabaseUserToken.id == token_id) + ).scalar_one_or_none() + + def delete_token( - db: orm.Session, existing_token: models.DatabaseUserTokenModel -) -> models.DatabaseUserTokenModel: + db: orm.Session, existing_token: models.DatabaseUserToken +) -> None: db.delete(existing_token) db.commit() diff --git a/backend/capellacollab/users/tokens/injectables.py b/backend/capellacollab/users/tokens/injectables.py new file mode 100644 index 000000000..7daa64507 --- /dev/null +++ b/backend/capellacollab/users/tokens/injectables.py @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors +# SPDX-License-Identifier: Apache-2.0 + +from collections import abc + +import fastapi +from fastapi import status +from sqlalchemy import orm + +from capellacollab.core import database +from capellacollab.users import injectables as user_injectables +from capellacollab.users import models as users_models + +from . import crud, models + + +def get_own_user_tokens( + db: orm.Session = fastapi.Depends(database.get_db), + user: users_models.DatabaseUser = fastapi.Depends( + user_injectables.get_own_user + ), +) -> abc.Sequence[models.DatabaseUserToken]: + return crud.get_token_by_user(db, user.id) + + +def get_exisiting_own_user_token( + token_id: int, + db: orm.Session = fastapi.Depends(database.get_db), + user: users_models.DatabaseUser = fastapi.Depends( + user_injectables.get_own_user + ), +) -> models.DatabaseUserToken: + token = crud.get_token_by_user_and_id(db, user.id, token_id) + if not token: + raise fastapi.HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Token not found" + ) + return token diff --git a/backend/capellacollab/users/tokens/models.py b/backend/capellacollab/users/tokens/models.py index ccb2f3eca..83a222f9e 100644 --- a/backend/capellacollab/users/tokens/models.py +++ b/backend/capellacollab/users/tokens/models.py @@ -1,21 +1,50 @@ # SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors # SPDX-License-Identifier: Apache-2.0 - import datetime +import typing as t +import pydantic import sqlalchemy as sa from sqlalchemy import orm from capellacollab.core import database +if t.TYPE_CHECKING: + from capellacollab.users.models import DatabaseUser + + +class UserToken(pydantic.BaseModel): + model_config = pydantic.ConfigDict(from_attributes=True) + + id: int + user_id: int + hash: str + expiration_date: datetime.date + description: str + source: str + -class DatabaseUserTokenModel(database.Base): +class UserTokenWithPassword(UserToken): + password: str + + +class PostToken(pydantic.BaseModel): + expiration_date: datetime.datetime + description: str + source: str + + +class DatabaseUserToken(database.Base): __tablename__ = "basic_auth_token" id: orm.Mapped[int] = orm.mapped_column( primary_key=True, index=True, autoincrement=True ) user_id: orm.Mapped[int] = orm.mapped_column(sa.ForeignKey("users.id")) + user: orm.Mapped["DatabaseUser"] = orm.relationship( + back_populates="tokens", foreign_keys=[user_id] + ) hash: orm.Mapped[str] expiration_date: orm.Mapped[datetime.date] description: orm.Mapped[str] + source: orm.Mapped[str] diff --git a/backend/capellacollab/users/tokens/routes.py b/backend/capellacollab/users/tokens/routes.py index a9dbfb937..7112a0990 100644 --- a/backend/capellacollab/users/tokens/routes.py +++ b/backend/capellacollab/users/tokens/routes.py @@ -1,6 +1,8 @@ # SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors # SPDX-License-Identifier: Apache-2.0 +from collections import abc + import fastapi from sqlalchemy import orm @@ -9,7 +11,7 @@ from capellacollab.users import injectables as user_injectables from capellacollab.users import models as users_models -from . import crud +from . import crud, injectables, models router = fastapi.APIRouter( dependencies=[ @@ -22,38 +24,46 @@ ) -@router.post("/current/token") +@router.post("", response_model=models.UserTokenWithPassword) def create_token_for_user( + post_token: models.PostToken, user: users_models.DatabaseUser = fastapi.Depends( user_injectables.get_own_user ), db: orm.Session = fastapi.Depends(database.get_db), - description: str = fastapi.Body(), -): - _, password = crud.create_token(db, user.id, description) - return password +) -> models.UserTokenWithPassword: + token, password = crud.create_token( + db, + user.id, + post_token.description, + post_token.expiration_date, + post_token.source, + ) + return models.UserTokenWithPassword( + id=token.id, + user_id=token.user_id, + hash=token.hash, + expiration_date=token.expiration_date, + description=token.description, + source=token.source, + password=password, + ) -@router.get("/current/tokens") -def get_all_token_of_user( - user: users_models.DatabaseUser = fastapi.Depends( - user_injectables.get_own_user +@router.get("", response_model=list[models.UserToken]) +def get_all_tokens_of_user( + token_list: abc.Sequence[models.DatabaseUserToken] = fastapi.Depends( + injectables.get_own_user_tokens ), - db: orm.Session = fastapi.Depends(database.get_db), -): - return crud.get_token_by_user(db, user.id) +) -> abc.Sequence[models.DatabaseUserToken]: + return token_list -@router.delete("/current/token/{id}") +@router.delete("/{token_id}", status_code=204) def delete_token_for_user( - id: int, - user: users_models.DatabaseUser = fastapi.Depends( - user_injectables.get_own_user + token: models.DatabaseUserToken = fastapi.Depends( + injectables.get_exisiting_own_user_token ), db: orm.Session = fastapi.Depends(database.get_db), -): - token_list = crud.get_token_by_user(db, user.id) - if token_list: - token = [token for token in token_list if token.id == id][0] - return crud.delete_token(db, token) - return None +) -> None: + return crud.delete_token(db, token) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 43b63c25d..621c92ed8 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -131,6 +131,7 @@ module = [ "uvicorn.*", "alembic.*", "jwt.*", + "argon2.*", ] ignore_missing_imports = true diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index d2145a899..533bcd5f7 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -73,7 +73,7 @@ def fixture_executor_name(monkeypatch: pytest.MonkeyPatch) -> str: name = str(uuid1()) async def bearer_passthrough(self, request: fastapi.Request): - return name, None + return name monkeypatch.setattr(JWTBearer, "__call__", bearer_passthrough) @@ -99,20 +99,6 @@ def get_mock_own_user(): del app.dependency_overrides[users_injectables.get_own_user] -@pytest.fixture(name="unauthenticated_user") -def fixture_unauthenticated_user(db): - user = users_crud.create_user(db, str(uuid1()), users_models.Role.USER) - - def get_mock_own_user(): - return user - - app.dependency_overrides[ - users_injectables.get_own_user - ] = get_mock_own_user - yield user - del app.dependency_overrides[users_injectables.get_own_user] - - @pytest.fixture(name="project") def fixture_project(db: orm.Session) -> projects_models.DatabaseProject: return projects_crud.create_project(db, str(uuid1())) diff --git a/backend/tests/projects/test_projects_routes.py b/backend/tests/projects/test_projects_routes.py index a09d2018e..165128fe8 100644 --- a/backend/tests/projects/test_projects_routes.py +++ b/backend/tests/projects/test_projects_routes.py @@ -16,7 +16,7 @@ def test_get_projects_not_authenticated(client: testclient.TestClient): response = client.get("/api/v1/projects") - assert response.status_code == 401 + assert response.status_code == 403 assert response.json() == {"detail": "Not authenticated"} diff --git a/backend/tests/sessions/test_sessions_routes.py b/backend/tests/sessions/test_sessions_routes.py index 2bd715b6c..1b81017e2 100644 --- a/backend/tests/sessions/test_sessions_routes.py +++ b/backend/tests/sessions/test_sessions_routes.py @@ -165,7 +165,7 @@ def get_mock_own_user(): def test_get_sessions_not_authenticated(client): response = client.get("/api/v1/sessions") - assert response.status_code == 401 + assert response.status_code == 403 assert response.json() == {"detail": "Not authenticated"} diff --git a/backend/tests/settings/test_alerts.py b/backend/tests/settings/test_alerts.py index 207f1f708..5d41c2997 100644 --- a/backend/tests/settings/test_alerts.py +++ b/backend/tests/settings/test_alerts.py @@ -47,7 +47,7 @@ def test_create_alert_not_authenticated(client: TestClient): json={"title": "test", "message": "test", "level": "success"}, ) - assert response.status_code == 401 + assert response.status_code == 403 assert response.json() == {"detail": "Not authenticated"} diff --git a/backend/tests/users/conftest.py b/backend/tests/users/conftest.py new file mode 100644 index 000000000..c4a620722 --- /dev/null +++ b/backend/tests/users/conftest.py @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors +# SPDX-License-Identifier: Apache-2.0 + +from uuid import uuid1 + +import pytest + +import capellacollab.users.crud as users_crud +from capellacollab.__main__ import app +from capellacollab.users import injectables as users_injectables +from capellacollab.users import models as users_models + + +@pytest.fixture(name="unauthenticated_user") +def fixture_unauthenticated_user(db): + user = users_crud.create_user(db, str(uuid1()), users_models.Role.USER) + + def get_mock_own_user(): + return user + + app.dependency_overrides[ + users_injectables.get_own_user + ] = get_mock_own_user + yield user + del app.dependency_overrides[users_injectables.get_own_user] diff --git a/backend/tests/users/test_tokens.py b/backend/tests/users/test_tokens.py index 30d92eeca..024bbc67b 100644 --- a/backend/tests/users/test_tokens.py +++ b/backend/tests/users/test_tokens.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 import base64 +import datetime import json import fastapi @@ -11,88 +12,89 @@ from capellacollab.__main__ import app from capellacollab.core.authentication.basic_auth import HTTPBasicAuth -from capellacollab.users import models as user_models +from capellacollab.users import models as users_models +from capellacollab.users.tokens import models as tokens_models + +POST_TOKEN = { + "expiration_date": str(datetime.datetime.now()), + "description": "test_token", + "source": "test source", +} @responses.activate @pytest.mark.usefixtures("user") -def test_get_user_token(client: testclient.TestClient): - response = client.post( - f"/api/v1/users/current/token", json="my_test_description" - ) +def test_create_user_token(client: testclient.TestClient): + response = client.post("/api/v1/users/current/tokens", json=POST_TOKEN) assert response.status_code == 200 @responses.activate @pytest.mark.usefixtures("user") def test_get_user_tokens(client: testclient.TestClient): - response = client.get(f"/api/v1/users/current/tokens") + response = client.get("/api/v1/users/current/tokens") assert response.status_code == 200 @responses.activate def test_use_basic_token( client: testclient.TestClient, - unauthenticated_user: user_models.User, + unauthenticated_user: users_models.User, monkeypatch: pytest.MonkeyPatch, ): async def basic_passthrough(self, request: fastapi.Request): - return unauthenticated_user.name, None + return unauthenticated_user.name monkeypatch.setattr(HTTPBasicAuth, "__call__", basic_passthrough) - token_string = unauthenticated_user.name + ":" + "myTestPassword" + token_string = f"{unauthenticated_user.name}:myTestPassword" token = base64.b64encode(token_string.encode("ascii")) basic_response = client.post( - f"/api/v1/users/current/token", + "/api/v1/users/current/tokens", headers={"Authorization": f"basic {token.decode('ascii')}"}, - json="my_test_description2", + json=POST_TOKEN, ) assert basic_response.status_code == 200 @responses.activate -def test_use_wrong_basic_token(unauthenticated_user: user_models.User): - token_string = unauthenticated_user.name + ":" + "testPassword" +def test_use_wrong_basic_token(unauthenticated_user: users_models.User): + token_string = f"{unauthenticated_user.name}:myTestPassword" token = base64.b64encode(token_string.encode("ascii")) basic_client = testclient.TestClient(app) basic_response = basic_client.post( - f"/api/v1/users/current/token", + "/api/v1/users/current/tokens", headers={"Authorization": f"basic {token.decode('ascii')}"}, - json="my_test_description", + json=POST_TOKEN, ) assert basic_response.status_code == 401 @responses.activate def test_create_and_delete_token( - client: testclient.TestClient, user: user_models.User + client: testclient.TestClient, user: users_models.User ): - response = client.post( - f"/api/v1/users/current/token", json="my_test_description" - ) + response = client.post("/api/v1/users/current/tokens", json=POST_TOKEN) assert response.status_code == 200 - response = client.delete(f"/api/v1/users/current/token/1") - assert response.status_code == 200 + response = client.delete("/api/v1/users/current/tokens/1") + assert response.status_code == 204 @responses.activate def test_token_lifecycle( - client: testclient.TestClient, user: user_models.User + client: testclient.TestClient, user: users_models.User ): - response = client.post( - f"/api/v1/users/current/token", json="my_test_description" - ) + response = client.post("/api/v1/users/current/tokens", json=POST_TOKEN) assert response.status_code == 200 - response = client.get(f"/api/v1/users/current/tokens") + response = client.get("/api/v1/users/current/tokens") response_string = response.content.decode("utf-8") assert len(json.loads(response_string)) == 1 - response = client.delete(f"/api/v1/users/current/token/1") - assert response.status_code == 200 + response = client.delete("/api/v1/users/current/tokens/1") + assert response.status_code == 204 - response = client.get(f"/api/v1/users/current/tokens") + response = client.get("/api/v1/users/current/tokens") response_string = response.content.decode("utf-8") assert len(json.loads(response_string)) == 0 diff --git a/docs/user/docs/tokens.md b/docs/user/docs/tokens.md index ce605575f..ab04022ed 100644 --- a/docs/user/docs/tokens.md +++ b/docs/user/docs/tokens.md @@ -3,53 +3,59 @@ ~ SPDX-License-Identifier: Apache-2.0 --> -# Token Authentication +# Authentication with Personal Access Tokens (PAT) To authenticate against the API you can either use a Bearer token (which the -browser usually uses) or authenticate with a longer lived basic authentication -token which can be used e.g. in scripts. +browser usually uses) or authenticate with a longer lived personal access token +(PAT) which can be used e.g. in scripts. -## Token Creation +## PAT Creation -To create a Token you can go to Profile > Token and insert a short description -to create a token. +To create a personal access token (PAT) you can go to Profile > Token and +insert a short description and pick a date until when the token should be +valid. - - -!!! info The password which is generated can only be copied. Make sure you save -it - you won't be able to access it again - -## Token Scope + +!!! info + The password which is generated can only be copied once. Make sure you save + it - you won't be able to access it again -Basic authentication token have the same scope as the bearer token. +## PAT Scope - -!!! warning - As currently Basic Authentication token have the same scope as you logged in your browser they can act as your user. Please do not share the password and if the information gets lost please revoke the token as soon as possible. +Personal access token have almost (not for requesting oauth or azure tokens) +the same scope as the user who created it. It is therefore important that you +never pass on the token and treat it responsibly. If you feel that the token +has been lost, revoke it immediately and inform the Systems Engineering +Toolchain team. -## Revoke a Token +## Revoke a PAT In order to revoke a token go to Profile > Token. There you can see a list of -all tokens that belong to your user. Clicking on the trash symbol you can -delete a token which will no longer be valid to authenticate. +all tokens that belong to your user. By clicking on the trash symbol, you can +delete a token, which will no longer be valid for authentication. -## Token Usage +## PAT Usage -The token created is a basic authentication token. There are different ways to +The token created is a personal access token. There are different ways to authenticate with that against the Collaboration Manager API. One example is: ```zsh -curl --basic -u yourUsername:yourPassword https://baseurl/api/v1/users/current/tokens +curl --basic -u : https:///api/v1/users/current/tokens ``` -or to work with the diagram cache +or to work with the diagram cache of Capellambse. Capellambse is an +implementation of the capella modelling tool using python and lets you read and +write models. For more information have a look at the +[documentation](https://dsd-dbs.github.io/py-capellambse/) or the +[github repository](https://github.com/DSD-DBS/py-capellambse). ```python capellambse.model.MelodyModel( - path="path to the model on your machine", - diagram_cache={ - "path": "https://baseurl/api/projects/[yourProjectSlug]/[yourModelName]/diagrams/%s", - "username": "yourUsername", - "password": "yourPassword", - }) + path="path to the model on your machine", + diagram_cache={ + "path": "https:///api/projects/[yourProjectSlug]/[yourModelSlug]/diagrams/%s", + "username": "", + "password": "", + } +) ``` diff --git a/docs/user/mkdocs.yml b/docs/user/mkdocs.yml index 44adfae10..38d9f8f31 100644 --- a/docs/user/mkdocs.yml +++ b/docs/user/mkdocs.yml @@ -10,7 +10,6 @@ theme: nav: - Introduction: index.md - Installation: installation.md - - Authentication: tokens.md - Projects: - Get access to a project: projects/access.md - Add a user to a project: projects/add-user.md @@ -68,6 +67,7 @@ nav: - Update a TeamForCapella based model: tools/capella/teamforcapella/update.md - Git: - Working with Git: tools/capella/working-with-git.md + - Authentication: tokens.md - Release Notes: release-notes.md - About: about.md diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 230790f8c..5dc705859 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -5,7 +5,6 @@ import { NgModule } from '@angular/core'; import { Data, RouterModule, Routes } from '@angular/router'; -import { BasicAuthTokenComponent } from 'src/app/general/auth/basic-auth-token/basic-auth-token.component'; import { JobRunOverviewComponent } from 'src/app/projects/models/backup-settings/job-run-overview/job-run-overview.component'; import { PipelineRunWrapperComponent } from 'src/app/projects/models/backup-settings/pipeline-runs/wrapper/pipeline-run-wrapper/pipeline-run-wrapper.component'; import { ViewLogsDialogComponent } from 'src/app/projects/models/backup-settings/view-logs-dialog/view-logs-dialog.component'; @@ -14,6 +13,7 @@ import { ModelRestrictionsComponent } from 'src/app/projects/models/model-restri import { EditProjectMetadataComponent } from 'src/app/projects/project-detail/edit-project-metadata/edit-project-metadata.component'; import { SessionComponent } from 'src/app/sessions/session/session.component'; import { PipelinesOverviewComponent } from 'src/app/settings/core/pipelines-overview/pipelines-overview.component'; +import { BasicAuthTokenComponent } from 'src/app/users/basic-auth-token/basic-auth-token.component'; import { EventsComponent } from './events/events.component'; import { AuthComponent } from './general/auth/auth/auth.component'; import { AuthGuardService } from './general/auth/auth-guard/auth-guard.service'; @@ -444,7 +444,7 @@ const routes: Routes = [ }, { path: 'tokens', - data: { breadcrumb: 'tokens' }, + data: { breadcrumb: 'Tokens' }, component: BasicAuthTokenComponent, }, ], diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 45ebb7c1d..b49d28613 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -14,6 +14,8 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatBadgeModule } from '@angular/material/badge'; import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { MatRippleModule } from '@angular/material/core'; +import { MatNativeDateModule } from '@angular/material/core'; +import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatExpansionModule } from '@angular/material/expansion'; import { MatIconModule } from '@angular/material/icon'; @@ -44,7 +46,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { CookieModule } from 'ngx-cookie'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { ToastrModule } from 'ngx-toastr'; -import { BasicAuthTokenComponent } from 'src/app/general/auth/basic-auth-token/basic-auth-token.component'; +import { BasicAuthTokenComponent } from 'src/app/users/basic-auth-token/basic-auth-token.component'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { EventsComponent } from './events/events.component'; @@ -238,6 +240,7 @@ import { SettingsComponent } from './settings/settings.component'; MatButtonToggleModule, MatCardModule, MatCheckboxModule, + MatDatepickerModule, MatDialogModule, MatExpansionModule, MatFormFieldModule, @@ -245,6 +248,7 @@ import { SettingsComponent } from './settings/settings.component'; MatInputModule, MatListModule, MatMenuModule, + MatNativeDateModule, MatPaginatorModule, MatProgressBarModule, MatProgressSpinnerModule, diff --git a/frontend/src/app/general/auth/basic-auth-token/basic-auth-token.component.html b/frontend/src/app/general/auth/basic-auth-token/basic-auth-token.component.html deleted file mode 100644 index ff78b17ec..000000000 --- a/frontend/src/app/general/auth/basic-auth-token/basic-auth-token.component.html +++ /dev/null @@ -1,78 +0,0 @@ - - -
-

Basic Authentication Token

-
-
-
- - -
-
- - Generated Token: - - {{ - password - }} - - -
- Make sure you save the token - you won't be able to access it again. -
-
-

Token overview

- - - - Description: {{ token.description }}
- Expiration date: {{ token.expiration_date | date }} -
- warning - This token has expired! -
-
- -
-
-
diff --git a/frontend/src/app/general/auth/basic-auth-token/basic-auth-token.component.ts b/frontend/src/app/general/auth/basic-auth-token/basic-auth-token.component.ts deleted file mode 100644 index 8fc3b028a..000000000 --- a/frontend/src/app/general/auth/basic-auth-token/basic-auth-token.component.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors - * SPDX-License-Identifier: Apache-2.0 - */ -import { Component, OnInit } from '@angular/core'; -import { - TokenService, - Token, -} from 'src/app/general/auth/basic-auth-service/basic-auth-token.service'; -import { ToastService } from 'src/app/helpers/toast/toast.service'; - -@Component({ - selector: 'app-token-settings', - templateUrl: './basic-auth-token.component.html', - styleUrls: ['./basic-auth-token.component.css'], -}) -export class BasicAuthTokenComponent implements OnInit { - tokenDescription: string = ''; - password?: string; - passwordRevealed = false; - constructor( - public tokenService: TokenService, - private toastService: ToastService - ) {} - - ngOnInit() { - this.tokenService.loadTokens(); - } - - createNewToken(description?: string) { - if (!description) { - description = this.tokenDescription; - } - description = 'Token-overview-' + description; - this.tokenService - .createToken(description) - .subscribe((token) => (this.password = token.replaceAll('"', ''))); - this.tokenDescription = ''; - } - - deleteToken(token: Token) { - this.tokenService.deleteToken(token).subscribe(); - } - - isTokenExpired(expirationDate: string): boolean { - const expirationDateObj = new Date(expirationDate); - return expirationDateObj < new Date(); - } - - showClipboardMessage(): void { - this.toastService.showSuccess( - 'Token copied', - 'The token was copied to your clipboard.' - ); - } -} diff --git a/frontend/src/app/general/nav-bar-menu/nav-bar-menu.component.html b/frontend/src/app/general/nav-bar-menu/nav-bar-menu.component.html index 3b9985035..3e9c89f97 100644 --- a/frontend/src/app/general/nav-bar-menu/nav-bar-menu.component.html +++ b/frontend/src/app/general/nav-bar-menu/nav-bar-menu.component.html @@ -47,9 +47,9 @@ >Events - Tokens + + Tokens + ( - undefined - ); + private _tokens = new BehaviorSubject(undefined); readonly tokens$ = this._tokens.asObservable(); @@ -27,21 +25,27 @@ export class TokenService { }); } - createToken(tokenDescription: string): Observable { - const password = this.http - .post( - environment.backend_url + `/users/current/token`, - tokenDescription + createToken( + description: string, + expiration_date: Date, + source: string + ): Observable { + return this.http + .post( + environment.backend_url + `/users/current/tokens`, + { + description, + expiration_date, + source, + } ) .pipe(tap(() => this.loadTokens())); - - return password; } - deleteToken(token: Token): Observable { + deleteToken(token: Token): Observable { return this.http - .delete( - environment.backend_url + `/users/current/token/${token.id}` + .delete( + environment.backend_url + `/users/current/tokens/${token.id}` ) .pipe(tap(() => this.loadTokens())); } @@ -50,5 +54,10 @@ export class TokenService { export type Token = { description: string; expiration_date: string; + source: string; id: number; }; + +export type CreateTokenResponse = Token & { + password: string; +}; diff --git a/frontend/src/app/general/auth/basic-auth-token/basic-auth-token.component.css b/frontend/src/app/users/basic-auth-token/basic-auth-token.component.css similarity index 54% rename from frontend/src/app/general/auth/basic-auth-token/basic-auth-token.component.css rename to frontend/src/app/users/basic-auth-token/basic-auth-token.component.css index d49deaffd..30302f39f 100644 --- a/frontend/src/app/general/auth/basic-auth-token/basic-auth-token.component.css +++ b/frontend/src/app/users/basic-auth-token/basic-auth-token.component.css @@ -2,3 +2,10 @@ * SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors * SPDX-License-Identifier: Apache-2.0 */ + +.border { + margin-right: 5px; + padding: 5px; + border: 2px solid var(--primary-color); + border-radius: 5px; +} diff --git a/frontend/src/app/users/basic-auth-token/basic-auth-token.component.html b/frontend/src/app/users/basic-auth-token/basic-auth-token.component.html new file mode 100644 index 000000000..375badb04 --- /dev/null +++ b/frontend/src/app/users/basic-auth-token/basic-auth-token.component.html @@ -0,0 +1,108 @@ + + +
+

Personal Access Tokens

+ To create a Personal Access Token please choose an expiration date and provide + a short description. +
+ + Token description + + Note: The created token has the same permissions as you have when + being logged in. + +
+ + Choose an expiration date + + MM/DD/YYYY + + + +
+ +
+
+ + Generated Token: + + {{ + password + }} + + + +
+ Make sure you save the token - you won't be able to access it again. +
+
+

Token overview

+ + + + Description: {{ token.description }}
+ Expiration date: {{ token.expiration_date | date }}
+ Creation location: {{ token.source }} +
+ warning + This token has expired! +
+
+ +
+
+ No token created for your user yet. +
diff --git a/frontend/src/app/users/basic-auth-token/basic-auth-token.component.ts b/frontend/src/app/users/basic-auth-token/basic-auth-token.component.ts new file mode 100644 index 000000000..654d23af5 --- /dev/null +++ b/frontend/src/app/users/basic-auth-token/basic-auth-token.component.ts @@ -0,0 +1,84 @@ +/* + * SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, Validators } from '@angular/forms'; +import { ToastService } from 'src/app/helpers/toast/toast.service'; +import { + TokenService, + Token, +} from 'src/app/users/basic-auth-service/basic-auth-token.service'; + +@Component({ + selector: 'app-token-settings', + templateUrl: './basic-auth-token.component.html', + styleUrls: ['./basic-auth-token.component.css'], +}) +export class BasicAuthTokenComponent implements OnInit { + password?: string; + passwordRevealed = false; + minDate: Date; + maxDate: Date; + + tokenForm = this.formBuilder.group({ + description: ['', [Validators.required, Validators.minLength(1)]], + date: [this.getTomorrow(), [Validators.required]], + }); + constructor( + public tokenService: TokenService, + private toastService: ToastService, + private formBuilder: FormBuilder + ) { + this.minDate = this.getTomorrow(); + this.maxDate = new Date( + this.minDate.getFullYear() + 1, + this.minDate.getMonth(), + this.minDate.getDate() + ); + } + + ngOnInit() { + this.tokenService.loadTokens(); + } + + getTomorrow(): Date { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + return tomorrow; + } + + createToken(): void { + if (this.tokenForm.valid) { + this.tokenService + .createToken( + this.tokenForm.value.description!, + this.tokenForm.value.date!, + 'Token-overview' + ) + .subscribe((token) => { + this.password = token.password; + this.tokenForm.controls.date.setValue(this.getTomorrow()); + }); + } + } + + deleteToken(token: Token) { + this.tokenService.deleteToken(token).subscribe(); + this.toastService.showSuccess( + 'Token deleted', + `The token ${token.description} was successfully deleted!` + ); + } + + isTokenExpired(expirationDate: string): boolean { + return new Date(expirationDate) < new Date(); + } + + showClipboardMessage(): void { + this.toastService.showSuccess( + 'Token copied', + 'The token was copied to your clipboard.' + ); + } +}