From 3c051c93db89f4667644a680d9f35339d1275b3a Mon Sep 17 00:00:00 2001 From: hay-kot Date: Thu, 6 May 2021 10:52:01 -0800 Subject: [PATCH] token CRUD --- frontend/src/api/users.js | 28 ++-- .../src/pages/Admin/Profile/APITokenCard.vue | 147 ++++++++++++++++++ frontend/src/pages/Admin/Profile/index.vue | 3 + mealie/db/database.py | 12 +- mealie/db/models/users.py | 25 ++- mealie/routes/users/__init__.py | 3 +- mealie/routes/users/api_tokens.py | 56 +++++++ mealie/schema/user.py | 25 +++ 8 files changed, 282 insertions(+), 17 deletions(-) create mode 100644 frontend/src/pages/Admin/Profile/APITokenCard.vue create mode 100644 mealie/routes/users/api_tokens.py diff --git a/frontend/src/api/users.js b/frontend/src/api/users.js index 11609ff29..1ded3c6e9 100644 --- a/frontend/src/api/users.js +++ b/frontend/src/api/users.js @@ -16,17 +16,10 @@ const usersURLs = { userID: id => `${userPrefix}/${id}`, password: id => `${userPrefix}/${id}/password`, resetPassword: id => `${userPrefix}/${id}/reset-password`, + userAPICreate: `${userPrefix}/api-tokens`, + userAPIDelete: id => `${userPrefix}/api-tokens/${id}`, }; -function deleteErrorText(response) { - switch (response.data.detail) { - case "SUPER_USER": - return i18n.t("user.error-cannot-delete-super-user"); - - default: - return i18n.t("user.you-are-not-allowed-to-delete-this-user"); - } -} export const userAPI = { async login(formData) { let response = await apiReq.post(authURLs.token, formData, null, function() { @@ -90,4 +83,21 @@ export const userAPI = { () => i18n.t("user.password-has-been-reset-to-the-default-password") ); }, + async createAPIToken(name) { + const response = await apiReq.post(usersURLs.userAPICreate, { name }); + return response.data; + }, + async deleteAPIToken(id) { + const response = await apiReq.delete(usersURLs.userAPIDelete(id)); + return response.data; + }, +}; + +const deleteErrorText = response => { + switch (response.data.detail) { + case "SUPER_USER": + return i18n.t("user.error-cannot-delete-super-user"); + default: + return i18n.t("user.you-are-not-allowed-to-delete-this-user"); + } }; diff --git a/frontend/src/pages/Admin/Profile/APITokenCard.vue b/frontend/src/pages/Admin/Profile/APITokenCard.vue new file mode 100644 index 000000000..c31ff7ca2 --- /dev/null +++ b/frontend/src/pages/Admin/Profile/APITokenCard.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/frontend/src/pages/Admin/Profile/index.vue b/frontend/src/pages/Admin/Profile/index.vue index 7f8c7a02b..88127c2a8 100644 --- a/frontend/src/pages/Admin/Profile/index.vue +++ b/frontend/src/pages/Admin/Profile/index.vue @@ -7,6 +7,7 @@ + @@ -19,12 +20,14 @@ diff --git a/mealie/db/database.py b/mealie/db/database.py index c15ae3320..d70fbd1e7 100644 --- a/mealie/db/database.py +++ b/mealie/db/database.py @@ -8,7 +8,7 @@ from mealie.db.models.recipe.recipe import Category, RecipeModel, Tag from mealie.db.models.settings import CustomPage, SiteSettings from mealie.db.models.sign_up import SignUp from mealie.db.models.theme import SiteThemeModel -from mealie.db.models.users import User +from mealie.db.models.users import LongLiveToken, User from mealie.schema.category import RecipeCategoryResponse, RecipeTagResponse from mealie.schema.events import Event as EventSchema from mealie.schema.meal import MealPlanInDB @@ -17,7 +17,7 @@ from mealie.schema.settings import CustomPageOut from mealie.schema.settings import SiteSettings as SiteSettingsSchema from mealie.schema.sign_up import SignUpOut from mealie.schema.theme import SiteTheme -from mealie.schema.user import GroupInDB, UserInDB +from mealie.schema.user import GroupInDB, LongLiveTokenInDB, UserInDB from sqlalchemy.orm.session import Session logger = getLogger() @@ -106,6 +106,13 @@ class _Users(BaseDocument): return self.schema.from_orm(entry) +class _LongLiveToken(BaseDocument): + def __init__(self) -> None: + self.primary_key = "id" + self.sql_model = LongLiveToken + self.schema = LongLiveTokenInDB + + class _Groups(BaseDocument): def __init__(self) -> None: self.primary_key = "id" @@ -158,6 +165,7 @@ class Database: self.categories = _Categories() self.tags = _Tags() self.users = _Users() + self.api_tokens = _LongLiveToken() self.sign_ups = _SignUps() self.groups = _Groups() self.custom_pages = _CustomPages() diff --git a/mealie/db/models/users.py b/mealie/db/models/users.py index fe62ddc0a..3945e59bb 100644 --- a/mealie/db/models/users.py +++ b/mealie/db/models/users.py @@ -3,11 +3,19 @@ from mealie.db.models.group import Group from mealie.db.models.model_base import BaseMixins, SqlAlchemyBase from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, orm -# I'm not sure this is necessasry, browser based settings may be sufficient -# class UserSettings(SqlAlchemyBase, BaseMixins): -# __tablename__ = "user_settings" -# id = Column(Integer, primary_key=True, index=True) -# parent_id = Column(String, ForeignKey("users.id")) + +class LongLiveToken(SqlAlchemyBase, BaseMixins): + __tablename__ = "long_live_tokens" + id = Column(Integer, primary_key=True) + parent_id = Column(Integer, ForeignKey("users.id")) + name = Column(String) + token = Column(String, unique=True, nullable=False) + user = orm.relationship("User") + + def __init__(self, session, name, token, parent_id) -> None: + self.name = name + self.token = token + self.user = User.get_ref(session, parent_id) class User(SqlAlchemyBase, BaseMixins): @@ -19,6 +27,9 @@ class User(SqlAlchemyBase, BaseMixins): group_id = Column(Integer, ForeignKey("groups.id")) group = orm.relationship("Group", back_populates="users") admin = Column(Boolean, default=False) + tokens: list[LongLiveToken] = orm.relationship( + LongLiveToken, back_populates="user", cascade="all, delete, delete-orphan", single_parent=True + ) def __init__( self, @@ -49,3 +60,7 @@ class User(SqlAlchemyBase, BaseMixins): def update_password(self, password): self.password = password + + @staticmethod + def get_ref(session, id: str): + return session.query(User).filter(User.id == id).one() diff --git a/mealie/routes/users/__init__.py b/mealie/routes/users/__init__.py index de249ec01..727fa4138 100644 --- a/mealie/routes/users/__init__.py +++ b/mealie/routes/users/__init__.py @@ -1,9 +1,10 @@ from fastapi import APIRouter -from . import auth, crud, sign_up +from . import api_tokens, auth, crud, sign_up user_router = APIRouter() user_router.include_router(auth.router) user_router.include_router(sign_up.router) user_router.include_router(crud.router) +user_router.include_router(api_tokens.router) diff --git a/mealie/routes/users/api_tokens.py b/mealie/routes/users/api_tokens.py new file mode 100644 index 000000000..e15431cdb --- /dev/null +++ b/mealie/routes/users/api_tokens.py @@ -0,0 +1,56 @@ +from datetime import timedelta + +from fastapi import APIRouter, HTTPException, status +from fastapi.param_functions import Depends +from mealie.core.security import create_access_token +from mealie.db.database import db +from mealie.db.db_setup import generate_session +from mealie.routes.deps import get_current_user +from mealie.schema.user import CreateToken, LoingLiveTokenIn, LongLiveTokenInDB, UserInDB +from sqlalchemy.orm.session import Session + +router = APIRouter(prefix="/api/users", tags=["User API Tokens"]) + + +@router.post("/api-tokens") +async def create_api_token( + token_name: LoingLiveTokenIn, + current_user: UserInDB = Depends(get_current_user), + session: Session = Depends(generate_session), +): + """ Create api_token in the Database """ + + token_data = {"long_token": True, "user": current_user.email} + + five_years = timedelta(1825) + token = create_access_token(token_data, five_years) + + token_model = CreateToken( + name=token_name.name, + token=token, + parent_id=current_user.id, + ) + + new_token_in_db = db.api_tokens.create(session, token_model) + + if new_token_in_db: + return {"token": token} + + +@router.delete("/api-tokens/{token_id}") +async def delete_api_token( + token_id: int, + current_user: UserInDB = Depends(get_current_user), + session: Session = Depends(generate_session), +): + """ Delete api_token from the Database """ + token: LongLiveTokenInDB = db.api_tokens.get(session, token_id) + + if not token: + raise HTTPException(status.HTTP_404_NOT_FOUND, f"Could not locate token with id '{token_id}' in database") + + if token.user.email == current_user.email: + deleted_token = db.api_tokens.delete(session, token_id) + return {"token_delete": deleted_token.name} + else: + raise HTTPException(status.HTTP_401_UNAUTHORIZED) diff --git a/mealie/schema/user.py b/mealie/schema/user.py index 78d4f831e..1a2fe988a 100644 --- a/mealie/schema/user.py +++ b/mealie/schema/user.py @@ -10,6 +10,22 @@ from pydantic.types import constr from pydantic.utils import GetterDict +class LoingLiveTokenIn(CamelModel): + name: str + + +class LongLiveTokenOut(LoingLiveTokenIn): + id: int + + class Config: + orm_mode = True + + +class CreateToken(LoingLiveTokenIn): + parent_id: int + token: str + + class ChangePassword(CamelModel): current_password: str new_password: str @@ -53,6 +69,7 @@ class UserIn(UserBase): class UserOut(UserBase): id: int group: str + tokens: Optional[list[LongLiveTokenOut]] class Config: orm_mode = True @@ -96,3 +113,11 @@ class GroupInDB(UpdateGroup): **GetterDict(orm_model), "webhook_urls": [x.url for x in orm_model.webhook_urls if x], } + + +class LongLiveTokenInDB(LoingLiveTokenIn): + id: int + user: UserInDB + + class Config: + orm_mode = True