Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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 === undefined) return;

if (!account)
return (
<>
<div />
<ButtonComponent color="light" onPointerDown={login}>
Login
</ButtonComponent>
</>
);

return (
<>
<div />
<div className={styles.header}>
<label className={styles.username}>{account.username}</label>
<div onPointerDown={logout} className={styles.logout}>
logout
</div>
</div>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.header {
display: flex;
gap: 1rem;

.username {
}
.logout {
cursor: pointer;
opacity: 0.35;
}
}
1 change: 1 addition & 0 deletions app/client/src/modules/account/components/header/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./header.component";
1 change: 1 addition & 0 deletions app/client/src/modules/account/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./header";
1 change: 1 addition & 0 deletions app/client/src/modules/account/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./components";
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { Outlet } from "react-router";
import { TitleProvider } from "shared/hooks";
import { TitleProvider, AppSessionProvider } from "shared/hooks";
import React from "react";
import { ModalProvider } from "@openhotel/web-components";

export const ProvidersComponent = () => {
return (
<TitleProvider>
<ModalProvider>
<Outlet />
</ModalProvider>
</TitleProvider>
<AppSessionProvider>
<TitleProvider>
<ModalProvider>
<Outlet />
</ModalProvider>
</TitleProvider>
</AppSessionProvider>
);
};
Original file line number Diff line number Diff line change
@@ -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 <MainLayoutComponent children={children} />;
return (
<MainLayoutComponent
children={children}
headerChildren={<HeaderComponent />}
/>
);
};
7 changes: 6 additions & 1 deletion app/client/src/modules/home/home.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ export const HomeComponent: React.FC = () => {
return (
<div>
<h2>Home</h2>
<label>Welcome to OpenHotel!</label>
<label>
{/*{account*/}
{/* ? `Welcome back ${account.username}!`*/}
{/* : "Welcome to OpenHotel!"}*/}
</label>
<p />
<ButtonComponent
color="yellow"
Expand All @@ -16,6 +20,7 @@ export const HomeComponent: React.FC = () => {
>
Play!
</ButtonComponent>
<p />
</div>
);
};
1 change: 1 addition & 0 deletions app/client/src/shared/enums/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./request.enums";
11 changes: 11 additions & 0 deletions app/client/src/shared/enums/request.enums.ts
Original file line number Diff line number Diff line change
@@ -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",
}
3 changes: 3 additions & 0 deletions app/client/src/shared/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export * from "./useTitle";
export * from "./useApi";
export * from "./useAppSession";
export * from "./useCookies";
81 changes: 81 additions & 0 deletions app/client/src/shared/hooks/useApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Request } from "shared/types";
import { RequestMethod } from "../enums";
import { useCallback } from "react";

export const useApi = () => {
const $fetch = useCallback(
async ({
method = RequestMethod.GET,
pathname,
body,
headers = {},
cache = true,
rawResponse = false,
preventReload = false,
}: Request) => {
const response = await fetch(`/api/${pathname}`, {
method,
headers: new Headers({
...headers,
}),
body,
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 (response.status === 403 && !preventReload) {
globalThis.location.reload();
return;
}

if (response.status !== 200) throw response;

return response;
},
[],
);

const getVersion = useCallback(async (): Promise<string> => {
const {
data: { version },
} = await $fetch({
method: RequestMethod.GET,
pathname: "/version",
});
return version;
}, [$fetch]);

return {
fetch: $fetch,
getVersion,
};
};
125 changes: 125 additions & 0 deletions app/client/src/shared/hooks/useAppSession.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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<string, string>;
account: Account | null | undefined;
login: () => Promise<void>;
logout: () => void;
};

const AppSessionContext = React.createContext<AppSessionState>(undefined);

type ProviderProps = {
children: ReactNode;
};

export const AppSessionProvider: React.FunctionComponent<ProviderProps> = ({
children,
}) => {
const { get, set, remove } = useCookies();
const { fetch } = useApi();

const [account, setAccount] = useState<Account>(undefined);

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");
}
}

setAccount(null);
})();
}, [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 (
<AppSessionContext.Provider
value={{
getHeaders,
account,
login,
logout,
}}
children={children}
/>
);
};

export const useAppSession = (): AppSessionState =>
useContext(AppSessionContext);
21 changes: 21 additions & 0 deletions app/client/src/shared/hooks/useCookies.ts
Original file line number Diff line number Diff line change
@@ -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,
};
};
6 changes: 6 additions & 0 deletions app/client/src/shared/types/account.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type Account = {
accountId: string;
username: string;
languages: string[];
admin?: boolean;
};
2 changes: 2 additions & 0 deletions app/client/src/shared/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./request.types";
export * from "./account.types";
11 changes: 11 additions & 0 deletions app/client/src/shared/types/request.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { RequestMethod } from "shared/enums";

export type Request = {
method?: RequestMethod;
pathname: string;
body?: any;
headers?: Record<string, string>;
cache?: boolean;
rawResponse?: boolean;
preventReload?: boolean;
};
1 change: 1 addition & 0 deletions app/server/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/",
Expand Down
2 changes: 1 addition & 1 deletion app/server/mod.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand Down
1 change: 1 addition & 0 deletions app/server/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./system/main.ts";
Loading