token CRUD

This commit is contained in:
hay-kot 2021-05-06 10:52:01 -08:00
commit 3c051c93db
8 changed files with 282 additions and 17 deletions

View file

@ -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");
}
};

View file

@ -0,0 +1,147 @@
<template>
<StatCard icon="mdi-api" color="accent">
<template v-slot:after-heading>
<div class="ml-auto text-right">
<div class="body-3 grey--text font-weight-light" v-text="'API Tokens'" />
<h3 class="display-2 font-weight-light text--primary">
<small> {{ user.tokens.length }} </small>
</h3>
</div>
</template>
<template v-slot:bottom>
<v-subheader class="mb-n2">ACTIVE TOKENS</v-subheader>
<v-virtual-scroll height="210" item-height="70" :items="user.tokens" class="mt-2">
<template v-slot:default="{ item }">
<v-divider></v-divider>
<v-list-item @click.prevent>
<v-list-item-avatar>
<v-icon large dark color="accent">
mdi-api
</v-icon>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title v-text="item.name"></v-list-item-title>
</v-list-item-content>
<v-list-item-action class="ml-auto">
<v-btn large icon @click.stop="deleteToken(item.id)">
<v-icon color="accent">mdi-delete</v-icon>
</v-btn>
</v-list-item-action>
</v-list-item>
<v-divider></v-divider>
</template>
</v-virtual-scroll>
<v-divider></v-divider>
<v-card-actions class="pb-1 pt-3">
<v-spacer></v-spacer>
<BaseDialog
:title="'Create an API Token'"
title-icon="mdi-api"
@submit="createToken"
:submit-text="buttonText"
:loading="loading"
>
<v-card-text>
<v-form ref="newTokenForm">
<v-text-field v-model="name" label="Token Name" required> </v-text-field>
</v-form>
<div v-if="createdToken != ''">
<v-textarea
class="mb-0 pb-0"
label="API Token"
read
v-model="createdToken"
append-outer-icon="mdi-content-copy"
@click:append-outer="copyToken"
>
</v-textarea>
<v-subheader class="text-center">
Copy this token for use with an external application. This token will not be viewable again.
</v-subheader>
</div>
</v-card-text>
<template v-slot:open="{ open }">
<v-btn color="success" @click="open">
<v-icon left> mdi-plus </v-icon>
{{ $t("general.create") }}
</v-btn>
</template>
</BaseDialog>
</v-card-actions>
</template>
</StatCard>
</template>
<script>
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
import StatCard from "@/components/UI/StatCard";
import { api } from "@/api";
import { validators } from "@/mixins/validators";
import { initials } from "@/mixins/initials";
export default {
components: {
BaseDialog,
StatCard,
},
mixins: [validators, initials],
data() {
return {
name: "",
loading: false,
createdToken: "",
};
},
mounted() {
this.$store.dispatch("requestUserData");
},
computed: {
user() {
return this.$store.getters.getUserData;
},
buttonText() {
if (this.createdToken === "") {
return "Create";
} else {
return "Close";
}
},
},
methods: {
async createToken() {
if (this.loading === true) {
this.loading = false;
this.$store.dispatch("requestUserData");
this.createdToken = "";
this.name = "";
return;
}
this.loading = true;
if (this.$refs.newTokenForm.validate()) {
const response = await api.users.createAPIToken(this.name);
this.createdToken = response.token;
}
},
async deleteToken(id) {
await api.users.deleteAPIToken(id);
this.$store.dispatch("requestUserData");
},
copyToken() {
const copyText = this.createdToken;
navigator.clipboard.writeText(copyText).then(
() => console.log("Copied", copyText),
() => console.log("Copied Failed", copyText)
);
},
},
};
</script>
<style></style>

View file

@ -7,6 +7,7 @@
</v-col>
<v-col cols="12" sm="12" lg="6">
<ProfileGroupCard />
<APITokenCard class="mt-10" />
</v-col>
</v-row>
<v-row class="mt-7">
@ -19,12 +20,14 @@
<script>
import ProfileThemeCard from "./ProfileThemeCard";
import ProfileGroupCard from "./ProfileGroupCard";
import APITokenCard from "./APITokenCard";
import UserCard from "./UserCard";
export default {
components: {
UserCard,
ProfileThemeCard,
ProfileGroupCard,
APITokenCard,
},
};
</script>

View file

@ -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()

View file

@ -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()

View file

@ -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)

View file

@ -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)

View file

@ -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