diff --git a/app/client/src/modules/account/components/header/header.component.tsx b/app/client/src/modules/account/components/header/header.component.tsx index 489ce23..86eef96 100644 --- a/app/client/src/modules/account/components/header/header.component.tsx +++ b/app/client/src/modules/account/components/header/header.component.tsx @@ -1,12 +1,10 @@ import React from "react"; -import { useAppSession } from "shared/hooks"; import { ButtonComponent } from "@openhotel/web-components"; import styles from "./header.module.scss"; +import { useSession } from "shared/hooks"; export const HeaderComponent: React.FC = () => { - const { account, login, logout } = useAppSession(); - - if (account === undefined) return; + const { login, logout, account } = useSession(); if (!account) return ( diff --git a/app/client/src/modules/application/components/providers/providers.component.tsx b/app/client/src/modules/application/components/providers/providers.component.tsx index c2ab40b..2720ffc 100644 --- a/app/client/src/modules/application/components/providers/providers.component.tsx +++ b/app/client/src/modules/application/components/providers/providers.component.tsx @@ -1,16 +1,17 @@ import { Outlet } from "react-router"; -import { TitleProvider, AppSessionProvider } from "shared/hooks"; +import { TitleProvider } from "shared/hooks"; import React from "react"; import { ModalProvider } from "@openhotel/web-components"; +import { SessionProvider } from "shared/hooks"; export const ProvidersComponent = () => { return ( - + - + ); }; diff --git a/app/client/src/modules/application/components/wrapper-layout/wrapper-layout.component.tsx b/app/client/src/modules/application/components/wrapper-layout/wrapper-layout.component.tsx index 4db7b2b..e4af00d 100644 --- a/app/client/src/modules/application/components/wrapper-layout/wrapper-layout.component.tsx +++ b/app/client/src/modules/application/components/wrapper-layout/wrapper-layout.component.tsx @@ -1,16 +1,18 @@ import React from "react"; import { MainLayoutComponent } from "@openhotel/web-components"; import { HeaderComponent } from "modules/account"; +import { useSession } from "shared/hooks"; type Props = { children: React.ReactNode; }; export const WrapperLayoutComponent = ({ children }: Props) => { + const { account } = useSession(); return ( } + headerChildren={account !== undefined ? : null} /> ); }; diff --git a/app/client/src/shared/hooks/index.ts b/app/client/src/shared/hooks/index.ts index afd4144..cecf1b5 100644 --- a/app/client/src/shared/hooks/index.ts +++ b/app/client/src/shared/hooks/index.ts @@ -1,4 +1,4 @@ export * from "./useTitle"; export * from "./useApi"; -export * from "./useAppSession"; +export * from "./useSession"; export * from "./useCookies"; diff --git a/app/client/src/shared/hooks/useAppSession.tsx b/app/client/src/shared/hooks/useAppSession.tsx index 8c0f547..1c650db 100644 --- a/app/client/src/shared/hooks/useAppSession.tsx +++ b/app/client/src/shared/hooks/useAppSession.tsx @@ -100,7 +100,6 @@ export const AppSessionProvider: React.FunctionComponent = ({ }, []); const logout = useCallback(() => { - console.log(account); if (!account) return; remove("account-id"); diff --git a/app/client/src/shared/hooks/useSession.tsx b/app/client/src/shared/hooks/useSession.tsx new file mode 100644 index 0000000..9da7702 --- /dev/null +++ b/app/client/src/shared/hooks/useSession.tsx @@ -0,0 +1,110 @@ +import React, { + ReactNode, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from "react"; +import { useApi, useCookies } from "shared/hooks"; +import { Account } from "shared/types"; + +type SessionState = { + // getHeaders: () => Record; + account: Account | null | undefined; + login: () => Promise; + logout: () => void; +}; + +const SessionContext = React.createContext(undefined); + +type ProviderProps = { + children: ReactNode; +}; + +export const SessionProvider: React.FunctionComponent = ({ + children, +}) => { + const { get, set, remove } = useCookies(); + const { fetch } = useApi(); + + const [account, setAccount] = useState(undefined); + const redirectRef = useRef(null); + + const params = new URLSearchParams(window.location.search); + // const $state = params.get("state"); + const $token = params.get("token"); + + const makeRequest = useCallback(async () => { + if (redirectRef.current) return; + + const { status, data } = await fetch({ + pathname: "auth/request", + preventReload: true, + }); + + switch (status) { + // auth disabled + case 410: + setAccount(null); + break; + case 200: + setAccount(null); + redirectRef.current = data.redirectUrl; + break; + } + }, []); + + useEffect(() => { + const token = $token ?? get("connection-token"); + if (token) set("connection-token", token, 1); + + if (window.location.search) + window.history.replaceState("", document.title, window.location.pathname); + + (async () => { + if (token) { + try { + const { status, data } = await fetch({ + pathname: "auth/user", + headers: { + token, + }, + preventReload: true, + }); + setAccount(data); + return; + } catch (e) { + remove("connection-token"); + } + } + + await makeRequest(); + })(); + }, []); + + const login = useCallback(async () => { + await makeRequest(); + window.location.replace(redirectRef.current); + }, [fetch]); + + const logout = useCallback(async () => { + if (!account) return; + + remove("connection-token"); + setAccount(null); + }, [account, fetch]); + + return ( + + ); +}; + +export const useSession = (): SessionState => useContext(SessionContext); diff --git a/app/server/src/modules/api/auth/main.ts b/app/server/src/modules/api/auth/main.ts index dc21f76..7ec4d1d 100644 --- a/app/server/src/modules/api/auth/main.ts +++ b/app/server/src/modules/api/auth/main.ts @@ -1,10 +1,9 @@ import { RequestType, getPathRequestList } from "@oh/utils"; -import { redirectRequest } from "./redirect.request.ts"; -import { userRequest } from "./user.request.ts"; -import { getRequest } from "./main.request.ts"; +import { requestRequest } from "modules/api/auth/request.request.ts"; +import { userRequest } from "modules/api/auth/user.request.ts"; export const authList: RequestType[] = getPathRequestList({ - requestList: [getRequest, redirectRequest, userRequest], + requestList: [requestRequest, userRequest], pathname: "/auth", }); diff --git a/app/server/src/modules/api/auth/redirect.request.ts b/app/server/src/modules/api/auth/redirect.request.ts deleted file mode 100644 index ef0dd8d..0000000 --- a/app/server/src/modules/api/auth/redirect.request.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { RequestType, RequestMethod } from "@oh/utils"; -import { System } from "system/main.ts"; -import { getTokenData } from "@oh/utils"; - -export const redirectRequest: RequestType = { - method: RequestMethod.GET, - pathname: "/redirect", - func: (request) => { - const { - auth: { url: authUrl, appToken }, - } = System.getConfig(); - const { id } = getTokenData(appToken); - - const $url = new URL(authUrl + "/apps"); - $url.searchParams.append("appId", id); - return Response.json( - { - status: 200, - data: { - url: $url, - }, - }, - { status: 200 }, - ); - }, -}; diff --git a/app/server/src/modules/api/auth/request.http b/app/server/src/modules/api/auth/request.http new file mode 100644 index 0000000..a58a758 --- /dev/null +++ b/app/server/src/modules/api/auth/request.http @@ -0,0 +1,2 @@ +# +GET http://localhost:2025/api/auth/request diff --git a/app/server/src/modules/api/auth/request.request.ts b/app/server/src/modules/api/auth/request.request.ts new file mode 100644 index 0000000..bdc74bb --- /dev/null +++ b/app/server/src/modules/api/auth/request.request.ts @@ -0,0 +1,37 @@ +import { + RequestType, + RequestMethod, + getResponse, + HttpStatusCode, +} from "@oh/utils"; +import { System } from "system/main.ts"; + +export const requestRequest: RequestType = { + method: RequestMethod.GET, + pathname: "/request", + func: (request) => { + const config = System.getConfig(); + + if (!config.auth.enabled) return getResponse(HttpStatusCode.GONE); + + const hotelId = System.auth.getHotelId(); + const integrationId = System.auth.getIntegrationId(); + + const composedRedirectUrl = new URL(`${config.auth.api}/connection`); + composedRedirectUrl.searchParams.append("state", "test"); + + if (hotelId) composedRedirectUrl.searchParams.append("hotelId", hotelId); + if (integrationId) + composedRedirectUrl.searchParams.append("integrationId", integrationId); + + return Response.json( + { + status: 200, + data: { + redirectUrl: composedRedirectUrl.href, + }, + }, + { status: 200 }, + ); + }, +}; diff --git a/app/server/src/modules/api/auth/user.request.ts b/app/server/src/modules/api/auth/user.request.ts index e4294bf..dbffbcc 100644 --- a/app/server/src/modules/api/auth/user.request.ts +++ b/app/server/src/modules/api/auth/user.request.ts @@ -6,22 +6,26 @@ export const userRequest: RequestType = { pathname: "/user", kind: RequestKind.ACCOUNT, func: async (request: Request) => { - const accountId = request.headers.get("account-id"); - const accountToken = request.headers.get("account-token"); - const { - auth: { url: authUrl, appToken }, - } = System.getConfig(); + const connectionToken = request.headers.get("token"); - const data = await fetch(`${authUrl}/api/v3/user/@me`, { - headers: { - "app-token": appToken, - "account-id": accountId, - "account-token": accountToken, - }, - }).then((response) => response.json()); - - return Response.json(data, { - status: data.status, + const { accountId, username, admin, languages } = await System.auth.fetch({ + url: "/user/@me", + connectionToken, }); + + return Response.json( + { + status: 200, + data: { + accountId, + username, + admin, + languages, + }, + }, + { + status: 200, + }, + ); }, }; diff --git a/app/server/src/shared/consts/config.consts.ts b/app/server/src/shared/consts/config.consts.ts index 4b902a7..38f6cd2 100644 --- a/app/server/src/shared/consts/config.consts.ts +++ b/app/server/src/shared/consts/config.consts.ts @@ -23,7 +23,7 @@ export const CONFIG_DEFAULT: ConfigTypes = { }, auth: { enabled: false, - appToken: "", - url: "http://localhost:2024", + licenseToken: "", + api: "http://localhost:2024", }, }; diff --git a/app/server/src/shared/types/config.types.ts b/app/server/src/shared/types/config.types.ts index 316cfcf..6f3875c 100644 --- a/app/server/src/shared/types/config.types.ts +++ b/app/server/src/shared/types/config.types.ts @@ -21,7 +21,7 @@ export type ConfigTypes = { }; auth: { enabled: boolean; - appToken: string; - url: string; + licenseToken: string; + api: string; }; }; diff --git a/app/server/src/system/api.ts b/app/server/src/system/api.ts index 00ad3a0..05e4555 100644 --- a/app/server/src/system/api.ts +++ b/app/server/src/system/api.ts @@ -82,8 +82,7 @@ export const api = () => { kind: RequestKind | RequestKind[]; }): Promise => { const check = async (kind: RequestKind) => { - const accountId = request.headers.get("account-id"); - const accountToken = request.headers.get("account-token"); + const connectionToken = request.headers.get("token"); if (!System.getConfig().auth.enabled) return true; @@ -91,7 +90,18 @@ export const api = () => { case RequestKind.PUBLIC: return true; case RequestKind.ACCOUNT: - if (!accountId || !accountToken) return false; + if (!connectionToken) return false; + + try { + const { accountId } = await System.auth.fetch({ + url: "/user/@me", + connectionToken, + }); + return Boolean(accountId); + } catch (e) { + return false; + } + return true; default: return false; diff --git a/app/server/src/system/auth.ts b/app/server/src/system/auth.ts new file mode 100644 index 0000000..7a5ac54 --- /dev/null +++ b/app/server/src/system/auth.ts @@ -0,0 +1,69 @@ +import { ConfigTypes } from "shared/types/config.types.ts"; + +type Props = { + url: string; + connectionToken?: string; +}; + +export const auth = () => { + let $config: ConfigTypes; + + let $hotelId: string; + let $integrationId: string; + //TODO permanent op + let $ownerId: string; + + const load = async (config: ConfigTypes) => { + $config = config; + if (!(await isAuthEnabled())) + console.error("/!\\ Auth service is down or Hotel License is not valid!"); + }; + + const isAuthEnabled = async () => { + if (!$config.auth.enabled) return true; + + try { + const { hotelId, accountId, integrationId } = await $fetch({ + url: "/hotel/license", + }); + $hotelId = hotelId; + $integrationId = integrationId; + $ownerId = accountId; + return true; + } catch (e) { + return false; + } + }; + + const $fetch = async ({ url, connectionToken }: Props) => { + const { status, data } = await ( + await fetch(`${$config.auth.api}/api/v3${url}`, { + headers: new Headers({ + "Content-Type": "application/json", + "license-token": $config.auth.licenseToken, + ...(connectionToken + ? { + "connection-token": connectionToken, + } + : {}), + }), + }) + ).json(); + return data; + }; + + const getHotelId = () => $hotelId; + const getIntegrationId = () => $integrationId; + const getOwnerId = () => $ownerId; + + return { + load, + isAuthEnabled, + + fetch: $fetch, + + getHotelId, + getIntegrationId, + getOwnerId, + }; +}; diff --git a/app/server/src/system/main.ts b/app/server/src/system/main.ts index 4422329..298d0cf 100644 --- a/app/server/src/system/main.ts +++ b/app/server/src/system/main.ts @@ -5,6 +5,7 @@ import { CONFIG_DEFAULT } from "shared/consts/config.consts.ts"; import { Migrations } from "./migrations/main.ts"; import { backups } from "./backups.ts"; import { accounts } from "./accounts.ts"; +import { auth } from "system/auth.ts"; export const System = (() => { let $config: ConfigTypes; @@ -15,6 +16,7 @@ export const System = (() => { const $api = api(); const $backups = backups(); const $accounts = accounts(); + const $auth = auth(); let $db: DbMutable; const load = async (envs: Envs, testMode: boolean = false) => { @@ -28,6 +30,8 @@ export const System = (() => { $isTestMode = testMode; + await $auth.load($config); + const isProduction = !testMode && $config.version !== "development"; if ( @@ -90,5 +94,6 @@ export const System = (() => { api: $api, backups: $backups, accounts: $accounts, + auth: $auth, }; })();