From e48a0bcb06b9d1585646924c18352eddc0c2590d Mon Sep 17 00:00:00 2001 From: pablo Date: Sat, 31 May 2025 02:50:47 +0200 Subject: [PATCH 1/6] wip --- app/client/package.json | 1 + .../providers/providers.component.tsx | 6 +- .../src/modules/home/home.component.tsx | 59 ++++++++++++- app/client/src/shared/enums/index.ts | 1 + app/client/src/shared/enums/request.enums.ts | 11 +++ app/client/src/shared/hooks/index.ts | 3 + app/client/src/shared/hooks/useApi.ts | 86 ++++++++++++++++++ app/client/src/shared/hooks/useCookies.ts | 21 +++++ .../src/shared/hooks/useFingerprint.tsx | 45 ++++++++++ app/client/src/shared/types/index.ts | 1 + app/client/src/shared/types/request.types.ts | 11 +++ app/client/yarn.lock | 8 ++ .../modules/api/v1/account/account.request.ts | 24 +++++ app/server/src/modules/api/v1/account/main.ts | 7 ++ .../src/modules/api/v1/auth/auth.request.ts | 21 +++++ .../src/modules/api/v1/auth/login.request.ts | 42 +++++++++ app/server/src/modules/api/v1/auth/main.ts | 8 ++ app/server/src/modules/api/v1/main.ts | 4 +- app/server/src/modules/system/accounts.ts | 34 +++++++ app/server/src/modules/system/auth.ts | 88 +++++++++++++++++++ app/server/src/modules/system/main.ts | 7 ++ app/server/src/shared/consts/config.consts.ts | 5 ++ app/server/src/shared/enums/main.ts | 1 + app/server/src/shared/enums/scopes.enums.ts | 9 ++ app/server/src/shared/types/config.types.ts | 5 ++ 25 files changed, 503 insertions(+), 5 deletions(-) create mode 100644 app/client/src/shared/enums/index.ts create mode 100644 app/client/src/shared/enums/request.enums.ts create mode 100644 app/client/src/shared/hooks/useApi.ts create mode 100644 app/client/src/shared/hooks/useCookies.ts create mode 100644 app/client/src/shared/hooks/useFingerprint.tsx create mode 100644 app/client/src/shared/types/index.ts create mode 100644 app/client/src/shared/types/request.types.ts create mode 100644 app/server/src/modules/api/v1/account/account.request.ts create mode 100644 app/server/src/modules/api/v1/account/main.ts create mode 100644 app/server/src/modules/api/v1/auth/auth.request.ts create mode 100644 app/server/src/modules/api/v1/auth/login.request.ts create mode 100644 app/server/src/modules/api/v1/auth/main.ts create mode 100644 app/server/src/modules/system/accounts.ts create mode 100644 app/server/src/modules/system/auth.ts create mode 100644 app/server/src/shared/enums/scopes.enums.ts diff --git a/app/client/package.json b/app/client/package.json index 2e5e3bb..7365292 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@openhotel/web-components": "0.3.2", + "@thumbmarkjs/thumbmarkjs": "^0.20.2", "@vitejs/plugin-react": "^4.3.1", "@vitejs/plugin-react-refresh": "1.3.6", "dayjs": "1.11.13", 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 5bddd2b..bab6f35 100644 --- a/app/client/src/modules/application/components/providers/providers.component.tsx +++ b/app/client/src/modules/application/components/providers/providers.component.tsx @@ -1,5 +1,5 @@ import { Outlet } from "react-router"; -import { TitleProvider } from "shared/hooks"; +import { FingerprintProvider, TitleProvider } from "shared/hooks"; import React from "react"; import { ModalProvider } from "@openhotel/web-components"; @@ -7,7 +7,9 @@ export const ProvidersComponent = () => { return ( - + + + ); diff --git a/app/client/src/modules/home/home.component.tsx b/app/client/src/modules/home/home.component.tsx index 8c0b428..c4dce6d 100644 --- a/app/client/src/modules/home/home.component.tsx +++ b/app/client/src/modules/home/home.component.tsx @@ -1,11 +1,57 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { ButtonComponent } from "@openhotel/web-components"; +import { useApi, useCookies } from "shared/hooks"; +import { RequestMethod } from "shared/enums"; +import { useSearchParams } from "react-router-dom"; export const HomeComponent: React.FC = () => { + const { fetch } = useApi(); + const { set, get } = useCookies(); + const [searchParams] = useSearchParams(); + + const [account, setAccount] = useState(null); + const [connectionUrl, setConnectionUrl] = useState(null); + + useEffect(() => { + fetch({ + method: RequestMethod.GET, + pathname: "/account", + headers: { + token: get("token"), + }, + }).then(({ data: { account } }) => setAccount(account)); + + fetch({ method: RequestMethod.GET, pathname: "/auth" }).then( + ({ data: { connectionUrl } }) => setConnectionUrl(connectionUrl), + ); + }, [fetch, setAccount, setConnectionUrl, get]); + + useEffect(() => { + if (!searchParams.has("state") || !searchParams.has("token")) return; + + const body = { + state: searchParams.get("state"), + token: searchParams.get("token"), + }; + window.history.pushState(null, null, "/"); + + fetch({ method: RequestMethod.POST, pathname: "/auth/login", body }).then( + ({ data: { account, token } }) => { + setAccount(account); + set("token", token, 10_000); + }, + ); + }, [searchParams, setAccount, set]); + + console.log(account); return (

Home

- +

{ > Play! +

+ {account ? null : ( + window.location.replace(connectionUrl)} + > + Login + + )}

); }; 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/index.ts b/app/client/src/shared/hooks/index.ts index 19097ee..ea00975 100644 --- a/app/client/src/shared/hooks/index.ts +++ b/app/client/src/shared/hooks/index.ts @@ -1 +1,4 @@ export * from "./useTitle"; +export * from "./useApi"; +export * from "./useFingerprint"; +export * from "./useCookies"; diff --git a/app/client/src/shared/hooks/useApi.ts b/app/client/src/shared/hooks/useApi.ts new file mode 100644 index 0000000..b00ac52 --- /dev/null +++ b/app/client/src/shared/hooks/useApi.ts @@ -0,0 +1,86 @@ +import { Request } from "shared/types"; +import { RequestMethod } from "../enums"; +import { useCallback } from "react"; +import { useFingerprint } from "./useFingerprint"; + +export const useApi = () => { + const { fingerprint } = useFingerprint(); + + const $fetch = useCallback( + async ({ + method = RequestMethod.GET, + pathname, + body, + headers = {}, + cache = true, + rawResponse = false, + preventReload = false, + }: Request) => { + const response = await fetch(`/api/v1${pathname}`, { + method, + headers: new Headers({ + "Content-Type": "application/json", + fingerprint, + ...headers, + }), + body: body ? JSON.stringify(body) : undefined, + credentials: "include", + cache: cache ? "default" : "no-store", + }).then(async (data) => { + if (rawResponse) return data; + + const contentType = data.headers.get("content-type"); + + // Check that the response is JSON before calling `.json()`, + // otherwise there is no way to recover the response_text in case of an error + if (contentType && contentType.indexOf("application/json") !== -1) { + try { + return await data.json(); + } catch (e) { + throw { + status: data.status, + message: data.statusText, + response_text: null, + }; + } + } + // If the response is not JSON, throw an error + const response_text = await data.text(); + throw { + status: data.status, + message: response_text.length + ? `${data.status}: Invalid JSON response` + : `${data.status}: Empty response`, + response_text, + }; + }); + + if (rawResponse) return response; + + if (!preventReload && response.status === 403) { + globalThis.location.reload(); + return; + } + + if (response.status !== 200) throw response; + + return response; + }, + [fingerprint], + ); + + const getVersion = useCallback(async (): Promise => { + const { + data: { version }, + } = await $fetch({ + method: RequestMethod.GET, + pathname: "/_/version", + }); + return version; + }, [$fetch]); + + return { + fetch: $fetch, + getVersion, + }; +}; diff --git a/app/client/src/shared/hooks/useCookies.ts b/app/client/src/shared/hooks/useCookies.ts new file mode 100644 index 0000000..c02b51b --- /dev/null +++ b/app/client/src/shared/hooks/useCookies.ts @@ -0,0 +1,21 @@ +import Cookies from "js-cookie"; +import { useCallback, useMemo } from "react"; + +export const useCookies = () => { + const set = useCallback((key: string, value: string, expires?: number) => { + Cookies.set(key, value, { + expires, + sameSite: "strict", + secure: true, + }); + }, []); + + const get = useMemo(() => Cookies.get, []); + const remove = useMemo(() => Cookies.remove, []); + + return { + set, + get, + remove, + }; +}; diff --git a/app/client/src/shared/hooks/useFingerprint.tsx b/app/client/src/shared/hooks/useFingerprint.tsx new file mode 100644 index 0000000..8a83073 --- /dev/null +++ b/app/client/src/shared/hooks/useFingerprint.tsx @@ -0,0 +1,45 @@ +import { getFingerprint, setOption } from "@thumbmarkjs/thumbmarkjs"; +import React, { ReactNode, useContext, useEffect, useState } from "react"; + +type FingerprintState = { + fingerprint: string; +}; + +const FingerprintContext = React.createContext(undefined); + +type ProviderProps = { + children: ReactNode; +}; + +export const FingerprintProvider: React.FunctionComponent = ({ + children, +}) => { + const [fingerprint, setFingerprint] = useState(null); + + useEffect(() => { + setOption("exclude", [ + //prevents browser version change + "system.browser.version", + "system.useragent", + //prevents updates on webgl + "webgl", + //prevents screen changes + "screen", + //prevent audio changes + "audio", + //prevents plugin installs/uninstalls + "plugins", + ]); + getFingerprint().then(setFingerprint); + }, [setFingerprint]); + + return ( + + ); +}; + +export const useFingerprint = (): FingerprintState => + useContext(FingerprintContext); diff --git a/app/client/src/shared/types/index.ts b/app/client/src/shared/types/index.ts new file mode 100644 index 0000000..f3db1a8 --- /dev/null +++ b/app/client/src/shared/types/index.ts @@ -0,0 +1 @@ +export * from "./request.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..bcd6ea4 --- /dev/null +++ b/app/client/src/shared/types/request.types.ts @@ -0,0 +1,11 @@ +import { RequestMethod } from "shared/enums"; + +export type Request = { + method?: RequestMethod; + pathname: string; + body?: unknown; + headers?: Record; + cache?: boolean; + rawResponse?: boolean; + preventReload?: boolean; +}; diff --git a/app/client/yarn.lock b/app/client/yarn.lock index 86f78a8..d411c7b 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -684,6 +684,13 @@ __metadata: languageName: node linkType: hard +"@thumbmarkjs/thumbmarkjs@npm:^0.20.2": + version: 0.20.2 + resolution: "@thumbmarkjs/thumbmarkjs@npm:0.20.2" + checksum: 10c0/1fd6ec8854553d724a28803384d05812eded198739524f914329f5d2a23f580e968c1a05ba8378a9e7499dead3b9bfdfb9a4b76a45e1c4d8f3558851084f3026 + languageName: node + linkType: hard + "@types/babel__core@npm:^7.20.5": version: 7.20.5 resolution: "@types/babel__core@npm:7.20.5" @@ -2062,6 +2069,7 @@ __metadata: dependencies: "@openhotel/web-components": "npm:0.3.2" "@rollup/plugin-alias": "npm:5.1.0" + "@thumbmarkjs/thumbmarkjs": "npm:^0.20.2" "@types/js-cookie": "npm:3.0.6" "@types/react": "npm:18.3.3" "@vitejs/plugin-react": "npm:^4.3.1" diff --git a/app/server/src/modules/api/v1/account/account.request.ts b/app/server/src/modules/api/v1/account/account.request.ts new file mode 100644 index 0000000..5538658 --- /dev/null +++ b/app/server/src/modules/api/v1/account/account.request.ts @@ -0,0 +1,24 @@ +import { + RequestType, + RequestMethod, + getResponse, + HttpStatusCode, + RequestKind, +} from "@oh/utils"; +import { System } from "modules/system/main.ts"; + +export const getRequest: RequestType = { + method: RequestMethod.GET, + pathname: "", + kind: RequestKind.PUBLIC, + func: async (request: Request) => { + const fingerprint = request.headers.get("fingerprint"); + const token = request.headers.get("token"); + + return getResponse(HttpStatusCode.OK, { + data: { + account: System.accounts.get(fingerprint, token), + }, + }); + }, +}; diff --git a/app/server/src/modules/api/v1/account/main.ts b/app/server/src/modules/api/v1/account/main.ts new file mode 100644 index 0000000..51b8a15 --- /dev/null +++ b/app/server/src/modules/api/v1/account/main.ts @@ -0,0 +1,7 @@ +import { RequestType, getPathRequestList } from "@oh/utils"; +import { getRequest } from "./account.request.ts"; + +export const accountRequestList: RequestType[] = getPathRequestList({ + requestList: [getRequest], + pathname: "/account", +}); diff --git a/app/server/src/modules/api/v1/auth/auth.request.ts b/app/server/src/modules/api/v1/auth/auth.request.ts new file mode 100644 index 0000000..374a046 --- /dev/null +++ b/app/server/src/modules/api/v1/auth/auth.request.ts @@ -0,0 +1,21 @@ +import { + RequestType, + RequestMethod, + getResponse, + HttpStatusCode, + RequestKind, +} from "@oh/utils"; +import { System } from "modules/system/main.ts"; + +export const getRequest: RequestType = { + method: RequestMethod.GET, + pathname: "", + kind: RequestKind.PUBLIC, + func: () => { + return getResponse(HttpStatusCode.OK, { + data: { + connectionUrl: System.auth.getConnectionUrl(), + }, + }); + }, +}; diff --git a/app/server/src/modules/api/v1/auth/login.request.ts b/app/server/src/modules/api/v1/auth/login.request.ts new file mode 100644 index 0000000..135939d --- /dev/null +++ b/app/server/src/modules/api/v1/auth/login.request.ts @@ -0,0 +1,42 @@ +import { + RequestType, + RequestMethod, + getResponse, + HttpStatusCode, + RequestKind, +} from "@oh/utils"; +import { System } from "modules/system/main.ts"; + +export const postLoginRequest: RequestType = { + method: RequestMethod.POST, + pathname: "/login", + kind: RequestKind.PUBLIC, + func: async (request: Request) => { + const { state, token: connectionToken } = await request.json(); + const fingerprint = request.headers.get("fingerprint"); + + if (state !== System.auth.getState()) + return getResponse(HttpStatusCode.BAD_REQUEST); + + const { accountId, username, admin, languages } = await System.auth.fetch({ + url: "/user/@me", + connectionToken, + }); + + const account = { + accountId, + username, + admin, + languages, + }; + + const token = System.accounts.set(fingerprint, { ...account }); + + return getResponse(HttpStatusCode.OK, { + data: { + token, + account, + }, + }); + }, +}; diff --git a/app/server/src/modules/api/v1/auth/main.ts b/app/server/src/modules/api/v1/auth/main.ts new file mode 100644 index 0000000..641feba --- /dev/null +++ b/app/server/src/modules/api/v1/auth/main.ts @@ -0,0 +1,8 @@ +import { RequestType, getPathRequestList } from "@oh/utils"; +import { getRequest } from "./auth.request.ts"; +import { postLoginRequest } from "./login.request.ts"; + +export const authRequestList: RequestType[] = getPathRequestList({ + requestList: [getRequest, postLoginRequest], + pathname: "/auth", +}); diff --git a/app/server/src/modules/api/v1/main.ts b/app/server/src/modules/api/v1/main.ts index b6b02e3..91e9ec8 100644 --- a/app/server/src/modules/api/v1/main.ts +++ b/app/server/src/modules/api/v1/main.ts @@ -1,8 +1,10 @@ import { RequestType, getPathRequestList } from "@oh/utils"; import { miscRequestList } from "./misc/main.ts"; +import { authRequestList } from "./auth/main.ts"; +import { accountRequestList } from "./account/main.ts"; export const requestV1List: RequestType[] = getPathRequestList({ - requestList: [...miscRequestList], + requestList: [...miscRequestList, ...authRequestList, ...accountRequestList], pathname: "/api/v1", }); diff --git a/app/server/src/modules/system/accounts.ts b/app/server/src/modules/system/accounts.ts new file mode 100644 index 0000000..555f952 --- /dev/null +++ b/app/server/src/modules/system/accounts.ts @@ -0,0 +1,34 @@ +import { getRandomString } from "@oh/utils"; + +type Account = { + accountId: string; + username: string; + admin: boolean; + languages: string[]; + + token?: string; +}; + +export const accounts = () => { + let accountMap: Record = {}; + + const set = (fingerprint: string, account: Account) => { + account.token = getRandomString(64); + accountMap[fingerprint] = account; + return account.token; + }; + + const get = (fingerprint: string, token: string) => { + const account = { ...accountMap[fingerprint] }; + + if (account.token !== token) return null; + + delete account.token; + return account; + }; + + return { + set, + get, + }; +}; diff --git a/app/server/src/modules/system/auth.ts b/app/server/src/modules/system/auth.ts new file mode 100644 index 0000000..a5864ad --- /dev/null +++ b/app/server/src/modules/system/auth.ts @@ -0,0 +1,88 @@ +import { System } from "modules/system/main.ts"; +import { getRandomString } from "@oh/utils"; + +type Props = { + url: string; + connectionToken?: string; +}; + +export const auth = () => { + let $hotelId: string; + let $integrationId: string; + //TODO permanent op + let $ownerId: string; + + const state = getRandomString(64); + + const load = async () => { + if (!(await isAuthEnabled())) + console.error("/!\\ Auth service is down or Hotel License is not valid!"); + }; + + const isAuthEnabled = async () => { + if (!System.getConfig().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 config = System.getConfig(); + 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; + + const getState = () => state; + + const getConnectionUrl = () => { + const config = System.getConfig(); + + const composedRedirectUrl = new URL(`${config.auth.api}/connection`); + composedRedirectUrl.searchParams.append("state", state); + + composedRedirectUrl.searchParams.append("hotelId", $hotelId); + composedRedirectUrl.searchParams.append("integrationId", $integrationId); + + return composedRedirectUrl.href; + }; + + return { + load, + isAuthEnabled, + + fetch: $fetch, + + getHotelId, + getIntegrationId, + getOwnerId, + + getState, + + getConnectionUrl, + }; +}; diff --git a/app/server/src/modules/system/main.ts b/app/server/src/modules/system/main.ts index e93928e..7dc0061 100644 --- a/app/server/src/modules/system/main.ts +++ b/app/server/src/modules/system/main.ts @@ -4,6 +4,8 @@ import { getConfig as $getConfig, getDb, update, DbMutable } from "@oh/utils"; import { CONFIG_DEFAULT } from "shared/consts/config.consts.ts"; import { Migrations } from "modules/migrations/main.ts"; import { backups } from "modules/system/backups.ts"; +import { auth } from "modules/system/auth.ts"; +import { accounts } from "modules/system/accounts.ts"; export const System = (() => { let $config: ConfigTypes; @@ -13,6 +15,8 @@ export const System = (() => { const $api = api(); const $backups = backups(); + const $auth = auth(); + const $accounts = accounts(); let $db: DbMutable; const load = async (envs: Envs, testMode: boolean = false) => { @@ -60,6 +64,7 @@ export const System = (() => { if (isProduction) await $backups.load(); + await $auth.load(); $api.load(testMode); }; @@ -87,5 +92,7 @@ export const System = (() => { }, api: $api, backups: $backups, + auth: $auth, + accounts: $accounts, }; })(); diff --git a/app/server/src/shared/consts/config.consts.ts b/app/server/src/shared/consts/config.consts.ts index 6e5138e..3626368 100644 --- a/app/server/src/shared/consts/config.consts.ts +++ b/app/server/src/shared/consts/config.consts.ts @@ -21,4 +21,9 @@ export const CONFIG_DEFAULT: ConfigTypes = { database: { filename: "database", }, + auth: { + enabled: true, + api: "http://localhost:2024", + licenseToken: "", + }, }; diff --git a/app/server/src/shared/enums/main.ts b/app/server/src/shared/enums/main.ts index e69de29..c07b17f 100644 --- a/app/server/src/shared/enums/main.ts +++ b/app/server/src/shared/enums/main.ts @@ -0,0 +1 @@ +export * from "./scopes.enums.ts"; 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..cc097c9 --- /dev/null +++ b/app/server/src/shared/enums/scopes.enums.ts @@ -0,0 +1,9 @@ +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", + + ONET_TELEPORTS_LINK = "onet.teleports.link", +} diff --git a/app/server/src/shared/types/config.types.ts b/app/server/src/shared/types/config.types.ts index 6a56710..6f3875c 100644 --- a/app/server/src/shared/types/config.types.ts +++ b/app/server/src/shared/types/config.types.ts @@ -19,4 +19,9 @@ export type ConfigTypes = { database: { filename: string; }; + auth: { + enabled: boolean; + licenseToken: string; + api: string; + }; }; From 01f6bf10a9dda3ae6629ed8488b0d615eee6e918 Mon Sep 17 00:00:00 2001 From: pablo Date: Sat, 20 Sep 2025 22:09:27 +0200 Subject: [PATCH 2/6] more --- app/client/package.json | 1 - .../components/header/header.component.tsx | 30 ++++ .../components/header/header.module.scss | 11 ++ .../account/components/header/index.ts | 2 + .../src/modules/account/components/index.ts | 2 + app/client/src/modules/account/index.ts | 1 + .../providers/providers.component.tsx | 14 +- .../wrapper-layout.component.tsx | 8 +- .../src/modules/home/home.component.tsx | 58 +----- app/client/src/shared/hooks/index.ts | 2 +- app/client/src/shared/hooks/useApi.ts | 15 +- app/client/src/shared/hooks/useAppSession.tsx | 123 +++++++++++++ .../src/shared/hooks/useFingerprint.tsx | 45 ----- app/client/src/shared/types/account.types.ts | 6 + app/client/src/shared/types/index.ts | 1 + app/client/src/shared/types/request.types.ts | 2 +- app/server/deno.json | 1 + app/server/mod.ts | 2 +- app/server/src/main.ts | 1 + .../src/modules/api/auth/main.request.ts | 22 +++ app/server/src/modules/api/auth/main.ts | 10 ++ .../src/modules/api/auth/redirect.request.ts | 26 +++ .../src/modules/api/auth/user.request.ts | 27 +++ app/server/src/modules/api/main.ts | 9 + .../src/modules/api/{v1 => }/misc/main.ts | 0 .../modules/api/{v1 => }/misc/version.http | 0 .../api/{v1 => }/misc/version.request.ts | 2 +- .../modules/api/v1/account/account.request.ts | 24 --- app/server/src/modules/api/v1/account/main.ts | 7 - .../src/modules/api/v1/auth/auth.request.ts | 21 --- .../src/modules/api/v1/auth/login.request.ts | 42 ----- app/server/src/modules/api/v1/auth/main.ts | 8 - app/server/src/modules/api/v1/main.ts | 10 -- app/server/src/modules/system/api.ts | 167 ------------------ app/server/src/modules/system/auth.ts | 88 --------- app/server/src/shared/consts/config.consts.ts | 6 +- app/server/src/shared/types/config.types.ts | 4 +- .../src/{modules => }/system/accounts.ts | 0 app/server/src/system/api.ts | 109 ++++++++++++ .../src/{modules => }/system/backups.ts | 2 +- app/server/src/{modules => }/system/main.ts | 10 +- .../migrations/9999-test.migration.ts | 0 .../{modules => system}/migrations/main.ts | 0 43 files changed, 417 insertions(+), 502 deletions(-) create mode 100644 app/client/src/modules/account/components/header/header.component.tsx create mode 100644 app/client/src/modules/account/components/header/header.module.scss create mode 100644 app/client/src/modules/account/components/header/index.ts create mode 100644 app/client/src/modules/account/components/index.ts create mode 100644 app/client/src/modules/account/index.ts create mode 100644 app/client/src/shared/hooks/useAppSession.tsx delete mode 100644 app/client/src/shared/hooks/useFingerprint.tsx create mode 100644 app/client/src/shared/types/account.types.ts create mode 100644 app/server/src/main.ts create mode 100644 app/server/src/modules/api/auth/main.request.ts create mode 100644 app/server/src/modules/api/auth/main.ts create mode 100644 app/server/src/modules/api/auth/redirect.request.ts create mode 100644 app/server/src/modules/api/auth/user.request.ts create mode 100644 app/server/src/modules/api/main.ts rename app/server/src/modules/api/{v1 => }/misc/main.ts (100%) rename app/server/src/modules/api/{v1 => }/misc/version.http (100%) rename app/server/src/modules/api/{v1 => }/misc/version.request.ts (88%) delete mode 100644 app/server/src/modules/api/v1/account/account.request.ts delete mode 100644 app/server/src/modules/api/v1/account/main.ts delete mode 100644 app/server/src/modules/api/v1/auth/auth.request.ts delete mode 100644 app/server/src/modules/api/v1/auth/login.request.ts delete mode 100644 app/server/src/modules/api/v1/auth/main.ts delete mode 100644 app/server/src/modules/api/v1/main.ts delete mode 100644 app/server/src/modules/system/api.ts delete mode 100644 app/server/src/modules/system/auth.ts rename app/server/src/{modules => }/system/accounts.ts (100%) create mode 100644 app/server/src/system/api.ts rename app/server/src/{modules => }/system/backups.ts (90%) rename app/server/src/{modules => }/system/main.ts (88%) rename app/server/src/{modules => system}/migrations/9999-test.migration.ts (100%) rename app/server/src/{modules => system}/migrations/main.ts (100%) diff --git a/app/client/package.json b/app/client/package.json index 7365292..2e5e3bb 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -10,7 +10,6 @@ }, "dependencies": { "@openhotel/web-components": "0.3.2", - "@thumbmarkjs/thumbmarkjs": "^0.20.2", "@vitejs/plugin-react": "^4.3.1", "@vitejs/plugin-react-refresh": "1.3.6", "dayjs": "1.11.13", diff --git a/app/client/src/modules/account/components/header/header.component.tsx b/app/client/src/modules/account/components/header/header.component.tsx new file mode 100644 index 0000000..06128cd --- /dev/null +++ b/app/client/src/modules/account/components/header/header.component.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { useAppSession } from "shared/hooks"; +import { ButtonComponent } from "@openhotel/web-components"; +import styles from "./header.module.scss"; + +export const HeaderComponent: React.FC = () => { + const { account, login, logout } = useAppSession(); + + if (!account) + return ( + <> +
+ + Login + + + ); + + return ( + <> +
+
+ +
+ logout +
+
+ + ); +}; diff --git a/app/client/src/modules/account/components/header/header.module.scss b/app/client/src/modules/account/components/header/header.module.scss new file mode 100644 index 0000000..503ef6c --- /dev/null +++ b/app/client/src/modules/account/components/header/header.module.scss @@ -0,0 +1,11 @@ + +.header { + display: flex; + gap: 1rem; + + .username {} + .logout { + cursor: pointer; + opacity: .35; + } +} \ No newline at end of file diff --git a/app/client/src/modules/account/components/header/index.ts b/app/client/src/modules/account/components/header/index.ts new file mode 100644 index 0000000..3a09897 --- /dev/null +++ b/app/client/src/modules/account/components/header/index.ts @@ -0,0 +1,2 @@ + +export * from './header.component' \ No newline at end of file diff --git a/app/client/src/modules/account/components/index.ts b/app/client/src/modules/account/components/index.ts new file mode 100644 index 0000000..a897d50 --- /dev/null +++ b/app/client/src/modules/account/components/index.ts @@ -0,0 +1,2 @@ + +export * from './header' \ No newline at end of file diff --git a/app/client/src/modules/account/index.ts b/app/client/src/modules/account/index.ts new file mode 100644 index 0000000..40b494c --- /dev/null +++ b/app/client/src/modules/account/index.ts @@ -0,0 +1 @@ +export * from "./components"; 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 bab6f35..c2ab40b 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,16 @@ import { Outlet } from "react-router"; -import { FingerprintProvider, TitleProvider } from "shared/hooks"; +import { TitleProvider, AppSessionProvider } from "shared/hooks"; import React from "react"; import { ModalProvider } from "@openhotel/web-components"; 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 ec184b4..4db7b2b 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,10 +1,16 @@ import React from "react"; import { MainLayoutComponent } from "@openhotel/web-components"; +import { HeaderComponent } from "modules/account"; type Props = { children: React.ReactNode; }; export const WrapperLayoutComponent = ({ children }: Props) => { - return ; + return ( + } + /> + ); }; diff --git a/app/client/src/modules/home/home.component.tsx b/app/client/src/modules/home/home.component.tsx index c4dce6d..e9e9a62 100644 --- a/app/client/src/modules/home/home.component.tsx +++ b/app/client/src/modules/home/home.component.tsx @@ -1,56 +1,14 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; import { ButtonComponent } from "@openhotel/web-components"; -import { useApi, useCookies } from "shared/hooks"; -import { RequestMethod } from "shared/enums"; -import { useSearchParams } from "react-router-dom"; export const HomeComponent: React.FC = () => { - const { fetch } = useApi(); - const { set, get } = useCookies(); - const [searchParams] = useSearchParams(); - - const [account, setAccount] = useState(null); - const [connectionUrl, setConnectionUrl] = useState(null); - - useEffect(() => { - fetch({ - method: RequestMethod.GET, - pathname: "/account", - headers: { - token: get("token"), - }, - }).then(({ data: { account } }) => setAccount(account)); - - fetch({ method: RequestMethod.GET, pathname: "/auth" }).then( - ({ data: { connectionUrl } }) => setConnectionUrl(connectionUrl), - ); - }, [fetch, setAccount, setConnectionUrl, get]); - - useEffect(() => { - if (!searchParams.has("state") || !searchParams.has("token")) return; - - const body = { - state: searchParams.get("state"), - token: searchParams.get("token"), - }; - window.history.pushState(null, null, "/"); - - fetch({ method: RequestMethod.POST, pathname: "/auth/login", body }).then( - ({ data: { account, token } }) => { - setAccount(account); - set("token", token, 10_000); - }, - ); - }, [searchParams, setAccount, set]); - - console.log(account); return (

Home

{ Play!

- {account ? null : ( - window.location.replace(connectionUrl)} - > - Login - - )}

); }; diff --git a/app/client/src/shared/hooks/index.ts b/app/client/src/shared/hooks/index.ts index ea00975..afd4144 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 "./useFingerprint"; +export * from "./useAppSession"; export * from "./useCookies"; diff --git a/app/client/src/shared/hooks/useApi.ts b/app/client/src/shared/hooks/useApi.ts index b00ac52..48aa43f 100644 --- a/app/client/src/shared/hooks/useApi.ts +++ b/app/client/src/shared/hooks/useApi.ts @@ -1,11 +1,8 @@ import { Request } from "shared/types"; import { RequestMethod } from "../enums"; import { useCallback } from "react"; -import { useFingerprint } from "./useFingerprint"; export const useApi = () => { - const { fingerprint } = useFingerprint(); - const $fetch = useCallback( async ({ method = RequestMethod.GET, @@ -16,14 +13,12 @@ export const useApi = () => { rawResponse = false, preventReload = false, }: Request) => { - const response = await fetch(`/api/v1${pathname}`, { + const response = await fetch(`/api/${pathname}`, { method, headers: new Headers({ - "Content-Type": "application/json", - fingerprint, ...headers, }), - body: body ? JSON.stringify(body) : undefined, + body, credentials: "include", cache: cache ? "default" : "no-store", }).then(async (data) => { @@ -57,7 +52,7 @@ export const useApi = () => { if (rawResponse) return response; - if (!preventReload && response.status === 403) { + if (response.status === 403 && !preventReload) { globalThis.location.reload(); return; } @@ -66,7 +61,7 @@ export const useApi = () => { return response; }, - [fingerprint], + [], ); const getVersion = useCallback(async (): Promise => { @@ -74,7 +69,7 @@ export const useApi = () => { data: { version }, } = await $fetch({ method: RequestMethod.GET, - pathname: "/_/version", + pathname: "/version", }); return version; }, [$fetch]); diff --git a/app/client/src/shared/hooks/useAppSession.tsx b/app/client/src/shared/hooks/useAppSession.tsx new file mode 100644 index 0000000..6adbdb5 --- /dev/null +++ b/app/client/src/shared/hooks/useAppSession.tsx @@ -0,0 +1,123 @@ +import React, { + ReactNode, + useCallback, + useContext, + useEffect, + useState, +} from "react"; +import { useApi, useCookies } from "shared/hooks"; +import { Account } from "shared/types"; +import { ulid } from "ulidx"; + +type AppSessionState = { + getHeaders: () => Record; + account: Account | null; + login: () => Promise; + logout: () => void; +}; + +const AppSessionContext = React.createContext(undefined); + +type ProviderProps = { + children: ReactNode; +}; + +export const AppSessionProvider: React.FunctionComponent = ({ + children, +}) => { + const { get, set, remove } = useCookies(); + const { fetch } = useApi(); + + const [account, setAccount] = useState(null); + + const params = new URLSearchParams(window.location.hash.replace("#", "?")); + const $accountId = params.get("accountId"); + const $accountToken = params.get("accountToken"); + + const getHeaders = useCallback( + () => ({ + "account-id": $accountId ?? get("account-id"), + "account-token": $accountToken ?? get("account-token"), + }), + [$accountId, $accountToken, get], + ); + + useEffect(() => { + const accountId = $accountId ?? get("account-id"); + const accountToken = $accountToken ?? get("account-token"); + + if (window.location.hash) + window.history.replaceState("", document.title, window.location.pathname); + + (async () => { + const { + data: { enabled }, + } = await fetch({ + pathname: "auth", + }); + + if (!enabled) { + setAccount({ + accountId: ulid(), + username: "development", + admin: true, + languages: ["en"], + }); + return; + } + + if (accountId && accountToken) { + try { + const { status, data } = await fetch({ + pathname: `auth/user`, + headers: getHeaders(), + preventReload: true, + }); + + if (status === 200) { + set("account-id", accountId, 1); + set("account-token", accountToken, 1); + setAccount(data); + return; + } + } catch (e) { + remove("account-id"); + remove("account-token"); + } + } + })(); + }, [fetch, getHeaders, setAccount, remove, set, ulid]); + + const login = useCallback(async () => { + if (account) return; + + const { data } = await fetch({ + pathname: "auth/redirect", + }); + window.location.replace(data.url); + }, []); + + const logout = useCallback(() => { + console.log(account); + if (!account) return; + + remove("account-id"); + remove("account-token"); + setAccount(null); + }, [remove, setAccount, account]); + + return ( + + ); +}; + +export const useAppSession = (): AppSessionState => + useContext(AppSessionContext); diff --git a/app/client/src/shared/hooks/useFingerprint.tsx b/app/client/src/shared/hooks/useFingerprint.tsx deleted file mode 100644 index 8a83073..0000000 --- a/app/client/src/shared/hooks/useFingerprint.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { getFingerprint, setOption } from "@thumbmarkjs/thumbmarkjs"; -import React, { ReactNode, useContext, useEffect, useState } from "react"; - -type FingerprintState = { - fingerprint: string; -}; - -const FingerprintContext = React.createContext(undefined); - -type ProviderProps = { - children: ReactNode; -}; - -export const FingerprintProvider: React.FunctionComponent = ({ - children, -}) => { - const [fingerprint, setFingerprint] = useState(null); - - useEffect(() => { - setOption("exclude", [ - //prevents browser version change - "system.browser.version", - "system.useragent", - //prevents updates on webgl - "webgl", - //prevents screen changes - "screen", - //prevent audio changes - "audio", - //prevents plugin installs/uninstalls - "plugins", - ]); - getFingerprint().then(setFingerprint); - }, [setFingerprint]); - - return ( - - ); -}; - -export const useFingerprint = (): FingerprintState => - useContext(FingerprintContext); diff --git a/app/client/src/shared/types/account.types.ts b/app/client/src/shared/types/account.types.ts new file mode 100644 index 0000000..a80e669 --- /dev/null +++ b/app/client/src/shared/types/account.types.ts @@ -0,0 +1,6 @@ +export type Account = { + accountId: string; + username: string; + languages: string[]; + admin?: boolean; +}; diff --git a/app/client/src/shared/types/index.ts b/app/client/src/shared/types/index.ts index f3db1a8..b66aa04 100644 --- a/app/client/src/shared/types/index.ts +++ b/app/client/src/shared/types/index.ts @@ -1 +1,2 @@ export * from "./request.types"; +export * from "./account.types"; diff --git a/app/client/src/shared/types/request.types.ts b/app/client/src/shared/types/request.types.ts index bcd6ea4..57054c0 100644 --- a/app/client/src/shared/types/request.types.ts +++ b/app/client/src/shared/types/request.types.ts @@ -3,7 +3,7 @@ import { RequestMethod } from "shared/enums"; export type Request = { method?: RequestMethod; pathname: string; - body?: unknown; + body?: any; headers?: Record; cache?: boolean; rawResponse?: boolean; diff --git a/app/server/deno.json b/app/server/deno.json index 1a726bf..9d3e371 100644 --- a/app/server/deno.json +++ b/app/server/deno.json @@ -21,6 +21,7 @@ "@oh/utils": "jsr:@oh/utils@1.5.2", "shared/": "./src/shared/", + "system/": "./src/system/", "modules/": "./src/modules/", "deno/": "https://deno.land/std@0.221.0/", diff --git a/app/server/mod.ts b/app/server/mod.ts index 19d69bc..5012b0a 100644 --- a/app/server/mod.ts +++ b/app/server/mod.ts @@ -1,4 +1,4 @@ -import { System } from "modules/system/main.ts"; +import { System } from "system/main.ts"; import { getProcessedEnvs } from "shared/utils/main.ts"; const envs = getProcessedEnvs({ diff --git a/app/server/src/main.ts b/app/server/src/main.ts new file mode 100644 index 0000000..ec02433 --- /dev/null +++ b/app/server/src/main.ts @@ -0,0 +1 @@ +export * from "./system/main.ts"; diff --git a/app/server/src/modules/api/auth/main.request.ts b/app/server/src/modules/api/auth/main.request.ts new file mode 100644 index 0000000..ab3ca42 --- /dev/null +++ b/app/server/src/modules/api/auth/main.request.ts @@ -0,0 +1,22 @@ +import { RequestType, RequestMethod } from "@oh/utils"; +import { System } from "system/main.ts"; + +export const getRequest: RequestType = { + method: RequestMethod.GET, + pathname: "", + func: (request) => { + const { + auth: { enabled }, + } = System.getConfig(); + + return Response.json( + { + status: 200, + data: { + enabled, + }, + }, + { status: 200 }, + ); + }, +}; diff --git a/app/server/src/modules/api/auth/main.ts b/app/server/src/modules/api/auth/main.ts new file mode 100644 index 0000000..dc21f76 --- /dev/null +++ b/app/server/src/modules/api/auth/main.ts @@ -0,0 +1,10 @@ +import { RequestType, getPathRequestList } from "@oh/utils"; + +import { redirectRequest } from "./redirect.request.ts"; +import { userRequest } from "./user.request.ts"; +import { getRequest } from "./main.request.ts"; + +export const authList: RequestType[] = getPathRequestList({ + requestList: [getRequest, redirectRequest, 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 new file mode 100644 index 0000000..ef0dd8d --- /dev/null +++ b/app/server/src/modules/api/auth/redirect.request.ts @@ -0,0 +1,26 @@ +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/user.request.ts b/app/server/src/modules/api/auth/user.request.ts new file mode 100644 index 0000000..e4294bf --- /dev/null +++ b/app/server/src/modules/api/auth/user.request.ts @@ -0,0 +1,27 @@ +import { RequestType, RequestMethod, RequestKind } from "@oh/utils"; +import { System } from "system/main.ts"; + +export const userRequest: RequestType = { + method: RequestMethod.GET, + 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 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, + }); + }, +}; diff --git a/app/server/src/modules/api/main.ts b/app/server/src/modules/api/main.ts new file mode 100644 index 0000000..4cd3336 --- /dev/null +++ b/app/server/src/modules/api/main.ts @@ -0,0 +1,9 @@ +import { RequestType, getPathRequestList } from "@oh/utils"; + +import { miscRequestList } from "./misc/main.ts"; +import { authList } from "./auth/main.ts"; + +export const requestV1List: RequestType[] = getPathRequestList({ + requestList: [...miscRequestList, ...authList], + pathname: "/api", +}); diff --git a/app/server/src/modules/api/v1/misc/main.ts b/app/server/src/modules/api/misc/main.ts similarity index 100% rename from app/server/src/modules/api/v1/misc/main.ts rename to app/server/src/modules/api/misc/main.ts diff --git a/app/server/src/modules/api/v1/misc/version.http b/app/server/src/modules/api/misc/version.http similarity index 100% rename from app/server/src/modules/api/v1/misc/version.http rename to app/server/src/modules/api/misc/version.http diff --git a/app/server/src/modules/api/v1/misc/version.request.ts b/app/server/src/modules/api/misc/version.request.ts similarity index 88% rename from app/server/src/modules/api/v1/misc/version.request.ts rename to app/server/src/modules/api/misc/version.request.ts index 27c03c9..6993a5f 100644 --- a/app/server/src/modules/api/v1/misc/version.request.ts +++ b/app/server/src/modules/api/misc/version.request.ts @@ -5,7 +5,7 @@ import { HttpStatusCode, RequestKind, } from "@oh/utils"; -import { System } from "modules/system/main.ts"; +import { System } from "system/main.ts"; export const versionRequest: RequestType = { method: RequestMethod.GET, diff --git a/app/server/src/modules/api/v1/account/account.request.ts b/app/server/src/modules/api/v1/account/account.request.ts deleted file mode 100644 index 5538658..0000000 --- a/app/server/src/modules/api/v1/account/account.request.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { - RequestType, - RequestMethod, - getResponse, - HttpStatusCode, - RequestKind, -} from "@oh/utils"; -import { System } from "modules/system/main.ts"; - -export const getRequest: RequestType = { - method: RequestMethod.GET, - pathname: "", - kind: RequestKind.PUBLIC, - func: async (request: Request) => { - const fingerprint = request.headers.get("fingerprint"); - const token = request.headers.get("token"); - - return getResponse(HttpStatusCode.OK, { - data: { - account: System.accounts.get(fingerprint, token), - }, - }); - }, -}; diff --git a/app/server/src/modules/api/v1/account/main.ts b/app/server/src/modules/api/v1/account/main.ts deleted file mode 100644 index 51b8a15..0000000 --- a/app/server/src/modules/api/v1/account/main.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { RequestType, getPathRequestList } from "@oh/utils"; -import { getRequest } from "./account.request.ts"; - -export const accountRequestList: RequestType[] = getPathRequestList({ - requestList: [getRequest], - pathname: "/account", -}); diff --git a/app/server/src/modules/api/v1/auth/auth.request.ts b/app/server/src/modules/api/v1/auth/auth.request.ts deleted file mode 100644 index 374a046..0000000 --- a/app/server/src/modules/api/v1/auth/auth.request.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { - RequestType, - RequestMethod, - getResponse, - HttpStatusCode, - RequestKind, -} from "@oh/utils"; -import { System } from "modules/system/main.ts"; - -export const getRequest: RequestType = { - method: RequestMethod.GET, - pathname: "", - kind: RequestKind.PUBLIC, - func: () => { - return getResponse(HttpStatusCode.OK, { - data: { - connectionUrl: System.auth.getConnectionUrl(), - }, - }); - }, -}; diff --git a/app/server/src/modules/api/v1/auth/login.request.ts b/app/server/src/modules/api/v1/auth/login.request.ts deleted file mode 100644 index 135939d..0000000 --- a/app/server/src/modules/api/v1/auth/login.request.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { - RequestType, - RequestMethod, - getResponse, - HttpStatusCode, - RequestKind, -} from "@oh/utils"; -import { System } from "modules/system/main.ts"; - -export const postLoginRequest: RequestType = { - method: RequestMethod.POST, - pathname: "/login", - kind: RequestKind.PUBLIC, - func: async (request: Request) => { - const { state, token: connectionToken } = await request.json(); - const fingerprint = request.headers.get("fingerprint"); - - if (state !== System.auth.getState()) - return getResponse(HttpStatusCode.BAD_REQUEST); - - const { accountId, username, admin, languages } = await System.auth.fetch({ - url: "/user/@me", - connectionToken, - }); - - const account = { - accountId, - username, - admin, - languages, - }; - - const token = System.accounts.set(fingerprint, { ...account }); - - return getResponse(HttpStatusCode.OK, { - data: { - token, - account, - }, - }); - }, -}; diff --git a/app/server/src/modules/api/v1/auth/main.ts b/app/server/src/modules/api/v1/auth/main.ts deleted file mode 100644 index 641feba..0000000 --- a/app/server/src/modules/api/v1/auth/main.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { RequestType, getPathRequestList } from "@oh/utils"; -import { getRequest } from "./auth.request.ts"; -import { postLoginRequest } from "./login.request.ts"; - -export const authRequestList: RequestType[] = getPathRequestList({ - requestList: [getRequest, postLoginRequest], - pathname: "/auth", -}); diff --git a/app/server/src/modules/api/v1/main.ts b/app/server/src/modules/api/v1/main.ts deleted file mode 100644 index 91e9ec8..0000000 --- a/app/server/src/modules/api/v1/main.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { RequestType, getPathRequestList } from "@oh/utils"; - -import { miscRequestList } from "./misc/main.ts"; -import { authRequestList } from "./auth/main.ts"; -import { accountRequestList } from "./account/main.ts"; - -export const requestV1List: RequestType[] = getPathRequestList({ - requestList: [...miscRequestList, ...authRequestList, ...accountRequestList], - pathname: "/api/v1", -}); diff --git a/app/server/src/modules/system/api.ts b/app/server/src/modules/system/api.ts deleted file mode 100644 index daa8d9c..0000000 --- a/app/server/src/modules/system/api.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { - getApiHandler, - getContentType, - getCORSHeaders, - getResponse, - HttpStatusCode, - RequestMethod, - RequestKind, -} from "@oh/utils"; -import { System } from "./main.ts"; -import { requestV1List } from "modules/api/v1/main.ts"; - -export const api = () => { - const load = (testMode: boolean = false) => { - const $apiHandler = getApiHandler({ - testMode, - requests: requestV1List, - checkAccess: checkAccess, - }); - - $apiHandler.overview(); - - const { version, port } = System.getConfig(); - const isDevelopment = version === "development"; - Deno.serve( - { - port: port * (isDevelopment ? 10 : 1), - }, - async ($request: Request, connInfo) => { - const headers = new Headers($request.headers); - headers.set("remote-address", connInfo.remoteAddr.hostname); - const request = new Request($request, { headers }); - - try { - const { url, method } = request; - if (method === RequestMethod.OPTIONS) { - return new Response(null, { - headers: getCORSHeaders(), - status: 204, - }); - } - - const parsedUrl = new URL(url); - - if (!parsedUrl.pathname.startsWith("/api")) { - try { - const fileData = await Deno.readFile( - "./client" + parsedUrl.pathname, - ); - - return new Response(fileData, { - headers: { - "Content-Type": getContentType(parsedUrl.pathname), - }, - }); - } catch (e) { - return new Response( - await Deno.readTextFile(`./client/index.html`), - { - headers: { - "content-type": "text/html", - }, - }, - ); - } - } - - return await $apiHandler.on(request); - } catch (e) { - console.log(e); - } - return getResponse(HttpStatusCode.INTERNAL_SERVER_ERROR); - }, - ); - }; - - const checkAccess = async ({ - request, - kind, - }: { - request: Request; - kind: RequestKind | RequestKind[]; - }): Promise => { - const check = async (kind: RequestKind) => { - // const accountId = request.headers.get("account-id"); - // const licenseToken = request.headers.get("license-token"); - // let account: AccountMutable; - - switch (kind) { - case RequestKind.PUBLIC: - return true; - // case RequestKind.ACCOUNT_REFRESH: - // if (!accountId) return false; - // - // account = await System.accounts.getAccount({ accountId }); - // if (!account) return false; - // - // return await account.checkRefreshToken(request); - // case RequestKind.ACCOUNT: - // if (!accountId) return false; - // - // account = await System.accounts.getAccount({ accountId }); - // if (!account) return false; - // - // return await account.checkToken(request); - // case RequestKind.LICENSE: { - // if (!licenseToken) return false; - // - // const licenseHotel = await System.hotels.getHotel({ licenseToken }); - // return Boolean(licenseHotel); - // } - // case RequestKind.CONNECTION: { - // if (!licenseToken) return false; - // - // const hotel = await System.hotels.getHotel({ licenseToken }); - // if (!hotel) return false; - // - // account = await System.accounts.getAccount({ request }); - // if (!account) return false; - // - // const connection = await account.connections.active.get(); - // if (!connection) return false; - // - // const hotelData = hotel.getObject(); - // if (hotelData.hotelId !== connection.hotelId) return false; - // - // const hotelIntegration = hotel.integrations.getIntegration( - // connection.integrationId, - // ); - // return Boolean(hotelIntegration); - // } - // case RequestKind.ADMIN: { - // if (!accountId) return false; - // - // account = await System.accounts.getAccount({ accountId }); - // if (!account || !(await account.checkToken(request))) return false; - // - // const { version } = System.getConfig(); - // const isDevelopment = version === "development"; - // - // const isAdmin = await account.isAdmin(); - // const isVerified = await account.otp.isVerified(); - // - // return isAdmin && (isDevelopment || isVerified); - // } - // case RequestKind.TOKEN: { - // const appToken = request.headers.get("app-token"); - // return appToken && (await System.tokens.verify(appToken)); - // } - // case RequestKind.APPS: { - // const $appToken = request.headers.get("app-token"); - // return $appToken && (await System.apps.verify($appToken)); - // } - default: - return false; - } - }; - - return Array.isArray(kind) - ? (await Promise.all(kind.map(check))).includes(true) - : check(kind); - }; - - return { - load, - }; -}; diff --git a/app/server/src/modules/system/auth.ts b/app/server/src/modules/system/auth.ts deleted file mode 100644 index a5864ad..0000000 --- a/app/server/src/modules/system/auth.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { System } from "modules/system/main.ts"; -import { getRandomString } from "@oh/utils"; - -type Props = { - url: string; - connectionToken?: string; -}; - -export const auth = () => { - let $hotelId: string; - let $integrationId: string; - //TODO permanent op - let $ownerId: string; - - const state = getRandomString(64); - - const load = async () => { - if (!(await isAuthEnabled())) - console.error("/!\\ Auth service is down or Hotel License is not valid!"); - }; - - const isAuthEnabled = async () => { - if (!System.getConfig().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 config = System.getConfig(); - 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; - - const getState = () => state; - - const getConnectionUrl = () => { - const config = System.getConfig(); - - const composedRedirectUrl = new URL(`${config.auth.api}/connection`); - composedRedirectUrl.searchParams.append("state", state); - - composedRedirectUrl.searchParams.append("hotelId", $hotelId); - composedRedirectUrl.searchParams.append("integrationId", $integrationId); - - return composedRedirectUrl.href; - }; - - return { - load, - isAuthEnabled, - - fetch: $fetch, - - getHotelId, - getIntegrationId, - getOwnerId, - - getState, - - getConnectionUrl, - }; -}; diff --git a/app/server/src/shared/consts/config.consts.ts b/app/server/src/shared/consts/config.consts.ts index 3626368..4b902a7 100644 --- a/app/server/src/shared/consts/config.consts.ts +++ b/app/server/src/shared/consts/config.consts.ts @@ -22,8 +22,8 @@ export const CONFIG_DEFAULT: ConfigTypes = { filename: "database", }, auth: { - enabled: true, - api: "http://localhost:2024", - licenseToken: "", + enabled: false, + appToken: "", + url: "http://localhost:2024", }, }; diff --git a/app/server/src/shared/types/config.types.ts b/app/server/src/shared/types/config.types.ts index 6f3875c..316cfcf 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; - licenseToken: string; - api: string; + appToken: string; + url: string; }; }; diff --git a/app/server/src/modules/system/accounts.ts b/app/server/src/system/accounts.ts similarity index 100% rename from app/server/src/modules/system/accounts.ts rename to app/server/src/system/accounts.ts diff --git a/app/server/src/system/api.ts b/app/server/src/system/api.ts new file mode 100644 index 0000000..00ad3a0 --- /dev/null +++ b/app/server/src/system/api.ts @@ -0,0 +1,109 @@ +import { + getApiHandler, + getContentType, + getCORSHeaders, + getResponse, + HttpStatusCode, + RequestMethod, + RequestKind, +} from "@oh/utils"; +import { System } from "./main.ts"; +import { requestV1List } from "modules/api/main.ts"; + +export const api = () => { + const load = (testMode: boolean = false) => { + const $apiHandler = getApiHandler({ + testMode, + requests: requestV1List, + checkAccess: checkAccess, + }); + + $apiHandler.overview(); + + const { version, port } = System.getConfig(); + const isDevelopment = version === "development"; + Deno.serve( + { + port: port * (isDevelopment ? 10 : 1), + }, + async ($request: Request, connInfo) => { + const headers = new Headers($request.headers); + headers.set("remote-address", connInfo.remoteAddr.hostname); + const request = new Request($request, { headers }); + + try { + const { url, method } = request; + if (method === RequestMethod.OPTIONS) { + return new Response(null, { + headers: getCORSHeaders(), + status: 204, + }); + } + + const parsedUrl = new URL(url); + + if (!parsedUrl.pathname.startsWith("/api")) { + try { + const fileData = await Deno.readFile( + "./client" + parsedUrl.pathname, + ); + + return new Response(fileData, { + headers: { + "Content-Type": getContentType(parsedUrl.pathname), + }, + }); + } catch (e) { + return new Response( + await Deno.readTextFile(`./client/index.html`), + { + headers: { + "content-type": "text/html", + }, + }, + ); + } + } + + return await $apiHandler.on(request); + } catch (e) { + console.log(e); + } + return getResponse(HttpStatusCode.INTERNAL_SERVER_ERROR); + }, + ); + }; + + const checkAccess = async ({ + request, + kind, + }: { + request: Request; + kind: RequestKind | RequestKind[]; + }): Promise => { + const check = async (kind: RequestKind) => { + const accountId = request.headers.get("account-id"); + const accountToken = request.headers.get("account-token"); + + if (!System.getConfig().auth.enabled) return true; + + switch (kind) { + case RequestKind.PUBLIC: + return true; + case RequestKind.ACCOUNT: + if (!accountId || !accountToken) return false; + return true; + default: + return false; + } + }; + + return Array.isArray(kind) + ? (await Promise.all(kind.map(check))).includes(true) + : check(kind); + }; + + return { + load, + }; +}; diff --git a/app/server/src/modules/system/backups.ts b/app/server/src/system/backups.ts similarity index 90% rename from app/server/src/modules/system/backups.ts rename to app/server/src/system/backups.ts index e42c8c3..a676c21 100644 --- a/app/server/src/modules/system/backups.ts +++ b/app/server/src/system/backups.ts @@ -1,4 +1,4 @@ -import { System } from "modules/system/main.ts"; +import { System } from "./main.ts"; export const backups = () => { let abortCronController: AbortController = new AbortController(); diff --git a/app/server/src/modules/system/main.ts b/app/server/src/system/main.ts similarity index 88% rename from app/server/src/modules/system/main.ts rename to app/server/src/system/main.ts index 7dc0061..4422329 100644 --- a/app/server/src/modules/system/main.ts +++ b/app/server/src/system/main.ts @@ -2,10 +2,9 @@ import { api } from "./api.ts"; import { ConfigTypes, Envs } from "shared/types/main.ts"; import { getConfig as $getConfig, getDb, update, DbMutable } from "@oh/utils"; import { CONFIG_DEFAULT } from "shared/consts/config.consts.ts"; -import { Migrations } from "modules/migrations/main.ts"; -import { backups } from "modules/system/backups.ts"; -import { auth } from "modules/system/auth.ts"; -import { accounts } from "modules/system/accounts.ts"; +import { Migrations } from "./migrations/main.ts"; +import { backups } from "./backups.ts"; +import { accounts } from "./accounts.ts"; export const System = (() => { let $config: ConfigTypes; @@ -15,7 +14,6 @@ export const System = (() => { const $api = api(); const $backups = backups(); - const $auth = auth(); const $accounts = accounts(); let $db: DbMutable; @@ -64,7 +62,6 @@ export const System = (() => { if (isProduction) await $backups.load(); - await $auth.load(); $api.load(testMode); }; @@ -92,7 +89,6 @@ export const System = (() => { }, api: $api, backups: $backups, - auth: $auth, accounts: $accounts, }; })(); diff --git a/app/server/src/modules/migrations/9999-test.migration.ts b/app/server/src/system/migrations/9999-test.migration.ts similarity index 100% rename from app/server/src/modules/migrations/9999-test.migration.ts rename to app/server/src/system/migrations/9999-test.migration.ts diff --git a/app/server/src/modules/migrations/main.ts b/app/server/src/system/migrations/main.ts similarity index 100% rename from app/server/src/modules/migrations/main.ts rename to app/server/src/system/migrations/main.ts From a5d7c02ebbba46f045263e24ec0187e81440e562 Mon Sep 17 00:00:00 2001 From: pablo Date: Sat, 20 Sep 2025 22:12:47 +0200 Subject: [PATCH 3/6] more --- .../modules/account/components/header/header.component.tsx | 2 ++ app/client/src/shared/hooks/useAppSession.tsx | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) 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 06128cd..489ce23 100644 --- a/app/client/src/modules/account/components/header/header.component.tsx +++ b/app/client/src/modules/account/components/header/header.component.tsx @@ -6,6 +6,8 @@ import styles from "./header.module.scss"; export const HeaderComponent: React.FC = () => { const { account, login, logout } = useAppSession(); + if (account === undefined) return; + if (!account) return ( <> diff --git a/app/client/src/shared/hooks/useAppSession.tsx b/app/client/src/shared/hooks/useAppSession.tsx index 6adbdb5..8c0f547 100644 --- a/app/client/src/shared/hooks/useAppSession.tsx +++ b/app/client/src/shared/hooks/useAppSession.tsx @@ -11,7 +11,7 @@ import { ulid } from "ulidx"; type AppSessionState = { getHeaders: () => Record; - account: Account | null; + account: Account | null | undefined; login: () => Promise; logout: () => void; }; @@ -28,7 +28,7 @@ export const AppSessionProvider: React.FunctionComponent = ({ const { get, set, remove } = useCookies(); const { fetch } = useApi(); - const [account, setAccount] = useState(null); + const [account, setAccount] = useState(undefined); const params = new URLSearchParams(window.location.hash.replace("#", "?")); const $accountId = params.get("accountId"); @@ -85,6 +85,8 @@ export const AppSessionProvider: React.FunctionComponent = ({ remove("account-token"); } } + + setAccount(null); })(); }, [fetch, getHeaders, setAccount, remove, set, ulid]); From ac0388b09d8e997064f33cef825317289570e780 Mon Sep 17 00:00:00 2001 From: pablo Date: Sat, 20 Sep 2025 22:20:46 +0200 Subject: [PATCH 4/6] more --- app/client/yarn.lock | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/client/yarn.lock b/app/client/yarn.lock index d411c7b..86f78a8 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -684,13 +684,6 @@ __metadata: languageName: node linkType: hard -"@thumbmarkjs/thumbmarkjs@npm:^0.20.2": - version: 0.20.2 - resolution: "@thumbmarkjs/thumbmarkjs@npm:0.20.2" - checksum: 10c0/1fd6ec8854553d724a28803384d05812eded198739524f914329f5d2a23f580e968c1a05ba8378a9e7499dead3b9bfdfb9a4b76a45e1c4d8f3558851084f3026 - languageName: node - linkType: hard - "@types/babel__core@npm:^7.20.5": version: 7.20.5 resolution: "@types/babel__core@npm:7.20.5" @@ -2069,7 +2062,6 @@ __metadata: dependencies: "@openhotel/web-components": "npm:0.3.2" "@rollup/plugin-alias": "npm:5.1.0" - "@thumbmarkjs/thumbmarkjs": "npm:^0.20.2" "@types/js-cookie": "npm:3.0.6" "@types/react": "npm:18.3.3" "@vitejs/plugin-react": "npm:^4.3.1" From 37e45554aa3bed26814b67a2bfa4828f253580bc Mon Sep 17 00:00:00 2001 From: pablo Date: Sat, 20 Sep 2025 22:21:30 +0200 Subject: [PATCH 5/6] more --- .../components/header/header.module.scss | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/client/src/modules/account/components/header/header.module.scss b/app/client/src/modules/account/components/header/header.module.scss index 503ef6c..081dea1 100644 --- a/app/client/src/modules/account/components/header/header.module.scss +++ b/app/client/src/modules/account/components/header/header.module.scss @@ -1,11 +1,11 @@ - .header { - display: flex; - gap: 1rem; + display: flex; + gap: 1rem; - .username {} - .logout { - cursor: pointer; - opacity: .35; - } -} \ No newline at end of file + .username { + } + .logout { + cursor: pointer; + opacity: 0.35; + } +} From a4cf322e8f871d6a55f3a1a94b24d84126ccc8cb Mon Sep 17 00:00:00 2001 From: pablo Date: Sat, 20 Sep 2025 22:23:08 +0200 Subject: [PATCH 6/6] more --- app/client/src/modules/account/components/header/index.ts | 3 +-- app/client/src/modules/account/components/index.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/client/src/modules/account/components/header/index.ts b/app/client/src/modules/account/components/header/index.ts index 3a09897..c2d38d3 100644 --- a/app/client/src/modules/account/components/header/index.ts +++ b/app/client/src/modules/account/components/header/index.ts @@ -1,2 +1 @@ - -export * from './header.component' \ No newline at end of file +export * from "./header.component"; diff --git a/app/client/src/modules/account/components/index.ts b/app/client/src/modules/account/components/index.ts index a897d50..49ac70f 100644 --- a/app/client/src/modules/account/components/index.ts +++ b/app/client/src/modules/account/components/index.ts @@ -1,2 +1 @@ - -export * from './header' \ No newline at end of file +export * from "./header";