feat: cloud and ee (#805)

* stripe init
git submodules for enterprise modules

* * Cloud billing UI - WIP
* Proxy websockets in dev mode
* Separate workspace login and creation for cloud
* Other fixes

* feat: billing (cloud)

* * add domain service
* prepare links from workspace hostname

* WIP

* Add exchange token generation
* Validate JWT token type during verification

* domain service

* add SkipTransform decorator

* * updates (server)
* add new packages
* new sso migration file

* WIP

* Fix hostname generation

* WIP

* WIP

* Reduce input error font-size
* set max password length

* jwt package

* license page - WIP

* * License management UI
* Move license key store to db

* add reflector

* SSO enforcement

* * Add default plan
* Add usePlan hook

* * Fix auth container margin in mobile
* Redirect login and home to select page in cloud

* update .gitignore

* Default to yearly

* * Trial messaging
* Handle ended trials

* Don't set to readonly on collab disconnect (Cloud)

* Refine trial (UI)
* Fix bug caused by using jotai optics atom in AppHeader component

* configurable database maximum pool

* Close SSO form on save

* wip

* sync

* Only show sign-in in cloud

* exclude base api part from workspaceId check

* close db connection beforeApplicationShutdown

* Add health/live endpoint

* clear cookie on hostname change

* reset currentUser atom

* Change text

* return 401 if workspace does not match

* feat: show user workspace list in cloud login page

* sync

* Add home path

* Prefetch to speed up queries

* * Add robots.txt
* Disallow login and forgot password routes

* wildcard user-agent

* Fix space query cache

* fix

* fix

* use space uuid for recent pages

* prefetch billing plans

* enhance license page

* sync
This commit is contained in:
Philip Okugbe
2025-03-06 13:38:37 +00:00
committed by GitHub
parent 91596be70e
commit b81c9ee10c
148 changed files with 8947 additions and 3458 deletions

View File

@ -1,6 +1,6 @@
import { LoginForm } from "@/features/auth/components/login-form";
import { Helmet } from "react-helmet-async";
import {getAppName} from "@/lib/config.ts";
import { getAppName } from "@/lib/config.ts";
import { useTranslation } from "react-i18next";
export default function LoginPage() {
@ -9,7 +9,9 @@ export default function LoginPage() {
return (
<>
<Helmet>
<title>{t("Login")} - {getAppName()}</title>
<title>
{t("Login")} - {getAppName()}
</title>
</Helmet>
<LoginForm />
</>

View File

@ -3,7 +3,8 @@ import { SetupWorkspaceForm } from "@/features/auth/components/setup-workspace-f
import { Helmet } from "react-helmet-async";
import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import {getAppName} from "@/lib/config.ts";
import APP_ROUTE from "@/lib/app-route.ts";
import { getAppName } from "@/lib/config.ts";
import { useTranslation } from "react-i18next";
export default function SetupWorkspace() {
@ -18,10 +19,10 @@ export default function SetupWorkspace() {
const navigate = useNavigate();
useEffect(() => {
if (!isLoading && !isError && workspace) {
navigate("/");
if (!isLoading && workspace) {
navigate(APP_ROUTE.AUTH.LOGIN);
}
}, [isLoading, isError, workspace]);
}, [isLoading, workspace]);
if (isLoading) {
return <div></div>;
@ -35,7 +36,9 @@ export default function SetupWorkspace() {
return (
<>
<Helmet>
<title>{t("Setup Workspace")} - {getAppName()}</title>
<title>
{t("Setup Workspace")} - {getAppName()}
</title>
</Helmet>
<SetupWorkspaceForm />
</>

View File

@ -1,22 +1,27 @@
import {Container, Space} from "@mantine/core";
import { Container, Space } from "@mantine/core";
import HomeTabs from "@/features/home/components/home-tabs";
import SpaceGrid from "@/features/space/components/space-grid.tsx";
import {getAppName} from "@/lib/config.ts";
import {Helmet} from "react-helmet-async";
import { getAppName } from "@/lib/config.ts";
import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next";
export default function Home() {
return (
<>
<Helmet>
<title>Home - {getAppName()}</title>
</Helmet>
<Container size={"800"} pt="xl">
<SpaceGrid/>
const { t } = useTranslation();
<Space h="xl"/>
return (
<>
<Helmet>
<title>
{t("Home")} - {getAppName()}
</title>
</Helmet>
<Container size={"800"} pt="xl">
<SpaceGrid />
<HomeTabs/>
</Container>
</>
);
<Space h="xl" />
<HomeTabs />
</Container>
</>
);
}

View File

@ -1,69 +1,77 @@
import WorkspaceInviteModal from "@/features/workspace/components/members/components/workspace-invite-modal";
import {Group, SegmentedControl, Space, Text} from "@mantine/core";
import { Group, SegmentedControl, Space, Text } from "@mantine/core";
import WorkspaceMembersTable from "@/features/workspace/components/members/components/workspace-members-table";
import SettingsTitle from "@/components/settings/settings-title.tsx";
import {useEffect, useState} from "react";
import {useNavigate, useSearchParams} from "react-router-dom";
import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import WorkspaceInvitesTable from "@/features/workspace/components/members/components/workspace-invites-table.tsx";
import useUserRole from "@/hooks/use-user-role.tsx";
import {getAppName} from "@/lib/config.ts";
import {Helmet} from "react-helmet-async";
import { getAppName } from "@/lib/config.ts";
import { Helmet } from "react-helmet-async";
import { useTranslation } from "react-i18next";
import { useAtom } from "jotai";
import { workspaceAtom } from "@/features/user/atoms/current-user-atom.ts";
export default function WorkspaceMembers() {
const [segmentValue, setSegmentValue] = useState("members");
const [searchParams] = useSearchParams();
const {isAdmin} = useUserRole();
const navigate = useNavigate();
const { t } = useTranslation();
const { t } = useTranslation();
const [segmentValue, setSegmentValue] = useState("members");
const [workspace] = useAtom(workspaceAtom);
const [searchParams] = useSearchParams();
const { isAdmin } = useUserRole();
const navigate = useNavigate();
useEffect(() => {
const currentTab = searchParams.get("tab");
if (currentTab === "invites") {
setSegmentValue(currentTab);
}
}, [searchParams.get("tab")]);
useEffect(() => {
const currentTab = searchParams.get("tab");
if (currentTab === "invites") {
setSegmentValue(currentTab);
}
}, [searchParams.get("tab")]);
const handleSegmentChange = (value: string) => {
setSegmentValue(value);
if (value === "invites") {
navigate(`?tab=${value}`);
} else {
navigate("");
}
};
const handleSegmentChange = (value: string) => {
setSegmentValue(value);
if (value === "invites") {
navigate(`?tab=${value}`);
} else {
navigate("");
}
};
return (
<>
<Helmet>
<title>{t("Members")} - {getAppName()}</title>
</Helmet>
<SettingsTitle title={t("Members")}/>
return (
<>
<Helmet>
<title>
{t("Members")} - {getAppName()}
</title>
</Helmet>
<SettingsTitle title={t("Members")} />
{/* <WorkspaceInviteSection /> */}
{/* <Divider my="lg" /> */}
{/* <WorkspaceInviteSection /> */}
{/* <Divider my="lg" /> */}
<Group justify="space-between">
<SegmentedControl
value={segmentValue}
onChange={handleSegmentChange}
data={[
{ label: t("Members"), value: "members" },
{ label: t("Pending"), value: "invites" },
]}
withItemsBorders={false}
/>
<Group justify="space-between">
<SegmentedControl
value={segmentValue}
onChange={handleSegmentChange}
data={[
{
label: t("Members") + ` (${workspace?.memberCount})`,
value: "members",
},
{ label: t("Pending"), value: "invites" },
]}
withItemsBorders={false}
/>
{isAdmin && <WorkspaceInviteModal/>}
</Group>
{isAdmin && <WorkspaceInviteModal />}
</Group>
<Space h="lg"/>
<Space h="lg" />
{segmentValue === "invites" ? (
<WorkspaceInvitesTable/>
) : (
<WorkspaceMembersTable/>
)}
</>
);
{segmentValue === "invites" ? (
<WorkspaceInvitesTable />
) : (
<WorkspaceMembersTable />
)}
</>
);
}

View File

@ -1,18 +1,27 @@
import SettingsTitle from "@/components/settings/settings-title.tsx";
import WorkspaceNameForm from "@/features/workspace/components/settings/components/workspace-name-form";
import { useTranslation } from "react-i18next";
import {getAppName} from "@/lib/config.ts";
import {Helmet} from "react-helmet-async";
import { getAppName, isCloud } from "@/lib/config.ts";
import { Helmet } from "react-helmet-async";
import ManageHostname from "@/ee/components/manage-hostname.tsx";
import { Divider } from "@mantine/core";
export default function WorkspaceSettings() {
const { t } = useTranslation();
return (
return (
<>
<Helmet>
<title>Workspace Settings - {getAppName()}</title>
</Helmet>
<SettingsTitle title={t("General")} />
<WorkspaceNameForm />
{isCloud() && (
<>
<Helmet>
<title>Workspace Settings - {getAppName()}</title>
</Helmet>
<SettingsTitle title={t("General")} />
<WorkspaceNameForm/>
<Divider my="md" />
<ManageHostname />
</>
);
)}
</>
);
}