diff --git a/app/client/package.json b/app/client/package.json index 81ec091..9303fdb 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -18,7 +18,8 @@ "react-dom": "18.3.1", "react-router": "6.26.0", "react-router-dom": "6.26.0", - "sass": "1.77.8" + "sass": "1.77.8", + "dayjs": "1.11.13" }, "packageManager": "yarn@1.22.22", "scripts": { diff --git a/app/client/src/modules/account/account.component.tsx b/app/client/src/modules/account/account.component.tsx deleted file mode 100644 index 03c86c9..0000000 --- a/app/client/src/modules/account/account.component.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import React, { FormEvent, useCallback, useEffect, useState } from "react"; -import { useAccount, useApi, useQR } from "shared/hooks"; -import { useNavigate } from "react-router-dom"; -import { LinkComponent } from "shared/components"; -import { AdminComponent } from "modules/admin"; -import { BskyComponent } from "./components"; -import { Account } from "shared/types"; - -export const AccountComponent: React.FC = () => { - const [loaded, setLoaded] = useState(false); - const [otpLoaded, setOTPLoaded] = useState(false); - - const [account, setAccount] = useState(); - const [otpUrl, setOTPUrl] = useState(); - let navigate = useNavigate(); - - const { refreshSession } = useApi(); - const { getAccount, otp } = useAccount(); - const { getQR } = useQR(); - - const $reloadOTP = () => { - otp.get().then(async (uri) => { - if (uri) setOTPUrl(await getQR(uri)); - setOTPLoaded(true); - }); - }; - - useEffect(() => { - refreshSession() - .then(async () => { - setLoaded(true); - - setAccount(await getAccount()); - $reloadOTP(); - }) - .catch(() => { - navigate("/login"); - }); - }, []); - - useEffect(() => {}, []); - - const onSubmit = useCallback(async (event: FormEvent) => { - event.preventDefault(); - - const data = new FormData(event.target as unknown as HTMLFormElement); - const token = data.get("token") as string; - - if (!token || token.length !== 6) return; - - const isVerified = await otp.verify(token); - if (isVerified) setOTPUrl(null); - }, []); - - const onDeleteOTP = async () => { - setOTPLoaded(false); - await otp.remove(); - $reloadOTP(); - }; - - if (!loaded || !account) return
loading....
; - - return ( -
-
-

Account

-

{account.username}

-

{account.email}

-
-
-

2FA

- {otpLoaded && - (otpUrl ? ( -
- - - -
- ) : ( -
- OTP active -
- ))} -
- -
-

Actions

- Go to hotel -

- Logout -

-

- {account?.isAdmin ? ( -
-
- -
- ) : null} -
- ); -}; diff --git a/app/client/src/modules/account/components/bsky/bsky.component.tsx b/app/client/src/modules/account/components/bsky/bsky.component.tsx deleted file mode 100644 index cd9b630..0000000 --- a/app/client/src/modules/account/components/bsky/bsky.component.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import React, { FormEvent, useCallback } from "react"; -import { Account } from "shared/types"; -import { useAccount } from "shared/hooks"; -import { PROTO_DID_REGEX } from "shared/consts"; - -type Props = { - account: Account; -}; - -export const BskyComponent: React.FC = ({ account }) => { - const { at } = useAccount(); - - const onSubmit = useCallback(async (event: FormEvent) => { - event.preventDefault(); - - const data = new FormData(event.target as unknown as HTMLFormElement); - const did = data.get("did") as string; - - if (!did || !new RegExp(PROTO_DID_REGEX).test(did)) return; - - await at.create(did); - }, []); - - const handler = `${account.username}.openhotel.club`; - - return ( -
-

Bluesky handler claimer

-
- {"Go to Settings -> Change Handle -> I have my own domain "} -
-
- -
-
-
- 2. Copy the (did) red part only to the input
- - - did:plc:xxxxxxxxxxxxxxxxxxxxxxxx - -
-
- -
-
- -
-
- -
-
- -
- -
-
- - - -
-
- ); -}; diff --git a/app/client/src/modules/account/components/index.ts b/app/client/src/modules/account/components/index.ts deleted file mode 100644 index 59fd358..0000000 --- a/app/client/src/modules/account/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./bsky"; diff --git a/app/client/src/modules/account/index.ts b/app/client/src/modules/account/index.ts deleted file mode 100644 index aedc9fa..0000000 --- a/app/client/src/modules/account/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "modules/account/account.component"; diff --git a/app/client/src/modules/admin/admin.component.tsx b/app/client/src/modules/admin/admin.component.tsx index 8bffe63..538a0ea 100644 --- a/app/client/src/modules/admin/admin.component.tsx +++ b/app/client/src/modules/admin/admin.component.tsx @@ -1,91 +1,19 @@ -import React, { FormEvent, useCallback, useEffect, useState } from "react"; -import { useAdmin, useApi } from "shared/hooks"; -import { Account } from "shared/types"; -import { TokensComponent } from "modules/admin/tokens.component"; +import React, { useEffect } from "react"; +import { useAccount } from "shared/hooks"; +import { useNavigate } from "react-router-dom"; export const AdminComponent: React.FC = () => { - const { getList, remove, add, update, account } = useAdmin(); - const { getVersion } = useApi(); - - const [adminList, setAdminList] = useState([]); - const [accountList, setAccountList] = useState([]); - const [version, setVersion] = useState(); - - const $reloadList = () => - getList().then(({ data }) => setAdminList(data.adminList)); + const { isLogged, setAsAdmin } = useAccount(); + let navigate = useNavigate(); useEffect(() => { - $reloadList(); - getVersion().then(({ version }) => setVersion(version)); - account.getList().then(({ data }) => setAccountList(data.accountList)); - }, []); - - const removeAdmin = (email) => () => { - remove(email).then(() => $reloadList()); - }; - - const onSubmit = useCallback(async (event: FormEvent) => { - event.preventDefault(); - - const data = new FormData(event.target as unknown as HTMLFormElement); - const email = data.get("email") as string; - - if (!email) return; - - add(email).then(() => $reloadList()); - }, []); - - const $update = () => { - update().then(({ status }) => { - if (status === 200) - //TODO is updating! - setTimeout(() => { - window.location.reload(); - }, 10_000); - }); - }; + if (isLogged === null) return; + if (!isLogged) return navigate("/login"); - if (!adminList.length) return
Loading...
; + setAsAdmin() + .then(() => navigate("/")) + .catch(() => navigate("/")); + }, [isLogged, setAsAdmin]); - return ( -
-

Admin

-
- {adminList.map((user) => ( -
- {user.email} - {user.username} - -
- ))} -
-
- - -
-
-
-
-

Update

- -
- {version} -
- -

Accounts

-
-
Accounts: ({accountList.length})
- {accountList.map((user) => ( -
- - - {user.username} -
- ))} -
-
-
-
- ); + return <>; }; diff --git a/app/client/src/modules/admin/tokens.component.tsx b/app/client/src/modules/admin/tokens.component.tsx deleted file mode 100644 index 1cc9f08..0000000 --- a/app/client/src/modules/admin/tokens.component.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React, { FormEvent, useCallback, useEffect, useState } from "react"; -import { useAdmin, useApi } from "shared/hooks"; - -export const TokensComponent: React.FC = () => { - const { tokens } = useAdmin(); - - const [list, setList] = useState([]); - const [data, setData] = useState< - Record - >({}); - - useEffect(() => { - tokens.getList().then(({ data: { list } }) => setList(list)); - }, []); - - const onSubmit = useCallback( - (service: string) => async (event: FormEvent) => { - event.preventDefault(); - - const data = new FormData(event.target as unknown as HTMLFormElement); - const url = data.get("url") as string; - - if (!url) return; - - tokens.generate(service, url).then(({ data }) => - setData((currentData) => ({ - ...currentData, - [service]: data, - })), - ); - }, - [], - ); - - return ( -
-

Tokens

-
- {list?.map((service) => ( -
- - - {data[service] ? ( - <> -
- -
- - - ) : null} -
- ))} -
-
- ); -}; diff --git a/app/client/src/modules/application/components/router/router.component.tsx b/app/client/src/modules/application/components/router/router.component.tsx index 65d7cf1..3ea3d4d 100644 --- a/app/client/src/modules/application/components/router/router.component.tsx +++ b/app/client/src/modules/application/components/router/router.component.tsx @@ -1,20 +1,15 @@ -import { RouterProvider, createBrowserRouter, Outlet } from "react-router-dom"; +import { RouterProvider, createBrowserRouter } from "react-router-dom"; import React from "react"; import { NotFoundComponent } from "../not-found"; import { LoginComponent } from "modules/login"; import { RegisterComponent } from "modules/register"; import { HomeComponent } from "modules/home"; import { RedirectComponent } from "shared/components"; -import { VerifyComponent } from "modules/verify"; import { LogoutComponent } from "modules/logout"; -import { AccountComponent } from "modules/account"; -import { - MainLayoutComponent, - CardLayoutComponent, - AccountItemComponent, - NavItemComponent, - HotelIconComponent, -} from "@oh/components"; +import { MainLayoutComponent, CardLayoutComponent } from "@oh/components"; +import { ConnectionComponent, PingComponent } from "modules/connection"; +import { AdminComponent } from "modules/admin"; +import { HomeNavigatorComponent } from "modules/home/components"; const router = createBrowserRouter([ { @@ -28,28 +23,40 @@ const router = createBrowserRouter([ element: } />, path: "/register", }, - { - element: } />, - path: "/verify", - }, + // { + // element: } />, + // path: "/verify", + // }, { path: "/logout", Component: () => , }, { - element: } />, - path: "/account", - children: [ - { - path: "", - Component: () => , - }, - ], + element: } />, + path: "/connection", }, + // { + // element: } />, + // path: "/account", + // children: [ + // { + // path: "", + // Component: () => , + // }, + // ], + // }, + { path: "/ping", element: }, + { path: "/admin", element: }, { - path: "/", - Component: () => , + path: "/home", + element: ( + } + navigatorChildren={} + /> + ), }, + { path: "/", element: }, { path: "/404", Component: () => , diff --git a/app/client/src/modules/connection/components/index.ts b/app/client/src/modules/connection/components/index.ts new file mode 100644 index 0000000..8daae0d --- /dev/null +++ b/app/client/src/modules/connection/components/index.ts @@ -0,0 +1 @@ +export * from "./ping"; diff --git a/app/client/src/modules/connection/components/ping/index.ts b/app/client/src/modules/connection/components/ping/index.ts new file mode 100644 index 0000000..cbd78dc --- /dev/null +++ b/app/client/src/modules/connection/components/ping/index.ts @@ -0,0 +1 @@ +export * from "./ping.component"; diff --git a/app/client/src/modules/connection/components/ping/ping.component.tsx b/app/client/src/modules/connection/components/ping/ping.component.tsx new file mode 100644 index 0000000..be9ee55 --- /dev/null +++ b/app/client/src/modules/connection/components/ping/ping.component.tsx @@ -0,0 +1,23 @@ +import React, { useCallback, useEffect } from "react"; +import { useSearchParams } from "react-router-dom"; +import { useConnection } from "shared/hooks"; + +export const PingComponent: React.FC = () => { + const [searchParams] = useSearchParams(); + const { ping } = useConnection(); + + const connectionId = searchParams.get("connectionId"); + + const $ping = useCallback(() => { + ping(connectionId).then(({ estimatedNextPingIn }) => { + setTimeout($ping, estimatedNextPingIn); + }); + }, [ping]); + + useEffect(() => { + if (!connectionId) return; + $ping(); + }, [connectionId]); + + return
; +}; diff --git a/app/client/src/modules/connection/connection.component.tsx b/app/client/src/modules/connection/connection.component.tsx new file mode 100644 index 0000000..8d1f818 --- /dev/null +++ b/app/client/src/modules/connection/connection.component.tsx @@ -0,0 +1,63 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { RedirectComponent } from "shared/components"; +import { useAccount, useConnection, useRedirect } from "shared/hooks"; +import { Hotel } from "shared/types"; +import { arraysMatch } from "shared/utils"; + +export const ConnectionComponent: React.FC = () => { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + + const { refresh, isLogged } = useAccount(); + const { add, get } = useConnection(); + const { set: setRedirect } = useRedirect(); + + const [host, setHost] = useState(undefined); + + const state = searchParams.get("state"); + const redirectUrl = searchParams.get("redirectUrl"); + const scopes = searchParams.get("scopes")?.split(",") || []; + + const onAddHost = useCallback(() => { + add(state, redirectUrl, scopes).then(({ data: { redirectUrl } }) => { + window.location.href = redirectUrl; + }); + }, [add, state, redirectUrl, scopes]); + + if (!state || !redirectUrl) return ; + + useEffect(() => { + setRedirect(redirectUrl); + }, [redirectUrl]); + + useEffect(() => { + if (isLogged === null) return; + try { + refresh().then(() => { + const redirectUrlParsed = new URL(redirectUrl); + get(redirectUrlParsed.hostname).then((host) => { + if (!host) return setHost(null); + if (arraysMatch(host.scopes, scopes)) return onAddHost(); + + setHost(host); + }); + }); + } catch (e) { + navigate("/"); + } + }, [isLogged]); + + if (isLogged !== null && !isLogged) return ; + if (host === undefined) return
loading...
; + + return ( +
+ +

{redirectUrl}

+

{scopes.join(", ")}

+ + +
+ ); +}; diff --git a/app/client/src/modules/connection/index.ts b/app/client/src/modules/connection/index.ts new file mode 100644 index 0000000..dcbb346 --- /dev/null +++ b/app/client/src/modules/connection/index.ts @@ -0,0 +1,2 @@ +export * from "./connection.component"; +export * from "./components"; diff --git a/app/client/src/modules/home/components/account/account.component.tsx b/app/client/src/modules/home/components/account/account.component.tsx new file mode 100644 index 0000000..90e86f8 --- /dev/null +++ b/app/client/src/modules/home/components/account/account.component.tsx @@ -0,0 +1,18 @@ +import { useUser } from "shared/hooks"; +import React from "react"; +import { getCensoredEmail } from "shared/utils"; + +export const AccountComponent = () => { + const { user } = useUser(); + + if (!user) return
loading...
; + + return ( +
+

Account

+

{getCensoredEmail(user?.email)}

+

{user?.username}

+

{user?.admin ? "ADMIN" : null}

+
+ ); +}; diff --git a/app/client/src/modules/home/components/account/index.ts b/app/client/src/modules/home/components/account/index.ts new file mode 100644 index 0000000..54dcb38 --- /dev/null +++ b/app/client/src/modules/home/components/account/index.ts @@ -0,0 +1 @@ +export * from "./account.component"; diff --git a/app/client/src/modules/home/components/actions/actions.component.tsx b/app/client/src/modules/home/components/actions/actions.component.tsx new file mode 100644 index 0000000..98f750e --- /dev/null +++ b/app/client/src/modules/home/components/actions/actions.component.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { LinkComponent } from "shared/components"; + +export const ActionsComponent = () => { + return ( +
+

Actions

+
+ + + +
+
+ ); +}; diff --git a/app/client/src/modules/home/components/actions/index.ts b/app/client/src/modules/home/components/actions/index.ts new file mode 100644 index 0000000..59627f6 --- /dev/null +++ b/app/client/src/modules/home/components/actions/index.ts @@ -0,0 +1 @@ +export * from "./actions.component"; diff --git a/app/client/src/modules/home/components/admin/admin.component.tsx b/app/client/src/modules/home/components/admin/admin.component.tsx new file mode 100644 index 0000000..e33f6bd --- /dev/null +++ b/app/client/src/modules/home/components/admin/admin.component.tsx @@ -0,0 +1,27 @@ +import { AdminProvider, useUser } from "shared/hooks"; +import React from "react"; +import { + ActionsComponent, + HotelsComponent, + TokensComponent, + UsersComponent, +} from "./components"; + +export const AdminComponent = () => { + const { user } = useUser(); + + if (!user || !user?.admin) return null; + + return ( + +
+

Admin

+
+ + + + +
+
+ ); +}; diff --git a/app/client/src/modules/home/components/admin/components/actions/actions.component.tsx b/app/client/src/modules/home/components/admin/components/actions/actions.component.tsx new file mode 100644 index 0000000..4c7ea31 --- /dev/null +++ b/app/client/src/modules/home/components/admin/components/actions/actions.component.tsx @@ -0,0 +1,29 @@ +import { useAdmin, useApi } from "shared/hooks"; +import React, { useCallback, useEffect, useState } from "react"; + +export const ActionsComponent = () => { + const { getVersion } = useApi(); + const { update } = useAdmin(); + + const [version, setVersion] = useState(null); + + useEffect(() => { + getVersion().then(setVersion); + }, [getVersion]); + + const onUpdate = useCallback(() => { + update().then(() => { + setTimeout(() => location.reload(), 10_000); + }); + }, [update]); + + return ( +
+

Actions

+
+

{version}

+ +
+
+ ); +}; diff --git a/app/client/src/modules/home/components/admin/components/actions/index.ts b/app/client/src/modules/home/components/admin/components/actions/index.ts new file mode 100644 index 0000000..59627f6 --- /dev/null +++ b/app/client/src/modules/home/components/admin/components/actions/index.ts @@ -0,0 +1 @@ +export * from "./actions.component"; diff --git a/app/client/src/modules/home/components/admin/components/hotels/hotels.component.tsx b/app/client/src/modules/home/components/admin/components/hotels/hotels.component.tsx new file mode 100644 index 0000000..75571a5 --- /dev/null +++ b/app/client/src/modules/home/components/admin/components/hotels/hotels.component.tsx @@ -0,0 +1,23 @@ +import { useAdmin } from "shared/hooks"; +import React from "react"; +//@ts-ignore +import styles from "./hotels.module.scss"; + +export const HotelsComponent = () => { + const { hotels } = useAdmin(); + + return ( +
+

Hotels

+
+ {hotels.map((hotel) => ( +
+ + + +
+ ))} +
+
+ ); +}; diff --git a/app/client/src/modules/home/components/admin/components/hotels/hotels.module.scss b/app/client/src/modules/home/components/admin/components/hotels/hotels.module.scss new file mode 100644 index 0000000..c40efb5 --- /dev/null +++ b/app/client/src/modules/home/components/admin/components/hotels/hotels.module.scss @@ -0,0 +1,14 @@ +.hotels { + .list { + display: flex; + flex-direction: column; + gap: 0.5rem; + + .item { + display: flex; + gap: 1rem; + background-color: var(--bg); + padding: 1rem 2rem; + } + } +} diff --git a/app/client/src/modules/home/components/admin/components/hotels/index.ts b/app/client/src/modules/home/components/admin/components/hotels/index.ts new file mode 100644 index 0000000..9133142 --- /dev/null +++ b/app/client/src/modules/home/components/admin/components/hotels/index.ts @@ -0,0 +1 @@ +export * from "./hotels.component"; diff --git a/app/client/src/modules/home/components/admin/components/index.ts b/app/client/src/modules/home/components/admin/components/index.ts new file mode 100644 index 0000000..2b1a604 --- /dev/null +++ b/app/client/src/modules/home/components/admin/components/index.ts @@ -0,0 +1,4 @@ +export * from "./users"; +export * from "./tokens"; +export * from "./hotels"; +export * from "./actions"; diff --git a/app/client/src/modules/home/components/admin/components/tokens/index.ts b/app/client/src/modules/home/components/admin/components/tokens/index.ts new file mode 100644 index 0000000..bd97587 --- /dev/null +++ b/app/client/src/modules/home/components/admin/components/tokens/index.ts @@ -0,0 +1 @@ +export * from "./tokens.component"; diff --git a/app/client/src/modules/home/components/admin/components/tokens/tokens.component.tsx b/app/client/src/modules/home/components/admin/components/tokens/tokens.component.tsx new file mode 100644 index 0000000..9c45d19 --- /dev/null +++ b/app/client/src/modules/home/components/admin/components/tokens/tokens.component.tsx @@ -0,0 +1,52 @@ +import { useAdmin } from "shared/hooks"; +import React, { FormEvent, useCallback, useState } from "react"; +//@ts-ignore +import styles from "./tokens.module.scss"; + +export const TokensComponent = () => { + const { tokens, addToken, removeToken } = useAdmin(); + + const [lastToken, setLastToken] = useState(null); + + const onSubmit = useCallback( + async (event: FormEvent) => { + event.preventDefault(); + + const data = new FormData(event.target as unknown as HTMLFormElement); + const label = data.get("label") as string; + + if (!label) return; + + const rawToken = await addToken(label); + setLastToken(rawToken); + }, + [setLastToken], + ); + + const onDelete = useCallback( + (id: string) => async () => removeToken(id), + [removeToken], + ); + + return ( +
+

Tokens

+
+ {tokens.map((token, index) => ( +
+ + + {lastToken && index === tokens.length - 1 ? ( + + ) : null} + +
+ ))} +
+ + +
+
+
+ ); +}; diff --git a/app/client/src/modules/home/components/admin/components/tokens/tokens.module.scss b/app/client/src/modules/home/components/admin/components/tokens/tokens.module.scss new file mode 100644 index 0000000..46e0a41 --- /dev/null +++ b/app/client/src/modules/home/components/admin/components/tokens/tokens.module.scss @@ -0,0 +1,14 @@ +.tokens { + .list { + display: flex; + flex-direction: column; + gap: 0.5rem; + + .item { + display: flex; + gap: 1rem; + background-color: var(--bg); + padding: 1rem 2rem; + } + } +} diff --git a/app/client/src/modules/home/components/admin/components/users/index.ts b/app/client/src/modules/home/components/admin/components/users/index.ts new file mode 100644 index 0000000..a91f377 --- /dev/null +++ b/app/client/src/modules/home/components/admin/components/users/index.ts @@ -0,0 +1 @@ +export * from "./users.component"; diff --git a/app/client/src/modules/home/components/admin/components/users/users.component.tsx b/app/client/src/modules/home/components/admin/components/users/users.component.tsx new file mode 100644 index 0000000..0f0afbd --- /dev/null +++ b/app/client/src/modules/home/components/admin/components/users/users.component.tsx @@ -0,0 +1,25 @@ +import { useAdmin } from "shared/hooks"; +import React from "react"; +//@ts-ignore +import styles from "./users.module.scss"; +import { getCensoredEmail } from "shared/utils"; + +export const UsersComponent = () => { + const { users } = useAdmin(); + + return ( +
+

Users

+
+ {users.map((user) => ( +
+ + + + +
+ ))} +
+
+ ); +}; diff --git a/app/client/src/modules/home/components/admin/components/users/users.module.scss b/app/client/src/modules/home/components/admin/components/users/users.module.scss new file mode 100644 index 0000000..6fa19b7 --- /dev/null +++ b/app/client/src/modules/home/components/admin/components/users/users.module.scss @@ -0,0 +1,14 @@ +.users { + .list { + display: flex; + flex-direction: column; + gap: 0.5rem; + + .item { + display: flex; + gap: 1rem; + background-color: var(--bg); + padding: 1rem 2rem; + } + } +} diff --git a/app/client/src/modules/home/components/admin/index.ts b/app/client/src/modules/home/components/admin/index.ts new file mode 100644 index 0000000..ec32d19 --- /dev/null +++ b/app/client/src/modules/home/components/admin/index.ts @@ -0,0 +1 @@ +export * from "./admin.component"; diff --git a/app/client/src/modules/home/components/bsky/bsky.component.tsx b/app/client/src/modules/home/components/bsky/bsky.component.tsx new file mode 100644 index 0000000..5081fcf --- /dev/null +++ b/app/client/src/modules/home/components/bsky/bsky.component.tsx @@ -0,0 +1,118 @@ +import React from "react"; +import { useUser } from "shared/hooks"; + +type Props = {}; + +export const BskyComponent: React.FC = ({}) => { + const { user } = useUser(); + + if (!user) return null; + // const { at } = useAccount(); + // + // const onSubmit = useCallback(async (event: FormEvent) => { + // event.preventDefault(); + // + // const data = new FormData(event.target as unknown as HTMLFormElement); + // const did = data.get("did") as string; + // + // if (!did || !new RegExp(PROTO_DID_REGEX).test(did)) return; + // + // await at.create(did); + // }, []); + + const onSubmit = () => {}; + + const handler = `${user.username}.openhotel.club`; + + return ( +
+

Bluesky handler claimer

+
+
+ + Go to{" "} + + Settings + + {" -> Handle -> I have my own domain "} + +
+
+ +
+
+
+ 2. Copy the (did) red part only to the input
+ + did=did:plc:xxxxxxxxxxxxxxxxxxxxxxxx + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + +
+
+
+ ); +}; diff --git a/app/client/src/modules/account/components/bsky/index.ts b/app/client/src/modules/home/components/bsky/index.ts similarity index 100% rename from app/client/src/modules/account/components/bsky/index.ts rename to app/client/src/modules/home/components/bsky/index.ts diff --git a/app/client/src/modules/home/components/connections/connections.component.tsx b/app/client/src/modules/home/components/connections/connections.component.tsx new file mode 100644 index 0000000..f842270 --- /dev/null +++ b/app/client/src/modules/home/components/connections/connections.component.tsx @@ -0,0 +1,57 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { Connection } from "shared/types"; +//@ts-ignore +import styles from "./connections.module.scss"; +import { useConnection } from "shared/hooks"; + +type Props = {} & React.HTMLProps; + +export const ConnectionsComponent: React.FC = () => { + const { remove, getList } = useConnection(); + + const [hosts, setHosts] = useState([]); + + const $reload = useCallback(() => getList().then(setHosts), []); + + useEffect(() => { + $reload(); + }, []); + + const onRemoveHost = useCallback( + (host: Connection) => async () => { + await remove(host.hostname); + $reload(); + }, + [], + ); + + return ( +
+

Connections

+ + Servers can only access your account scopes if the connection is ACTIVE + +
+ {hosts.map((connection) => ( +
+ + {connection.hostname} {connection.isActive ? "(ACTIVE)" : ""} + +

accounts: {connection.accounts}

+ {connection.scopes.length ? ( + <> + + {connection.scopes.map((scope) => ( +
- {scope}
+ ))} + + ) : null} +

+ +

+
+ ))} +
+
+ ); +}; diff --git a/app/client/src/modules/home/components/connections/connections.module.scss b/app/client/src/modules/home/components/connections/connections.module.scss new file mode 100644 index 0000000..fadf37a --- /dev/null +++ b/app/client/src/modules/home/components/connections/connections.module.scss @@ -0,0 +1,10 @@ +.list { + display: flex; + flex-direction: column; + gap: 1rem; + + .hotel { + background-color: var(--bg); + padding: 2rem 3rem; + } +} diff --git a/app/client/src/modules/home/components/connections/index.ts b/app/client/src/modules/home/components/connections/index.ts new file mode 100644 index 0000000..7787190 --- /dev/null +++ b/app/client/src/modules/home/components/connections/index.ts @@ -0,0 +1 @@ +export * from "./connections.component"; diff --git a/app/client/src/modules/home/components/index.ts b/app/client/src/modules/home/components/index.ts new file mode 100644 index 0000000..001b8d6 --- /dev/null +++ b/app/client/src/modules/home/components/index.ts @@ -0,0 +1,8 @@ +export * from "./otp"; +export * from "./account"; +export * from "./connections"; +export * from "./license"; +export * from "./admin"; +export * from "./actions"; +export * from "./bsky"; +export * from "./navigator"; diff --git a/app/client/src/modules/home/components/license/index.ts b/app/client/src/modules/home/components/license/index.ts new file mode 100644 index 0000000..9c2968d --- /dev/null +++ b/app/client/src/modules/home/components/license/index.ts @@ -0,0 +1 @@ +export * from "./license.component"; diff --git a/app/client/src/modules/home/components/license/license.component.tsx b/app/client/src/modules/home/components/license/license.component.tsx new file mode 100644 index 0000000..fd280cb --- /dev/null +++ b/app/client/src/modules/home/components/license/license.component.tsx @@ -0,0 +1,35 @@ +import React, { useCallback, useState } from "react"; +import { useUser } from "shared/hooks"; +//@ts-ignore +import styles from "./license.module.scss"; + +export const LicenseComponent: React.FC = () => { + const { getLicense } = useUser(); + + const [licenseToken, setLicenseToken] = useState(); + + const generateLicense = useCallback(() => { + getLicense().then(setLicenseToken); + }, [getLicense]); + + return ( +
+

License

+
+
Generating a new license will remove the old license.
+ + {licenseToken ? ( +
{licenseToken}
+ ) : null} +
+ This token grants access to the auth system and can be used only for + one hotel. +
+
+ Don’t share this token with anyone. It’s unique to your account, and + any misuse could lead to your account being banned. Keep it safe! +
+
+
+ ); +}; diff --git a/app/client/src/modules/home/components/license/license.module.scss b/app/client/src/modules/home/components/license/license.module.scss new file mode 100644 index 0000000..23a1039 --- /dev/null +++ b/app/client/src/modules/home/components/license/license.module.scss @@ -0,0 +1,14 @@ +.container { + display: flex; + flex-direction: column; + gap: 0.5rem; + + .license { + border-radius: 0.5rem; + padding: 0.5rem 1rem; + background-color: var(--inv-bg); + color: var(--bg-main); + font-weight: bold; + text-align: center; + } +} diff --git a/app/client/src/modules/home/components/navigator/index.ts b/app/client/src/modules/home/components/navigator/index.ts new file mode 100644 index 0000000..fba7058 --- /dev/null +++ b/app/client/src/modules/home/components/navigator/index.ts @@ -0,0 +1 @@ +export * from "./navigator.component"; diff --git a/app/client/src/modules/home/components/navigator/navigator.component.tsx b/app/client/src/modules/home/components/navigator/navigator.component.tsx new file mode 100644 index 0000000..ae60bec --- /dev/null +++ b/app/client/src/modules/home/components/navigator/navigator.component.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { useRedirect } from "shared/hooks"; +import { HotelIconComponent, NavItemComponent } from "@oh/components"; + +export const HomeNavigatorComponent: React.FC = () => { + const { navigate } = useRedirect(); + + return ( + <> + }> + Last Hotel + + + ); +}; diff --git a/app/client/src/modules/home/components/otp/index.ts b/app/client/src/modules/home/components/otp/index.ts new file mode 100644 index 0000000..355ddd0 --- /dev/null +++ b/app/client/src/modules/home/components/otp/index.ts @@ -0,0 +1 @@ +export * from "./otp.component"; diff --git a/app/client/src/modules/home/components/otp/otp.component.tsx b/app/client/src/modules/home/components/otp/otp.component.tsx new file mode 100644 index 0000000..d127745 --- /dev/null +++ b/app/client/src/modules/home/components/otp/otp.component.tsx @@ -0,0 +1,72 @@ +import React, { FormEvent, useCallback, useEffect, useState } from "react"; +import { useOTP, useQR } from "shared/hooks"; +//@ts-ignore +import styles from "./otp.module.scss"; + +export const OtpComponent: React.FC = () => { + const { get, verify, remove } = useOTP(); + const { getQR } = useQR(); + + const [isLoaded, setLoaded] = useState(false); + const [uri, setUri] = useState(null); + + const $reload = useCallback( + () => + get() + .then(async ({ data }) => { + setUri(await getQR(data.uri)); + setLoaded(true); + }) + .catch(() => { + setLoaded(true); + }), + [get, setUri, setLoaded, getQR], + ); + + useEffect(() => { + $reload(); + }, []); + + const onSubmit = useCallback( + async (event: FormEvent) => { + event.preventDefault(); + + const data = new FormData(event.target as unknown as HTMLFormElement); + const token = data.get("token") as string; + + if (!token || token.length !== 6) return; + + const isVerified = await verify(token); + if (isVerified) setUri(null); + }, + [verify, setUri], + ); + + const onDeleteOTP = useCallback(async () => { + setLoaded(false); + await remove(); + $reload(); + }, [$reload]); + + return ( +
+

2FA

+ {isLoaded && + (uri ? ( +
+ + + Use an app like Google Authenticator and scan the QR to verify the + 2FA + + + +
+ ) : ( +
+ 2FA is active +
+ ))} +
+ ); +}; diff --git a/app/client/src/modules/account/login.module.scss b/app/client/src/modules/home/components/otp/otp.module.scss similarity index 59% rename from app/client/src/modules/account/login.module.scss rename to app/client/src/modules/home/components/otp/otp.module.scss index f27c385..e5bdf08 100644 --- a/app/client/src/modules/account/login.module.scss +++ b/app/client/src/modules/home/components/otp/otp.module.scss @@ -1,7 +1,9 @@ -@import "../../shared/styles/consts.module"; - .form { display: flex; flex-direction: column; gap: 1rem; + + .qr { + max-width: 20rem; + } } diff --git a/app/client/src/modules/home/home.component.tsx b/app/client/src/modules/home/home.component.tsx index 592d1df..0b9c66a 100644 --- a/app/client/src/modules/home/home.component.tsx +++ b/app/client/src/modules/home/home.component.tsx @@ -1,23 +1,35 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; +import { + OtpComponent, + ConnectionsComponent, + LicenseComponent, + AccountComponent, + AdminComponent, + ActionsComponent, + BskyComponent, +} from "./components"; +import { Outlet } from "react-router-dom"; +import { useAccount, UserProvider } from "shared/hooks"; import { RedirectComponent } from "shared/components"; -import { useApi } from "shared/hooks"; export const HomeComponent: React.FC = () => { - const [isReady, setIsReady] = useState(false); - const { refreshSession, getTicketId } = useApi(); + const { isLogged } = useAccount(); - useEffect(() => { - if (window.location.pathname === "/logout") return; + if (isLogged === null) return
Loading...
; + if (!isLogged) return ; - refreshSession(getTicketId() ?? "refresh") - .then(({ redirectUrl }) => { - window.location.href = redirectUrl; - }) - .catch(() => { - console.log("Cannot refresh session!"); - setIsReady(true); - }); - }, [refreshSession]); - - return !isReady ?
: ; + return ( + +
+ + + + + + + + +
+
+ ); }; diff --git a/app/client/src/modules/login/login.component.tsx b/app/client/src/modules/login/login.component.tsx index d087c91..acbd945 100644 --- a/app/client/src/modules/login/login.component.tsx +++ b/app/client/src/modules/login/login.component.tsx @@ -1,31 +1,22 @@ import React, { FormEvent, useCallback, useEffect, useState } from "react"; -import { CaptchaComponent, LinkComponent } from "shared/components"; -import { useApi } from "shared/hooks"; +import { + CaptchaComponent, + LinkComponent, + RedirectComponent, +} from "shared/components"; +import { useAccount } from "shared/hooks"; import styles from "./login.module.scss"; import { useNavigate } from "react-router-dom"; + export const LoginComponent: React.FC = () => { const [submittedAt, setSubmittedAt] = useState(); + const [errorMessage, setErrorMessage] = useState(); const [captchaId, setCaptchaId] = useState(null); - const [loaded, setLoaded] = useState(false); const [showOTP, setShowOTP] = useState(false); const [showCaptcha, setShowCaptcha] = useState(false); - const { login, refreshSession, getTicketId } = useApi(); - let navigate = useNavigate(); - - useEffect(() => { - if (window.location.pathname === "/logout") return; - - refreshSession(getTicketId()) - .then(({ redirectUrl }) => { - if (!redirectUrl) return navigate("/account"); - window.location.href = redirectUrl; - }) - .catch(() => { - setLoaded(true); - console.log("Cannot refresh session!"); - }); - }, []); + const { login, isLogged } = useAccount(); + const navigate = useNavigate(); const onSubmit = useCallback( async (event: FormEvent) => { @@ -36,22 +27,20 @@ export const LoginComponent: React.FC = () => { const password = data.get("password") as string; const otpToken = data.get("otpToken") as string; - login(email, password, captchaId, getTicketId(), otpToken) - .then(({ redirectUrl }) => { - if (!redirectUrl) return navigate("/account"); - - window.location.href = redirectUrl; - }) - .catch(({ status }) => { + login({ email, password, captchaId, otpToken }) + .then(() => navigate("/")) + .catch(({ status, message }) => { if (status === 461 || status === 451) setShowCaptcha(true); if (status === 461 || status === 441) setShowOTP(true); setSubmittedAt(performance.now()); + setErrorMessage(message); }); }, - [captchaId, getTicketId], + [captchaId, navigate, setSubmittedAt, setErrorMessage], ); - if (!loaded) return
loading...
; + if (isLogged === null) return
Loading...
; + if (isLogged) return ; return (
@@ -66,6 +55,7 @@ export const LoginComponent: React.FC = () => { )} {showOTP && } + {errorMessage ? : null} /register
diff --git a/app/client/src/modules/logout/logout.component.tsx b/app/client/src/modules/logout/logout.component.tsx index 63bc799..3071dc7 100644 --- a/app/client/src/modules/logout/logout.component.tsx +++ b/app/client/src/modules/logout/logout.component.tsx @@ -1,17 +1,17 @@ -import React, { useEffect, useState } from "react"; -import { RedirectComponent } from "shared/components"; -import { useApi } from "shared/hooks"; +import React, { useEffect } from "react"; +import { useAccount } from "shared/hooks"; +import { useNavigate } from "react-router-dom"; export const LogoutComponent: React.FC = () => { - const [isLogout, setIsLogout] = useState(false); + const navigate = useNavigate(); - const { logout } = useApi(); + const { logout } = useAccount(); useEffect(() => { - logout().then(() => { - setIsLogout(true); - }); + logout() + .then(() => navigate("/login")) + .catch(() => navigate("/")); }, [logout]); - return isLogout ? :
; + return
; }; diff --git a/app/client/src/modules/register/register.component.tsx b/app/client/src/modules/register/register.component.tsx index 0bd2a10..3bf1081 100644 --- a/app/client/src/modules/register/register.component.tsx +++ b/app/client/src/modules/register/register.component.tsx @@ -4,15 +4,16 @@ import { LinkComponent, RedirectComponent, } from "shared/components"; -import { useApi } from "shared/hooks"; +import { useAccount } from "shared/hooks"; import styles from "./register.module.scss"; +import { useNavigate } from "react-router-dom"; export const RegisterComponent: React.FC = () => { const [submittedAt, setSubmittedAt] = useState(); const [captchaId, setCaptchaId] = useState(); - const [canRedirectToLogin, setCanRedirectToLogin] = useState(false); - const { register } = useApi(); + const { register, isLogged } = useAccount(); + let navigate = useNavigate(); const onSubmit = useCallback( async (event: FormEvent) => { @@ -24,19 +25,18 @@ export const RegisterComponent: React.FC = () => { const password = data.get("password") as string; const rePassword = data.get("rePassword") as string; - register(email, username, password, rePassword, captchaId) + register({ email, username, password, rePassword, captchaId }) .then(() => { - console.log(":D"); - setCanRedirectToLogin(true); + navigate("/login"); }) .catch(() => { setSubmittedAt(performance.now()); }); }, - [captchaId], + [captchaId, navigate], ); - if (canRedirectToLogin) return ; + if (isLogged) return ; return (
diff --git a/app/client/src/shared/consts/at.consts.ts b/app/client/src/shared/consts/at.consts.ts index 73e05ad..86c3252 100644 --- a/app/client/src/shared/consts/at.consts.ts +++ b/app/client/src/shared/consts/at.consts.ts @@ -1 +1,2 @@ -export const PROTO_DID_REGEX = /^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$/; +export const PROTO_DID_REGEX = + /^did=(did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-])$/; diff --git a/app/client/src/shared/enums/index.ts b/app/client/src/shared/enums/index.ts new file mode 100644 index 0000000..af7d66a --- /dev/null +++ b/app/client/src/shared/enums/index.ts @@ -0,0 +1 @@ +export * from "./request.enums"; diff --git a/app/client/src/shared/enums/request.enums.ts b/app/client/src/shared/enums/request.enums.ts new file mode 100644 index 0000000..030677b --- /dev/null +++ b/app/client/src/shared/enums/request.enums.ts @@ -0,0 +1,11 @@ +export enum RequestMethod { + OPTIONS = "OPTIONS", + GET = "GET", + HEAD = "HEAD", + POST = "POST", + PUT = "PUT", + DELETE = "DELETE", + TRACE = "TRACE", + CONNECT = "CONNECT", + PATCH = "PATCH", +} diff --git a/app/client/src/shared/hooks/_useTemplate.tsx b/app/client/src/shared/hooks/_useTemplate.tsx new file mode 100644 index 0000000..2ebafe4 --- /dev/null +++ b/app/client/src/shared/hooks/_useTemplate.tsx @@ -0,0 +1,17 @@ +import React, { ReactNode, useContext } from "react"; + +type _TemplateState = {}; + +const _TemplateContext = React.createContext<_TemplateState>(undefined); + +type ProviderProps = { + children: ReactNode; +}; + +export const _TemplateProvider: React.FunctionComponent = ({ + children, +}) => { + return <_TemplateContext.Provider value={{}} children={children} />; +}; + +export const useTemplate = (): _TemplateState => useContext(_TemplateContext); diff --git a/app/client/src/shared/hooks/index.ts b/app/client/src/shared/hooks/index.ts index be2771e..610f47f 100644 --- a/app/client/src/shared/hooks/index.ts +++ b/app/client/src/shared/hooks/index.ts @@ -1,4 +1,8 @@ export * from "./useApi"; export * from "./useAccount"; export * from "./useQR"; +export * from "./useUser"; +export * from "./useOTP"; +export * from "./useConnection"; export * from "./useAdmin"; +export * from "./useRedirect"; diff --git a/app/client/src/shared/hooks/useAccount.ts b/app/client/src/shared/hooks/useAccount.ts index 70cd56c..d5c58eb 100644 --- a/app/client/src/shared/hooks/useAccount.ts +++ b/app/client/src/shared/hooks/useAccount.ts @@ -1,79 +1,140 @@ import { useApi } from "./useApi"; +import { RequestMethod } from "shared/enums"; +import { AccountLoginProps, AccountRegisterProps } from "shared/types"; +import { useCallback, useEffect, useState } from "react"; +import Cookies from "js-cookie"; export const useAccount = () => { - const { getSessionId, getToken } = useApi(); + const { fetch } = useApi(); - const getHeaders = () => { - const headers = new Headers(); - headers.append("sessionId", getSessionId()); - headers.append("token", getToken()); + const [isLogged, setIsLogged] = useState(null); - return headers; - }; + useEffect(() => { + refresh() + .then(() => setIsLogged(true)) + .catch(() => setIsLogged(false)); + }, []); - const getAccount = async () => { - const { data } = await fetch(`/api/v2/account`, { - headers: getHeaders(), - }).then((response) => response.json()); - return data; - }; + const getAccountHeaders = useCallback( + () => ({ + "account-id": Cookies.get("account-id"), + token: Cookies.get("token"), + }), + [], + ); - const otp = () => { - const get = async (): Promise => { - const { data } = await fetch(`/api/v2/account/otp`, { - headers: getHeaders(), - method: "POST", - }).then((response) => response.json()); - - return data?.uri; - }; - - const verify = async (token: string): Promise => { - const { status } = await fetch( - `/api/v2/account/otp/verify?token=${token}`, - { - headers: getHeaders(), + const login = useCallback( + async (body: AccountLoginProps) => { + const { + data: { + accountId, + token, + refreshToken, + durations: [tokenDuration, refreshTokenDuration], }, - ).then((response) => response.json()); - - return status === 200; - }; - - const remove = async () => { - await fetch(`/api/v2/account/otp`, { - headers: getHeaders(), - method: "DELETE", - }).then((response) => response.json()); - }; - - return { - get, - verify, - remove, - }; - }; + } = await fetch({ + method: RequestMethod.POST, + pathname: "/account/login", + body, + }); - const at = () => { - const create = async (did: string) => { - return await fetch(`/api/v2/at/create`, { - headers: getHeaders(), - method: "POST", - body: JSON.stringify({ - did, - }), - }).then((response) => response.json()); - }; - - return { - create, - }; - }; + Cookies.set("account-id", accountId, { + expires: refreshTokenDuration, + sameSite: "strict", + secure: true, + }); + Cookies.set("refresh-token", refreshToken, { + expires: refreshTokenDuration, + sameSite: "strict", + secure: true, + }); + Cookies.set("token", token, { + expires: tokenDuration, + sameSite: "strict", + secure: true, + }); + }, + [fetch], + ); + + const register = useCallback( + async (body: AccountRegisterProps) => + fetch({ + method: RequestMethod.POST, + pathname: "/account/register", + body, + }), + [fetch], + ); + const logout = useCallback(async () => { + fetch({ + method: RequestMethod.POST, + pathname: "/account/logout", + headers: getAccountHeaders(), + }); + + Cookies.remove("account-id"); + Cookies.remove("refresh-token"); + Cookies.remove("token"); + }, [fetch, getAccountHeaders]); + + const refresh = useCallback(async () => { + let accountId = Cookies.get("account-id"); + let token = Cookies.get("token"); + let refreshToken = Cookies.get("refresh-token"); + + if (!accountId || (!token && !refreshToken)) throw "Not logged"; + if (accountId && token) return; + + const { data } = await fetch({ + method: RequestMethod.GET, + pathname: "/account/refresh", + headers: { + "account-id": Cookies.get("account-id"), + "refresh-token": Cookies.get("refresh-token"), + }, + }); + + token = data.token; + refreshToken = data.refreshToken; + const [tokenDuration, refreshTokenDuration] = data.durations; + + Cookies.set("account-id", accountId, { + expires: refreshTokenDuration, + sameSite: "strict", + secure: true, + }); + Cookies.set("refresh-token", refreshToken, { + expires: refreshTokenDuration, + sameSite: "strict", + secure: true, + }); + Cookies.set("token", token, { + expires: tokenDuration, + sameSite: "strict", + secure: true, + }); + }, [fetch]); + + const setAsAdmin = useCallback(() => { + return fetch({ + method: RequestMethod.POST, + pathname: "/admin", + headers: getAccountHeaders(), + }); + }, []); return { - getAccount, - getHeaders, + getAccountHeaders, + + login, + register, + logout, + + refresh, + + isLogged, - otp: otp(), - at: at(), + setAsAdmin, }; }; diff --git a/app/client/src/shared/hooks/useAdmin.tsx b/app/client/src/shared/hooks/useAdmin.tsx new file mode 100644 index 0000000..cc713ba --- /dev/null +++ b/app/client/src/shared/hooks/useAdmin.tsx @@ -0,0 +1,130 @@ +import React, { + ReactNode, + useCallback, + useContext, + useEffect, + useState, +} from "react"; +import { useApi } from "shared/hooks/useApi"; +import { useAccount } from "shared/hooks/useAccount"; +import { Hotel, Token, User } from "shared/types"; +import { RequestMethod } from "shared/enums"; + +type AdminState = { + users: User[]; + tokens: Token[]; + hotels: Hotel[]; + + addToken: (label: string) => Promise; + removeToken: (id: string) => Promise; + + update: () => Promise; +}; + +const AdminContext = React.createContext(undefined); + +type ProviderProps = { + children: ReactNode; +}; + +export const AdminProvider: React.FunctionComponent = ({ + children, +}) => { + const { fetch } = useApi(); + const { getAccountHeaders } = useAccount(); + + const [users, setUsers] = useState([]); + const [tokens, setTokens] = useState([]); + const [hotels, setHotels] = useState([]); + + const fetchUsers = useCallback(async () => { + return fetch({ + method: RequestMethod.GET, + pathname: "/admin/users", + headers: getAccountHeaders(), + }); + }, [fetch, getAccountHeaders]); + + const fetchTokens = useCallback(async () => { + return fetch({ + method: RequestMethod.GET, + pathname: "/admin/tokens", + headers: getAccountHeaders(), + }); + }, [fetch, getAccountHeaders]); + + const addToken = useCallback( + async (label: string): Promise => { + const { id, token } = ( + await fetch({ + method: RequestMethod.POST, + pathname: "/admin/tokens", + headers: getAccountHeaders(), + body: { + label, + }, + }) + ).data; + setTokens((tokens) => [ + ...tokens, + { + id, + label, + }, + ]); + return token; + }, + [fetch, getAccountHeaders, setTokens], + ); + + const removeToken = useCallback( + async (id: string): Promise => { + await fetch({ + method: RequestMethod.DELETE, + pathname: `/admin/tokens?id=${id}`, + headers: getAccountHeaders(), + }); + setTokens((tokens) => tokens.filter((token) => token.id !== id)); + }, + [fetch, getAccountHeaders, setTokens], + ); + + const fetchHotels = useCallback(async () => { + return fetch({ + method: RequestMethod.GET, + pathname: "/admin/hotels", + headers: getAccountHeaders(), + }); + }, [fetch, getAccountHeaders]); + + const update = useCallback(() => { + return fetch({ + method: RequestMethod.PATCH, + pathname: "/admin/update", + headers: getAccountHeaders(), + }); + }, []); + + useEffect(() => { + fetchUsers().then((response) => setUsers(response.data.users)); + fetchTokens().then((response) => setTokens(response.data.tokens)); + fetchHotels().then((response) => setHotels(response.data.hosts)); + }, [fetchUsers, fetchTokens, fetchHotels, setUsers, setTokens, setHotels]); + + return ( + + ); +}; + +export const useAdmin = (): AdminState => useContext(AdminContext); diff --git a/app/client/src/shared/hooks/useApi.ts b/app/client/src/shared/hooks/useApi.ts index e0d3bec..1a56ccb 100644 --- a/app/client/src/shared/hooks/useApi.ts +++ b/app/client/src/shared/hooks/useApi.ts @@ -1,180 +1,43 @@ -import Cookies from "js-cookie"; -import { redirectToFallbackRedirectUrl } from "shared/utils/urls.utils"; +import { Request } from "shared/types"; +import { RequestMethod } from "../enums"; +import { useCallback } from "react"; export const useApi = () => { - const getTicketId = () => new URLSearchParams(location.hash).get("#ticketId"); - - const clearSessionCookies = () => { - Cookies.remove("sessionId"); - Cookies.remove("token"); - Cookies.remove("refreshToken"); - }; - - const getSessionId = () => Cookies.get("sessionId"); - const getToken = () => Cookies.get("token"); - const getRefreshToken = () => Cookies.get("refreshToken"); - - const setFallbackRedirectUrl = (redirectUrl: string) => { - const { href, search } = new URL(redirectUrl); - localStorage.setItem("fallbackRedirectUrl", href.replace(search, "")); - }; - - const login = ( - email: string, - password: string, - captchaId: string, - ticketId?: string, - otpToken?: string, - ) => - new Promise((resolve, reject) => { - fetch("/api/v2/account/login", { - method: "POST", - body: JSON.stringify({ - ticketId, - - email, - password, - captchaId, - - otpToken, + const $fetch = useCallback( + async ({ + method = RequestMethod.GET, + pathname, + body, + headers = {}, + }: Request) => { + const response = await fetch(`/api/v3${pathname}`, { + method, + headers: new Headers({ + ...headers, + "Content-Type": "application/json", }), - }) - .then((data) => data.json()) - .then(({ status, data }) => { - if (status === 410) return redirectToFallbackRedirectUrl(); - if (status !== 200) return reject({ status }); - Cookies.set("sessionId", data.sessionId, { - expires: 7, - sameSite: "strict", - }); - Cookies.set("refreshToken", data.refreshToken, { - expires: 7, - sameSite: "strict", - }); - if (data.redirectUrl) setFallbackRedirectUrl(data.redirectUrl); - else - Cookies.set("token", data.token, { - expires: 1, - sameSite: "strict", - }); - resolve(data); - }) - .catch(() => reject({ status: 600 })); + body: body ? JSON.stringify(body) : undefined, + }).then((data) => data.json()); + + if (response.status !== 200) throw response; + + return response; + }, + [], + ); + + const getVersion = useCallback(async (): Promise => { + const { + data: { version }, + } = await $fetch({ + method: RequestMethod.GET, + pathname: "/version", }); - - const refreshSession = ( - ticketId?: string, - ): Promise<{ redirectUrl: string }> => - new Promise((resolve, reject) => { - const sessionId = getSessionId(); - const refreshToken = getRefreshToken(); - - if (!sessionId || !refreshToken) return reject(); - - fetch("/api/v2/account/refresh-session", { - method: "POST", - body: JSON.stringify({ - ticketId, - - sessionId, - refreshToken, - }), - }) - .then((data) => data.json()) - .then(({ status, data }) => { - if (status === 410) return redirectToFallbackRedirectUrl(); - if (status === 200) { - Cookies.set("sessionId", sessionId, { - expires: 7, - sameSite: "strict", - }); - Cookies.set("refreshToken", data.refreshToken, { - expires: 7, - sameSite: "strict", - }); - if (data.redirectUrl) setFallbackRedirectUrl(data.redirectUrl); - else - Cookies.set("token", data.token, { - expires: 1, - sameSite: "strict", - }); - return resolve(data); - } - clearSessionCookies(); - reject({ status }); - }) - .catch(() => reject({ status: 600 })); - }); - - const register = ( - email: string, - username: string, - password: string, - rePassword: string, - captchaId: string, - ) => - new Promise((resolve, reject) => { - fetch("/api/v2/account/register", { - method: "POST", - body: JSON.stringify({ - email, - username, - password, - rePassword, - captchaId, - }), - }) - .then((data) => data.json()) - .then(({ status }) => (status === 200 ? resolve() : reject({ status }))) - .catch(() => reject({ status: 600 })); - }); - - const logout = () => - new Promise((resolve) => { - const sessionId = getSessionId(); - const refreshToken = getRefreshToken(); - - clearSessionCookies(); - - if (!sessionId || !refreshToken) return resolve({}); - - fetch("/api/v2/account/logout", { - method: "POST", - body: JSON.stringify({ - sessionId, - refreshToken, - }), - }) - .then((data) => data.json()) - .then(resolve) - .catch(resolve); - }); - - const verify = (id: string, token: string) => - new Promise((resolve, reject) => { - if (!id || !token) return reject({ status: 700 }); - - fetch(`/api/v2/account/verify?id=${id}&token=${token}`) - .then((data) => data.json()) - .then(({ status }) => (status === 200 ? resolve() : reject({ status }))) - .catch(() => reject({ status: 600 })); - }); - - const getVersion = () => fetch(`/api/v2/version`).then((data) => data.json()); + return version; + }, [$fetch]); return { - getSessionId, - getRefreshToken, - getToken, - getTicketId, - - login, - refreshSession, - register, - logout, - clearSessionCookies, - verify, - + fetch: $fetch, getVersion, }; }; diff --git a/app/client/src/shared/hooks/useConnection.ts b/app/client/src/shared/hooks/useConnection.ts new file mode 100644 index 0000000..ce903ce --- /dev/null +++ b/app/client/src/shared/hooks/useConnection.ts @@ -0,0 +1,70 @@ +import { useApi } from "./useApi"; +import { useCallback } from "react"; +import { RequestMethod } from "shared/enums"; +import { useAccount } from "./useAccount"; + +export const useConnection = () => { + const { fetch } = useApi(); + const { getAccountHeaders } = useAccount(); + + const remove = useCallback( + async (hostname: string) => { + return fetch({ + method: RequestMethod.DELETE, + pathname: `/user/@me/connection?hostname=${hostname}`, + headers: getAccountHeaders(), + }); + }, + [fetch, getAccountHeaders], + ); + + const getList = useCallback(async () => { + const { data } = await fetch({ + method: RequestMethod.GET, + pathname: "/user/@me/connection", + headers: getAccountHeaders(), + }); + + return data.hosts; + }, []); + + const get = useCallback(async (hostname: string) => { + const { data } = await fetch({ + method: RequestMethod.GET, + pathname: `/user/@me/connection?hostname=${hostname}`, + headers: getAccountHeaders(), + }); + + return data.host; + }, []); + + const add = useCallback( + async (state: string, redirectUrl: string, scopes: string[]) => { + return fetch({ + method: RequestMethod.POST, + pathname: `/user/@me/connection`, + headers: getAccountHeaders(), + body: { state, redirectUrl, scopes }, + }); + }, + [fetch, getAccountHeaders], + ); + + const ping = useCallback(async (connectionId: string) => { + const { data } = await fetch({ + method: RequestMethod.PATCH, + pathname: `/user/@me/connection/ping?connectionId=${connectionId}`, + headers: getAccountHeaders(), + }); + + return data; + }, []); + + return { + add, + remove, + get, + getList, + ping, + }; +}; diff --git a/app/client/src/shared/hooks/useOTP.ts b/app/client/src/shared/hooks/useOTP.ts new file mode 100644 index 0000000..b06a7bd --- /dev/null +++ b/app/client/src/shared/hooks/useOTP.ts @@ -0,0 +1,45 @@ +import { useApi } from "./useApi"; +import { useCallback } from "react"; +import { RequestMethod } from "shared/enums"; +import { useAccount } from "./useAccount"; + +export const useOTP = () => { + const { fetch } = useApi(); + const { getAccountHeaders } = useAccount(); + + const get = useCallback( + () => + fetch({ + method: RequestMethod.GET, + headers: getAccountHeaders(), + pathname: "/account/otp", + }), + [fetch, getAccountHeaders], + ); + + const remove = useCallback( + () => + fetch({ + method: RequestMethod.DELETE, + headers: getAccountHeaders(), + pathname: "/account/otp", + }), + [fetch, getAccountHeaders], + ); + + const verify = useCallback( + (token: string) => + fetch({ + method: RequestMethod.GET, + headers: getAccountHeaders(), + pathname: `/account/otp/verify?token=${token}`, + }), + [fetch, getAccountHeaders], + ); + + return { + get, + remove, + verify, + }; +}; diff --git a/app/client/src/shared/hooks/useRedirect.ts b/app/client/src/shared/hooks/useRedirect.ts new file mode 100644 index 0000000..8113fe7 --- /dev/null +++ b/app/client/src/shared/hooks/useRedirect.ts @@ -0,0 +1,13 @@ +export const useRedirect = () => { + const set = (redirectUrl: string) => { + localStorage.setItem("lastRedirect", redirectUrl); + }; + const navigate = () => { + window.location.href = localStorage.getItem("lastRedirect"); + }; + + return { + set, + navigate, + }; +}; diff --git a/app/client/src/shared/hooks/useUser.tsx b/app/client/src/shared/hooks/useUser.tsx new file mode 100644 index 0000000..1a857e3 --- /dev/null +++ b/app/client/src/shared/hooks/useUser.tsx @@ -0,0 +1,78 @@ +import React, { + ReactNode, + useCallback, + useContext, + useEffect, + useState, +} from "react"; +import { User } from "shared/types"; +import { useApi } from "shared/hooks/useApi"; +import { useAccount } from "shared/hooks/useAccount"; +import { RequestMethod } from "shared/enums"; +import { useNavigate } from "react-router-dom"; + +type UserState = { + user: User | null; + + getLicense: () => Promise; +}; + +const UserContext = React.createContext(undefined); + +type ProviderProps = { + children: ReactNode; +}; + +export const UserProvider: React.FunctionComponent = ({ + children, +}) => { + const { fetch } = useApi(); + const { getAccountHeaders } = useAccount(); + const navigate = useNavigate(); + + const [user, setUser] = useState(null); + + const fetchUser = useCallback(async (): Promise => { + return ( + await Promise.all([ + fetch({ + method: RequestMethod.GET, + pathname: "/user/@me", + headers: getAccountHeaders(), + }), + fetch({ + method: RequestMethod.GET, + pathname: "/user/@me/email", + headers: getAccountHeaders(), + }), + ]) + ).reduce((currentData, { data }) => ({ ...currentData, ...data }), {}); + }, [fetch, getAccountHeaders]); + + const getLicense = useCallback(async () => { + const { data } = await fetch({ + method: RequestMethod.GET, + pathname: "/user/@me/license", + headers: getAccountHeaders(), + }); + return data.licenseToken; + }, [fetch, getAccountHeaders]); + + useEffect(() => { + fetchUser() + .then(setUser) + .catch(() => navigate("/login")); + }, [fetchUser]); + + return ( + + ); +}; + +export const useUser = (): UserState => useContext(UserContext); diff --git a/app/client/src/shared/hooks1/index.ts b/app/client/src/shared/hooks1/index.ts new file mode 100644 index 0000000..be2771e --- /dev/null +++ b/app/client/src/shared/hooks1/index.ts @@ -0,0 +1,4 @@ +export * from "./useApi"; +export * from "./useAccount"; +export * from "./useQR"; +export * from "./useAdmin"; diff --git a/app/client/src/shared/hooks1/useAccount.ts b/app/client/src/shared/hooks1/useAccount.ts new file mode 100644 index 0000000..70cd56c --- /dev/null +++ b/app/client/src/shared/hooks1/useAccount.ts @@ -0,0 +1,79 @@ +import { useApi } from "./useApi"; + +export const useAccount = () => { + const { getSessionId, getToken } = useApi(); + + const getHeaders = () => { + const headers = new Headers(); + headers.append("sessionId", getSessionId()); + headers.append("token", getToken()); + + return headers; + }; + + const getAccount = async () => { + const { data } = await fetch(`/api/v2/account`, { + headers: getHeaders(), + }).then((response) => response.json()); + return data; + }; + + const otp = () => { + const get = async (): Promise => { + const { data } = await fetch(`/api/v2/account/otp`, { + headers: getHeaders(), + method: "POST", + }).then((response) => response.json()); + + return data?.uri; + }; + + const verify = async (token: string): Promise => { + const { status } = await fetch( + `/api/v2/account/otp/verify?token=${token}`, + { + headers: getHeaders(), + }, + ).then((response) => response.json()); + + return status === 200; + }; + + const remove = async () => { + await fetch(`/api/v2/account/otp`, { + headers: getHeaders(), + method: "DELETE", + }).then((response) => response.json()); + }; + + return { + get, + verify, + remove, + }; + }; + + const at = () => { + const create = async (did: string) => { + return await fetch(`/api/v2/at/create`, { + headers: getHeaders(), + method: "POST", + body: JSON.stringify({ + did, + }), + }).then((response) => response.json()); + }; + + return { + create, + }; + }; + + return { + getAccount, + getHeaders, + + otp: otp(), + at: at(), + }; +}; diff --git a/app/client/src/shared/hooks/useAdmin.ts b/app/client/src/shared/hooks1/useAdmin.ts similarity index 100% rename from app/client/src/shared/hooks/useAdmin.ts rename to app/client/src/shared/hooks1/useAdmin.ts diff --git a/app/client/src/shared/hooks1/useApi.ts b/app/client/src/shared/hooks1/useApi.ts new file mode 100644 index 0000000..33dd184 --- /dev/null +++ b/app/client/src/shared/hooks1/useApi.ts @@ -0,0 +1,186 @@ +import Cookies from "js-cookie"; +import { redirectToFallbackRedirectUrl } from "shared/utils/urls.utils"; + +export const useApi = () => { + const getTicketId = () => new URLSearchParams(location.hash).get("#ticketId"); + + const clearSessionCookies = () => { + Cookies.remove("sessionId"); + Cookies.remove("token"); + Cookies.remove("refreshToken"); + }; + + const getSessionId = () => Cookies.get("sessionId"); + const getToken = () => Cookies.get("token"); + const getRefreshToken = () => Cookies.get("refreshToken"); + + const setFallbackRedirectUrl = (redirectUrl: string) => { + const { href, search } = new URL(redirectUrl); + localStorage.setItem("fallbackRedirectUrl", href.replace(search, "")); + }; + + const login = ( + email: string, + password: string, + captchaId: string, + ticketId?: string, + otpToken?: string, + ) => + new Promise((resolve, reject) => { + fetch("/api/v2/account/login", { + method: "POST", + body: JSON.stringify({ + ticketId, + + email, + password, + captchaId, + + otpToken, + }), + }) + .then((data) => data.json()) + .then(({ status, data }) => { + if (status === 410) return redirectToFallbackRedirectUrl(); + if (status !== 200) return reject({ status }); + Cookies.set("sessionId", data.sessionId, { + expires: 7, + sameSite: "strict", + secure: true, + }); + Cookies.set("refreshToken", data.refreshToken, { + expires: 7, + sameSite: "strict", + secure: true, + }); + if (data.redirectUrl) setFallbackRedirectUrl(data.redirectUrl); + else + Cookies.set("token", data.token, { + expires: 1, + sameSite: "strict", + secure: true, + }); + resolve(data); + }) + .catch(() => reject({ status: 600 })); + }); + + const refreshSession = ( + ticketId?: string, + ): Promise<{ redirectUrl: string }> => + new Promise((resolve, reject) => { + const sessionId = getSessionId(); + const refreshToken = getRefreshToken(); + + if (!sessionId || !refreshToken) return reject(); + + fetch("/api/v2/account/refresh-session", { + method: "POST", + body: JSON.stringify({ + ticketId, + + sessionId, + refreshToken, + }), + }) + .then((data) => data.json()) + .then(({ status, data }) => { + if (status === 410) return redirectToFallbackRedirectUrl(); + if (status === 200) { + Cookies.set("sessionId", sessionId, { + expires: 7, + sameSite: "strict", + secure: true, + }); + Cookies.set("refreshToken", data.refreshToken, { + expires: 7, + sameSite: "strict", + secure: true, + }); + if (data.redirectUrl) setFallbackRedirectUrl(data.redirectUrl); + else + Cookies.set("token", data.token, { + expires: 1, + sameSite: "strict", + secure: true, + }); + return resolve(data); + } + clearSessionCookies(); + reject({ status }); + }) + .catch(() => reject({ status: 600 })); + }); + + const register = ( + email: string, + username: string, + password: string, + rePassword: string, + captchaId: string, + ) => + new Promise((resolve, reject) => { + fetch("/api/v2/account/register", { + method: "POST", + body: JSON.stringify({ + email, + username, + password, + rePassword, + captchaId, + }), + }) + .then((data) => data.json()) + .then(({ status }) => (status === 200 ? resolve() : reject({ status }))) + .catch(() => reject({ status: 600 })); + }); + + const logout = () => + new Promise((resolve) => { + const sessionId = getSessionId(); + const refreshToken = getRefreshToken(); + + clearSessionCookies(); + + if (!sessionId || !refreshToken) return resolve({}); + + fetch("/api/v2/account/logout", { + method: "POST", + body: JSON.stringify({ + sessionId, + refreshToken, + }), + }) + .then((data) => data.json()) + .then(resolve) + .catch(resolve); + }); + + const verify = (id: string, token: string) => + new Promise((resolve, reject) => { + if (!id || !token) return reject({ status: 700 }); + + fetch(`/api/v2/account/verify?id=${id}&token=${token}`) + .then((data) => data.json()) + .then(({ status }) => (status === 200 ? resolve() : reject({ status }))) + .catch(() => reject({ status: 600 })); + }); + + const getVersion = () => fetch(`/api/v2/version`).then((data) => data.json()); + + return { + getSessionId, + getRefreshToken, + getToken, + getTicketId, + + login, + refreshSession, + register, + logout, + clearSessionCookies, + verify, + + getVersion, + }; +}; diff --git a/app/client/src/shared/types/account.types.ts b/app/client/src/shared/types/account.types.ts index e75a0fe..6ff2f49 100644 --- a/app/client/src/shared/types/account.types.ts +++ b/app/client/src/shared/types/account.types.ts @@ -1,6 +1,16 @@ -export type Account = { - accountId: string; - username: string; +export type AccountLoginProps = { + email: string; + password: string; + + otpToken?: string; + captchaId?: string; +}; + +export type AccountRegisterProps = { email: string; - isAdmin?: boolean; + username: string; + password: string; + rePassword: string; + + captchaId?: string; }; diff --git a/app/client/src/shared/types/connections.types.ts b/app/client/src/shared/types/connections.types.ts new file mode 100644 index 0000000..f375a1a --- /dev/null +++ b/app/client/src/shared/types/connections.types.ts @@ -0,0 +1,7 @@ +export type Connection = { + hostname: string; + scopes: string[]; + updatedAt: number; + isActive: boolean; + accounts: number; +}; diff --git a/app/client/src/shared/types/hotels.types.ts b/app/client/src/shared/types/hotels.types.ts new file mode 100644 index 0000000..fa94e33 --- /dev/null +++ b/app/client/src/shared/types/hotels.types.ts @@ -0,0 +1,5 @@ +export type Hotel = { + hostname: string; + accounts: string[]; + verified: boolean; +}; diff --git a/app/client/src/shared/types/index.ts b/app/client/src/shared/types/index.ts index ae3de0b..f58041b 100644 --- a/app/client/src/shared/types/index.ts +++ b/app/client/src/shared/types/index.ts @@ -1 +1,6 @@ export * from "./account.types"; +export * from "./request.types"; +export * from "./connections.types"; +export * from "./user.types"; +export * from "./tokens.types"; +export * from "./hotels.types"; diff --git a/app/client/src/shared/types/request.types.ts b/app/client/src/shared/types/request.types.ts new file mode 100644 index 0000000..982c377 --- /dev/null +++ b/app/client/src/shared/types/request.types.ts @@ -0,0 +1,8 @@ +import { RequestMethod } from "shared/enums"; + +export type Request = { + method?: RequestMethod; + pathname: string; + body?: unknown; + headers?: Record; +}; diff --git a/app/client/src/shared/types/tokens.types.ts b/app/client/src/shared/types/tokens.types.ts new file mode 100644 index 0000000..7197b53 --- /dev/null +++ b/app/client/src/shared/types/tokens.types.ts @@ -0,0 +1,4 @@ +export type Token = { + id: string; + label: string; +}; diff --git a/app/client/src/shared/types/user.types.ts b/app/client/src/shared/types/user.types.ts new file mode 100644 index 0000000..045246a --- /dev/null +++ b/app/client/src/shared/types/user.types.ts @@ -0,0 +1,7 @@ +export type User = { + accountId: string; + username: string; + email: string; + admin?: boolean; + otp?: boolean; +}; diff --git a/app/client/src/shared/utils/array.utils.ts b/app/client/src/shared/utils/array.utils.ts new file mode 100644 index 0000000..962ec9d --- /dev/null +++ b/app/client/src/shared/utils/array.utils.ts @@ -0,0 +1,4 @@ +export const arraysMatch = (arr1: unknown[], arr2: unknown[]): boolean => { + if (arr1.length !== arr2.length) return false; + return arr1.every((val, index) => val === arr2[index]); +}; diff --git a/app/client/src/shared/utils/email.utils.ts b/app/client/src/shared/utils/email.utils.ts new file mode 100644 index 0000000..efab6cb --- /dev/null +++ b/app/client/src/shared/utils/email.utils.ts @@ -0,0 +1,4 @@ +export const getCensoredEmail = (email: string): string => + email.substring(0, 1) + + "**@**.**" + + email.substring(email.length - 1, email.length); diff --git a/app/client/src/shared/utils/index.ts b/app/client/src/shared/utils/index.ts index 5011492..bf506d0 100644 --- a/app/client/src/shared/utils/index.ts +++ b/app/client/src/shared/utils/index.ts @@ -1 +1,4 @@ export * from "./class-name.utils"; +export * from "./array.utils"; +export * from "./urls.utils"; +export * from "./email.utils"; diff --git a/app/client/vite.config.ts b/app/client/vite.config.ts index 15af595..035a1e7 100644 --- a/app/client/vite.config.ts +++ b/app/client/vite.config.ts @@ -3,6 +3,7 @@ import reactRefresh from "@vitejs/plugin-react-refresh"; import react from "@vitejs/plugin-react"; export default defineConfig({ + clearScreen: false, server: { port: 2024, proxy: { diff --git a/app/client/yarn.lock b/app/client/yarn.lock index f7ddf12..0cbdb20 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -639,6 +639,11 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== +dayjs@1.11.13: + version "1.11.13" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" + integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== + debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: version "4.3.6" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" diff --git a/app/server/deno.json b/app/server/deno.json index 46f4ec8..4687a76 100644 --- a/app/server/deno.json +++ b/app/server/deno.json @@ -15,7 +15,9 @@ }, "imports": { "@oh/queue": "jsr:@oh/queue@1.1.1", - "@oh/utils": "jsr:@oh/utils@1.3.3", + "@oh/utils": "jsr:@oh/utils@1.3.10", + + "@da/bcrypt": "jsr:@da/bcrypt@1.0.0", "shared/": "./src/shared/", "modules/": "./src/modules/", @@ -23,7 +25,6 @@ "deno/": "https://deno.land/std@0.221.0/", "loadenv": "https://deno.land/x/loadenv@v1.0.1/mod.ts", - "bcrypt": "https://deno.land/x/bcrypt@v0.4.1/mod.ts", "nodemailer": "npm:nodemailer@6.9.15", "otp": "https://deno.land/x/otpauth@v9.3.3/dist/otpauth.esm.js" }, diff --git a/app/server/deno.lock b/app/server/deno.lock index 6199128..bbeeb4e 100644 --- a/app/server/deno.lock +++ b/app/server/deno.lock @@ -1,27 +1,34 @@ { "version": "4", "specifiers": { + "jsr:@da/bcrypt@1.0.0": "1.0.0", "jsr:@oh/queue@1.1.1": "1.1.1", - "jsr:@oh/utils@1.3.3": "1.3.3", + "jsr:@oh/utils@1.3.10": "1.3.10", "jsr:@oh/yaml@1.1.0": "1.1.0", "jsr:@std/fs@1.0.4": "1.0.4", "jsr:@std/path@1.0.6": "1.0.6", "jsr:@std/path@^1.0.6": "1.0.6", "jsr:@std/yaml@1.0.5": "1.0.5", + "jsr:@zip-js/zip-js@2.7.53": "2.7.53", "npm:dayjs@1.11.13": "1.11.13", "npm:nodemailer@6.9.15": "6.9.15" }, "jsr": { + "@da/bcrypt@1.0.0": { + "integrity": "615d5875bf153c825e98f3d065b24ec3de78051f5a8f352e1fea792228ac1060" + }, "@oh/queue@1.1.1": { "integrity": "8e8ec809202c48beaf436b3ea01b4c3a17f8c015ca686d906822482c9c881390" }, - "@oh/utils@1.3.3": { - "integrity": "bb178465cd4a507e5a7b31ec9440df2306b28679784796b63a674e075489e56d", + "@oh/utils@1.3.10": { + "integrity": "592a90888359f3a7b83899637c3470a5af7a45850f51a5f15c9fc0c489812cd8", "dependencies": [ + "jsr:@da/bcrypt", "jsr:@oh/yaml", "jsr:@std/fs", "jsr:@std/path@1.0.6", "jsr:@std/yaml", + "jsr:@zip-js/zip-js", "npm:dayjs" ] }, @@ -44,6 +51,9 @@ }, "@std/yaml@1.0.5": { "integrity": "71ba3d334305ee2149391931508b2c293a8490f94a337eef3a09cade1a2a2742" + }, + "@zip-js/zip-js@2.7.53": { + "integrity": "acea5bd8e01feb3fe4c242cfbde7d33dd5e006549a4eb1d15283bc0c778ed672" } }, "npm": { @@ -195,8 +205,9 @@ }, "workspace": { "dependencies": [ + "jsr:@da/bcrypt@1.0.0", "jsr:@oh/queue@1.1.1", - "jsr:@oh/utils@1.3.3", + "jsr:@oh/utils@1.3.10", "npm:nodemailer@6.9.15" ] } diff --git a/app/server/src/modules/api/v2/account/account.request.ts b/app/server/src/modules/api/v2/account/account.request.ts deleted file mode 100644 index 7b37fd9..0000000 --- a/app/server/src/modules/api/v2/account/account.request.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { RequestType, RequestMethod } from "@oh/utils"; -import { - getAccountFromRequest, - isAccountAuthValid, -} from "shared/utils/account.utils.ts"; - -export const accountGetRequest: RequestType = { - method: RequestMethod.GET, - pathname: "", - func: async (request, url) => { - const status = await isAccountAuthValid(request); - - if (status !== 200) - return Response.json( - { status }, - { - status, - }, - ); - - const account = await getAccountFromRequest(request); - - let data: any = { - username: account.username, - email: account.email, - }; - if (account.isAdmin) data.isAdmin = true; - - return Response.json( - { - status: 200, - data, - }, - { - status: 200, - }, - ); - }, -}; diff --git a/app/server/src/modules/api/v2/account/login.http b/app/server/src/modules/api/v2/account/login.http deleted file mode 100644 index 206155e..0000000 --- a/app/server/src/modules/api/v2/account/login.http +++ /dev/null @@ -1,11 +0,0 @@ -# Login -POST http://localhost:2024/api/v2/account/login -Content-Type: application/json - -{ - "ticketId": "9045fcb6-a58f-461a-9ae9-c14fc1e02290", - - "email": "pagoru@gmail.com", - "password": "123456Abc*", - "otpToken": "259461" -} \ No newline at end of file diff --git a/app/server/src/modules/api/v2/account/login.request.ts b/app/server/src/modules/api/v2/account/login.request.ts deleted file mode 100644 index 22f7860..0000000 --- a/app/server/src/modules/api/v2/account/login.request.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { - RequestType, - RequestMethod, - getIpFromRequest, - getRandomString, - getIpFromUrl, -} from "@oh/utils"; -import { System } from "modules/system/main.ts"; -import * as bcrypt from "bcrypt"; -import { getRedirectUrl } from "shared/utils/main.ts"; -import { - REFRESH_TOKEN_EXPIRE_TIME, - SERVER_SESSION_EXPIRE_TIME, - SESSION_EXPIRE_TIME, - SESSION_WITHOUT_TICKET_EXPIRE_TIME, -} from "shared/consts/main.ts"; -import { Session } from "shared/types/session.types.ts"; - -export const loginRequest: RequestType = { - method: RequestMethod.POST, - pathname: "/login", - func: async (request, url) => { - const { ticketId, email, password, captchaId, otpToken } = - await request.json(); - - if (!email || !password) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - let ticket; - if (ticketId) { - ticket = await System.db.get(["tickets", ticketId]); - - if (!ticket || ticket.isUsed) - return Response.json( - { status: 410 }, - { - status: 410, - }, - ); - } - - const accountByEmail = await System.db.get(["accountsByEmail", email]); - - if (!accountByEmail) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - const account = await System.db.get(["accounts", accountByEmail]); - if (!account) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - const result = bcrypt.compareSync(password, account.passwordHash); - - if (!result) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - if (account.verifyId) - return Response.json( - { status: 403, message: "Account is not verified!" }, - { - status: 403, - }, - ); - - const accountOTP = await System.db.get(["accountOTP", account.accountId]); - - let isValidOTP = true; - - if ( - accountOTP?.verified && - (!otpToken || !System.otp.verify(accountOTP.secret, otpToken)) - ) - isValidOTP = false; - - if (!(await System.captcha.verify(captchaId))) - return Response.json( - { status: isValidOTP ? 451 : 461, message: "Captcha is not valid!" }, - { - status: isValidOTP ? 451 : 461, - }, - ); - - if (!isValidOTP) - return Response.json( - { status: 441, message: "OTP is not valid!" }, - { - status: 441, - }, - ); - - if (account.sessionId) { - await System.db.delete(["accountsBySession", account.sessionId]); - await System.db.delete(["accountsByRefreshSession", account.sessionId]); - await System.db.delete(["ticketBySession", account.sessionId]); - } - - const sessionId = crypto.randomUUID(); - const token = getRandomString(64); - const refreshToken = getRandomString(128); - - await System.db.set(["accounts", account.accountId], { - ...account, - sessionId, - updatedAt: Date.now(), - tokenHash: bcrypt.hashSync(token, bcrypt.genSaltSync(8)), - refreshTokenHash: bcrypt.hashSync(refreshToken, bcrypt.genSaltSync(8)), - }); - await System.db.set(["accountsBySession", sessionId], account.accountId, { - expireIn: ticket - ? SESSION_EXPIRE_TIME - : SESSION_WITHOUT_TICKET_EXPIRE_TIME, - }); - await System.db.set( - ["accountsByRefreshSession", sessionId], - account.accountId, - { expireIn: REFRESH_TOKEN_EXPIRE_TIME }, - ); - if (ticket) { - await System.db.set( - ["tickets", ticket.ticketId], - { - ...ticket, - isUsed: true, - }, - { - expireIn: SESSION_EXPIRE_TIME, - }, - ); - await System.db.set(["ticketBySession", sessionId], ticket.ticketId, { - expireIn: SESSION_EXPIRE_TIME, - }); - - //server session - const ip = getIpFromRequest(request); - const serverIp = await getIpFromUrl(ticket.redirectUrl); - const session: Session = { - sessionId, - ticketId, - serverIp, - ip, - }; - await System.db.set( - ["serverSessionByAccount", account.accountId], - session, - { - //first time 5 minutes, next, 60 seconds - expireIn: SERVER_SESSION_EXPIRE_TIME * 5, - }, - ); - } - - return Response.json( - { - status: 200, - data: { - redirectUrl: ticket - ? getRedirectUrl({ - redirectUrl: ticket.redirectUrl, - ticketId, - sessionId, - token, - accountId: account.accountId, - }) - : null, - sessionId, - token, - refreshToken, - }, - }, - { status: 200 }, - ); - }, -}; diff --git a/app/server/src/modules/api/v2/account/logout.request.ts b/app/server/src/modules/api/v2/account/logout.request.ts deleted file mode 100644 index 49467da..0000000 --- a/app/server/src/modules/api/v2/account/logout.request.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { RequestType, RequestMethod } from "@oh/utils"; -import { System } from "modules/system/main.ts"; -import * as bcrypt from "bcrypt"; - -export const logoutRequest: RequestType = { - method: RequestMethod.POST, - pathname: "/logout", - func: async (request, url) => { - let { sessionId, refreshToken } = await request.json(); - - if (!sessionId || !refreshToken) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - const accountByRefreshSession = await System.db.get([ - "accountsByRefreshSession", - sessionId, - ]); - - if (!accountByRefreshSession) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - const account = await System.db.get(["accounts", accountByRefreshSession]); - - if (!account) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - const result = bcrypt.compareSync(refreshToken, account.refreshTokenHash); - - if (!result) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - delete account.sessionId; - delete account.tokenHash; - delete account.refreshTokenHash; - - await System.db.set(["accounts", account.accountId], account); - await System.db.delete(["accountsBySession", sessionId]); - await System.db.delete(["accountsByRefreshSession", sessionId]); - - return Response.json( - { - status: 200, - }, - { - status: 200, - }, - ); - }, -}; diff --git a/app/server/src/modules/api/v2/account/main.ts b/app/server/src/modules/api/v2/account/main.ts deleted file mode 100644 index 1117321..0000000 --- a/app/server/src/modules/api/v2/account/main.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { RequestType, getPathRequestList } from "@oh/utils"; - -import { otpRequestList } from "./otp/main.ts"; - -import { registerRequest } from "./register.request.ts"; -import { loginRequest } from "./login.request.ts"; -import { verifyRequest } from "./verify.request.ts"; -import { refreshSessionRequest } from "./refresh-session.request.ts"; -import { logoutRequest } from "./logout.request.ts"; -import { accountGetRequest } from "./account.request.ts"; -import { pingRequest } from "./ping.request.ts"; - -export const accountRequestList: RequestType[] = getPathRequestList({ - requestList: [ - registerRequest, - loginRequest, - verifyRequest, - refreshSessionRequest, - logoutRequest, - accountGetRequest, - pingRequest, - - ...otpRequestList, - ], - pathname: "/account", -}); diff --git a/app/server/src/modules/api/v2/account/otp/post.request.ts b/app/server/src/modules/api/v2/account/otp/post.request.ts deleted file mode 100644 index 1774d33..0000000 --- a/app/server/src/modules/api/v2/account/otp/post.request.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { RequestType, RequestMethod } from "@oh/utils"; -import { - getAccountFromRequest, - isAccountAuthValid, -} from "shared/utils/account.utils.ts"; -import { System } from "modules/system/main.ts"; - -export const postRequest: RequestType = { - method: RequestMethod.POST, - pathname: "", - func: async (request, url) => { - const status = await isAccountAuthValid(request); - - if (status !== 200) - return Response.json( - { status }, - { - status, - }, - ); - - const account = await getAccountFromRequest(request); - const accountOTP = await System.db.get(["accountOTP", account.accountId]); - - if (accountOTP?.verified) - return Response.json( - { - status: 409, - }, - { - status: 409, - }, - ); - - const secret = System.otp.generateSecret(); - const uri = System.otp.generateURI(account.email, secret); - - await System.db.set(["accountOTP", account.accountId], { - secret, - verified: false, - }); - - return Response.json( - { - status: 200, - data: { - uri, - }, - }, - { - status: 200, - }, - ); - }, -}; diff --git a/app/server/src/modules/api/v2/account/ping.http b/app/server/src/modules/api/v2/account/ping.http deleted file mode 100644 index 54b64a3..0000000 --- a/app/server/src/modules/api/v2/account/ping.http +++ /dev/null @@ -1,9 +0,0 @@ -# Ping -POST http://localhost:2024/api/v2/account/ping -Content-Type: application/json - -{ - "accountId": "52107710-cad6-4780-9967-8ed7f61ffff5", - "ticketId": "32f2f8b5-7c81-4f43-8727-a7c4a14cbc29", - "server": "https://client.openhotel.club" -} \ No newline at end of file diff --git a/app/server/src/modules/api/v2/account/ping.request.ts b/app/server/src/modules/api/v2/account/ping.request.ts deleted file mode 100644 index e8f831c..0000000 --- a/app/server/src/modules/api/v2/account/ping.request.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { - RequestType, - RequestMethod, - getIpFromRequest, - getIpFromUrl, - compareIps, -} from "@oh/utils"; -import { System } from "modules/system/main.ts"; -import { SERVER_SESSION_EXPIRE_TIME } from "shared/consts/main.ts"; -import { Session } from "shared/types/session.types.ts"; - -export const pingRequest: RequestType = { - method: RequestMethod.POST, - pathname: "/ping", - func: async (request, url) => { - const { accountId, server, ticketId } = await request.json(); - - if (!accountId || !server || !ticketId) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - // const account = await getAccountFromRequest(request); - const serverSession: Session = await System.db.get([ - "serverSessionByAccount", - accountId, - ]); - - if (!serverSession) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - const ip = getIpFromRequest(request); - const serverIp = await getIpFromUrl(server); - - if ( - serverSession?.ip !== ip || - !compareIps(serverIp, serverSession?.serverIp) || - serverSession?.ticketId !== ticketId - ) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - //update expire in time - await System.db.set(["serverSessionByAccount", accountId], serverSession, { - expireIn: SERVER_SESSION_EXPIRE_TIME, - }); - - return Response.json( - { - status: 200, - }, - { status: 200 }, - ); - }, -}; diff --git a/app/server/src/modules/api/v2/account/refresh-session.http b/app/server/src/modules/api/v2/account/refresh-session.http deleted file mode 100644 index 57ef7bc..0000000 --- a/app/server/src/modules/api/v2/account/refresh-session.http +++ /dev/null @@ -1,10 +0,0 @@ -# Login -POST http://localhost:2024/api/v2/account/refresh-session -Content-Type: application/json - -{ - "ticketId": "f20c73a4-cef7-41d1-ab0e-8cf4e809d2d6", - - "sessionId": "292f5f28-7882-4dfc-a4f5-486131b8eb2f", - "refreshToken": "PxPJqa7mwIuxzlL9IT7CBoOdw24cLCZGD5eyghqOTq4mQYs0PgsRDVXO2lAkt9QsC41zf72eeCFKuHz5hTMeoUoTdTVCqBJTggmW6xwi52joKrRpVF0ClcmpwGRwNuBG" -} \ No newline at end of file diff --git a/app/server/src/modules/api/v2/account/refresh-session.request.ts b/app/server/src/modules/api/v2/account/refresh-session.request.ts deleted file mode 100644 index 1aac9fe..0000000 --- a/app/server/src/modules/api/v2/account/refresh-session.request.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { - RequestType, - RequestMethod, - getRandomString, - getIpFromRequest, - getIpFromUrl, -} from "@oh/utils"; -import { System } from "modules/system/main.ts"; -import * as bcrypt from "bcrypt"; -import { - SESSION_EXPIRE_TIME, - REFRESH_TOKEN_EXPIRE_TIME, - SESSION_WITHOUT_TICKET_EXPIRE_TIME, - SERVER_SESSION_EXPIRE_TIME, -} from "shared/consts/main.ts"; -import { getRedirectUrl } from "shared/utils/account.utils.ts"; -import { Session } from "shared/types/session.types.ts"; - -export const refreshSessionRequest: RequestType = { - method: RequestMethod.POST, - pathname: "/refresh-session", - func: async (request, url) => { - let { ticketId, sessionId, refreshToken } = await request.json(); - - if (!sessionId || !refreshToken) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - let ticket; - if (ticketId) { - const foundTicket = await System.db.get(["tickets", ticketId]); - - ticket = foundTicket; - if (!foundTicket || foundTicket.isUsed) - return Response.json( - { status: 410 }, - { - status: 410, - }, - ); - } - - const accountByRefreshSession = await System.db.get([ - "accountsByRefreshSession", - sessionId, - ]); - - if (!accountByRefreshSession) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - const account = await System.db.get(["accounts", accountByRefreshSession]); - - if (!account) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - const result = bcrypt.compareSync(refreshToken, account.refreshTokenHash); - - if (!result) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - const token = getRandomString(64); - refreshToken = getRandomString(128); - - await System.db.set(["accounts", account.accountId], { - ...account, - sessionId, - tokenHash: bcrypt.hashSync(token, bcrypt.genSaltSync(8)), - refreshTokenHash: bcrypt.hashSync(refreshToken, bcrypt.genSaltSync(8)), - }); - await System.db.set(["accountsBySession", sessionId], account.accountId, { - expireIn: ticket - ? SESSION_EXPIRE_TIME - : SESSION_WITHOUT_TICKET_EXPIRE_TIME, - }); - await System.db.set( - ["accountsByRefreshSession", sessionId], - account.accountId, - { expireIn: REFRESH_TOKEN_EXPIRE_TIME }, - ); - if (ticket) { - await System.db.set( - ["tickets", ticket.ticketId], - { - ...ticket, - isUsed: true, - }, - { - expireIn: SESSION_EXPIRE_TIME, - }, - ); - await System.db.set(["ticketBySession", sessionId], ticket.ticketId, { - expireIn: SESSION_EXPIRE_TIME, - }); - - //server session - const ip = getIpFromRequest(request); - const serverIp = await getIpFromUrl(ticket.redirectUrl); - const session: Session = { - sessionId, - ticketId, - serverIp, - ip, - }; - await System.db.set( - ["serverSessionByAccount", account.accountId], - session, - { - //first time 5 minutes, next, 60 seconds - expireIn: SERVER_SESSION_EXPIRE_TIME * 5, - }, - ); - } - - return Response.json( - { - status: 200, - data: { - redirectUrl: ticket - ? getRedirectUrl({ - redirectUrl: ticket.redirectUrl, - ticketId: ticket.ticketId, - sessionId: account.sessionId, - token, - accountId: account.accountId, - }) - : null, - - token, - refreshToken, - }, - }, - { - status: 200, - }, - ); - }, -}; diff --git a/app/server/src/modules/api/v2/account/verify.http b/app/server/src/modules/api/v2/account/verify.http deleted file mode 100644 index bc3959b..0000000 --- a/app/server/src/modules/api/v2/account/verify.http +++ /dev/null @@ -1,2 +0,0 @@ -# Register -GET http://localhost:2024/api/v2/account/verify?id=IvvBsJWFOl8jZgz8&token=HWHXNyDqi0cGnSIibmdJKZ7izcvTZexL \ No newline at end of file diff --git a/app/server/src/modules/api/v2/admin/account/list.http b/app/server/src/modules/api/v2/admin/account/list.http deleted file mode 100644 index c0a2c5e..0000000 --- a/app/server/src/modules/api/v2/admin/account/list.http +++ /dev/null @@ -1,2 +0,0 @@ -# Get list -GET http://localhost:2024/api/v2/admin/account/list diff --git a/app/server/src/modules/api/v2/admin/account/list.request.ts b/app/server/src/modules/api/v2/admin/account/list.request.ts deleted file mode 100644 index ae8d46f..0000000 --- a/app/server/src/modules/api/v2/admin/account/list.request.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { RequestType, RequestMethod } from "@oh/utils"; -import { - getAccountList, - isAccountAdminValid, -} from "shared/utils/account.utils.ts"; - -export const listGetRequest: RequestType = { - method: RequestMethod.GET, - pathname: "/list", - func: async (request, url) => { - const status = await isAccountAdminValid(request); - - if (status !== 200) - return Response.json( - { status }, - { - status, - }, - ); - const accountList = (await getAccountList()).map((account) => ({ - accountId: account.accountId, - username: account.username, - email: account.email, - })); - - return Response.json( - { status: 200, data: { accountList } }, - { - status: 200, - }, - ); - }, -}; diff --git a/app/server/src/modules/api/v2/admin/account/main.ts b/app/server/src/modules/api/v2/admin/account/main.ts deleted file mode 100644 index c6d5c3a..0000000 --- a/app/server/src/modules/api/v2/admin/account/main.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { RequestType, getPathRequestList } from "@oh/utils"; - -import { listGetRequest } from "./list.request.ts"; - -export const accountRequestList: RequestType[] = getPathRequestList({ - requestList: [listGetRequest], - pathname: "/account", -}); diff --git a/app/server/src/modules/api/v2/admin/admin.http b/app/server/src/modules/api/v2/admin/admin.http deleted file mode 100644 index b01a2ad..0000000 --- a/app/server/src/modules/api/v2/admin/admin.http +++ /dev/null @@ -1,28 +0,0 @@ -# Get list -GET http://localhost:2024/api/v2/admin -token: x7GzeFJvLuNWUCCas2EBLB7an5rsJfpF3jOInSOG0sa17AGQAszHFwBVKsTJjWIh -sessionId: 3be671dd-bfb8-4cb0-bc68-bfe09b036f2e - -### - -# Add new -POST http://localhost:2024/api/v2/admin -token: dUpAHJIfu5n8MDVJasedEqfMDoqCaXRqsGNyhoEYKiR3WIM70goKHUsWkoPHmkfn -sessionId: 447321fd-361e-4697-9b36-1eba13fb16da -Content-Type: application/json - -{ - "email": "pagoru@gmail.com" -} -### - -# Remove new -DELETE http://localhost:2024/api/v2/admin -token: x7GzeFJvLuNWUCCas2EBLB7an5rsJfpF3jOInSOG0sa17AGQAszHFwBVKsTJjWIh -sessionId: 3be671dd-bfb8-4cb0-bc68-bfe09b036f2e -Content-Type: application/json - -{ - "email": "pagoru@gmail.com" -} -### \ No newline at end of file diff --git a/app/server/src/modules/api/v2/admin/admin.request.ts b/app/server/src/modules/api/v2/admin/admin.request.ts deleted file mode 100644 index 44fd8de..0000000 --- a/app/server/src/modules/api/v2/admin/admin.request.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { RequestType, RequestMethod } from "@oh/utils"; -import { - getAccountFromRequest, - getAccountList, - getAdminList, - isAccountAdminValid, - isAccountAuthValid, -} from "shared/utils/account.utils.ts"; -import { System } from "modules/system/main.ts"; - -export const accountGetRequest: RequestType = { - method: RequestMethod.GET, - pathname: "", - func: async (request, url) => { - const status = await isAccountAdminValid(request); - - if (status !== 200) - return Response.json( - { status }, - { - status, - }, - ); - const adminList = (await getAdminList()).map((account) => ({ - accountId: account.accountId, - username: account.username, - email: account.email, - })); - - return Response.json( - { status: 200, data: { adminList } }, - { - status: 200, - }, - ); - }, -}; - -export const accountPostRequest: RequestType = { - method: RequestMethod.POST, - pathname: "", - func: async (request, url) => { - //first time when admin account doesn't exist - if (!(await getAdminList()).length) { - const status = await isAccountAuthValid(request); - if (status !== 200) - return Response.json( - { status }, - { - status, - }, - ); - - const account = await getAccountFromRequest(request); - - //assign current user as admin - await System.db.set(["accounts", account.accountId], { - ...account, - isAdmin: true, - }); - - return Response.json( - { - status: 200, - }, - { - status: 200, - }, - ); - } - - const status = await isAccountAdminValid(request); - - if (status !== 200) - return Response.json( - { status }, - { - status, - }, - ); - let { email } = await request.json(); - - if (!email) - return Response.json( - { status: 400 }, - { - status: 400, - }, - ); - - const accountFound = (await getAccountList()).find( - (account) => account?.email === email, - ); - - if (!accountFound) - return Response.json( - { status: 400, message: "Account not found!" }, - { - status: 400, - }, - ); - - //assign current user as admin - await System.db.set(["accounts", accountFound.accountId], { - ...accountFound, - isAdmin: true, - }); - - return Response.json( - { - status: 200, - }, - { - status: 200, - }, - ); - }, -}; -export const accountDeleteRequest: RequestType = { - method: RequestMethod.DELETE, - pathname: "", - func: async (request, url) => { - const status = await isAccountAdminValid(request); - - if (status !== 200) - return Response.json( - { status }, - { - status, - }, - ); - let { email } = await request.json(); - - if (!email) - return Response.json( - { status: 400 }, - { - status: 400, - }, - ); - - const accountFound = (await getAccountList()).find( - (account) => account?.email === email, - ); - - if (!accountFound) - return Response.json( - { status: 400, message: "Account not found!" }, - { - status: 400, - }, - ); - - delete accountFound.isAdmin; - - await System.db.set(["accounts", accountFound.accountId], accountFound); - - return Response.json( - { - status: 200, - }, - { - status: 200, - }, - ); - }, -}; diff --git a/app/server/src/modules/api/v2/admin/main.ts b/app/server/src/modules/api/v2/admin/main.ts deleted file mode 100644 index 3b8ec95..0000000 --- a/app/server/src/modules/api/v2/admin/main.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { RequestType, getPathRequestList } from "@oh/utils"; - -import { - accountGetRequest, - accountPostRequest, - accountDeleteRequest, -} from "./admin.request.ts"; -import { updateGetRequest } from "./update.request.ts"; -import { accountRequestList } from "./account/main.ts"; -import { tokensRequestList } from "./tokens/main.ts"; - -export const adminRequestList: RequestType[] = getPathRequestList({ - requestList: [ - accountGetRequest, - accountPostRequest, - accountDeleteRequest, - updateGetRequest, - - ...accountRequestList, - ...tokensRequestList, - ], - pathname: "/admin", -}); diff --git a/app/server/src/modules/api/v2/admin/tokens/generate.http b/app/server/src/modules/api/v2/admin/tokens/generate.http deleted file mode 100644 index f892355..0000000 --- a/app/server/src/modules/api/v2/admin/tokens/generate.http +++ /dev/null @@ -1,10 +0,0 @@ -# POST -POST http://localhost:20240/api/v2/admin/tokens/generate -Content-Type: application/json - -{ - "api": "a", - "service": "ONET" -} - -### \ No newline at end of file diff --git a/app/server/src/modules/api/v2/admin/tokens/generate.request.ts b/app/server/src/modules/api/v2/admin/tokens/generate.request.ts deleted file mode 100644 index c1184d8..0000000 --- a/app/server/src/modules/api/v2/admin/tokens/generate.request.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { RequestType, RequestMethod } from "@oh/utils"; -import { isAccountAdminValid } from "shared/utils/account.utils.ts"; -import { System } from "modules/system/main.ts"; -import { isServiceValid } from "shared/utils/services.utils.ts"; - -export const postGenerateRequest: RequestType = { - method: RequestMethod.POST, - pathname: "/generate", - func: async (request) => { - const status = await isAccountAdminValid(request); - - if (status !== 200) - return Response.json( - { status }, - { - status, - }, - ); - - let { api, service } = await request.json(); - - if (!api || !service || !isServiceValid(service)) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - const data = await System.tokens.generateKey(service, api); - - return Response.json( - { status: 200, data }, - { - status: 200, - }, - ); - }, -}; diff --git a/app/server/src/modules/api/v2/admin/tokens/list.http b/app/server/src/modules/api/v2/admin/tokens/list.http deleted file mode 100644 index 0556265..0000000 --- a/app/server/src/modules/api/v2/admin/tokens/list.http +++ /dev/null @@ -1,4 +0,0 @@ -# Get list -GET http://localhost:20240/api/v2/admin/tokens/list - -### \ No newline at end of file diff --git a/app/server/src/modules/api/v2/admin/tokens/list.request.ts b/app/server/src/modules/api/v2/admin/tokens/list.request.ts deleted file mode 100644 index 7176930..0000000 --- a/app/server/src/modules/api/v2/admin/tokens/list.request.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { RequestType, RequestMethod } from "@oh/utils"; -import { isAccountAdminValid } from "shared/utils/account.utils.ts"; -import { Service } from "shared/enums/services.enums.ts"; - -export const getListRequest: RequestType = { - method: RequestMethod.GET, - pathname: "/list", - func: async (request) => { - const status = await isAccountAdminValid(request); - - if (status !== 200) - return Response.json( - { status }, - { - status, - }, - ); - - return Response.json( - { - status: 200, - data: { - list: Object.values(Service).filter( - (value) => typeof value === "string", - ), - }, - }, - { - status: 200, - }, - ); - }, -}; diff --git a/app/server/src/modules/api/v2/admin/tokens/main.ts b/app/server/src/modules/api/v2/admin/tokens/main.ts deleted file mode 100644 index 9939002..0000000 --- a/app/server/src/modules/api/v2/admin/tokens/main.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { RequestType, getPathRequestList } from "@oh/utils"; -import { postGenerateRequest } from "./generate.request.ts"; -import { getListRequest } from "./list.request.ts"; - -export const tokensRequestList: RequestType[] = getPathRequestList({ - requestList: [postGenerateRequest, getListRequest], - pathname: "/tokens", -}); diff --git a/app/server/src/modules/api/v2/at/main.ts b/app/server/src/modules/api/v2/at/main.ts deleted file mode 100644 index 20487b8..0000000 --- a/app/server/src/modules/api/v2/at/main.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { RequestType, getPathRequestList } from "@oh/utils"; - -import { postCreateRequest } from "./create.request.ts"; - -export const atRequestList: RequestType[] = getPathRequestList({ - requestList: [postCreateRequest], - pathname: "/at", -}); diff --git a/app/server/src/modules/api/v2/onet/get.request.ts b/app/server/src/modules/api/v2/onet/get.request.ts deleted file mode 100644 index 8fe58b2..0000000 --- a/app/server/src/modules/api/v2/onet/get.request.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { RequestMethod, RequestType } from "@oh/utils"; -import { System } from "modules/system/main.ts"; -import { Service } from "shared/enums/services.enums.ts"; - -export const getRequest: RequestType = { - method: RequestMethod.GET, - pathname: "", - func: async (request: Request, url) => { - const api = await System.tokens.getApiUrl(Service.ONET); - - return Response.json( - { - status: 200, - data: { - api, - }, - }, - { - status: 200, - }, - ); - }, -}; diff --git a/app/server/src/modules/api/v2/onet/main.http b/app/server/src/modules/api/v2/onet/main.http deleted file mode 100644 index ed3cf90..0000000 --- a/app/server/src/modules/api/v2/onet/main.http +++ /dev/null @@ -1,2 +0,0 @@ -# -GET http://localhost:2024/api/v2/onet diff --git a/app/server/src/modules/api/v2/onet/main.ts b/app/server/src/modules/api/v2/onet/main.ts deleted file mode 100644 index 24ce53a..0000000 --- a/app/server/src/modules/api/v2/onet/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { RequestType, getPathRequestList } from "@oh/utils"; - -import { serverRequest } from "./server.request.ts"; -import { validateAccountRequest } from "./validate-account.request.ts"; -import { getRequest } from "./get.request.ts"; - -export const onetRequestList: RequestType[] = getPathRequestList({ - requestList: [getRequest, serverRequest, validateAccountRequest], - pathname: "/onet", -}); diff --git a/app/server/src/modules/api/v2/onet/server.http b/app/server/src/modules/api/v2/onet/server.http deleted file mode 100644 index 9e1b1bc..0000000 --- a/app/server/src/modules/api/v2/onet/server.http +++ /dev/null @@ -1,9 +0,0 @@ -# -POST https://auth.openhotel.club/api/v2/onet/server -onet-key: wCqgvXz8El5AxhDv5Bub7smjZQs2kynFmkm0apZqKdSJ9C4Vir8jZHNSREPV8IFH - -{ - "serverId": "a", - "token": "b", - "ip": "172.25.0.3" -} \ No newline at end of file diff --git a/app/server/src/modules/api/v2/onet/server.request.ts b/app/server/src/modules/api/v2/onet/server.request.ts deleted file mode 100644 index c18aff6..0000000 --- a/app/server/src/modules/api/v2/onet/server.request.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { RequestMethod, RequestType } from "@oh/utils"; -import { System } from "modules/system/main.ts"; -import { Service } from "shared/enums/services.enums.ts"; - -export const serverRequest: RequestType = { - method: RequestMethod.POST, - pathname: "/server", - func: async (request: Request, url) => { - if (!(await System.tokens.isValidRequest(request, Service.ONET))) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - const { serverId, token, ip } = await request.json(); - - if (!serverId || !token || !ip) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - if (!(await System.servers.isValid(serverId, token, ip))) - return Response.json( - { status: 404 }, - { - status: 404, - }, - ); - - const serverData = await System.servers.getServerData(serverId); - - return Response.json( - { - status: 200, - data: { - hostname: serverData.hostname, - }, - }, - { - status: 200, - }, - ); - }, -}; diff --git a/app/server/src/modules/api/v2/onet/validate-account.request.ts b/app/server/src/modules/api/v2/onet/validate-account.request.ts deleted file mode 100644 index 5ec7ef5..0000000 --- a/app/server/src/modules/api/v2/onet/validate-account.request.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { RequestType, RequestMethod } from "@oh/utils"; -import { System } from "modules/system/main.ts"; -import { Session } from "shared/types/session.types.ts"; -import { Service } from "shared/enums/services.enums.ts"; - -export const validateAccountRequest: RequestType = { - method: RequestMethod.POST, - pathname: "/validate-account", - func: async (request: Request, url) => { - if (!(await System.tokens.isValidRequest(request, Service.ONET))) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - const { serverId, token, ip, accountId } = await request.json(); - - if (!serverId || !token || !ip || !accountId) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - if (!(await System.servers.isValid(serverId, token, ip))) - return Response.json( - { status: 404 }, - { - status: 404, - }, - ); - - // get active session - const serverSession: Session = await System.db.get([ - "serverSessionByAccount", - accountId, - ]); - - //user is not connected or not connected to the current server - if (!serverSession || serverSession?.serverIp !== ip) - return Response.json( - { status: 401 }, - { - status: 401, - }, - ); - - return Response.json( - { - status: 200, - }, - { - status: 200, - }, - ); - }, -}; diff --git a/app/server/src/modules/api/v2/server/claim-session.http b/app/server/src/modules/api/v2/server/claim-session.http deleted file mode 100644 index dbc8aa5..0000000 --- a/app/server/src/modules/api/v2/server/claim-session.http +++ /dev/null @@ -1,13 +0,0 @@ -# Claim Session -POST http://localhost:2024/api/v2/server/claim-session -Content-Type: application/json -server-id: 3bf6c370-6011-4c0e-a99d-a9ba0aad7413 -token: lSW0AZBg3frHtNm12NNau7cEwvCBDk0F - -{ - "ticketId": "9a17faeb-d037-4552-b9b7-35a6b8378fb6", - "ticketKey": "THIS_IS_A_PRIVATE_TOKEN", - - "sessionId": "27b185b9-1c0a-4f0b-b3b8-ce8b73882551", - "token": "PSMKAA2KhxuYEAUVo4frSkNqoc1eF7u1JfwpyhoNN0n02omeS4mZkAZEJdZjCxlI" -} \ No newline at end of file diff --git a/app/server/src/modules/api/v2/server/claim-session.request.ts b/app/server/src/modules/api/v2/server/claim-session.request.ts deleted file mode 100644 index d6c26bf..0000000 --- a/app/server/src/modules/api/v2/server/claim-session.request.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { - RequestType, - RequestMethod, - getIpFromRequest, - getRandomString, -} from "@oh/utils"; -import { System } from "modules/system/main.ts"; -import * as bcrypt from "bcrypt"; -import { SERVER_SESSION_EXPIRE_TIME } from "shared/consts/session.consts.ts"; -import { Session } from "shared/types/session.types.ts"; - -export const claimSessionRequest: RequestType = { - method: RequestMethod.POST, - pathname: "/claim-session", - func: async (request, url) => { - if (!(await System.servers.isRequestValid(request))) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - let { ticketId, ticketKey, sessionId, token } = await request.json(); - - if (!ticketId || !ticketKey || !sessionId || !token) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - const ticket = await System.db.get(["tickets", ticketId]); - - if (!ticket || !ticket.isUsed) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - const ticketResult = bcrypt.compareSync(ticketKey, ticket.ticketKeyHash); - if (!ticketResult) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - const accountBySession = await System.db.get([ - "accountsBySession", - sessionId, - ]); - - if (!accountBySession) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - const account = await System.db.get(["accounts", accountBySession]); - - if (!account) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - const serverSession: Session = await System.db.get([ - "serverSessionByAccount", - account.accountId, - ]); - - if (!serverSession) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - const result = bcrypt.compareSync(token, account.tokenHash); - - if (!result) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - delete account.tokenHash; - - //destroy session token (but not refresh token) - await System.db.set(["accounts", account.accountId], account); - await System.db.delete(["accountsBySession", sessionId]); - await System.db.delete(["ticketBySession", sessionId]); - - //destroy ticket - await System.db.delete(["tickets", ticketId]); - - const serverSessionToken = getRandomString(64); - - const serverId = request.headers.get("server-id"); - //save current server ip to verify identity on future petitions - const serverIp = getIpFromRequest(request); - const session: Session = { - ...serverSession, - serverIp, - serverId, - serverToken: bcrypt.hashSync(serverSessionToken, bcrypt.genSaltSync(8)), - claimed: true, - }; - await System.db.set( - ["serverSessionByAccount", account.accountId], - session, - { - expireIn: SERVER_SESSION_EXPIRE_TIME, - }, - ); - await System.sessions.checkAccountSession(account.accountId); - - return Response.json( - { - status: 200, - data: { - accountId: account.accountId, - username: account.username, - token: serverSessionToken, - }, - }, - { - status: 200, - }, - ); - }, -}; diff --git a/app/server/src/modules/api/v2/server/create-ticket.http b/app/server/src/modules/api/v2/server/create-ticket.http deleted file mode 100644 index ab73d35..0000000 --- a/app/server/src/modules/api/v2/server/create-ticket.http +++ /dev/null @@ -1,11 +0,0 @@ -# Create Ticket -POST http://localhost:2024/api/v2/server/create-ticket -Content-Type: application/json -server-id: 3bf6c370-6011-4c0e-a99d-a9ba0aad7413 -token: lSW0AZBg3frHtNm12NNau7cEwvCBDk0F - -{ - "ticketKey": "THIS_IS_A_PRIVATE_TOKEN", - "redirectUrl": "https://client.openhotel.club" -} - diff --git a/app/server/src/modules/api/v2/server/create-ticket.request.ts b/app/server/src/modules/api/v2/server/create-ticket.request.ts deleted file mode 100644 index 9759c01..0000000 --- a/app/server/src/modules/api/v2/server/create-ticket.request.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { RequestType, RequestMethod } from "@oh/utils"; -import { System } from "modules/system/main.ts"; -import * as bcrypt from "bcrypt"; -import { TICKET_EXPIRE_TIME } from "shared/consts/tickets.consts.ts"; - -export const createTicketRequest: RequestType = { - method: RequestMethod.POST, - pathname: "/create-ticket", - func: async (request: Request, url) => { - if (!(await System.servers.isRequestValid(request))) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - let { ticketKey, redirectUrl } = await request.json(); - - if (!ticketKey || !redirectUrl) - return Response.json( - { status: 400 }, - { - status: 400, - }, - ); - - const ticketId = crypto.randomUUID(); - await System.db.set( - ["tickets", ticketId], - { - ticketId, - ticketKeyHash: bcrypt.hashSync(ticketKey, bcrypt.genSaltSync(8)), - redirectUrl, - isUsed: false, - }, - { - expireIn: TICKET_EXPIRE_TIME, - }, - ); - - return Response.json({ - status: 200, - data: { - ticketId, - }, - }); - }, -}; diff --git a/app/server/src/modules/api/v2/server/main.ts b/app/server/src/modules/api/v2/server/main.ts deleted file mode 100644 index af8cb68..0000000 --- a/app/server/src/modules/api/v2/server/main.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { RequestType, getPathRequestList } from "@oh/utils"; - -import { claimSessionRequest } from "./claim-session.request.ts"; -import { createTicketRequest } from "./create-ticket.request.ts"; -import { registerRequest } from "./register.request.ts"; -import { validateRequest } from "./validate.request.ts"; - -export const serverRequestList: RequestType[] = getPathRequestList({ - requestList: [ - claimSessionRequest, - createTicketRequest, - registerRequest, - validateRequest, - ], - pathname: "/server", -}); diff --git a/app/server/src/modules/api/v2/server/register.http b/app/server/src/modules/api/v2/server/register.http deleted file mode 100644 index e73410f..0000000 --- a/app/server/src/modules/api/v2/server/register.http +++ /dev/null @@ -1,8 +0,0 @@ -# Claim Session -POST http://localhost:2024/api/v2/server/register -Content-Type: application/json - -{ - "version": "v0.0.0", - "ip": "http://a.localhost:99/a" -} \ No newline at end of file diff --git a/app/server/src/modules/api/v2/server/register.request.ts b/app/server/src/modules/api/v2/server/register.request.ts deleted file mode 100644 index 89c5c9b..0000000 --- a/app/server/src/modules/api/v2/server/register.request.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { - RequestType, - RequestMethod, - getIpFromRequest, - getIpFromUrl, - compareIps, - getRandomString, -} from "@oh/utils"; -import * as bcrypt from "bcrypt"; -import { System } from "modules/system/main.ts"; -import { Server } from "shared/types/server.types.ts"; - -export const registerRequest: RequestType = { - method: RequestMethod.POST, - pathname: "/register", - func: async (request: Request, url) => { - let { version, ip } = await request.json(); - - const { development, version: currentVersion } = System.getConfig(); - const isDevelopment = development || currentVersion === "development"; - - if (!version || (!isDevelopment && !ip)) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - const dnsIp = await getIpFromUrl(ip); - const requestIp = getIpFromRequest(request); - - if (!compareIps(dnsIp, requestIp)) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - const { hostname } = new URL(ip); - - //already exists - const serverByHostname = await System.db.get([ - "serverByHostname", - hostname, - ]); - - const serverId = serverByHostname ?? crypto.randomUUID(); - const token = getRandomString(32); - - await System.db.set(["servers", serverId], { - serverId, - hostname, - ip: requestIp, - tokenHash: bcrypt.hashSync(token, bcrypt.genSaltSync(8)), - } as Server); - - await System.db.set(["serverByHostname", hostname], serverId); - - return Response.json( - { - status: 200, - data: { - serverId, - token, - }, - }, - { - status: 200, - }, - ); - }, -}; diff --git a/app/server/src/modules/api/v2/server/validate.http b/app/server/src/modules/api/v2/server/validate.http deleted file mode 100644 index 11d5cb9..0000000 --- a/app/server/src/modules/api/v2/server/validate.http +++ /dev/null @@ -1,5 +0,0 @@ -# -GET http://localhost:2024/api/v2/server/validate -Content-Type: application/json -server-id: 3bf6c370-6011-4c0e-a99d-a9ba0aad7413 -token: lSW0AZBg3frHtNm12NNau7cEwvCBDk0F \ No newline at end of file diff --git a/app/server/src/modules/api/v2/server/validate.request.ts b/app/server/src/modules/api/v2/server/validate.request.ts deleted file mode 100644 index ada28fa..0000000 --- a/app/server/src/modules/api/v2/server/validate.request.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { RequestType, RequestMethod } from "@oh/utils"; -import { System } from "modules/system/main.ts"; - -export const validateRequest: RequestType = { - method: RequestMethod.GET, - pathname: "/validate", - func: async (request: Request, url) => { - if (!(await System.servers.isRequestValid(request))) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - return Response.json( - { - status: 200, - }, - { - status: 200, - }, - ); - }, -}; diff --git a/app/server/src/modules/api/v2/tokens/validate.http b/app/server/src/modules/api/v2/tokens/validate.http deleted file mode 100644 index e46d993..0000000 --- a/app/server/src/modules/api/v2/tokens/validate.http +++ /dev/null @@ -1,6 +0,0 @@ -# -GET http://localhost:20240/api/v2/tokens/validate -token-key: aaa -token-service: onet - -### \ No newline at end of file diff --git a/app/server/src/modules/api/v2/tokens/validate.request.ts b/app/server/src/modules/api/v2/tokens/validate.request.ts deleted file mode 100644 index b89156d..0000000 --- a/app/server/src/modules/api/v2/tokens/validate.request.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { RequestType, RequestMethod } from "@oh/utils"; -import { System } from "modules/system/main.ts"; - -export const getValidateRequest: RequestType = { - method: RequestMethod.GET, - pathname: "/validate", - func: async (request) => { - if (!(await System.tokens.isValidRequest(request))) - return Response.json( - { status: 403 }, - { - status: 403, - }, - ); - - return Response.json( - { status: 200 }, - { - status: 200, - }, - ); - }, -}; diff --git a/app/server/src/modules/api/v2/version.http b/app/server/src/modules/api/v2/version.http deleted file mode 100644 index 11a3921..0000000 --- a/app/server/src/modules/api/v2/version.http +++ /dev/null @@ -1,2 +0,0 @@ -# Version -GET http://localhost:2024/api/v2/version diff --git a/app/server/src/modules/api/v3/account/login.http b/app/server/src/modules/api/v3/account/login.http new file mode 100644 index 0000000..564db93 --- /dev/null +++ b/app/server/src/modules/api/v3/account/login.http @@ -0,0 +1,12 @@ +# +POST http://localhost:2024/api/v3/account/login +Content-Type: application/json + +{ + + "email": "pagoru@gmail.com", + "password": "123456Abc*", + "otpToken": "259461", + + "captchaId": "123" +} \ No newline at end of file diff --git a/app/server/src/modules/api/v3/account/login.request.ts b/app/server/src/modules/api/v3/account/login.request.ts new file mode 100644 index 0000000..adf1e89 --- /dev/null +++ b/app/server/src/modules/api/v3/account/login.request.ts @@ -0,0 +1,147 @@ +import { + RequestType, + RequestMethod, + getRandomString, + getIpFromRequest, +} from "@oh/utils"; +import { System } from "modules/system/main.ts"; +import * as bcrypt from "@da/bcrypt"; +import { RequestKind } from "shared/enums/request.enums.ts"; + +export const loginPostRequest: RequestType = { + method: RequestMethod.POST, + pathname: "/login", + kind: RequestKind.PUBLIC, + func: async (request: Request, url) => { + const { + email, + password, + otpToken, + // + captchaId, + } = await request.json(); + + if (!email || !password) + return Response.json( + { status: 403, message: "Email or password not valid!" }, + { + status: 403, + }, + ); + + const accountByEmail = await System.db.get(["accountsByEmail", email]); + + if (!accountByEmail) + return Response.json( + { status: 403, message: "Email or password not valid!" }, + { + status: 403, + }, + ); + const account = await System.db.get(["accounts", accountByEmail]); + if (!account) + return Response.json( + { status: 403, message: "Contact an administrator!" }, + { + status: 403, + }, + ); + + const result = bcrypt.compareSync(password, account.passwordHash); + + if (!result) + return Response.json( + { status: 403, message: "Email or password not valid!" }, + { + status: 403, + }, + ); + + const accountByVerifyId = await System.db.get([ + "accountsByVerifyId", + account.accountId, + ]); + if (accountByVerifyId) + return Response.json( + { status: 403, message: "Account is not verified!" }, + { + status: 403, + }, + ); + + const accountOTP = await System.db.get([ + "otpByAccountId", + account.accountId, + ]); + + let isValidOTP = true; + + if ( + accountOTP?.verified && + (!otpToken || !System.otp.verify(accountOTP.secret, otpToken)) + ) + isValidOTP = false; + + if (!(await System.captcha.verify(captchaId))) + return Response.json( + { status: isValidOTP ? 451 : 461, message: "Captcha is not valid!" }, + { + status: isValidOTP ? 451 : 461, + }, + ); + + if (!isValidOTP) + return Response.json( + { status: 441, message: "OTP is not valid!" }, + { + status: 441, + }, + ); + + const token = getRandomString(64); + const refreshToken = getRandomString(128); + + const userAgent = request.headers.get("user-agent"); + const ip = getIpFromRequest(request); + + const { + times: { accountTokenDays, accountRefreshTokenDays }, + } = System.getConfig(); + const expireInToken = accountTokenDays * 24 * 60 * 60 * 1000; + const expireInRefreshToken = accountRefreshTokenDays * 24 * 60 * 60 * 1000; + + await System.db.set( + ["accountsByToken", account.accountId], + { + userAgent, + ip, + tokenHash: bcrypt.hashSync(token, bcrypt.genSaltSync(8)), + }, + { + expireIn: expireInToken, + }, + ); + await System.db.set( + ["accountsByRefreshToken", account.accountId], + { + userAgent, + ip, + refreshTokenHash: bcrypt.hashSync(refreshToken, bcrypt.genSaltSync(8)), + }, + { expireIn: expireInRefreshToken }, + ); + + return Response.json( + { + status: 200, + data: { + accountId: account.accountId, + token, + refreshToken, + durations: [accountTokenDays, accountRefreshTokenDays], + }, + }, + { status: 200 }, + ); + }, +}; diff --git a/app/server/src/modules/api/v3/account/logout.request.ts b/app/server/src/modules/api/v3/account/logout.request.ts new file mode 100644 index 0000000..466c1d6 --- /dev/null +++ b/app/server/src/modules/api/v3/account/logout.request.ts @@ -0,0 +1,31 @@ +import { RequestType, RequestMethod } from "@oh/utils"; +import { System } from "modules/system/main.ts"; +import { hasRequestAccess } from "shared/utils/scope.utils.ts"; +import { RequestKind } from "shared/enums/request.enums.ts"; + +export const logoutPostRequest: RequestType = { + method: RequestMethod.POST, + pathname: "/logout", + kind: RequestKind.ACCOUNT, + func: async (request: Request, url) => { + if (!(await hasRequestAccess({ request }))) + return Response.json( + { + status: 403, + }, + { status: 403 }, + ); + + const account = await System.accounts.getFromRequest(request); + + await System.db.delete(["accountsByToken", account.accountId]); + await System.db.delete(["accountsByRefreshToken", account.accountId]); + + return Response.json( + { + status: 200, + }, + { status: 200 }, + ); + }, +}; diff --git a/app/server/src/modules/api/v3/account/main.ts b/app/server/src/modules/api/v3/account/main.ts new file mode 100644 index 0000000..491959a --- /dev/null +++ b/app/server/src/modules/api/v3/account/main.ts @@ -0,0 +1,23 @@ +import { RequestType, getPathRequestList } from "@oh/utils"; + +import { loginPostRequest } from "./login.request.ts"; +import { registerPostRequest } from "./registerRequest.ts"; +import { verifyGetRequest } from "./verify.request.ts"; +import { refreshGetRequest } from "./refresh.request.ts"; +import { logoutPostRequest } from "./logout.request.ts"; +import { otpRequestList } from "./otp/main.ts"; +import { miscRequestList } from "./misc/main.ts"; + +export const accountRequestList: RequestType[] = getPathRequestList({ + requestList: [ + loginPostRequest, + registerPostRequest, + verifyGetRequest, + refreshGetRequest, + logoutPostRequest, + + ...otpRequestList, + ...miscRequestList, + ], + pathname: "/account", +}); diff --git a/app/server/src/modules/api/v2/at/create.request.ts b/app/server/src/modules/api/v3/account/misc/bsky.request.ts similarity index 62% rename from app/server/src/modules/api/v2/at/create.request.ts rename to app/server/src/modules/api/v3/account/misc/bsky.request.ts index 6f4409f..c027e64 100644 --- a/app/server/src/modules/api/v2/at/create.request.ts +++ b/app/server/src/modules/api/v3/account/misc/bsky.request.ts @@ -1,24 +1,20 @@ import { RequestMethod, RequestType } from "@oh/utils"; import { System } from "modules/system/main.ts"; -import { - getAccountFromRequest, - isAccountAuthValid, -} from "shared/utils/account.utils.ts"; -import { Service } from "shared/enums/services.enums.ts"; import { PROTO_DID_REGEX } from "shared/consts/at.consts.ts"; +import { hasRequestAccess } from "shared/utils/scope.utils.ts"; +import { RequestKind } from "shared/enums/request.enums.ts"; -export const postCreateRequest: RequestType = { +export const bskyPostRequest: RequestType = { method: RequestMethod.POST, - pathname: "/create", + pathname: "/bsky", + kind: RequestKind.ACCOUNT, func: async (request: Request, url) => { - const status = await isAccountAuthValid(request); - - if (status !== 200) + if (!(await hasRequestAccess({ request }))) return Response.json( - { status }, { - status, + status: 403, }, + { status: 403 }, ); const { did } = await request.json(); @@ -30,11 +26,11 @@ export const postCreateRequest: RequestType = { status: 403, }, ); - const account = await getAccountFromRequest(request); + const account = await System.accounts.getFromRequest(request); await System.tokens.$fetch(RequestMethod.POST, "/create", Service.AT, { username: account.username, - did, + did: new RegExp(PROTO_DID_REGEX).exec(did)[0], }); return Response.json( diff --git a/app/server/src/modules/api/v3/account/misc/main.ts b/app/server/src/modules/api/v3/account/misc/main.ts new file mode 100644 index 0000000..1cfe1ee --- /dev/null +++ b/app/server/src/modules/api/v3/account/misc/main.ts @@ -0,0 +1,7 @@ +import { RequestType, getPathRequestList } from "@oh/utils"; +import { bskyPostRequest } from "./bsky.request.ts"; + +export const miscRequestList: RequestType[] = getPathRequestList({ + requestList: [bskyPostRequest], + pathname: "/misc", +}); diff --git a/app/server/src/modules/api/v2/account/otp/delete.request.ts b/app/server/src/modules/api/v3/account/otp/delete.request.ts similarity index 51% rename from app/server/src/modules/api/v2/account/otp/delete.request.ts rename to app/server/src/modules/api/v3/account/otp/delete.request.ts index fa5e590..a51e61d 100644 --- a/app/server/src/modules/api/v2/account/otp/delete.request.ts +++ b/app/server/src/modules/api/v3/account/otp/delete.request.ts @@ -1,26 +1,23 @@ import { RequestType, RequestMethod } from "@oh/utils"; -import { - getAccountFromRequest, - isAccountAuthValid, -} from "shared/utils/account.utils.ts"; import { System } from "modules/system/main.ts"; +import { hasRequestAccess } from "shared/utils/scope.utils.ts"; +import { RequestKind } from "shared/enums/request.enums.ts"; export const deleteRequest: RequestType = { method: RequestMethod.DELETE, pathname: "", + kind: RequestKind.ACCOUNT, func: async (request, url) => { - const status = await isAccountAuthValid(request); - - if (status !== 200) + if (!(await hasRequestAccess({ request }))) return Response.json( - { status }, + { status: 403 }, { - status, + status: 403, }, ); - const account = await getAccountFromRequest(request); - await System.db.delete(["accountOTP", account.accountId]); + const account = await System.accounts.getFromRequest(request); + await System.db.delete(["otpByAccountId", account.accountId]); return Response.json( { diff --git a/app/server/src/modules/api/v3/account/otp/get.request.ts b/app/server/src/modules/api/v3/account/otp/get.request.ts new file mode 100644 index 0000000..fd2fbd3 --- /dev/null +++ b/app/server/src/modules/api/v3/account/otp/get.request.ts @@ -0,0 +1,43 @@ +import { RequestType, RequestMethod } from "@oh/utils"; +import { System } from "modules/system/main.ts"; +import { hasRequestAccess } from "shared/utils/scope.utils.ts"; +import { RequestKind } from "shared/enums/request.enums.ts"; + +export const getRequest: RequestType = { + method: RequestMethod.GET, + pathname: "", + kind: RequestKind.ACCOUNT, + func: async (request, url) => { + if (!(await hasRequestAccess({ request }))) + return Response.json( + { status: 403 }, + { + status: 403, + }, + ); + + const account = await System.accounts.getFromRequest(request); + if (await System.otp.isOTPVerified(account.accountId)) + return Response.json( + { + status: 409, + }, + { + status: 409, + }, + ); + + const uri = await System.otp.generateOTP(account.accountId, account.email); + return Response.json( + { + status: 200, + data: { + uri, + }, + }, + { + status: 200, + }, + ); + }, +}; diff --git a/app/server/src/modules/api/v2/account/otp/main.ts b/app/server/src/modules/api/v3/account/otp/main.ts similarity index 52% rename from app/server/src/modules/api/v2/account/otp/main.ts rename to app/server/src/modules/api/v3/account/otp/main.ts index dac8dd6..4b84f9a 100644 --- a/app/server/src/modules/api/v2/account/otp/main.ts +++ b/app/server/src/modules/api/v3/account/otp/main.ts @@ -1,10 +1,9 @@ import { RequestType, getPathRequestList } from "@oh/utils"; - -import { postRequest } from "modules/api/v2/account/otp/post.request.ts"; -import { verifyRequest } from "./verify.request.ts"; import { deleteRequest } from "./delete.request.ts"; +import { getRequest } from "./get.request.ts"; +import { verifyGetRequest } from "./verify.request.ts"; export const otpRequestList: RequestType[] = getPathRequestList({ - requestList: [postRequest, verifyRequest, deleteRequest], + requestList: [getRequest, deleteRequest, verifyGetRequest], pathname: "/otp", }); diff --git a/app/server/src/modules/api/v2/account/otp/verify.request.ts b/app/server/src/modules/api/v3/account/otp/verify.request.ts similarity index 67% rename from app/server/src/modules/api/v2/account/otp/verify.request.ts rename to app/server/src/modules/api/v3/account/otp/verify.request.ts index 2d9c5ae..bbd0d5a 100644 --- a/app/server/src/modules/api/v2/account/otp/verify.request.ts +++ b/app/server/src/modules/api/v3/account/otp/verify.request.ts @@ -1,36 +1,36 @@ import { RequestType, RequestMethod } from "@oh/utils"; -import { - getAccountFromRequest, - isAccountAuthValid, -} from "shared/utils/account.utils.ts"; import { System } from "modules/system/main.ts"; +import { hasRequestAccess } from "shared/utils/scope.utils.ts"; +import { RequestKind } from "shared/enums/request.enums.ts"; -export const verifyRequest: RequestType = { +export const verifyGetRequest: RequestType = { method: RequestMethod.GET, pathname: "/verify", + kind: RequestKind.ACCOUNT, func: async (request, url) => { - const token = url.searchParams.get("token"); - if (!token) + if (!(await hasRequestAccess({ request }))) return Response.json( - { status: 400 }, + { status: 403 }, { - status: 400, + status: 403, }, ); - const status = await isAccountAuthValid(request); - - if (status !== 200) + const token = url.searchParams.get("token"); + if (!token) return Response.json( - { status }, + { status: 400 }, { - status, + status: 400, }, ); - const account = await getAccountFromRequest(request); + const account = await System.accounts.getFromRequest(request); - const accountOTP = await System.db.get(["accountOTP", account.accountId]); + const accountOTP = await System.db.get([ + "otpByAccountId", + account.accountId, + ]); if (!accountOTP) return Response.json( { @@ -63,7 +63,7 @@ export const verifyRequest: RequestType = { }, ); - await System.db.set(["accountOTP", account.accountId], { + await System.db.set(["otpByAccountId", account.accountId], { ...accountOTP, verified: true, }); diff --git a/app/server/src/modules/api/v3/account/refresh.http b/app/server/src/modules/api/v3/account/refresh.http new file mode 100644 index 0000000..c1c047a --- /dev/null +++ b/app/server/src/modules/api/v3/account/refresh.http @@ -0,0 +1,5 @@ +# +GET http://localhost:2024/api/v3/account/refresh +Content-Type: application/json +account-id: c1277daa-65a8-42fa-9c92-b47b3482deda +refresh-token: pzvMidyL28LWrT9lQDgkDhPu3PhGHtw0yc2EhTprxcWoCumuQZSK04ozN10w17ECpwOtiHRAVtNYzbe1FxkGpyLRNHZKWwe4lviL2F4U8KgzAmiVySJWFwt704EJuFqt diff --git a/app/server/src/modules/api/v3/account/refresh.request.ts b/app/server/src/modules/api/v3/account/refresh.request.ts new file mode 100644 index 0000000..c8b36ef --- /dev/null +++ b/app/server/src/modules/api/v3/account/refresh.request.ts @@ -0,0 +1,121 @@ +import { + RequestType, + RequestMethod, + getRandomString, + compareIps, + getIpFromRequest, +} from "@oh/utils"; +import { System } from "modules/system/main.ts"; +import * as bcrypt from "@da/bcrypt"; +import { RequestKind } from "shared/enums/request.enums.ts"; + +export const refreshGetRequest: RequestType = { + method: RequestMethod.GET, + pathname: "/refresh", + kind: RequestKind.ACCOUNT, + func: async (request: Request, url) => { + const accountId = request.headers.get("account-id"); + let refreshToken = request.headers.get("refresh-token"); + + if (!accountId || !refreshToken) + return Response.json( + { status: 403 }, + { + status: 403, + }, + ); + + const accountByRefreshToken = await System.db.get([ + "accountsByRefreshToken", + accountId, + ]); + + if (!accountByRefreshToken) + return Response.json( + { status: 403 }, + { + status: 403, + }, + ); + + const userAgent = request.headers.get("user-agent"); + const ip = getIpFromRequest(request); + + if ( + accountByRefreshToken.userAgent !== userAgent || + !compareIps(ip, accountByRefreshToken.ip) + ) + return Response.json( + { status: 403 }, + { + status: 403, + }, + ); + + const account = await System.db.get(["accounts", accountId]); + + if (!account) + return Response.json( + { status: 403 }, + { + status: 403, + }, + ); + + const result = bcrypt.compareSync( + refreshToken, + accountByRefreshToken.refreshTokenHash, + ); + + if (!result) + return Response.json( + { status: 403 }, + { + status: 403, + }, + ); + + const token = getRandomString(64); + refreshToken = getRandomString(128); + + const { + times: { accountTokenDays, accountRefreshTokenDays }, + } = System.getConfig(); + const expireInToken = accountTokenDays * 24 * 60 * 60 * 1000; + const expireInRefreshToken = accountRefreshTokenDays * 24 * 60 * 60 * 1000; + + await System.db.set( + ["accountsByToken", account.accountId], + { + userAgent, + ip, + tokenHash: bcrypt.hashSync(token, bcrypt.genSaltSync(8)), + }, + { + expireIn: expireInToken, + }, + ); + await System.db.set( + ["accountsByRefreshToken", account.accountId], + { + userAgent, + ip, + refreshTokenHash: bcrypt.hashSync(refreshToken, bcrypt.genSaltSync(8)), + }, + { expireIn: expireInRefreshToken }, + ); + + return Response.json( + { + status: 200, + data: { + accountId: account.accountId, + token, + refreshToken, + durations: [accountTokenDays, accountRefreshTokenDays], + }, + }, + { status: 200 }, + ); + }, +}; diff --git a/app/server/src/modules/api/v2/account/register.http b/app/server/src/modules/api/v3/account/register.http similarity index 80% rename from app/server/src/modules/api/v2/account/register.http rename to app/server/src/modules/api/v3/account/register.http index d907cc2..3d9aefe 100644 --- a/app/server/src/modules/api/v2/account/register.http +++ b/app/server/src/modules/api/v3/account/register.http @@ -1,5 +1,5 @@ # Register -POST http://localhost:2024/api/v2/account/register +POST http://localhost:2024/api/v3/account/register Content-Type: application/json { diff --git a/app/server/src/modules/api/v2/account/register.request.ts b/app/server/src/modules/api/v3/account/registerRequest.ts similarity index 76% rename from app/server/src/modules/api/v2/account/register.request.ts rename to app/server/src/modules/api/v3/account/registerRequest.ts index 16da418..656ffc6 100644 --- a/app/server/src/modules/api/v2/account/register.request.ts +++ b/app/server/src/modules/api/v3/account/registerRequest.ts @@ -1,16 +1,17 @@ import { RequestType, RequestMethod, getRandomString } from "@oh/utils"; import { System } from "modules/system/main.ts"; -import * as bcrypt from "bcrypt"; +import * as bcrypt from "@da/bcrypt"; import { PASSWORD_REGEX, EMAIL_REGEX, - ACCOUNT_EXPIRE_TIME, USERNAME_REGEX, } from "shared/consts/main.ts"; +import { RequestKind } from "shared/enums/request.enums.ts"; -export const registerRequest: RequestType = { +export const registerPostRequest: RequestType = { method: RequestMethod.POST, pathname: "/register", + kind: RequestKind.PUBLIC, func: async (request, url) => { const { email, username, password, rePassword, captchaId } = await request.json(); @@ -23,7 +24,7 @@ export const registerRequest: RequestType = { !rePassword ) return Response.json( - { status: 403 }, + { status: 403, message: "Some input is missing or invalid captcha!" }, { status: 403, }, @@ -36,7 +37,7 @@ export const registerRequest: RequestType = { password !== rePassword ) return Response.json( - { status: 400 }, + { status: 400, message: "Invalid email, username or password!" }, { status: 400, }, @@ -50,7 +51,7 @@ export const registerRequest: RequestType = { if (accountByUsername || accountByEmail) return Response.json( - { status: 409 }, + { status: 409, message: "Username or email already in use!" }, { status: 409, }, @@ -70,9 +71,12 @@ export const registerRequest: RequestType = { verifyUrl, `${verifyUrl}

`, ); + const { email: { enabled: isEmailVerificationEnabled }, + times: { accountWithoutVerificationDays }, } = System.getConfig(); + const expireIn = accountWithoutVerificationDays * 24 * 60 * 60 * 1000; // Every key related to the account is temporary until the account is verified or freed if not await System.db.set( @@ -82,23 +86,28 @@ export const registerRequest: RequestType = { username, email, passwordHash: bcrypt.hashSync(password, bcrypt.genSaltSync(8)), - - verifyId: isEmailVerificationEnabled ? verifyId : null, + createdAt: Date.now(), + }, + isEmailVerificationEnabled ? { expireIn } : {}, + ); + await System.db.set( + ["accountsByVerifyId", verifyId], + { + accountId, verifyTokensHash: isEmailVerificationEnabled ? bcrypt.hashSync(verifyToken, bcrypt.genSaltSync(8)) : null, }, - isEmailVerificationEnabled ? { expireIn: ACCOUNT_EXPIRE_TIME } : {}, + { + expireIn, + }, ); - await System.db.set(["accountsByVerifyId", verifyId], accountId, { - expireIn: ACCOUNT_EXPIRE_TIME, - }); await System.db.set( ["accountsByEmail", email], accountId, isEmailVerificationEnabled ? { - expireIn: ACCOUNT_EXPIRE_TIME, + expireIn, } : {}, ); @@ -107,7 +116,7 @@ export const registerRequest: RequestType = { accountId, isEmailVerificationEnabled ? { - expireIn: ACCOUNT_EXPIRE_TIME, + expireIn, } : {}, ); diff --git a/app/server/src/modules/api/v3/account/verify.http b/app/server/src/modules/api/v3/account/verify.http new file mode 100644 index 0000000..096785e --- /dev/null +++ b/app/server/src/modules/api/v3/account/verify.http @@ -0,0 +1,2 @@ +# Register +GET http://localhost:2024/api/v3/account/verify?id=IvvBsJWFOl8jZgz8&token=HWHXNyDqi0cGnSIibmdJKZ7izcvTZexL \ No newline at end of file diff --git a/app/server/src/modules/api/v2/account/verify.request.ts b/app/server/src/modules/api/v3/account/verify.request.ts similarity index 82% rename from app/server/src/modules/api/v2/account/verify.request.ts rename to app/server/src/modules/api/v3/account/verify.request.ts index 7d62bdf..bc17117 100644 --- a/app/server/src/modules/api/v2/account/verify.request.ts +++ b/app/server/src/modules/api/v3/account/verify.request.ts @@ -1,10 +1,12 @@ import { RequestType, RequestMethod } from "@oh/utils"; import { System } from "modules/system/main.ts"; -import * as bcrypt from "bcrypt"; +import * as bcrypt from "@da/bcrypt"; +import { RequestKind } from "shared/enums/request.enums.ts"; -export const verifyRequest: RequestType = { +export const verifyGetRequest: RequestType = { method: RequestMethod.GET, pathname: "/verify", + kind: RequestKind.PUBLIC, func: async (request, url) => { const id = url.searchParams.get("id"); const token = url.searchParams.get("token"); @@ -28,7 +30,7 @@ export const verifyRequest: RequestType = { ); const account = await System.db.get(["accounts", accountByVerifyId]); - if (!account || !account.verifyId || !account.verifyTokensHash) + if (!account) return Response.json( { status: 403 }, { @@ -36,7 +38,10 @@ export const verifyRequest: RequestType = { }, ); - const result = bcrypt.compareSync(token, account.verifyTokensHash); + const result = bcrypt.compareSync( + token, + accountByVerifyId.verifyTokensHash, + ); if (!result) return Response.json( @@ -46,9 +51,6 @@ export const verifyRequest: RequestType = { }, ); - delete account.verifyId; - delete account.verifyTokensHash; - await System.db.delete(["accountsByVerifyId", id]); await System.db.set(["accounts", account.accountId], account); diff --git a/app/server/src/modules/api/v3/admin/hotels.http b/app/server/src/modules/api/v3/admin/hotels.http new file mode 100644 index 0000000..fb0f0ff --- /dev/null +++ b/app/server/src/modules/api/v3/admin/hotels.http @@ -0,0 +1,15 @@ +# +GET http://localhost:2024/api/v3/admin/hotels?hostname=openhotel.club +Content-Type: application/json +account-id: c1277daa-65a8-42fa-9c92-b47b3482deda +token: xFvr9GWOoiXerVnwkrCnhtp7VyTKWerjuPv2yABpiYSa90xa5Rr2ORmKDPUKo3cJ + +### + +# +GET http://localhost:2024/api/v3/admin/hotels +Content-Type: application/json +account-id: c1277daa-65a8-42fa-9c92-b47b3482deda +token: xFvr9GWOoiXerVnwkrCnhtp7VyTKWerjuPv2yABpiYSa90xa5Rr2ORmKDPUKo3cJ + +### \ No newline at end of file diff --git a/app/server/src/modules/api/v3/admin/hotels.request.ts b/app/server/src/modules/api/v3/admin/hotels.request.ts new file mode 100644 index 0000000..dca8e71 --- /dev/null +++ b/app/server/src/modules/api/v3/admin/hotels.request.ts @@ -0,0 +1,46 @@ +import { RequestType, RequestMethod } from "@oh/utils"; +import { hasRequestAccess } from "shared/utils/scope.utils.ts"; +import { System } from "modules/system/main.ts"; +import { RequestKind } from "shared/enums/request.enums.ts"; + +export const hotelsGetRequest: RequestType = { + method: RequestMethod.GET, + pathname: "/hotels", + kind: RequestKind.ADMIN, + func: async (request, url) => { + if (!(await hasRequestAccess({ request, admin: true }))) + return Response.json( + { status: 403 }, + { + status: 403, + }, + ); + + const hostname = url.searchParams.get("hostname"); + + const hosts = await System.hosts.getList(); + + if (hostname) + return Response.json( + { + status: 200, + data: { + host: hosts.find((host) => host.hostname === hostname), + }, + }, + { + status: 200, + }, + ); + + return Response.json( + { + status: 200, + data: { + hosts, + }, + }, + { status: 200 }, + ); + }, +}; diff --git a/app/server/src/modules/api/v3/admin/main.http b/app/server/src/modules/api/v3/admin/main.http new file mode 100644 index 0000000..99cd9f9 --- /dev/null +++ b/app/server/src/modules/api/v3/admin/main.http @@ -0,0 +1,15 @@ +# +POST http://localhost:2024/api/v3/admin +Content-Type: application/json +account-id: c1277daa-65a8-42fa-9c92-b47b3482deda +token: tpOTecDoBDyUBX5a5KR97hMk2cBnQxORVVOSx1DvrOWbrXzqMG5JrbxhycB4sTEE + +### + +# +POST http://localhost:2024/api/v3/admin?accountId=c1277daa-65a8-42fa-9c92-b47b3482deda +Content-Type: application/json +account-id: c1277daa-65a8-42fa-9c92-b47b3482deda +token: xFvr9GWOoiXerVnwkrCnhtp7VyTKWerjuPv2yABpiYSa90xa5Rr2ORmKDPUKo3cJ + +### \ No newline at end of file diff --git a/app/server/src/modules/api/v3/admin/main.request.ts b/app/server/src/modules/api/v3/admin/main.request.ts new file mode 100644 index 0000000..1d89b82 --- /dev/null +++ b/app/server/src/modules/api/v3/admin/main.request.ts @@ -0,0 +1,97 @@ +import { RequestType, RequestMethod } from "@oh/utils"; +import { RequestKind } from "shared/enums/request.enums.ts"; +import { System } from "modules/system/main.ts"; +import { hasRequestAccess } from "shared/utils/scope.utils.ts"; + +export const adminPostRequest: RequestType = { + method: RequestMethod.POST, + pathname: "", + kind: RequestKind.ADMIN, + func: async (request: Request, url) => { + if (!(await hasRequestAccess({ request }))) + return Response.json( + { + status: 403, + }, + { status: 403 }, + ); + + const adminList = await System.admins.getList(); + + //if no admins, first to call the request, is admin + if (!adminList.length) { + const account = await System.accounts.getFromRequest(request); + await System.admins.set(account.accountId); + + return Response.json( + { + status: 200, + }, + { status: 200 }, + ); + } + + if (!(await hasRequestAccess({ request, admin: true }))) + return Response.json( + { + status: 403, + }, + { status: 403 }, + ); + + const accountId = url.searchParams.get("accountId"); + if ( + !accountId || + !(await System.accounts.get(accountId)) || + (await System.admins.get(accountId)) + ) + return Response.json( + { + status: 400, + }, + { status: 400 }, + ); + + await System.admins.set(accountId); + + return Response.json( + { + status: 200, + }, + { status: 200 }, + ); + }, +}; + +export const adminDeleteRequest: RequestType = { + method: RequestMethod.DELETE, + pathname: "", + kind: RequestKind.ADMIN, + func: async (request: Request, url) => { + if (!(await hasRequestAccess({ request, admin: true }))) + return Response.json( + { + status: 403, + }, + { status: 403 }, + ); + + const accountId = url.searchParams.get("accountId"); + if (!accountId || !(await System.admins.get(accountId))) + return Response.json( + { + status: 400, + }, + { status: 400 }, + ); + + await System.admins.remove(accountId); + + return Response.json( + { + status: 200, + }, + { status: 200 }, + ); + }, +}; diff --git a/app/server/src/modules/api/v3/admin/main.ts b/app/server/src/modules/api/v3/admin/main.ts new file mode 100644 index 0000000..010f550 --- /dev/null +++ b/app/server/src/modules/api/v3/admin/main.ts @@ -0,0 +1,24 @@ +import { RequestType, getPathRequestList } from "@oh/utils"; +import { updateGetRequest } from "./update.request.ts"; +import { adminPostRequest, adminDeleteRequest } from "./main.request.ts"; +import { + tokensDeleteRequest, + tokensGetRequest, + tokensPostRequest, +} from "./tokens.request.ts"; +import { hotelsGetRequest } from "./hotels.request.ts"; +import { usersGetRequest } from "./users.request.ts"; + +export const adminRequestList: RequestType[] = getPathRequestList({ + requestList: [ + adminPostRequest, + adminDeleteRequest, + updateGetRequest, + tokensDeleteRequest, + tokensGetRequest, + tokensPostRequest, + hotelsGetRequest, + usersGetRequest, + ], + pathname: "/admin", +}); diff --git a/app/server/src/modules/api/v3/admin/tokens.http b/app/server/src/modules/api/v3/admin/tokens.http new file mode 100644 index 0000000..3577485 --- /dev/null +++ b/app/server/src/modules/api/v3/admin/tokens.http @@ -0,0 +1,28 @@ +# +GET http://localhost:2024/api/v3/misc/tokens +account-id: c1277daa-65a8-42fa-9c92-b47b3482deda +token: tpOTecDoBDyUBX5a5KR97hMk2cBnQxORVVOSx1DvrOWbrXzqMG5JrbxhycB4sTEE +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0 + +### + +# +POST http://localhost:2024/api/v3/misc/tokens +account-id: c1277daa-65a8-42fa-9c92-b47b3482deda +token: tpOTecDoBDyUBX5a5KR97hMk2cBnQxORVVOSx1DvrOWbrXzqMG5JrbxhycB4sTEE +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0 +Content-Type: application/json + +{ + "label": "test" +} + +### + +# +DELETE http://localhost:2024/api/v3/misc/tokens?id=wp3kOBtv +account-id: c1277daa-65a8-42fa-9c92-b47b3482deda +token: tpOTecDoBDyUBX5a5KR97hMk2cBnQxORVVOSx1DvrOWbrXzqMG5JrbxhycB4sTEE +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0 + +### \ No newline at end of file diff --git a/app/server/src/modules/api/v3/admin/tokens.request.ts b/app/server/src/modules/api/v3/admin/tokens.request.ts new file mode 100644 index 0000000..4b8ac4b --- /dev/null +++ b/app/server/src/modules/api/v3/admin/tokens.request.ts @@ -0,0 +1,110 @@ +import { RequestType, RequestMethod } from "@oh/utils"; +import { RequestKind } from "shared/enums/request.enums.ts"; +import { hasRequestAccess } from "shared/utils/scope.utils.ts"; +import { System } from "modules/system/main.ts"; + +export const tokensGetRequest: RequestType = { + method: RequestMethod.GET, + pathname: "/tokens", + kind: RequestKind.ADMIN, + func: async (request, url) => { + if (!(await hasRequestAccess({ request, admin: true }))) + return Response.json( + { + status: 403, + }, + { status: 403 }, + ); + + const tokens = (await System.tokens.getList()).map(({ id, label }) => ({ + id, + label, + })); + + return Response.json( + { + status: 200, + data: { tokens }, + }, + { + status: 200, + }, + ); + }, +}; + +export const tokensPostRequest: RequestType = { + method: RequestMethod.POST, + pathname: "/tokens", + kind: RequestKind.ADMIN, + func: async (request, url) => { + if (!(await hasRequestAccess({ request, admin: true }))) + return Response.json( + { + status: 403, + }, + { status: 403 }, + ); + + const { label } = await request.json(); + + if (!label) + return Response.json( + { + status: 400, + }, + { status: 400 }, + ); + + const { id, token } = await System.tokens.generate(label); + + return Response.json( + { + status: 200, + data: { + id, + label, + token, + }, + }, + { + status: 200, + }, + ); + }, +}; + +export const tokensDeleteRequest: RequestType = { + method: RequestMethod.DELETE, + pathname: "/tokens", + kind: RequestKind.ADMIN, + func: async (request, url) => { + if (!(await hasRequestAccess({ request, admin: true }))) + return Response.json( + { + status: 403, + }, + { status: 403 }, + ); + + const id = url.searchParams.get("id"); + if (!id) + return Response.json( + { + status: 400, + }, + { status: 400 }, + ); + + await System.tokens.remove(id); + + return Response.json( + { + status: 200, + }, + { + status: 200, + }, + ); + }, +}; diff --git a/app/server/src/modules/api/v2/admin/update.http b/app/server/src/modules/api/v3/admin/update.http similarity index 69% rename from app/server/src/modules/api/v2/admin/update.http rename to app/server/src/modules/api/v3/admin/update.http index 78337f3..5cc873e 100644 --- a/app/server/src/modules/api/v2/admin/update.http +++ b/app/server/src/modules/api/v3/admin/update.http @@ -1,5 +1,5 @@ -# Update -GET http://localhost:2024/api/v2/admin/update +# +PATCH http://localhost:2024/api/v3/admin/update token: FbYcuj0qRTMwsvnb9d5I8d1l1KWrrUnFZrSWAxnBYRgtho1lpUFJDSk63hB79fIv sessionId: 3be671dd-bfb8-4cb0-bc68-bfe09b036f2e diff --git a/app/server/src/modules/api/v2/admin/update.request.ts b/app/server/src/modules/api/v3/admin/update.request.ts similarity index 74% rename from app/server/src/modules/api/v2/admin/update.request.ts rename to app/server/src/modules/api/v3/admin/update.request.ts index f9ea3a3..fa73cea 100644 --- a/app/server/src/modules/api/v2/admin/update.request.ts +++ b/app/server/src/modules/api/v3/admin/update.request.ts @@ -1,19 +1,19 @@ import { RequestType, RequestMethod, update } from "@oh/utils"; -import { isAccountAdminValid } from "shared/utils/account.utils.ts"; import { System } from "modules/system/main.ts"; +import { RequestKind } from "shared/enums/request.enums.ts"; +import { hasRequestAccess } from "shared/utils/scope.utils.ts"; export const updateGetRequest: RequestType = { - method: RequestMethod.GET, + method: RequestMethod.PATCH, pathname: "/update", + kind: RequestKind.ADMIN, func: async (request, url) => { - const status = await isAccountAdminValid(request); - - if (status !== 200) + if (!(await hasRequestAccess({ request, admin: true }))) return Response.json( - { status }, { - status, + status: 403, }, + { status: 403 }, ); const { version } = System.getEnvs(); diff --git a/app/server/src/modules/api/v3/admin/users.request.ts b/app/server/src/modules/api/v3/admin/users.request.ts new file mode 100644 index 0000000..8db7eb2 --- /dev/null +++ b/app/server/src/modules/api/v3/admin/users.request.ts @@ -0,0 +1,64 @@ +import { RequestType, RequestMethod } from "@oh/utils"; +import { hasRequestAccess } from "shared/utils/scope.utils.ts"; +import { RequestKind } from "shared/enums/request.enums.ts"; +import { System } from "modules/system/main.ts"; + +export const usersGetRequest: RequestType = { + method: RequestMethod.GET, + pathname: "/users", + kind: RequestKind.ADMIN, + func: async (request, url) => { + if (!(await hasRequestAccess({ request }))) + return Response.json( + { + status: 403, + }, + { status: 403 }, + ); + + const users = await Promise.all( + (await System.accounts.getList()).map(async (account) => ({ + accountId: account.accountId, + username: account.username, + email: account.email, + admin: Boolean(await System.admins.get(account.accountId)), + otp: await System.otp.isOTPVerified(account.accountId), + })), + ); + + const username = url.searchParams.get("username"); + if (username) + return Response.json( + { + status: 200, + data: { + user: users.find((account) => account.username === username), + }, + }, + { + status: 200, + }, + ); + + const accountId = url.searchParams.get("accountId"); + if (accountId) + return Response.json( + { + status: 200, + data: { + user: users.find((account) => account.accountId === accountId), + }, + }, + { + status: 200, + }, + ); + + return Response.json( + { status: 200, data: { users } }, + { + status: 200, + }, + ); + }, +}; diff --git a/app/server/src/modules/api/v3/hotels/check-license.http b/app/server/src/modules/api/v3/hotels/check-license.http new file mode 100644 index 0000000..95e5263 --- /dev/null +++ b/app/server/src/modules/api/v3/hotels/check-license.http @@ -0,0 +1,6 @@ +# +GET http://localhost:2024/api/v3/host/check-license +Content-Type: application/json +license-token: pSeQeZ0Vd9OQkqG9.gnRGJLot00UltWkoEknjsENAqjc4ScmG + +### diff --git a/app/server/src/modules/api/v3/hotels/check-license.request.ts b/app/server/src/modules/api/v3/hotels/check-license.request.ts new file mode 100644 index 0000000..8e6b274 --- /dev/null +++ b/app/server/src/modules/api/v3/hotels/check-license.request.ts @@ -0,0 +1,31 @@ +import { RequestType, RequestMethod } from "@oh/utils"; +import { System } from "modules/system/main.ts"; +import { RequestKind } from "shared/enums/request.enums.ts"; + +export const checkLicenseGetRequest: RequestType = { + method: RequestMethod.GET, + pathname: "/check-license", + kind: RequestKind.PUBLIC, + func: async (request, url) => { + const licenseToken = request.headers.get("license-token"); + + if (!licenseToken) + return Response.json( + { + status: 400, + }, + { status: 400 }, + ); + const valid = await System.licenses.verify(licenseToken); + + return Response.json( + { + status: 200, + data: { + valid, + }, + }, + { status: 200 }, + ); + }, +}; diff --git a/app/server/src/modules/api/v3/hotels/main.ts b/app/server/src/modules/api/v3/hotels/main.ts new file mode 100644 index 0000000..aee5a6e --- /dev/null +++ b/app/server/src/modules/api/v3/hotels/main.ts @@ -0,0 +1,8 @@ +import { RequestType, getPathRequestList } from "@oh/utils"; + +import { checkLicenseGetRequest } from "./check-license.request.ts"; + +export const hotelsRequestList: RequestType[] = getPathRequestList({ + requestList: [checkLicenseGetRequest], + pathname: "/hotels", +}); diff --git a/app/server/src/modules/api/v2/main.ts b/app/server/src/modules/api/v3/main.ts similarity index 56% rename from app/server/src/modules/api/v2/main.ts rename to app/server/src/modules/api/v3/main.ts index 4c40ef9..92a7d2c 100644 --- a/app/server/src/modules/api/v2/main.ts +++ b/app/server/src/modules/api/v3/main.ts @@ -1,22 +1,22 @@ import { RequestType, getPathRequestList } from "@oh/utils"; import { versionRequest } from "./version.request.ts"; + import { accountRequestList } from "./account/main.ts"; -import { serverRequestList } from "./server/main.ts"; -import { adminRequestList } from "./admin/main.ts"; -import { onetRequestList } from "./onet/main.ts"; +import { hotelsRequestList } from "./hotels/main.ts"; +import { userRequestList } from "./user/main.ts"; import { tokensRequestList } from "./tokens/main.ts"; -import { atRequestList } from "./at/main.ts"; +import { adminRequestList } from "./admin/main.ts"; -export const requestV2List: RequestType[] = getPathRequestList({ +export const requestV3List: RequestType[] = getPathRequestList({ requestList: [ versionRequest, + ...accountRequestList, - ...serverRequestList, - ...adminRequestList, - ...onetRequestList, + ...hotelsRequestList, + ...userRequestList, ...tokensRequestList, - ...atRequestList, + ...adminRequestList, ], - pathname: "/api/v2", + pathname: "/api/v3", }); diff --git a/app/server/src/modules/api/v3/tokens/check.http b/app/server/src/modules/api/v3/tokens/check.http new file mode 100644 index 0000000..8280bf4 --- /dev/null +++ b/app/server/src/modules/api/v3/tokens/check.http @@ -0,0 +1,6 @@ +# +GET http://localhost:2024/api/v3/tokens/check +Content-Type: application/json +app-token: pSeQeZ0Vd9OQkqG9.gnRGJLot00UltWkoEknjsENAqjc4ScmG + +### diff --git a/app/server/src/modules/api/v3/tokens/check.request.ts b/app/server/src/modules/api/v3/tokens/check.request.ts new file mode 100644 index 0000000..70fdffd --- /dev/null +++ b/app/server/src/modules/api/v3/tokens/check.request.ts @@ -0,0 +1,31 @@ +import { RequestType, RequestMethod } from "@oh/utils"; +import { RequestKind } from "shared/enums/request.enums.ts"; +import { System } from "modules/system/main.ts"; + +export const checkGetRequest: RequestType = { + method: RequestMethod.GET, + pathname: "/check", + kind: RequestKind.PUBLIC, + func: async (request, url) => { + const appToken = request.headers.get("app-token"); + + if (!appToken) + return Response.json( + { + status: 400, + }, + { status: 400 }, + ); + const valid = await System.tokens.verify(appToken); + + return Response.json( + { + status: 200, + data: { + valid, + }, + }, + { status: 200 }, + ); + }, +}; diff --git a/app/server/src/modules/api/v2/tokens/main.ts b/app/server/src/modules/api/v3/tokens/main.ts similarity index 61% rename from app/server/src/modules/api/v2/tokens/main.ts rename to app/server/src/modules/api/v3/tokens/main.ts index e30cad6..cdec147 100644 --- a/app/server/src/modules/api/v2/tokens/main.ts +++ b/app/server/src/modules/api/v3/tokens/main.ts @@ -1,7 +1,7 @@ import { RequestType, getPathRequestList } from "@oh/utils"; -import { getValidateRequest } from "./validate.request.ts"; +import { checkGetRequest } from "./check.request.ts"; export const tokensRequestList: RequestType[] = getPathRequestList({ - requestList: [getValidateRequest], + requestList: [checkGetRequest], pathname: "/tokens", }); diff --git a/app/server/src/modules/api/v3/user/@me/connection/main.http b/app/server/src/modules/api/v3/user/@me/connection/main.http new file mode 100644 index 0000000..c2ef589 --- /dev/null +++ b/app/server/src/modules/api/v3/user/@me/connection/main.http @@ -0,0 +1,39 @@ +# +POST http://localhost:2024/api/v3/user/@me/connection +Content-Type: application/json +account-id: c1277daa-65a8-42fa-9c92-b47b3482deda +token: SUWszYh6xp7xFAv3hpy1fJwZisbzdcEs3PmUxvlqtaxhSdpjDzQWCDGmJDHkni81 +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0 + +{ + "state": "EbQVj1vhS6onZQM8gE0YKAQw6KwnLLuXvf41f7n0a5TAfwJVSI6hyYWr14r3w6b2EbQVj1vhS6onZQM8gE0YKAQw6KwnLLuXvf41f7n0a5TAfwJVSI6hyYWr14r3w6b2", + "scopes": [ + "onet.messages.read", + "onet.messages.write", + "onet.friends.add" + ], + "redirectUrl": "https://dev.openhotel.club/session" +} + +### + +# +GET http://localhost:2024/api/v3/user/@me/connection +account-id: c1277daa-65a8-42fa-9c92-b47b3482deda +token: PtKvZwl3CAwh8m798ERefzwmAcadxvBfEsynaCFh3lhEET6aYHwqebE6hOIadLLx + +### + +# +GET http://localhost:2024/api/v3/user/@me/connection?hostname=openhotel.club +account-id: c1277daa-65a8-42fa-9c92-b47b3482deda +token: xFvr9GWOoiXerVnwkrCnhtp7VyTKWerjuPv2yABpiYSa90xa5Rr2ORmKDPUKo3cJ + +### + +# +DELETE http://localhost:2024/api/v3/user/@me/connection?hostname=openhotel.club +account-id: c1277daa-65a8-42fa-9c92-b47b3482deda +token: xFvr9GWOoiXerVnwkrCnhtp7VyTKWerjuPv2yABpiYSa90xa5Rr2ORmKDPUKo3cJ + +### \ No newline at end of file diff --git a/app/server/src/modules/api/v3/user/@me/connection/main.request.ts b/app/server/src/modules/api/v3/user/@me/connection/main.request.ts new file mode 100644 index 0000000..a92c41b --- /dev/null +++ b/app/server/src/modules/api/v3/user/@me/connection/main.request.ts @@ -0,0 +1,177 @@ +import { RequestType, RequestMethod, getIpFromRequest } from "@oh/utils"; +import { Scope } from "shared/enums/scopes.enums.ts"; +import { hasRequestAccess } from "shared/utils/scope.utils.ts"; +import { System } from "modules/system/main.ts"; +import { RequestKind } from "shared/enums/request.enums.ts"; + +export const mainPostRequest: RequestType = { + method: RequestMethod.POST, + pathname: "", + kind: RequestKind.ACCOUNT, + func: async (request, url) => { + if (!(await hasRequestAccess({ request }))) + return Response.json( + { status: 403 }, + { + status: 403, + }, + ); + + const { state, scopes, redirectUrl } = await request.json(); + + if (!redirectUrl) + return Response.json( + { status: 400, message: "Missing inputs" }, + { + status: 400, + }, + ); + + //Check scopes + const validScopes: string[] = scopes.filter((scope) => + Object.values(Scope).includes(scope), + ); + if (validScopes.length !== scopes.length) + return Response.json( + { + status: 400, + message: `Invalid scopes!`, + }, + { + status: 400, + }, + ); + + let hostname = ""; + + try { + hostname = new URL(redirectUrl).hostname; + } catch (e) { + return Response.json( + { status: 400, message: "Redirect URL is not valid!" }, + { + status: 400, + }, + ); + } + + const account = await System.accounts.getFromRequest(request); + + const userAgent = request.headers.get("user-agent"); + const ip = getIpFromRequest(request); + + const { + connectionId, + redirectUrl: redirect, + token, + } = await System.connections.generate({ + accountId: account.accountId, + scopes, + userAgent, + ip, + hostname, + redirectUrl, + state, + }); + + return Response.json( + { + status: 200, + data: { + connectionId, + token, + redirectUrl: redirect, + }, + }, + { status: 200 }, + ); + }, +}; + +export const mainGetRequest: RequestType = { + method: RequestMethod.GET, + pathname: "", + kind: RequestKind.ACCOUNT, + func: async (request: Request, url) => { + if (!(await hasRequestAccess({ request }))) + return Response.json( + { + status: 403, + }, + { status: 403 }, + ); + + const account = await System.accounts.getFromRequest(request); + const hosts = await System.hosts.getListByAccountId(account.accountId); + + const hostname = url.searchParams.get("hostname"); + if (hostname) { + return Response.json( + { + status: 200, + data: { host: hosts.find((host) => host.hostname === hostname) }, + }, + { + status: 200, + }, + ); + } + + return Response.json( + { + status: 200, + data: { hosts }, + }, + { + status: 200, + }, + ); + }, +}; + +export const mainDeleteRequest: RequestType = { + method: RequestMethod.DELETE, + pathname: "", + kind: RequestKind.ACCOUNT, + func: async (request: Request, url) => { + if (!(await hasRequestAccess({ request }))) + return Response.json( + { + status: 403, + }, + { status: 403 }, + ); + + const account = await System.accounts.getFromRequest(request); + + const hostname = url.searchParams.get("hostname"); + if (!hostname) + return Response.json( + { + status: 400, + }, + { + status: 400, + }, + ); + + if (!(await System.connections.remove(account.accountId, hostname))) + return Response.json( + { + status: 400, + }, + { + status: 400, + }, + ); + + return Response.json( + { + status: 200, + }, + { + status: 200, + }, + ); + }, +}; diff --git a/app/server/src/modules/api/v3/user/@me/connection/main.ts b/app/server/src/modules/api/v3/user/@me/connection/main.ts new file mode 100644 index 0000000..3b9c4cc --- /dev/null +++ b/app/server/src/modules/api/v3/user/@me/connection/main.ts @@ -0,0 +1,18 @@ +import { RequestType, getPathRequestList } from "@oh/utils"; + +import { + mainDeleteRequest, + mainGetRequest, + mainPostRequest, +} from "./main.request.ts"; +import { pingGetRequest } from "./ping.request.ts"; + +export const connectionRequestList: RequestType[] = getPathRequestList({ + requestList: [ + mainPostRequest, + mainGetRequest, + mainDeleteRequest, + pingGetRequest, + ], + pathname: "/connection", +}); diff --git a/app/server/src/modules/api/v3/user/@me/connection/ping.http b/app/server/src/modules/api/v3/user/@me/connection/ping.http new file mode 100644 index 0000000..0357f94 --- /dev/null +++ b/app/server/src/modules/api/v3/user/@me/connection/ping.http @@ -0,0 +1,6 @@ +# +PATCH http://localhost:2024/api/v3/user/@me/connection/ping?connectionToken=0cb9c82b-5d92-410c-9ec1-7b609820e133 +account-id: c1277daa-65a8-42fa-9c92-b47b3482deda +token: PtKvZwl3CAwh8m798ERefzwmAcadxvBfEsynaCFh3lhEET6aYHwqebE6hOIadLLx + +### \ No newline at end of file diff --git a/app/server/src/modules/api/v3/user/@me/connection/ping.request.ts b/app/server/src/modules/api/v3/user/@me/connection/ping.request.ts new file mode 100644 index 0000000..30e8c2b --- /dev/null +++ b/app/server/src/modules/api/v3/user/@me/connection/ping.request.ts @@ -0,0 +1,44 @@ +import { RequestType, RequestMethod } from "@oh/utils"; +import { System } from "modules/system/main.ts"; +import { RequestKind } from "shared/enums/request.enums.ts"; +import { hasRequestAccess } from "shared/utils/scope.utils.ts"; + +export const pingGetRequest: RequestType = { + method: RequestMethod.PATCH, + pathname: "/ping", + kind: RequestKind.ACCOUNT, + func: async (request, url) => { + if (!(await hasRequestAccess({ request }))) + return Response.json( + { status: 403 }, + { + status: 403, + }, + ); + + const account = await System.accounts.getFromRequest(request); + const connectionId = url.searchParams.get("connectionId"); + + const pingResult = await System.connections.ping( + account.accountId, + connectionId, + ); + if (!pingResult) + return Response.json( + { status: 403 }, + { + status: 403, + }, + ); + + return Response.json( + { + status: 200, + data: { + estimatedNextPingIn: pingResult.estimatedNextPingIn, + }, + }, + { status: 200 }, + ); + }, +}; diff --git a/app/server/src/modules/api/v3/user/@me/email.request.ts b/app/server/src/modules/api/v3/user/@me/email.request.ts new file mode 100644 index 0000000..80b57e7 --- /dev/null +++ b/app/server/src/modules/api/v3/user/@me/email.request.ts @@ -0,0 +1,31 @@ +import { RequestMethod, RequestType } from "@oh/utils"; +import { hasRequestAccess } from "shared/utils/scope.utils.ts"; +import { RequestKind } from "shared/enums/request.enums.ts"; +import { System } from "modules/system/main.ts"; + +export const emailGetRequest: RequestType = { + method: RequestMethod.GET, + pathname: "/email", + kind: RequestKind.ACCOUNT, + func: async (request: Request, url) => { + if (!(await hasRequestAccess({ request }))) + return Response.json( + { + status: 403, + }, + { status: 403 }, + ); + + const account = await System.accounts.getFromRequest(request); + + return Response.json( + { + status: 200, + data: { + email: account.email, + }, + }, + { status: 200 }, + ); + }, +}; diff --git a/app/server/src/modules/api/v3/user/@me/license/main.http b/app/server/src/modules/api/v3/user/@me/license/main.http new file mode 100644 index 0000000..3bebecd --- /dev/null +++ b/app/server/src/modules/api/v3/user/@me/license/main.http @@ -0,0 +1,8 @@ +# +GET http://localhost:2024/api/v3/user/@me/license +Content-Type: application/json +account-id: c1277daa-65a8-42fa-9c92-b47b3482deda +token: AwARFG3Ibltm2SxN8WqqiMvtrfPUrd8WUNkJpyJVQEJVFfMzjPdjq0B2P67d7wQL +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0 + +### \ No newline at end of file diff --git a/app/server/src/modules/api/v3/user/@me/license/main.request.ts b/app/server/src/modules/api/v3/user/@me/license/main.request.ts new file mode 100644 index 0000000..732d061 --- /dev/null +++ b/app/server/src/modules/api/v3/user/@me/license/main.request.ts @@ -0,0 +1,32 @@ +import { RequestType, RequestMethod } from "@oh/utils"; +import { hasRequestAccess } from "shared/utils/scope.utils.ts"; +import { System } from "modules/system/main.ts"; +import { RequestKind } from "shared/enums/request.enums.ts"; + +export const mainGetRequest: RequestType = { + method: RequestMethod.GET, + pathname: "", + kind: RequestKind.ACCOUNT, + func: async (request: Request, url) => { + if (!(await hasRequestAccess({ request }))) + return Response.json( + { + status: 403, + }, + { status: 403 }, + ); + + const account = await System.accounts.getFromRequest(request); + const licenseToken = await System.licenses.generate(account.accountId); + + return Response.json( + { + status: 200, + data: { licenseToken }, + }, + { + status: 200, + }, + ); + }, +}; diff --git a/app/server/src/modules/api/v3/user/@me/license/main.ts b/app/server/src/modules/api/v3/user/@me/license/main.ts new file mode 100644 index 0000000..a45a5e9 --- /dev/null +++ b/app/server/src/modules/api/v3/user/@me/license/main.ts @@ -0,0 +1,8 @@ +import { RequestType, getPathRequestList } from "@oh/utils"; + +import { mainGetRequest } from "./main.request.ts"; + +export const licenseRequestList: RequestType[] = getPathRequestList({ + requestList: [mainGetRequest], + pathname: "/license", +}); diff --git a/app/server/src/modules/api/v3/user/@me/main.http b/app/server/src/modules/api/v3/user/@me/main.http new file mode 100644 index 0000000..05baaef --- /dev/null +++ b/app/server/src/modules/api/v3/user/@me/main.http @@ -0,0 +1,15 @@ +# +GET http://localhost:2024/api/v3/user/@me +account-id: c1277daa-65a8-42fa-9c92-b47b3482deda +token: tpOTecDoBDyUBX5a5KR97hMk2cBnQxORVVOSx1DvrOWbrXzqMG5JrbxhycB4sTEE +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0 + +### + +# +GET http://localhost:2024/api/v3/user/@me +connection-id: 97a59ecd-a440-4ec9-869e-0757b42bc0c2 +token: 6f0m3f6sRsFx2Gt8DGblvUncZBHTeDCQsIvCOcPLgUBXj7bAgvMRrhC6Z6cYXkml +license-token: 6HuprPIVybhHSPHg#Zxrq8TA45DZZe0Z92SXHDzROrcKM6uAF + +### \ No newline at end of file diff --git a/app/server/src/modules/api/v3/user/@me/main.request.ts b/app/server/src/modules/api/v3/user/@me/main.request.ts new file mode 100644 index 0000000..46e356b --- /dev/null +++ b/app/server/src/modules/api/v3/user/@me/main.request.ts @@ -0,0 +1,34 @@ +import { RequestMethod, RequestType } from "@oh/utils"; +import { hasRequestAccess } from "shared/utils/scope.utils.ts"; +import { RequestKind } from "shared/enums/request.enums.ts"; +import { System } from "modules/system/main.ts"; + +export const mainGetRequest: RequestType = { + method: RequestMethod.GET, + pathname: "", + kind: RequestKind.CONNECTION, + func: async (request: Request, url) => { + if (!(await hasRequestAccess({ request, scopes: [] }))) + return Response.json( + { + status: 403, + }, + { status: 403 }, + ); + + const account = await System.accounts.getFromRequest(request); + const admin = Boolean(await System.admins.get(account.accountId)); + + return Response.json( + { + status: 200, + data: { + accountId: account.accountId, + username: account.username, + ...(admin ? { admin } : {}), + }, + }, + { status: 200 }, + ); + }, +}; diff --git a/app/server/src/modules/api/v3/user/@me/main.ts b/app/server/src/modules/api/v3/user/@me/main.ts new file mode 100644 index 0000000..e13567d --- /dev/null +++ b/app/server/src/modules/api/v3/user/@me/main.ts @@ -0,0 +1,17 @@ +import { RequestType, getPathRequestList } from "@oh/utils"; +import { mainGetRequest } from "./main.request.ts"; +import { connectionRequestList } from "./connection/main.ts"; +import { scopesGetRequest } from "./scopes.request.ts"; +import { emailGetRequest } from "./email.request.ts"; +import { licenseRequestList } from "./license/main.ts"; + +export const meRequestList: RequestType[] = getPathRequestList({ + requestList: [ + mainGetRequest, + scopesGetRequest, + emailGetRequest, + ...connectionRequestList, + ...licenseRequestList, + ], + pathname: "/@me", +}); diff --git a/app/server/src/modules/api/v3/user/@me/scopes.http b/app/server/src/modules/api/v3/user/@me/scopes.http new file mode 100644 index 0000000..7bc1cd5 --- /dev/null +++ b/app/server/src/modules/api/v3/user/@me/scopes.http @@ -0,0 +1,13 @@ +# +GET http://localhost:2024/api/v3/user/@me/scopes +account-id: c1277daa-65a8-42fa-9c92-b47b3482deda +token: xFvr9GWOoiXerVnwkrCnhtp7VyTKWerjuPv2yABpiYSa90xa5Rr2ORmKDPUKo3cJ + +### + +# +GET http://localhost:2024/api/v3/user/@me/scopes +connection-id: 2c125d00-26b2-428f-bd29-b6027ad21962 +token: GPlqm3IP5LcMe1OadE5s9yjnZsRS0yKKTAtYsrMFittOFMl4U45NXybCA4h7E4zM + +### \ No newline at end of file diff --git a/app/server/src/modules/api/v3/user/@me/scopes.request.ts b/app/server/src/modules/api/v3/user/@me/scopes.request.ts new file mode 100644 index 0000000..ba5116b --- /dev/null +++ b/app/server/src/modules/api/v3/user/@me/scopes.request.ts @@ -0,0 +1,51 @@ +import { RequestMethod, RequestType } from "@oh/utils"; +import { hasRequestAccess } from "shared/utils/scope.utils.ts"; +import { RequestKind } from "shared/enums/request.enums.ts"; +import { System } from "modules/system/main.ts"; + +export const scopesGetRequest: RequestType = { + method: RequestMethod.GET, + pathname: "/scopes", + kind: RequestKind.CONNECTION, + func: async (request: Request, url) => { + if (!(await hasRequestAccess({ request, scopes: [] }))) + return Response.json( + { + status: 403, + }, + { status: 403 }, + ); + const connectionToken = request.headers.get("connection-token"); + + if (!connectionToken) + return Response.json( + { + status: 200, + data: { + scopes: ["*"], + }, + }, + { status: 200 }, + ); + + const connection = await System.connections.get(connectionToken); + + if (!connection) + return Response.json( + { + status: 403, + }, + { status: 403 }, + ); + + return Response.json( + { + status: 200, + data: { + scopes: connection.scopes, + }, + }, + { status: 200 }, + ); + }, +}; diff --git a/app/server/src/modules/api/v3/user/main.http b/app/server/src/modules/api/v3/user/main.http new file mode 100644 index 0000000..23041e3 --- /dev/null +++ b/app/server/src/modules/api/v3/user/main.http @@ -0,0 +1,15 @@ +# +GET http://localhost:2024/api/v3/user +Content-Type: application/json +account-id: c1277daa-65a8-42fa-9c92-b47b3482deda +token: tpOTecDoBDyUBX5a5KR97hMk2cBnQxORVVOSx1DvrOWbrXzqMG5JrbxhycB4sTEE + +### + +# +GET http://localhost:2024/api/v3/user?accountId=c1277daa-65a8-42fa-9c92-b47b3482deda +Content-Type: application/json +account-id: c1277daa-65a8-42fa-9c92-b47b3482deda +token: xFvr9GWOoiXerVnwkrCnhtp7VyTKWerjuPv2yABpiYSa90xa5Rr2ORmKDPUKo3cJ + +### diff --git a/app/server/src/modules/api/v3/user/main.ts b/app/server/src/modules/api/v3/user/main.ts new file mode 100644 index 0000000..cf0e482 --- /dev/null +++ b/app/server/src/modules/api/v3/user/main.ts @@ -0,0 +1,8 @@ +import { RequestType, getPathRequestList } from "@oh/utils"; + +import { meRequestList } from "./@me/main.ts"; + +export const userRequestList: RequestType[] = getPathRequestList({ + requestList: [...meRequestList], + pathname: "/user", +}); diff --git a/app/server/src/modules/api/v3/version.http b/app/server/src/modules/api/v3/version.http new file mode 100644 index 0000000..5944ee5 --- /dev/null +++ b/app/server/src/modules/api/v3/version.http @@ -0,0 +1,2 @@ +# Version +GET http://localhost:2024/api/v3/version diff --git a/app/server/src/modules/api/v2/version.request.ts b/app/server/src/modules/api/v3/version.request.ts similarity index 65% rename from app/server/src/modules/api/v2/version.request.ts rename to app/server/src/modules/api/v3/version.request.ts index 35cb215..f152aca 100644 --- a/app/server/src/modules/api/v2/version.request.ts +++ b/app/server/src/modules/api/v3/version.request.ts @@ -1,12 +1,14 @@ import { RequestType, RequestMethod } from "@oh/utils"; import { System } from "modules/system/main.ts"; +import { RequestKind } from "shared/enums/request.enums.ts"; export const versionRequest: RequestType = { method: RequestMethod.GET, pathname: "/version", + kind: RequestKind.PUBLIC, func: (request, url) => { return Response.json( - { version: System.getEnvs().version }, + { status: 200, data: { version: System.getEnvs().version } }, { status: 200 }, ); }, diff --git a/app/server/src/modules/system/accounts.ts b/app/server/src/modules/system/accounts.ts new file mode 100644 index 0000000..bfac4a7 --- /dev/null +++ b/app/server/src/modules/system/accounts.ts @@ -0,0 +1,28 @@ +import { System } from "modules/system/main.ts"; + +export const accounts = () => { + const getList = async () => + (await System.db.list({ prefix: ["accounts"] })).map(({ value }) => value); + + const getFromRequest = async ({ headers }: Request) => { + let accountId = headers.get("account-id"); + const connectionToken = headers.get("connection-token"); + + if (!accountId && connectionToken) { + const connection = await System.connections.get(connectionToken); + accountId = connection.accountId; + } + + return await get(accountId); + }; + + const get = async (accountId: string) => { + return await System.db.get(["accounts", accountId]); + }; + + return { + getList, + get, + getFromRequest, + }; +}; diff --git a/app/server/src/modules/system/admins.ts b/app/server/src/modules/system/admins.ts new file mode 100644 index 0000000..9795536 --- /dev/null +++ b/app/server/src/modules/system/admins.ts @@ -0,0 +1,27 @@ +import { System } from "modules/system/main.ts"; + +export const admins = () => { + const getList = async () => + (await System.db.list({ prefix: ["adminsByAccountId"] })).map( + ({ value }) => value, + ); + + const get = async (accountId: string) => { + return await System.db.get(["adminsByAccountId", accountId]); + }; + + const set = async (accountId: string) => { + await System.db.set(["adminsByAccountId", accountId], accountId); + }; + + const remove = async (accountId: string) => { + await System.db.delete(["adminsByAccountId", accountId]); + }; + + return { + getList, + get, + set, + remove, + }; +}; diff --git a/app/server/src/modules/system/api.ts b/app/server/src/modules/system/api.ts index 606a1c8..d1fc5ad 100644 --- a/app/server/src/modules/system/api.ts +++ b/app/server/src/modules/system/api.ts @@ -1,11 +1,15 @@ -import { requestV2List } from "modules/api/v2/main.ts"; import { appendCORSHeaders, getContentType, getCORSHeaders } from "@oh/utils"; import { System } from "./main.ts"; +import { requestV3List } from "modules/api/v3/main.ts"; +import { REQUEST_KIND_COLOR_MAP } from "shared/consts/request.consts.ts"; export const api = () => { const load = () => { - for (const request of requestV2List) - console.info(request.method, request.pathname); + for (const request of requestV3List) + console.log( + `%c${request.method} ${request.pathname}`, + `color: ${REQUEST_KIND_COLOR_MAP[request.kind]}`, + ); const { development, version, port } = System.getConfig(); const isDevelopment = development || version === "development"; @@ -51,7 +55,7 @@ export const api = () => { } } - const foundRequests = requestV2List.filter( + const foundRequests = requestV3List.filter( ($request) => // $request.method === method && $request.pathname === parsedUrl.pathname, diff --git a/app/server/src/modules/system/connections.ts b/app/server/src/modules/system/connections.ts new file mode 100644 index 0000000..f88bebe --- /dev/null +++ b/app/server/src/modules/system/connections.ts @@ -0,0 +1,214 @@ +import { System } from "modules/system/main.ts"; +import * as bcrypt from "@da/bcrypt"; +import { generateToken, getTokenData } from "@oh/utils"; +import { + RequestType, + RequestMethod, + getRandomString, + getIpFromRequest, +} from "@oh/utils"; +import { Scope } from "shared/enums/scopes.enums.ts"; + +type GenerateProps = { + accountId: string; + userAgent: string; + ip: string; + + hostname: string; + scopes: string[]; + redirectUrl: string; + + state: string; +}; + +export const connections = () => { + const generate = async ({ + accountId, + userAgent, + ip, + hostname, + scopes: unfilteredScopes, + redirectUrl, + state, + }: GenerateProps): Promise<{ + connectionId: string; + token: string; + redirectUrl: string; + }> => { + const connectionsByAccountId = await System.db.get([ + "connectionsByAccountId", + accountId, + ]); + //remove current connection if exists + if (connectionsByAccountId) + await System.db.delete([ + "connections", + connectionsByAccountId.connectionId, + ]); + + const { token, id: connectionId, tokenHash } = generateToken("con", 24, 32); + + const { + times: { connectionTokenMinutes }, + } = System.getConfig(); + const expireIn = connectionTokenMinutes * 60 * 1000; + + const scopes = unfilteredScopes.filter((scope) => + Object.values(Scope).includes(scope as Scope), + ); + + await System.db.set( + ["connections", connectionId], + { + userAgent, + ip, + hostname, + accountId, + tokenHash, + scopes, + redirectUrl, + }, + { + expireIn, + }, + ); + + await System.db.set( + ["connectionsByAccountId", accountId], + { + connectionId, + }, + { + expireIn, + }, + ); + + await System.db.set(["hostsByHostname", hostname, accountId], { + hostname, + updatedAt: Date.now(), + }); + await System.db.set(["hostsByAccountId", accountId, hostname], { + hostname, + scopes, + updatedAt: Date.now(), + }); + + if (!(await System.db.get(["hosts", hostname]))) + await System.db.set(["hosts", hostname], { + hostname, + createdAt: Date.now(), + }); + + console.log(scopes); + return { + connectionId, + token, + redirectUrl: + redirectUrl + + `?state=${state}&token=${token}${scopes?.length ? `&scopes=${scopes.join(",")}` : ""}`, + }; + }; + + const remove = async ( + accountId: string, + hostname: string, + ): Promise => { + if (!(await System.db.get(["hostsByHostname", hostname, accountId]))) + return false; + + const currentConnection = await System.db.get([ + "connectionsByAccountId", + accountId, + ]); + + if (currentConnection) { + const connection = await System.db.get([ + "connections", + currentConnection.connectionId, + ]); + if (connection.hostname === hostname) + await System.db.delete(["connections", currentConnection.connectionId]); + } + + await System.db.delete(["connectionsByAccountId", accountId]); + await System.db.delete(["hostsByHostname", hostname, accountId]); + await System.db.delete(["hostsByAccountId", accountId, hostname]); + + return true; + }; + + const verify = async ( + rawToken: string, + scopes: string[], + ): Promise => { + if (!rawToken) return false; + + const { id: connectionId, token } = getTokenData(rawToken); + if (!connectionId || !token) return false; + + const foundConnection = await System.db.get(["connections", connectionId]); + if (!foundConnection) return false; + + return ( + scopes.every((scope) => foundConnection.scopes.includes(scope)) && + bcrypt.compareSync(token, foundConnection.tokenHash) + ); + }; + + const ping = async ( + accountId: string, + connectionId: string, + ): Promise => { + const connectionsByAccountId = await System.db.get([ + "connectionsByAccountId", + accountId, + ]); + + if ( + !connectionId || + !connectionsByAccountId || + connectionsByAccountId.connectionId !== connectionId + ) + return null; + + const connection = await System.db.get(["connections", connectionId]); + if (!connection) return null; + + const { + times: { connectionTokenMinutes }, + } = System.getConfig(); + const expireIn = connectionTokenMinutes * 60 * 1000; + + await System.db.set(["connections", connectionId], connection, { + expireIn, + }); + + await System.db.set( + ["connectionsByAccountId", connection.accountId], + { + connectionId, + }, + { + expireIn, + }, + ); + + const estimatedNextPingIn = expireIn / 2; + + return { estimatedNextPingIn }; + }; + + const get = async (rawToken: string) => { + const { id } = getTokenData(rawToken); + console.log(id, rawToken); + return await System.db.get(["connections", id]); + }; + + return { + generate, + verify, + remove, + ping, + get, + }; +}; diff --git a/app/server/src/modules/system/hosts.ts b/app/server/src/modules/system/hosts.ts new file mode 100644 index 0000000..cffb230 --- /dev/null +++ b/app/server/src/modules/system/hosts.ts @@ -0,0 +1,61 @@ +import { System } from "modules/system/main.ts"; + +export const hosts = () => { + const getList = async () => + await Promise.all( + (await System.db.list({ prefix: ["hosts"] })).map(({ key }) => + get(key[1]), + ), + ); + + const get = async (hostname: string) => { + const host = await System.db.get(["hosts", hostname]); + const accounts = ( + await System.db.list({ + prefix: ["hostsByHostname", hostname], + }) + ).map(({ key }) => key[2]); + + return { + hostname, + accounts, + verified: host?.verified ?? false, + }; + }; + + const getListByAccountId = async (accountId: string) => { + const connectionByAccountId = await System.db.get([ + "connectionsByAccountId", + accountId, + ]); + let hostname = null; + if (connectionByAccountId) { + const connection = await System.db.get([ + "connections", + connectionByAccountId.connectionId, + ]); + hostname = connection?.hostname; + } + + return await Promise.all( + ( + await System.db.list({ + prefix: ["hostsByAccountId", accountId], + }) + ).map(async ({ value }) => { + const data = await get(value.hostname); + return { + ...value, + accounts: data.accounts.length, + isActive: value.hostname === hostname, + }; + }), + ); + }; + + return { + getList, + get, + getListByAccountId, + }; +}; diff --git a/app/server/src/modules/system/licenses.ts b/app/server/src/modules/system/licenses.ts new file mode 100644 index 0000000..00773c6 --- /dev/null +++ b/app/server/src/modules/system/licenses.ts @@ -0,0 +1,48 @@ +import { System } from "modules/system/main.ts"; +import * as bcrypt from "@da/bcrypt"; +import { generateToken, getTokenData } from "@oh/utils"; + +export const licenses = () => { + const generate = async (accountId: string): Promise => { + const { token, id, tokenHash } = generateToken("lic", 16, 32); + + const licenseId = await System.db.get(["licensesByAccountId", accountId]); + if (licenseId) await System.db.delete(["licenses", licenseId]); + + await System.db.set(["licensesByAccountId", accountId], id); + await System.db.set(["licenses", id], { + id, + tokenHash, + accountId, + updatedAt: Date.now(), + }); + + return token; + }; + + const remove = async (accountId: string) => { + const licenseId = await System.db.get(["licensesByAccountId", accountId]); + if (!licenseId) return; + + await System.db.delete(["licensesByAccountId", accountId]); + await System.db.delete(["licenses", licenseId]); + }; + + const verify = async (token: string): Promise => { + if (!token) return false; + + const { id: licenseId, token: licenseToken } = getTokenData(token); + if (!licenseId || !licenseToken) return false; + + const license = await System.db.get(["licenses", licenseId]); + if (!license) return false; + + return bcrypt.compareSync(licenseToken, license.tokenHash); + }; + + return { + generate, + verify, + remove, + }; +}; diff --git a/app/server/src/modules/system/main.ts b/app/server/src/modules/system/main.ts index fbb7c50..d24281e 100644 --- a/app/server/src/modules/system/main.ts +++ b/app/server/src/modules/system/main.ts @@ -4,11 +4,14 @@ import { getConfig as $getConfig, getDb, update } from "@oh/utils"; import { captcha } from "./captcha.ts"; import { email } from "./email.ts"; import { otp } from "./otp.ts"; -import { tasks } from "./tasks.ts"; -import { sessions } from "./sessions.ts"; import { CONFIG_DEFAULT } from "shared/consts/config.consts.ts"; -import { servers } from "./servers.ts"; -import { tokens } from "modules/system/tokens.ts"; +import { tokens } from "./tokens.ts"; +import { accounts } from "./accounts.ts"; +import { hosts } from "./hosts.ts"; +import { admins } from "./admins.ts"; +import { licenses } from "./licenses.ts"; +import { connections } from "./connections.ts"; +import { Scope } from "shared/enums/scopes.enums.ts"; export const System = (() => { let $config: ConfigTypes; @@ -18,10 +21,12 @@ export const System = (() => { const $captcha = captcha(); const $email = email(); const $otp = otp(); - const $tasks = tasks(); - const $sessions = sessions(); - const $servers = servers(); const $tokens = tokens(); + const $accounts = accounts(); + const $hosts = hosts(); + const $admins = admins(); + const $licenses = licenses(); + const $connections = connections(); let $db; const load = async (envs: Envs) => { @@ -29,7 +34,7 @@ export const System = (() => { $envs = envs; if ( - !$config.development && + $config.version !== "development" && (await update({ targetVersion: "latest", version: envs.version, @@ -42,12 +47,9 @@ export const System = (() => { $db = getDb({ pathname: `./${$config.database.filename}` }); - $tasks.load(); await $db.load(); await $email.load(); $api.load(); - - $sessions.load(); }; const getConfig = (): ConfigTypes => $config; @@ -65,9 +67,11 @@ export const System = (() => { captcha: $captcha, email: $email, otp: $otp, - tasks: $tasks, - sessions: $sessions, - servers: $servers, tokens: $tokens, + accounts: $accounts, + hosts: $hosts, + admins: $admins, + licenses: $licenses, + connections: $connections, }; })(); diff --git a/app/server/src/modules/system/otp.ts b/app/server/src/modules/system/otp.ts index 94f31c2..24b9888 100644 --- a/app/server/src/modules/system/otp.ts +++ b/app/server/src/modules/system/otp.ts @@ -4,7 +4,7 @@ import { System } from "modules/system/main.ts"; export const otp = () => { const $getTOTP = (email: string, otpSecret: string): OTPAuth.TOTP => new OTPAuth.TOTP({ - issuer: `openhotel${System.getConfig().development ? "::development" : ""}`, + issuer: `openhotel${System.getConfig().version === "development" ? "::development" : ""}`, label: email, algorithm: "SHA1", digits: 6, @@ -20,9 +20,32 @@ export const otp = () => { const verify = (otpSecret: string, token: string): boolean => $getTOTP("", otpSecret).validate({ token }) === 0; + const isOTPVerified = async (accountId: string): Promise => { + const accountOTP = await System.db.get(["otpByAccountId", accountId]); + + return Boolean(accountOTP?.verified); + }; + + const generateOTP = async ( + accountId: string, + email: string, + ): Promise => { + const secret = generateSecret(); + const uri = generateURI(email, secret); + await System.db.set(["otpByAccountId", accountId], { + secret, + verified: false, + }); + + return uri; + }; + return { generateSecret, generateURI, verify, + + generateOTP, + isOTPVerified, }; }; diff --git a/app/server/src/modules/system/servers.ts b/app/server/src/modules/system/servers.ts deleted file mode 100644 index 557b214..0000000 --- a/app/server/src/modules/system/servers.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { System } from "modules/system/main.ts"; -import { getIpFromUrl, compareIps, getIpFromRequest } from "@oh/utils"; -import * as bcrypt from "bcrypt"; -import { Server } from "shared/types/server.types.ts"; - -export const servers = () => { - const isValid = async ( - serverId: string, - token: string, - requestIp: string, - ): Promise => { - if (!serverId || !token) return false; - - const server = await System.db.get(["servers", serverId]); - if (!server) return false; - - const dnsIp = await getIpFromUrl(server.hostname); - if (!compareIps(dnsIp, requestIp) || !compareIps(server.ip, requestIp)) - return false; - - return bcrypt.compareSync(token, server.tokenHash); - }; - - const isRequestValid = (request: Request): Promise => { - const serverId = request.headers.get("server-id"); - const token = request.headers.get("token"); - const requestIp = getIpFromRequest(request); - - return isValid(serverId, token, requestIp); - }; - - const getServerData = (serverId: string): Promise => - System.db.get(["servers", serverId]); - - return { - isValid, - isRequestValid, - getServerData, - }; -}; diff --git a/app/server/src/modules/system/sessions.ts b/app/server/src/modules/system/sessions.ts deleted file mode 100644 index 69c727d..0000000 --- a/app/server/src/modules/system/sessions.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { System } from "modules/system/main.ts"; -import { TickerQueue } from "@oh/queue"; -import { getServerSessionList } from "shared/utils/main.ts"; -import { Session } from "shared/types/session.types.ts"; - -export const sessions = () => { - const sessionMap: Record = {}; - - const $disconnectFromLastServer = async ( - accountId: string, - serverId: string, - ) => { - const headers = new Headers(); - headers.append("auth-server", performance.now() + ""); - - const server = await System.servers.getServerData(serverId); - if (!server) return; - - const protocol = `http${System.getEnvs().version === "development" ? "" : "s"}://`; - fetch( - `${protocol}${server.hostname}/auth/user-disconnected?accountId=${accountId}`, - { - headers, - }, - ) - .then(async (response) => console.error(await response.json())) - .catch((e) => { - console.error(e); - //we don't really care if server receives our petition, the session is being invalidated anyway - }); - }; - - const $checkSessions = async () => { - const currentSessions = Object.keys(sessionMap); - const targetSessions: string[] = (await getServerSessionList()).map( - ({ key: [, accountId] }) => accountId, - ); - console.log( - `Checking sessions... (${currentSessions.length}..${targetSessions.length})`, - ); - - const toDeleteSessions = currentSessions.filter( - (accountId) => !targetSessions.includes(accountId), - ); - - //remove not active sessions - for (const accountId of toDeleteSessions) { - if (!sessionMap[accountId]) return; - $disconnectFromLastServer(accountId, sessionMap[accountId].serverId); - delete sessionMap[accountId]; - } - - const accountCheckList = [ - ...new Set([...currentSessions, ...targetSessions]), - ].filter((accountId) => toDeleteSessions.includes(accountId)); - - //check accounts - for (const accountId of accountCheckList) checkAccountSession(accountId); - }; - - const load = () => { - System.tasks.add({ - type: TickerQueue.REPEAT, - repeatEvery: System.getConfig().sessions.checkInterval * 1000, - onFunc: $checkSessions, - }); - $checkSessions(); - }; - - const checkAccountSession = async (accountId: string) => { - const currentSession: Session = await System.db.get([ - "serverSessionByAccount", - accountId, - ]); - const session = sessionMap[accountId]; - - //check if session server exists and changed if so disconnect from last server - //only if last server is different from current one - if ( - session && - session.serverId !== currentSession?.serverId && - (session.sessionId !== currentSession?.sessionId || - session.ticketId !== currentSession?.ticketId) - ) { - $disconnectFromLastServer(accountId, session.serverId); - } - - //reassign server session - sessionMap[accountId] = currentSession; - }; - - return { - load, - checkAccountSession, - }; -}; diff --git a/app/server/src/modules/system/tasks.ts b/app/server/src/modules/system/tasks.ts deleted file mode 100644 index e7728b8..0000000 --- a/app/server/src/modules/system/tasks.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { queue, ticker, QueueItemProps } from "@oh/queue"; - -export const tasks = () => { - const $ticker = ticker(); - const $queue = queue(); - - const load = () => { - $ticker.onTick(({ delta }) => $queue.tick(delta)); - $ticker.load({ ticks: 20 }); - $ticker.start(); - }; - - const add: (props: QueueItemProps) => number = $queue.add; - const remove: (id: number) => void = $queue.remove; - - return { - load, - - add, - remove, - }; -}; diff --git a/app/server/src/modules/system/tokens.ts b/app/server/src/modules/system/tokens.ts index 837b0ed..68ebe3a 100644 --- a/app/server/src/modules/system/tokens.ts +++ b/app/server/src/modules/system/tokens.ts @@ -1,93 +1,50 @@ -import { System } from "./main.ts"; -import { - getRandomString, - getIpFromRequest, - compareIps, - getIpFromUrl, - RequestMethod, -} from "@oh/utils"; -import * as bcrypt from "bcrypt"; -import { Service } from "shared/enums/services.enums.ts"; -import { isServiceValid } from "shared/utils/services.utils.ts"; +import { System } from "modules/system/main.ts"; +import * as bcrypt from "@da/bcrypt"; +import { generateToken, getTokenData } from "@oh/utils"; export const tokens = () => { - const load = async () => {}; - - const generateKey = async ( - service: string, - api: string, - ): Promise<{ key: string; token: string }> => { - const key = getRandomString(64); - const token = getRandomString(64); - - const ip = await getIpFromUrl(api); - await System.db.set(["tokens", service], { - keyHash: bcrypt.hashSync(key, bcrypt.genSaltSync(8)), - token, - api, - ip, + const getList = async (): Promise<{ id: string; label: string }[]> => + (await System.db.list({ prefix: ["appTokens"] })).map( + ({ value }) => value, + ) as any[]; + + const generate = async ( + label: string, + ): Promise<{ id: string; token: string }> => { + const { token, id, tokenHash } = generateToken("tok", 8, 96); + await System.db.set(["appTokens", id], { + id, + label, + tokenHash, + updatedAt: Date.now(), }); - return { key, token }; + return { + id, + token, + }; }; - const getApiUrl = async (service: Service): Promise => { - const data = await System.db.get(["tokens", service]); - if (!data) return null; - return data.api; + const remove = async (id: string) => { + await System.db.delete(["appTokens", id]); }; - const isValidRequest = async ( - request: Request, - service?: Service, - ): Promise => { - const tokenService = request.headers.get("token-service"); - if (service ? tokenService !== service : !isServiceValid(tokenService)) - return false; - - const data = await System.db.get(["tokens", tokenService]); - if (!data) return false; - - const remoteIp = getIpFromRequest(request); - if (!remoteIp) return false; - - if (!compareIps(data.ip, remoteIp)) return false; - - const tokenKey = request.headers.get("token-key"); - return bcrypt.compareSync(tokenKey, data.keyHash); - }; - - const $fetch = async ( - method: RequestMethod, - pathname: string, - service: Service, - data?: unknown, - ): Promise => { - const tokenData = await System.db.get(["tokens", service]); - if (!tokenData) throw `No service ${service} data!`; - - const headers = new Headers(); - headers.append("token", tokenData.token); + const verify = async (rawToken: string): Promise => { + if (!rawToken) return false; - const { status, data: responseData } = await fetch( - `${tokenData.api}${pathname}`, - { - method, - body: data ? JSON.stringify(data) : null, - headers, - }, - ).then((response) => response.json()); + const { id: tokenId, token } = getTokenData(rawToken); + if (!tokenId || !token) return false; - if (status !== 200) throw Error(`Status ${status}!`); + const foundToken = await System.db.get(["appTokens", tokenId]); + if (!foundToken) return false; - return responseData as Data; + return bcrypt.compareSync(token, foundToken.tokenHash); }; return { - load, - generateKey, - isValidRequest, - getApiUrl, - $fetch, + getList, + generate, + verify, + remove, }; }; diff --git a/app/server/src/shared/consts/account.consts.ts b/app/server/src/shared/consts/account.consts.ts index a7b4a54..a787779 100644 --- a/app/server/src/shared/consts/account.consts.ts +++ b/app/server/src/shared/consts/account.consts.ts @@ -3,6 +3,3 @@ export const USERNAME_REGEX = /^[a-zA-Z0-9_]{3,16}$/; export const PASSWORD_REGEX = /^(?=.*[A-Z])(?=.*[0-9]).{8,}$/; export const EMAIL_REGEX = /^[\w\-\.]+@([\w-]+\.)+[\w-]{2,}$/; - -//account expiration without verification (1 day) -export const ACCOUNT_EXPIRE_TIME = 1000 * 60 * 60 * 24; diff --git a/app/server/src/shared/consts/at.consts.ts b/app/server/src/shared/consts/at.consts.ts index 73e05ad..86c3252 100644 --- a/app/server/src/shared/consts/at.consts.ts +++ b/app/server/src/shared/consts/at.consts.ts @@ -1 +1,2 @@ -export const PROTO_DID_REGEX = /^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$/; +export const PROTO_DID_REGEX = + /^did=(did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-])$/; diff --git a/app/server/src/shared/consts/config.consts.ts b/app/server/src/shared/consts/config.consts.ts index 6951563..6ddef2c 100644 --- a/app/server/src/shared/consts/config.consts.ts +++ b/app/server/src/shared/consts/config.consts.ts @@ -5,8 +5,12 @@ export const CONFIG_DEFAULT: ConfigTypes = { url: "http://localhost:2024", version: "latest", development: false, - sessions: { - checkInterval: 30, + times: { + accountTokenDays: 1, + accountRefreshTokenDays: 7, + accountWithoutVerificationDays: 1, + + connectionTokenMinutes: 20, }, database: { filename: "database", diff --git a/app/server/src/shared/consts/main.ts b/app/server/src/shared/consts/main.ts index acef065..6f4e080 100644 --- a/app/server/src/shared/consts/main.ts +++ b/app/server/src/shared/consts/main.ts @@ -1,5 +1,5 @@ export * from "./config.consts.ts"; -export * from "./session.consts.ts"; export * from "./account.consts.ts"; export * from "./tickets.consts.ts"; export * from "./at.consts.ts"; +export * from "./request.consts.ts"; diff --git a/app/server/src/shared/consts/request.consts.ts b/app/server/src/shared/consts/request.consts.ts new file mode 100644 index 0000000..7a3b595 --- /dev/null +++ b/app/server/src/shared/consts/request.consts.ts @@ -0,0 +1,9 @@ +import { RequestKind } from "shared/enums/request.enums.ts"; + +export const REQUEST_KIND_COLOR_MAP: Record = { + [RequestKind.PUBLIC]: "white", + [RequestKind.ACCOUNT]: "green", + [RequestKind.CONNECTION]: "yellow", + [RequestKind.ADMIN]: "red", + [RequestKind.TOKEN]: "orange", +}; diff --git a/app/server/src/shared/consts/session.consts.ts b/app/server/src/shared/consts/session.consts.ts deleted file mode 100644 index 681cd58..0000000 --- a/app/server/src/shared/consts/session.consts.ts +++ /dev/null @@ -1,9 +0,0 @@ -// 5 mins -export const SESSION_EXPIRE_TIME = 1000 * 60 * 5; -// 1 day -export const SESSION_WITHOUT_TICKET_EXPIRE_TIME = 1000 * 60 * 60 * 24; -// 7 days -export const REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 7; - -// 60 secs -export const SERVER_SESSION_EXPIRE_TIME = 1000 * 60; diff --git a/app/server/src/shared/enums/main.ts b/app/server/src/shared/enums/main.ts index 5ab8914..1b9cd13 100644 --- a/app/server/src/shared/enums/main.ts +++ b/app/server/src/shared/enums/main.ts @@ -1 +1,3 @@ export * from "./services.enums.ts"; +export * from "./scopes.enums.ts"; +export * from "./request.enums.ts"; diff --git a/app/server/src/shared/enums/request.enums.ts b/app/server/src/shared/enums/request.enums.ts new file mode 100644 index 0000000..c7dffce --- /dev/null +++ b/app/server/src/shared/enums/request.enums.ts @@ -0,0 +1,7 @@ +export enum RequestKind { + PUBLIC, + ACCOUNT, + CONNECTION, + ADMIN, + TOKEN, +} diff --git a/app/server/src/shared/enums/scopes.enums.ts b/app/server/src/shared/enums/scopes.enums.ts new file mode 100644 index 0000000..2e520f4 --- /dev/null +++ b/app/server/src/shared/enums/scopes.enums.ts @@ -0,0 +1,7 @@ +export enum Scope { + ONET_MESSAGES_READ = "onet.messages.read", + ONET_MESSAGES_WRITE = "onet.messages.write", + + ONET_FRIENDS_READ = "onet.friends.read", + ONET_FRIENDS_WRITE = "onet.friends.write", +} diff --git a/app/server/src/shared/enums/services.enums.ts b/app/server/src/shared/enums/services.enums.ts deleted file mode 100644 index 90a9cf2..0000000 --- a/app/server/src/shared/enums/services.enums.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum Service { - ONET = "ONET", - AT = "AT", -} diff --git a/app/server/src/shared/types/config.types.ts b/app/server/src/shared/types/config.types.ts index bec2d28..35fa6cd 100644 --- a/app/server/src/shared/types/config.types.ts +++ b/app/server/src/shared/types/config.types.ts @@ -2,9 +2,12 @@ export type ConfigTypes = { port: number; url: string; version: string; - development: boolean; - sessions: { - checkInterval: number; + times: { + accountTokenDays: number; + accountRefreshTokenDays: number; + accountWithoutVerificationDays: number; + + connectionTokenMinutes: number; }; database: { filename: string; diff --git a/app/server/src/shared/utils/account.utils.ts b/app/server/src/shared/utils/account.utils.ts index 17789eb..af8fdcb 100644 --- a/app/server/src/shared/utils/account.utils.ts +++ b/app/server/src/shared/utils/account.utils.ts @@ -1,68 +1,82 @@ -import { System } from "modules/system/main.ts"; -import * as bcrypt from "bcrypt"; - -export const getAccountFromRequest = async ({ headers }: Request) => { - const sessionId = headers.get("sessionId"); - - const accountBySession = await System.db.get([ - "accountsBySession", - sessionId, - ]); - - if (!accountBySession) return null; - - return await System.db.get(["accounts", accountBySession]); -}; - -export const isAccountAuthValid = async ({ - headers, -}: Request): Promise => { - const sessionId = headers.get("sessionId"); - const token = headers.get("token"); - - if (!sessionId || !token) return 403; - - const ticketBySession = await System.db.get(["ticketBySession", sessionId]); - - //cannot use a session generated with a ticket - if (ticketBySession) return 410; - - const account = await getAccountFromRequest({ headers } as Request); - if (!account) return 403; - - //verify token - const result = bcrypt.compareSync(token, account.tokenHash); - - if (!result) return 403; - return 200; -}; - -export const isAccountAdminValid = async ( - request: Request, -): Promise => { - const authStatus = await isAccountAuthValid(request); - if (authStatus !== 200) return authStatus; - - const account = await getAccountFromRequest(request); - return Boolean(account.isAdmin) ? 200 : 403; -}; - -export const getAccountList = async () => - (await System.db.list({ prefix: ["accounts"] })).map(({ value }) => value); - -export const getAdminList = async () => - (await getAccountList()).filter((account) => account.isAdmin); - -export const getServerSessionList = async () => - (await System.db.list({ prefix: ["serverSessionByAccount"] })).filter( - ({ value: { claimed } }) => claimed, - ); - -export const getRedirectUrl = ({ - redirectUrl, - ticketId, - sessionId, - token, - accountId, -}) => - `${redirectUrl}?ticketId=${ticketId}&sessionId=${sessionId}&token=${token}&accountId=${accountId}`; +// import { System } from "modules/system/main.ts"; +// import * as bcrypt from "@da/bcrypt"; + +// export const getAccountFromRequest = async ({ headers }: Request) => { +// const sessionId = headers.get("sessionId"); +// +// const accountBySession = await System.db.get([ +// "accountsBySession", +// sessionId, +// ]); +// +// if (!accountBySession) return null; +// +// return await System.db.get(["accounts", accountBySession]); +// }; +// +// export const isAccountAuthValid = async ({ +// headers, +// }: Request): Promise => { +// const sessionId = headers.get("sessionId"); +// const token = headers.get("token"); +// +// if (!sessionId || !token) return 403; +// +// const ticketBySession = await System.db.get(["ticketBySession", sessionId]); +// +// //cannot use a session generated with a ticket +// if (ticketBySession) return 410; +// +// const account = await getAccountFromRequest({ headers } as Request); +// if (!account) return 403; +// +// //verify token +// const result = bcrypt.compareSync(token, account.tokenHash); +// +// if (!result) return 403; +// return 200; +// }; +// +// export const isAccountAdminValid = async ( +// request: Request, +// ): Promise => { +// const authStatus = await isAccountAuthValid(request); +// if (authStatus !== 200) return authStatus; +// +// const account = await getAccountFromRequest(request); +// return Boolean(account.isAdmin) ? 200 : 403; +// }; +// +// export const getAccountList = async () => +// (await System.db.list({ prefix: ["accounts"] })).map(({ value }) => value); + +// export const getAdminList = async () => +// (await getAccountList()).filter((account) => account.isAdmin); +// +// export const getServerSessionList = async () => +// (await System.db.list({ prefix: ["serverSessionByAccount"] })).filter( +// ({ value: { claimed } }) => claimed, +// ); +// +// export const getRedirectUrl = ({ +// redirectUrl, +// ticketId, +// sessionId, +// token, +// accountId, +// }) => +// `${redirectUrl}?ticketId=${ticketId}&sessionId=${sessionId}&token=${token}&accountId=${accountId}`; + +///////////////// + +// export const getAccountFromRequest = async ({ headers }: Request) => { +// let accountId = headers.get("account-id"); +// const sessionId = headers.get("session-id"); +// +// if (!accountId && sessionId) { +// const session = await System.db.get(["sessions", sessionId]); +// accountId = session.accountId; +// } +// +// return await System.db.get(["accounts", accountId]); +// }; diff --git a/app/server/src/shared/utils/main.ts b/app/server/src/shared/utils/main.ts index 0dd90d4..009946d 100644 --- a/app/server/src/shared/utils/main.ts +++ b/app/server/src/shared/utils/main.ts @@ -1,3 +1,2 @@ export * from "./envs.utils.ts"; -export * from "./account.utils.ts"; -export * from "./services.utils.ts"; +export * from "./scope.utils.ts"; diff --git a/app/server/src/shared/utils/scope.utils.ts b/app/server/src/shared/utils/scope.utils.ts new file mode 100644 index 0000000..fb5c9f4 --- /dev/null +++ b/app/server/src/shared/utils/scope.utils.ts @@ -0,0 +1,51 @@ +import { Scope } from "shared/enums/scopes.enums.ts"; +import { System } from "modules/system/main.ts"; +import * as bcrypt from "@da/bcrypt"; +import { compareIps, getIpFromRequest } from "@oh/utils"; + +type Props = { + request: Request; + scopes?: Scope[]; + admin?: boolean; +}; + +export const hasRequestAccess = async ({ + request, + scopes = ["ACCOUNT_ONLY" as Scope], + admin = false, +}: Props): Promise => { + const accountId = request.headers.get("account-id"); + const token = request.headers.get("token"); + + const connectionToken = request.headers.get("connection-token"); + const licenseToken = request.headers.get("license-token"); + + if (accountId && token) { + const userAgent = request.headers.get("user-agent"); + const ip = getIpFromRequest(request); + + const accountsByToken = await System.db.get(["accountsByToken", accountId]); + + if ( + !accountsByToken || + accountsByToken.userAgent !== userAgent || + !compareIps(ip, accountsByToken.ip) + ) + return false; + + if (admin && !(await System.db.get(["adminsByAccountId", accountId]))) + return false; + + return bcrypt.compareSync(token, accountsByToken.tokenHash); + } + + if (scopes.includes("ACCOUNT_ONLY" as Scope)) return false; + + if (connectionToken && licenseToken) + return ( + (await System.connections.verify(connectionToken, scopes)) && + (await System.licenses.verify(licenseToken)) + ); + + return false; +}; diff --git a/app/server/src/shared/utils/services.utils.ts b/app/server/src/shared/utils/services.utils.ts deleted file mode 100644 index 72bd1cd..0000000 --- a/app/server/src/shared/utils/services.utils.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Service } from "shared/enums/services.enums.ts"; - -export const isServiceValid = (service: string): boolean => - Service[service] !== undefined;