From 695d7e96aead2989edd6529726643a01a7485777 Mon Sep 17 00:00:00 2001 From: hay-kot Date: Fri, 6 Aug 2021 16:28:12 -0800 Subject: [PATCH] refactor(frontend): :construction: Add group/user CRUD support for admins --- frontend/.eslintrc.js | 6 + frontend/api/class-interfaces/_base.ts | 18 +- frontend/api/class-interfaces/groups.ts | 26 +++ frontend/api/class-interfaces/recipes.ts | 28 +-- frontend/api/class-interfaces/users.ts | 63 +++++- frontend/api/index.ts | 3 + .../Domain/Admin/AdminBackupDialog.vue | 148 ++++++++++++ .../Domain/Admin/AdminBackupImportDialog.vue | 118 ++++++++++ .../Domain/Admin/AdminBackupImportOptions.vue | 69 ++++++ .../Domain/Admin/AdminBackupViewer.vue | 110 +++++++++ .../Domain/Admin/AdminEventViewer.vue | 110 +++++++++ .../Domain/User/UserProfileCard.vue | 159 +++++++++++++ .../components/Domain/User/UserThemeCard.vue | 211 ++++++++++++++++++ .../components/Layout/AppFloatingButton.vue | 2 +- frontend/components/Layout/AppSidebar.vue | 51 ++++- frontend/components/global/AutoForm.vue | 1 + .../global/BaseCardSectionTitle.vue | 11 +- .../components/global/BaseColorPicker.vue | 64 ++++++ frontend/components/global/BaseDialog.vue | 13 +- frontend/components/global/BaseDivider.vue | 14 +- frontend/components/global/BaseStatCard.vue | 103 +++++++++ frontend/composables/use-groups.ts | 51 +++++ frontend/composables/use-recipe-context.ts | 1 - frontend/composables/use-user.ts | 93 ++++++++ frontend/layouts/admin.vue | 48 ++++ frontend/layouts/default.vue | 3 + frontend/nuxt.config.js | 4 +- frontend/pages/admin/about.vue | 4 +- frontend/pages/admin/dashboard.vue | 100 ++++++++- .../pages/admin/manage-users/all-groups.vue | 122 ++++++++++ .../pages/admin/manage-users/all-users.vue | 178 +++++++++++++++ frontend/pages/admin/migrations.vue | 13 +- frontend/pages/admin/site-settings.vue | 13 +- frontend/pages/admin/toolbox/categories.vue | 19 ++ .../pages/admin/toolbox/notifications.vue | 21 ++ .../{toolbox.vue => toolbox/organize.vue} | 8 +- .../{manage-users.vue => toolbox/tags.vue} | 8 +- frontend/pages/recipe/_slug.vue | 5 +- frontend/pages/search.vue | 4 +- frontend/pages/user/group/index.vue | 24 ++ frontend/pages/user/group/pages.vue | 24 ++ frontend/pages/user/profile.vue | 14 +- frontend/types/api-types/recipe.ts | 4 + frontend/types/api.ts | 21 +- mealie/routes/groups/crud.py | 5 +- mealie/routes/recipe/all_recipe_routes.py | 2 +- 46 files changed, 2015 insertions(+), 102 deletions(-) create mode 100644 frontend/api/class-interfaces/groups.ts create mode 100644 frontend/components/Domain/Admin/AdminBackupDialog.vue create mode 100644 frontend/components/Domain/Admin/AdminBackupImportDialog.vue create mode 100644 frontend/components/Domain/Admin/AdminBackupImportOptions.vue create mode 100644 frontend/components/Domain/Admin/AdminBackupViewer.vue create mode 100644 frontend/components/Domain/Admin/AdminEventViewer.vue create mode 100644 frontend/components/Domain/User/UserProfileCard.vue create mode 100644 frontend/components/Domain/User/UserThemeCard.vue create mode 100644 frontend/components/global/BaseColorPicker.vue create mode 100644 frontend/components/global/BaseStatCard.vue create mode 100644 frontend/composables/use-groups.ts create mode 100644 frontend/composables/use-user.ts create mode 100644 frontend/pages/admin/manage-users/all-groups.vue create mode 100644 frontend/pages/admin/manage-users/all-users.vue create mode 100644 frontend/pages/admin/toolbox/categories.vue create mode 100644 frontend/pages/admin/toolbox/notifications.vue rename frontend/pages/admin/{toolbox.vue => toolbox/organize.vue} (64%) rename frontend/pages/admin/{manage-users.vue => toolbox/tags.vue} (65%) create mode 100644 frontend/pages/user/group/index.vue create mode 100644 frontend/pages/user/group/pages.vue diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 62d3de7aaa52..d8c49a718e06 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -21,5 +21,11 @@ module.exports = { "vue/multiline-html-element-content-newline": "off", "vue/no-mutating-props": "off", "vue/no-v-for-template-key-on-child": "off", + "vue/valid-v-slot": [ + "error", + { + allowModifiers: true, + }, + ], }, }; diff --git a/frontend/api/class-interfaces/_base.ts b/frontend/api/class-interfaces/_base.ts index 7e2f2ebf0781..ce034f70e8c2 100644 --- a/frontend/api/class-interfaces/_base.ts +++ b/frontend/api/class-interfaces/_base.ts @@ -5,7 +5,7 @@ export interface CrudAPIInterface { // Route Properties / Methods baseRoute: string; - itemRoute(itemId: string): string; + itemRoute(itemId: string | number): string; // Methods } @@ -21,6 +21,10 @@ export const crudMixins = ( }); } + async function createOne(payload: T) { + return await requests.post(baseRoute, payload); + } + async function getOne(itemId: string) { return await requests.get(itemRoute(itemId)); } @@ -37,14 +41,14 @@ export const crudMixins = ( return await requests.delete(itemRoute(itemId)); } - return { getAll, getOne, updateOne, patchOne, deleteOne }; + return { getAll, getOne, updateOne, patchOne, deleteOne, createOne }; }; -export abstract class BaseAPIClass implements CrudAPIInterface { +export abstract class BaseAPIClass implements CrudAPIInterface { requests: ApiRequestInstance; abstract baseRoute: string; - abstract itemRoute(itemId: string): string; + abstract itemRoute(itemId: string | number): string; constructor(requests: ApiRequestInstance) { this.requests = requests; @@ -56,6 +60,10 @@ export abstract class BaseAPIClass implements CrudAPIInterface { }); } + async createOne(payload: U) { + return await this.requests.post(this.baseRoute, payload); + } + async getOne(itemId: string) { return await this.requests.get(this.itemRoute(itemId)); } @@ -68,7 +76,7 @@ export abstract class BaseAPIClass implements CrudAPIInterface { return await this.requests.patch(this.itemRoute(itemId), payload); } - async deleteOne(itemId: string) { + async deleteOne(itemId: string | number) { return await this.requests.delete(this.itemRoute(itemId)); } } diff --git a/frontend/api/class-interfaces/groups.ts b/frontend/api/class-interfaces/groups.ts new file mode 100644 index 000000000000..fb846fc351b4 --- /dev/null +++ b/frontend/api/class-interfaces/groups.ts @@ -0,0 +1,26 @@ +import { requests } from "../requests"; +import { BaseAPIClass } from "./_base"; +import { GroupInDB } from "~/types/api-types/user"; + +const prefix = "/api"; + +const routes = { + groups: `${prefix}/groups`, + groupsSelf: `${prefix}/groups/self`, + + groupsId: (id: string | number) => `${prefix}/groups/${id}`, +}; + +export interface CreateGroup { + name: string; +} + +export class GroupAPI extends BaseAPIClass { + baseRoute = routes.groups; + itemRoute = routes.groupsId; + /** Returns the Group Data for the Current User + */ + async getCurrentUserGroup() { + return await requests.get(routes.groupsSelf); + } +} diff --git a/frontend/api/class-interfaces/recipes.ts b/frontend/api/class-interfaces/recipes.ts index 19227fc656c2..147204f84cc4 100644 --- a/frontend/api/class-interfaces/recipes.ts +++ b/frontend/api/class-interfaces/recipes.ts @@ -1,13 +1,12 @@ -import { BaseAPIClass, crudMixins } from "./_base"; +import { BaseAPIClass } from "./_base"; import { Recipe } from "~/types/api-types/admin"; -import { ApiRequestInstance } from "~/types/api"; +import { CreateRecipe } from "~/types/api-types/recipe"; const prefix = "/api"; const routes = { recipesCreate: `${prefix}/recipes/create`, recipesBase: `${prefix}/recipes`, - recipesSummary: `${prefix}/recipes/summary`, recipesTestScrapeUrl: `${prefix}/recipes/test-scrape-url`, recipesCreateUrl: `${prefix}/recipes/create-url`, recipesCreateFromZip: `${prefix}/recipes/create-from-zip`, @@ -19,25 +18,10 @@ const routes = { recipesRecipeSlugAssets: (recipe_slug: string) => `${prefix}/recipes/${recipe_slug}/assets`, }; -export class RecipeAPI extends BaseAPIClass { - baseRoute: string = routes.recipesSummary; +export class RecipeAPI extends BaseAPIClass { + baseRoute: string = routes.recipesBase; itemRoute = routes.recipesRecipeSlug; - constructor(requests: ApiRequestInstance) { - super(requests); - const { getAll, getOne, updateOne, patchOne, deleteOne } = crudMixins( - requests, - routes.recipesSummary, - routes.recipesRecipeSlug - ); - - this.getAll = getAll; - this.getOne = getOne; - this.updateOne = updateOne; - this.patchOne = patchOne; - this.deleteOne = deleteOne; - } - async getAllByCategory(categories: string[]) { return await this.requests.get(routes.recipesCategory, { categories, @@ -56,10 +40,6 @@ export class RecipeAPI extends BaseAPIClass { return this.requests.post(routes.recipesRecipeSlugImage(slug), { url }); } - async createOne(name: string) { - return await this.requests.post(routes.recipesBase, { name }); - } - async createOneByUrl(url: string) { return await this.requests.post(routes.recipesCreateUrl, { url }); } diff --git a/frontend/api/class-interfaces/users.ts b/frontend/api/class-interfaces/users.ts index 1288490d05f3..f0cd63772906 100644 --- a/frontend/api/class-interfaces/users.ts +++ b/frontend/api/class-interfaces/users.ts @@ -1,5 +1,18 @@ import { BaseAPIClass } from "./_base"; -import { UserOut } from "~/types/api-types/user"; +import { UserIn, UserOut } from "~/types/api-types/user"; + +// Interfaces + +interface ChangePassword { + currentPassword: string; + newPassword: string; +} + +interface CreateAPIToken { + name: string; +} + +// Code const prefix = "/api"; @@ -13,19 +26,45 @@ const routes = { usersIdPassword: (id: string) => `${prefix}/users/${id}/password`, usersIdFavorites: (id: string) => `${prefix}/users/${id}/favorites`, usersIdFavoritesSlug: (id: string, slug: string) => `${prefix}/users/${id}/favorites/${slug}`, + + usersApiTokens: `${prefix}/users/api-tokens`, + usersApiTokensTokenId: (token_id: string) => `${prefix}/users/api-tokens/${token_id}`, }; -export class UserApi extends BaseAPIClass { - baseRoute: string = routes.users; - itemRoute = (itemid: string) => routes.usersId(itemid); +export class UserApi extends BaseAPIClass { + baseRoute: string = routes.users; + itemRoute = (itemid: string) => routes.usersId(itemid); - async addFavorite(id: string, slug: string) { - const response = await this.requests.post(routes.usersIdFavoritesSlug(id, slug), {}); - return response.data; - } + async addFavorite(id: string, slug: string) { + return await this.requests.post(routes.usersIdFavoritesSlug(id, slug), {}); + } - async removeFavorite(id: string, slug: string) { - const response = await this.requests.delete(routes.usersIdFavoritesSlug(id, slug)); - return response.data; - } + async removeFavorite(id: string, slug: string) { + return await this.requests.delete(routes.usersIdFavoritesSlug(id, slug)); + } + + async getFavorites(id: string) { + await this.requests.get(routes.usersIdFavorites(id)); + } + + async changePassword(id: string, changePassword: ChangePassword) { + return await this.requests.put(routes.usersIdPassword(id), changePassword); + } + + async resetPassword(id: string) { + return await this.requests.post(routes.usersIdResetPassword(id), {}); + } + + async createAPIToken(tokenName: CreateAPIToken) { + return await this.requests.post(routes.usersApiTokens, tokenName); + } + + async deleteApiToken(tokenId: string) { + return await this.requests.delete(routes.usersApiTokensTokenId(tokenId)); + } + + userProfileImage(id: string) { + if (!id || id === undefined) return; + return `/api/users/${id}/image`; + } } diff --git a/frontend/api/index.ts b/frontend/api/index.ts index d74dba4046fe..ea134cdb45bb 100644 --- a/frontend/api/index.ts +++ b/frontend/api/index.ts @@ -1,11 +1,13 @@ import { RecipeAPI } from "./class-interfaces/recipes"; import { UserApi } from "./class-interfaces/users"; +import { GroupAPI } from "./class-interfaces/groups"; import { ApiRequestInstance } from "~/types/api"; class Api { private static instance: Api; public recipes: RecipeAPI; public users: UserApi; + public groups: GroupAPI; constructor(requests: ApiRequestInstance) { if (Api.instance instanceof Api) { @@ -14,6 +16,7 @@ class Api { this.recipes = new RecipeAPI(requests); this.users = new UserApi(requests); + this.groups = new GroupAPI(requests); Object.freeze(this); Api.instance = this; diff --git a/frontend/components/Domain/Admin/AdminBackupDialog.vue b/frontend/components/Domain/Admin/AdminBackupDialog.vue new file mode 100644 index 000000000000..54bb6ad65347 --- /dev/null +++ b/frontend/components/Domain/Admin/AdminBackupDialog.vue @@ -0,0 +1,148 @@ + + + diff --git a/frontend/components/Domain/Admin/AdminBackupImportDialog.vue b/frontend/components/Domain/Admin/AdminBackupImportDialog.vue new file mode 100644 index 000000000000..b4b6908c6d31 --- /dev/null +++ b/frontend/components/Domain/Admin/AdminBackupImportDialog.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/frontend/components/Domain/Admin/AdminBackupImportOptions.vue b/frontend/components/Domain/Admin/AdminBackupImportOptions.vue new file mode 100644 index 000000000000..4795a619a90e --- /dev/null +++ b/frontend/components/Domain/Admin/AdminBackupImportOptions.vue @@ -0,0 +1,69 @@ + + + \ No newline at end of file diff --git a/frontend/components/Domain/Admin/AdminBackupViewer.vue b/frontend/components/Domain/Admin/AdminBackupViewer.vue new file mode 100644 index 000000000000..43ab2ff8f3f0 --- /dev/null +++ b/frontend/components/Domain/Admin/AdminBackupViewer.vue @@ -0,0 +1,110 @@ + + + + + + \ No newline at end of file diff --git a/frontend/components/Domain/Admin/AdminEventViewer.vue b/frontend/components/Domain/Admin/AdminEventViewer.vue new file mode 100644 index 000000000000..5a13affc348d --- /dev/null +++ b/frontend/components/Domain/Admin/AdminEventViewer.vue @@ -0,0 +1,110 @@ + + + + + \ No newline at end of file diff --git a/frontend/components/Domain/User/UserProfileCard.vue b/frontend/components/Domain/User/UserProfileCard.vue new file mode 100644 index 000000000000..ca777acced4e --- /dev/null +++ b/frontend/components/Domain/User/UserProfileCard.vue @@ -0,0 +1,159 @@ + + + + + diff --git a/frontend/components/Domain/User/UserThemeCard.vue b/frontend/components/Domain/User/UserThemeCard.vue new file mode 100644 index 000000000000..cb35d784e537 --- /dev/null +++ b/frontend/components/Domain/User/UserThemeCard.vue @@ -0,0 +1,211 @@ + + + + + \ No newline at end of file diff --git a/frontend/components/Layout/AppFloatingButton.vue b/frontend/components/Layout/AppFloatingButton.vue index 287c48a0c4fd..5fde521a2884 100644 --- a/frontend/components/Layout/AppFloatingButton.vue +++ b/frontend/components/Layout/AppFloatingButton.vue @@ -250,7 +250,7 @@ export default defineComponent({ this.$router.push(`/recipe/${response.data.slug}`); }, async manualCreateRecipe() { - await this.api.recipes.createOne(this.createRecipeData.form.name); + await this.api.recipes.createOne({ name: this.createRecipeData.form.name }); }, async createOnByUrl() { this.error = false; diff --git a/frontend/components/Layout/AppSidebar.vue b/frontend/components/Layout/AppSidebar.vue index 908b8628f455..5f0f66336dc7 100644 --- a/frontend/components/Layout/AppSidebar.vue +++ b/frontend/components/Layout/AppSidebar.vue @@ -1,5 +1,5 @@