From 89bad7b99cec036a275d7a2040fe0939c94ef931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Gonz=C3=A1lez=20Rubio?= Date: Mon, 27 Jan 2025 19:52:57 +0100 Subject: [PATCH] feat: added admin remove account option - fix #267 (#283) * feat: added admin remove account option - fix #267 * feat: added admin remove account option - fix #267 --- app/client/package.json | 2 +- .../components/users/users.component.tsx | 56 +++++++++++++++---- .../admin/components/users/users.module.scss | 5 ++ .../providers/providers.component.tsx | 9 ++- .../components/hotels/hotels.component.tsx | 4 +- .../modules/register/register.component.tsx | 8 --- app/client/src/shared/hooks/useAdmin.tsx | 14 +++++ app/client/src/shared/hooks/useMyHotels.ts | 4 +- app/client/yarn.lock | 11 ++-- .../modules/api/v3/account/login.request.ts | 9 --- app/server/src/modules/api/v3/admin/main.ts | 3 +- .../src/modules/api/v3/admin/user.request.ts | 17 +++++- app/server/src/modules/system/accounts.ts | 23 ++++++++ app/server/src/modules/system/connections.ts | 14 +++++ app/server/src/modules/system/hotels.ts | 48 ++++++++++++++++ app/server/src/modules/system/main.ts | 17 ++++++ 16 files changed, 201 insertions(+), 43 deletions(-) diff --git a/app/client/package.json b/app/client/package.json index 0aa743b..cf23211 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -9,7 +9,7 @@ "vite-tsconfig-paths": "4.3.2" }, "dependencies": { - "@oh/components": "npm:@jsr/oh__components@0.1.26", + "@oh/components": "npm:@jsr/oh__components@0.1.29", "@vitejs/plugin-react": "^4.3.1", "@vitejs/plugin-react-refresh": "1.3.6", "dayjs": "1.11.13", diff --git a/app/client/src/modules/admin/components/users/users.component.tsx b/app/client/src/modules/admin/components/users/users.component.tsx index 1877506..b33bb24 100644 --- a/app/client/src/modules/admin/components/users/users.component.tsx +++ b/app/client/src/modules/admin/components/users/users.component.tsx @@ -9,17 +9,21 @@ import { CrossIconComponent, ButtonComponent, SelectorComponent, + ConfirmationModalComponent, } from "@oh/components"; import { User } from "shared/types"; import { EMAIL_REGEX, USERNAME_REGEX } from "shared/consts"; //@ts-ignore import styles from "./users.module.scss"; +import { useModal } from "@oh/components"; export const AdminUsersComponent = () => { - const { users, updateUser, refresh, resendVerificationUser } = useAdmin(); + const { users, updateUser, deleteUser, refresh, resendVerificationUser } = + useAdmin(); const [selectedUser, setSelectedUser] = useState(); + const { open, close } = useModal(); const today = dayjs(Date.now()); @@ -37,6 +41,7 @@ export const AdminUsersComponent = () => { email, createdAt: dayjs(createdAt).valueOf(), admin: selectedUser.admin, + languages: selectedUser.languages, }; await updateUser(user); @@ -61,6 +66,10 @@ export const AdminUsersComponent = () => { ), [selectedUser, adminOptions], ); + const $onRemoveAccount = useCallback(async () => { + await deleteUser(selectedUser); + refresh(); + }, [deleteUser, selectedUser]); return (
@@ -126,16 +135,37 @@ export const AdminUsersComponent = () => { } />
- Update +
+ {selectedUser.admin ? null : ( + + open({ + children: ( + + ), + }) + } + > + Delete + + )} + {/*//@ts-ignore*/} + {selectedUser.verified !== "✅" ? ( + + Resend verification email + + ) : null} + Update +
- {selectedUser.verified !== "✅" ? ( - - Resend verification email - - ) : null} ) : null} { verified: user.verified ? "✅" : `⏳ ${Math.floor(remainingMinutes / 60)} hours ${remainingMinutes % 60} minutes`, + githubLogin: user.githubLogin, createdAt: dayjs(user.createdAt).format("YYYY/MM/DD HH:mm:ss"), }; })} @@ -208,6 +239,11 @@ export const AdminUsersComponent = () => { key: "otp", label: "2FA", }, + { + sortable: true, + key: "githubLogin", + label: "github", + }, { sortable: true, key: "verified", diff --git a/app/client/src/modules/admin/components/users/users.module.scss b/app/client/src/modules/admin/components/users/users.module.scss index 3b3827c..7eaea7a 100644 --- a/app/client/src/modules/admin/components/users/users.module.scss +++ b/app/client/src/modules/admin/components/users/users.module.scss @@ -40,3 +40,8 @@ } } } +.actions { + display: flex; + gap: 1rem; + justify-content: space-between; +} diff --git a/app/client/src/modules/application/components/providers/providers.component.tsx b/app/client/src/modules/application/components/providers/providers.component.tsx index cdf0c38..53a8493 100644 --- a/app/client/src/modules/application/components/providers/providers.component.tsx +++ b/app/client/src/modules/application/components/providers/providers.component.tsx @@ -1,11 +1,14 @@ import { Outlet } from "react-router"; import { UserProvider } from "shared/hooks"; import React from "react"; +import { ModalProvider } from "@oh/components"; export const ProvidersComponent = () => { return ( - - - + + + + + ); }; diff --git a/app/client/src/modules/home/components/hotels/hotels.component.tsx b/app/client/src/modules/home/components/hotels/hotels.component.tsx index e2e6063..d2f0a87 100644 --- a/app/client/src/modules/home/components/hotels/hotels.component.tsx +++ b/app/client/src/modules/home/components/hotels/hotels.component.tsx @@ -50,8 +50,8 @@ export const HotelsComponent = () => { founded at {dayjs(hotel.createdAt).format("DD MMM YYYY")} - {hotel.accounts} account{hotel.accounts === 1 ? "" : "s"}{" "} - already joined! + {hotel.accounts} user{hotel.accounts === 1 ? "" : "s"} already + joined!
diff --git a/app/client/src/modules/register/register.component.tsx b/app/client/src/modules/register/register.component.tsx index 9aea1aa..60ccc58 100644 --- a/app/client/src/modules/register/register.component.tsx +++ b/app/client/src/modules/register/register.component.tsx @@ -49,14 +49,6 @@ export const RegisterComponent: React.FC = () => { const rePassword = data.get("rePassword") as string; const language = data.get("language") as string; - console.log({ - email, - username, - password, - rePassword, - captchaId, - languages: [language], - }); register({ email, username, diff --git a/app/client/src/shared/hooks/useAdmin.tsx b/app/client/src/shared/hooks/useAdmin.tsx index d016e3c..8cf5380 100644 --- a/app/client/src/shared/hooks/useAdmin.tsx +++ b/app/client/src/shared/hooks/useAdmin.tsx @@ -21,6 +21,7 @@ type AdminState = { update: () => Promise; updateUser: (user: User) => Promise; + deleteUser: (user: User) => Promise; resendVerificationUser: (accountId: string) => Promise; refresh: () => void; @@ -62,6 +63,18 @@ export const AdminProvider: React.FunctionComponent = ({ [fetch, getAccountHeaders], ); + const deleteUser = useCallback( + (user: User) => { + return fetch({ + method: RequestMethod.DELETE, + pathname: "/admin/user", + headers: getAccountHeaders(), + body: user, + }); + }, + [fetch, getAccountHeaders], + ); + const resendVerificationUser = useCallback( (accountId: string) => { return fetch({ @@ -149,6 +162,7 @@ export const AdminProvider: React.FunctionComponent = ({ value={{ users, updateUser, + deleteUser, resendVerificationUser, tokens, diff --git a/app/client/src/shared/hooks/useMyHotels.ts b/app/client/src/shared/hooks/useMyHotels.ts index f61cbf3..af2edc5 100644 --- a/app/client/src/shared/hooks/useMyHotels.ts +++ b/app/client/src/shared/hooks/useMyHotels.ts @@ -35,7 +35,7 @@ export const useMyHotels = () => { const update = useCallback( async (hotelId: string, name: string, $public: boolean) => { - const { data } = await fetch({ + await fetch({ method: RequestMethod.PATCH, pathname: `/user/@me/hotel`, headers: getAccountHeaders(), @@ -45,8 +45,6 @@ export const useMyHotels = () => { public: $public, }, }); - - return data.hotels; }, [fetch, getAccountHeaders], ); diff --git a/app/client/yarn.lock b/app/client/yarn.lock index d895b9b..b7e170b 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -496,12 +496,13 @@ __metadata: languageName: node linkType: hard -"@oh/components@npm:@jsr/oh__components@0.1.26": - version: 0.1.26 - resolution: "@jsr/oh__components@npm:0.1.26::__archiveUrl=https%3A%2F%2Fnpm.jsr.io%2F~%2F11%2F%40jsr%2Foh__components%2F0.1.26.tgz" +"@oh/components@npm:@jsr/oh__components@0.1.29": + version: 0.1.29 + resolution: "@jsr/oh__components@npm:0.1.29::__archiveUrl=https%3A%2F%2Fnpm.jsr.io%2F~%2F11%2F%40jsr%2Foh__components%2F0.1.29.tgz" dependencies: react: "npm:18.3.1" - checksum: 10c0/8c8ee65118dbe052cdbc3f5329678755d81aeddbedc32831dcb59669815b7030203416e776bab1575c9970ad8542daa084cf30b883c705bbfd2b77f6533d6d79 + react-dom: "npm:18.3.1" + checksum: 10c0/f528efaa57089c988272db1d1271a7700b9afa1f931e68e23f7ed4ea7f9dbeccbcfc2fc1c6068afb5a8f784d921cebe7c672ebd3d55e84947d4f7d0b384c9d16 languageName: node linkType: hard @@ -2056,7 +2057,7 @@ __metadata: version: 0.0.0-use.local resolution: "root-workspace-0b6124@workspace:." dependencies: - "@oh/components": "npm:@jsr/oh__components@0.1.26" + "@oh/components": "npm:@jsr/oh__components@0.1.29" "@rollup/plugin-alias": "npm:5.1.0" "@types/js-cookie": "npm:3.0.6" "@types/react": "npm:18.3.3" diff --git a/app/server/src/modules/api/v3/account/login.request.ts b/app/server/src/modules/api/v3/account/login.request.ts index 7ca3ddf..471b7f1 100644 --- a/app/server/src/modules/api/v3/account/login.request.ts +++ b/app/server/src/modules/api/v3/account/login.request.ts @@ -65,15 +65,6 @@ export const loginPostRequest: RequestType = { message: "Email or password not valid!", }); - const accountByVerifyId = await System.db.get([ - "accountsByVerifyId", - account.accountId, - ]); - if (accountByVerifyId) - return getResponse(HttpStatusCode.FORBIDDEN, { - message: "Account is not verified!", - }); - const accountOTP = await System.db.get([ "otpByAccountId", account.accountId, diff --git a/app/server/src/modules/api/v3/admin/main.ts b/app/server/src/modules/api/v3/admin/main.ts index e42b85a..53d65d8 100644 --- a/app/server/src/modules/api/v3/admin/main.ts +++ b/app/server/src/modules/api/v3/admin/main.ts @@ -8,7 +8,7 @@ import { } from "./tokens.request.ts"; import { hotelsDeleteRequest, hotelsGetRequest } from "./hotels.request.ts"; import { usersGetRequest } from "./users.request.ts"; -import { userPatchRequest } from "./user.request.ts"; +import { userDeleteRequest, userPatchRequest } from "./user.request.ts"; import { userResendVerificationRequest } from "./user-resend-verification.request.ts"; export const adminRequestList: RequestType[] = getPathRequestList({ @@ -20,6 +20,7 @@ export const adminRequestList: RequestType[] = getPathRequestList({ tokensGetRequest, tokensPostRequest, usersGetRequest, + userDeleteRequest, userPatchRequest, userResendVerificationRequest, hotelsGetRequest, diff --git a/app/server/src/modules/api/v3/admin/user.request.ts b/app/server/src/modules/api/v3/admin/user.request.ts index 585ea6e..27d3aaa 100644 --- a/app/server/src/modules/api/v3/admin/user.request.ts +++ b/app/server/src/modules/api/v3/admin/user.request.ts @@ -10,11 +10,26 @@ import { System } from "modules/system/main.ts"; import { EMAIL_REGEX, USERNAME_REGEX } from "shared/consts/main.ts"; import { getEmailHash, getEncryptedEmail } from "shared/utils/account.utils.ts"; +export const userDeleteRequest: RequestType = { + method: RequestMethod.DELETE, + pathname: "/user", + kind: RequestKind.ADMIN, + func: async (request: Request) => { + if (!(await hasRequestAccess({ request, admin: true }))) + return getResponse(HttpStatusCode.FORBIDDEN); + + let { accountId } = await request.json(); + await System.accounts.remove(accountId); + + return getResponse(HttpStatusCode.OK); + }, +}; + export const userPatchRequest: RequestType = { method: RequestMethod.PATCH, pathname: "/user", kind: RequestKind.ADMIN, - func: async (request: Request, url: URL) => { + func: async (request: Request) => { if (!(await hasRequestAccess({ request, admin: true }))) return getResponse(HttpStatusCode.FORBIDDEN); diff --git a/app/server/src/modules/system/accounts.ts b/app/server/src/modules/system/accounts.ts index 0ece5b0..17f1f8f 100644 --- a/app/server/src/modules/system/accounts.ts +++ b/app/server/src/modules/system/accounts.ts @@ -22,9 +22,32 @@ export const accounts = () => { return await System.db.get(["accounts", accountId]); }; + const remove = async (accountId: string) => { + const account = await get(accountId); + + await System.db.delete(["accounts", accountId]); + await System.db.delete(["accountsByEmail", account.emailHash]); + await System.db.delete(["accountsByRefreshToken", accountId]); + await System.db.delete(["accountsByToken", accountId]); + await System.db.delete(["accountsByUsername", account.username]); + + await System.db.delete(["emailsByHash", account.emailHash]); + + await System.db.delete(["github", accountId]); + await System.db.delete(["githubState", accountId]); + + await System.connections.removeAll(accountId); + await System.hotels.removeAll(accountId); + + await System.db.delete(["hotelsByAccountId", accountId]); + + await System.admins.remove(accountId); + }; + return { getList, get, getFromRequest, + remove, }; }; diff --git a/app/server/src/modules/system/connections.ts b/app/server/src/modules/system/connections.ts index 6c56031..a088707 100644 --- a/app/server/src/modules/system/connections.ts +++ b/app/server/src/modules/system/connections.ts @@ -293,10 +293,24 @@ export const connections = () => { return connections; }; + const removeAll = async (accountId: string) => { + await System.db.delete(["connections", accountId]); + + const connections = ( + await System.db.list({ + prefix: ["integrationsByAccountId", accountId], + }) + ).map(({ value }) => value); + + for (const connection of connections) + await remove(accountId, connection.hotelId, connection.integrationId); + }; + return { generate, verify, remove, + removeAll, ping, getList, getListByHotelIdIntegrationId, diff --git a/app/server/src/modules/system/hotels.ts b/app/server/src/modules/system/hotels.ts index 10aac43..f462828 100644 --- a/app/server/src/modules/system/hotels.ts +++ b/app/server/src/modules/system/hotels.ts @@ -19,6 +19,53 @@ export const hotels = () => { ).filter(({ key }) => key[2] === hotelId && key[3] === integrationId); }; + const removeAll = async (accountId: string) => { + const hotelList = + (await System.db.get(["hotelsByAccountId", accountId])) || []; + + //fetch account hotel list + for (const hotelId of hotelList) { + const hotel = await getHotel(hotelId); + + for (const { integrationId } of hotel.integrations) { + //fetch accounts linked with this integration + const accountIdList = ( + await System.db.list({ + prefix: ["integrationsByHotelsByAccountId", hotelId, integrationId], + }) + ).map((data) => data.value); + + //remove all the integrations from this accounts linked to this integration + for (const $accountId of accountIdList) { + await System.db.delete([ + "integrationsByHotelsByAccountId", + hotelId, + integrationId, + $accountId, + ]); + await System.db.delete([ + "integrationsByAccountId", + $accountId, + hotelId, + integrationId, + ]); + } + //remove license integrations + await integrations.remove(hotelId, integrationId); + } + //delete hotel from accounts + for (const { key, value } of await System.db.list({ + prefix: ["hotelsByAccountId"], + })) + await System.db.set( + key, + value.filter(($hotelId) => $hotelId !== hotelId), + ); + //delete hotel + await System.db.delete(["hotels", hotelId]); + } + }; + const getListByAccountId = async (accountId: string) => { const hotelsIdList = (await System.db.get(["hotelsByAccountId", accountId])) || []; @@ -232,6 +279,7 @@ export const hotels = () => { update, getList, remove, + removeAll, getListByAccountId, getAccountsByIntegrationId, diff --git a/app/server/src/modules/system/main.ts b/app/server/src/modules/system/main.ts index ce76be4..9864f8b 100644 --- a/app/server/src/modules/system/main.ts +++ b/app/server/src/modules/system/main.ts @@ -48,6 +48,23 @@ export const System = (() => { await $db.load(); await Migrations.load($db); + { + try { + const entries = []; + for (const entry of await $db.list({ prefix: [] })) { + // Fetch all entries + entries.push({ + key: JSON.stringify(entry.key), + value: JSON.stringify(entry.value)?.substring(0, 8), + }); + } + + console.table(entries); // Format as a table + console.log(`Total entries: ${entries.length}`); + } finally { + } + } + await $email.load(); $api.load(); };