From df85f13aea5a5cfb7da82a7c46ea867c787d98f4 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Thu, 4 Sep 2025 11:18:42 -0700 Subject: [PATCH] move all components to components dir --- .../routers/resource/getResourceAuthInfo.ts | 4 +- src/app/[orgId]/page.tsx | 4 +- .../settings/access/invitations/page.tsx | 4 +- .../[orgId]/settings/access/roles/page.tsx | 2 +- .../[orgId]/settings/access/users/page.tsx | 4 +- src/app/[orgId]/settings/api-keys/page.tsx | 4 +- .../settings/clients/[clientId]/layout.tsx | 2 +- src/app/[orgId]/settings/clients/page.tsx | 4 +- src/app/[orgId]/settings/domains/page.tsx | 2 +- .../[resourceId]/authentication/page.tsx | 4 +- .../resources/[resourceId]/general/page.tsx | 4 +- .../resources/[resourceId]/layout.tsx | 2 +- .../settings/resources/create/page.tsx | 2 +- src/app/[orgId]/settings/resources/page.tsx | 2 +- .../share-links/CreateShareLinkForm.tsx | 4 +- .../settings/share-links/ShareLinksTable.tsx | 4 +- src/app/[orgId]/settings/share-links/page.tsx | 4 +- .../settings/sites/[niceId]/layout.tsx | 2 +- src/app/[orgId]/settings/sites/page.tsx | 4 +- src/app/admin/api-keys/page.tsx | 2 +- src/app/admin/idp/[idpId]/policies/page.tsx | 2 +- src/app/admin/idp/page.tsx | 4 +- src/app/admin/license/page.tsx | 4 +- src/app/admin/users/AdminUsersTable.tsx | 2 +- src/app/admin/users/page.tsx | 2 +- .../auth/idp/[idpId]/oidc/callback/page.tsx | 2 +- src/app/auth/login/page.tsx | 2 +- .../auth/reset-password/ResetPasswordForm.tsx | 32 +- src/app/auth/reset-password/page.tsx | 2 +- src/app/auth/resource/[resourceId]/page.tsx | 10 +- src/app/auth/signup/page.tsx | 4 +- src/app/auth/verify-email/page.tsx | 2 +- src/app/invite/page.tsx | 6 +- src/app/s/[accessToken]/page.tsx | 2 +- .../AccessPageHeaderAndNav.tsx | 0 .../AccessToken.tsx | 0 .../AccessTokenUsage.tsx | 0 .../idp => components}/AdminIdpDataTable.tsx | 0 .../idp => components}/AdminIdpTable.tsx | 2 +- .../AdminUsersDataTable.tsx | 0 src/components/AdminUsersTable.tsx | 269 +++++++++ .../ApiKeysDataTable.tsx | 0 .../api-keys => components}/ApiKeysTable.tsx | 2 +- .../AutoLoginHandler.tsx | 0 .../ClientInfoCard.tsx | 0 .../ClientsDataTable.tsx | 0 .../clients => components}/ClientsTable.tsx | 2 +- .../CreateDomainForm.tsx | 0 .../roles => components}/CreateRoleForm.tsx | 0 src/components/CreateShareLinkForm.tsx | 549 ++++++++++++++++++ .../CustomDomainInput.tsx | 0 .../DashboardLoginForm.tsx | 0 .../roles => components}/DeleteRoleForm.tsx | 2 +- .../DomainsDataTable.tsx | 0 .../domains => components}/DomainsTable.tsx | 4 +- src/components/IdpCreateWizard.tsx | 429 ++++++++++++++ .../InvitationsDataTable.tsx | 0 .../InvitationsTable.tsx | 4 +- .../InviteStatusCard.tsx | 0 .../LicenseKeysDataTable.tsx | 0 .../MemberResourcesPortal.tsx | 0 .../OrgApiKeysDataTable.tsx | 0 .../OrgApiKeysTable.tsx | 2 +- .../OrganizationLandingCard.tsx | 0 .../PolicyDataTable.tsx | 0 .../policies => components}/PolicyTable.tsx | 2 +- .../RegenerateInvitationForm.tsx | 0 src/components/ResetPasswordForm.tsx | 533 +++++++++++++++++ .../ResourceAccessDenied.tsx | 0 .../ResourceAuthPortal.tsx | 2 +- .../ResourceInfoBox.tsx | 0 .../ResourceNotFound.tsx | 0 .../ResourcesTable.tsx | 0 .../roles => components}/RolesDataTable.tsx | 0 .../roles => components}/RolesTable.tsx | 6 +- .../SetResourcePasswordForm.tsx | 0 .../SetResourcePincodeForm.tsx | 0 .../ShareLinksDataTable.tsx | 0 .../ShareLinksSplash.tsx | 0 src/components/ShareLinksTable.tsx | 298 ++++++++++ .../auth/signup => components}/SignupForm.tsx | 0 .../[niceId] => components}/SiteInfoCard.tsx | 0 .../components/SitePriceCalculator.tsx | 0 .../sites => components}/SitesDataTable.tsx | 0 .../sites => components}/SitesSplashCard.tsx | 0 .../sites => components}/SitesTable.tsx | 2 +- .../users => components}/UsersDataTable.tsx | 0 .../users => components}/UsersTable.tsx | 2 +- .../ValidateOidcToken.tsx | 0 .../VerifyEmailForm.tsx | 2 +- 90 files changed, 2166 insertions(+), 86 deletions(-) rename src/{app/[orgId]/settings/access => components}/AccessPageHeaderAndNav.tsx (100%) rename src/{app/auth/resource/[resourceId] => components}/AccessToken.tsx (100%) rename src/{app/[orgId]/settings/share-links => components}/AccessTokenUsage.tsx (100%) rename src/{app/admin/idp => components}/AdminIdpDataTable.tsx (100%) rename src/{app/admin/idp => components}/AdminIdpTable.tsx (99%) rename src/{app/admin/users => components}/AdminUsersDataTable.tsx (100%) create mode 100644 src/components/AdminUsersTable.tsx rename src/{app/admin/api-keys => components}/ApiKeysDataTable.tsx (100%) rename src/{app/admin/api-keys => components}/ApiKeysTable.tsx (98%) rename src/{app/auth/resource/[resourceId] => components}/AutoLoginHandler.tsx (100%) rename src/{app/[orgId]/settings/clients/[clientId] => components}/ClientInfoCard.tsx (100%) rename src/{app/[orgId]/settings/clients => components}/ClientsDataTable.tsx (100%) rename src/{app/[orgId]/settings/clients => components}/ClientsTable.tsx (99%) rename src/{app/[orgId]/settings/domains => components}/CreateDomainForm.tsx (100%) rename src/{app/[orgId]/settings/access/roles => components}/CreateRoleForm.tsx (100%) create mode 100644 src/components/CreateShareLinkForm.tsx rename src/{app/[orgId]/settings/resources/[resourceId] => components}/CustomDomainInput.tsx (100%) rename src/{app/auth/login => components}/DashboardLoginForm.tsx (100%) rename src/{app/[orgId]/settings/access/roles => components}/DeleteRoleForm.tsx (99%) rename src/{app/[orgId]/settings/domains => components}/DomainsDataTable.tsx (100%) rename src/{app/[orgId]/settings/domains => components}/DomainsTable.tsx (98%) create mode 100644 src/components/IdpCreateWizard.tsx rename src/{app/[orgId]/settings/access/invitations => components}/InvitationsDataTable.tsx (100%) rename src/{app/[orgId]/settings/access/invitations => components}/InvitationsTable.tsx (97%) rename src/{app/invite => components}/InviteStatusCard.tsx (100%) rename src/{app/admin/license => components}/LicenseKeysDataTable.tsx (100%) rename src/{app/[orgId] => components}/MemberResourcesPortal.tsx (100%) rename src/{app/[orgId]/settings/api-keys => components}/OrgApiKeysDataTable.tsx (100%) rename src/{app/[orgId]/settings/api-keys => components}/OrgApiKeysTable.tsx (98%) rename src/{app/[orgId] => components}/OrganizationLandingCard.tsx (100%) rename src/{app/admin/idp/[idpId]/policies => components}/PolicyDataTable.tsx (100%) rename src/{app/admin/idp/[idpId]/policies => components}/PolicyTable.tsx (98%) rename src/{app/[orgId]/settings/access/invitations => components}/RegenerateInvitationForm.tsx (100%) create mode 100644 src/components/ResetPasswordForm.tsx rename src/{app/auth/resource/[resourceId] => components}/ResourceAccessDenied.tsx (100%) rename src/{app/auth/resource/[resourceId] => components}/ResourceAuthPortal.tsx (99%) rename src/{app/[orgId]/settings/resources/[resourceId] => components}/ResourceInfoBox.tsx (100%) rename src/{app/auth/resource/[resourceId] => components}/ResourceNotFound.tsx (100%) rename src/{app/[orgId]/settings/resources => components}/ResourcesTable.tsx (100%) rename src/{app/[orgId]/settings/access/roles => components}/RolesDataTable.tsx (100%) rename src/{app/[orgId]/settings/access/roles => components}/RolesTable.tsx (95%) rename src/{app/[orgId]/settings/resources/[resourceId]/authentication => components}/SetResourcePasswordForm.tsx (100%) rename src/{app/[orgId]/settings/resources/[resourceId]/authentication => components}/SetResourcePincodeForm.tsx (100%) rename src/{app/[orgId]/settings/share-links => components}/ShareLinksDataTable.tsx (100%) rename src/{app/[orgId]/settings/share-links => components}/ShareLinksSplash.tsx (100%) create mode 100644 src/components/ShareLinksTable.tsx rename src/{app/auth/signup => components}/SignupForm.tsx (100%) rename src/{app/[orgId]/settings/sites/[niceId] => components}/SiteInfoCard.tsx (100%) rename src/{app/admin/license => }/components/SitePriceCalculator.tsx (100%) rename src/{app/[orgId]/settings/sites => components}/SitesDataTable.tsx (100%) rename src/{app/[orgId]/settings/sites => components}/SitesSplashCard.tsx (100%) rename src/{app/[orgId]/settings/sites => components}/SitesTable.tsx (99%) rename src/{app/[orgId]/settings/access/users => components}/UsersDataTable.tsx (100%) rename src/{app/[orgId]/settings/access/users => components}/UsersTable.tsx (99%) rename src/{app/auth/idp/[idpId]/oidc/callback => components}/ValidateOidcToken.tsx (100%) rename src/{app/auth/verify-email => components}/VerifyEmailForm.tsx (99%) diff --git a/server/routers/resource/getResourceAuthInfo.ts b/server/routers/resource/getResourceAuthInfo.ts index 191221f1..c775564b 100644 --- a/server/routers/resource/getResourceAuthInfo.ts +++ b/server/routers/resource/getResourceAuthInfo.ts @@ -32,6 +32,7 @@ export type GetResourceAuthInfoResponse = { url: string; whitelist: boolean; skipToIdpId: number | null; + orgId: string; }; export async function getResourceAuthInfo( @@ -88,7 +89,8 @@ export async function getResourceAuthInfo( blockAccess: resource.blockAccess, url, whitelist: resource.emailWhitelistEnabled, - skipToIdpId: resource.skipToIdpId + skipToIdpId: resource.skipToIdpId, + orgId: resource.orgId }, success: true, error: false, diff --git a/src/app/[orgId]/page.tsx b/src/app/[orgId]/page.tsx index 4740198b..4c3ac07b 100644 --- a/src/app/[orgId]/page.tsx +++ b/src/app/[orgId]/page.tsx @@ -1,8 +1,8 @@ import { verifySession } from "@app/lib/auth/verifySession"; import UserProvider from "@app/providers/UserProvider"; import { cache } from "react"; -import OrganizationLandingCard from "./OrganizationLandingCard"; -import MemberResourcesPortal from "./MemberResourcesPortal"; +import OrganizationLandingCard from "../../components/OrganizationLandingCard"; +import MemberResourcesPortal from "../../components/MemberResourcesPortal"; import { GetOrgOverviewResponse } from "@server/routers/org/getOrgOverview"; import { internal } from "@app/lib/api"; import { AxiosResponse } from "axios"; diff --git a/src/app/[orgId]/settings/access/invitations/page.tsx b/src/app/[orgId]/settings/access/invitations/page.tsx index 665b9a43..d7fee322 100644 --- a/src/app/[orgId]/settings/access/invitations/page.tsx +++ b/src/app/[orgId]/settings/access/invitations/page.tsx @@ -1,13 +1,13 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { AxiosResponse } from "axios"; -import InvitationsTable, { InvitationRow } from "./InvitationsTable"; +import InvitationsTable, { InvitationRow } from "../../../../../components/InvitationsTable"; import { GetOrgResponse } from "@server/routers/org"; import { cache } from "react"; import OrgProvider from "@app/providers/OrgProvider"; import UserProvider from "@app/providers/UserProvider"; import { verifySession } from "@app/lib/auth/verifySession"; -import AccessPageHeaderAndNav from "../AccessPageHeaderAndNav"; +import AccessPageHeaderAndNav from "../../../../../components/AccessPageHeaderAndNav"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { getTranslations } from 'next-intl/server'; diff --git a/src/app/[orgId]/settings/access/roles/page.tsx b/src/app/[orgId]/settings/access/roles/page.tsx index 8faedbf8..cffe4ed9 100644 --- a/src/app/[orgId]/settings/access/roles/page.tsx +++ b/src/app/[orgId]/settings/access/roles/page.tsx @@ -5,7 +5,7 @@ import { GetOrgResponse } from "@server/routers/org"; import { cache } from "react"; import OrgProvider from "@app/providers/OrgProvider"; import { ListRolesResponse } from "@server/routers/role"; -import RolesTable, { RoleRow } from "./RolesTable"; +import RolesTable, { RoleRow } from "../../../../../components/RolesTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { getTranslations } from 'next-intl/server'; diff --git a/src/app/[orgId]/settings/access/users/page.tsx b/src/app/[orgId]/settings/access/users/page.tsx index 27b227fa..781aa2d1 100644 --- a/src/app/[orgId]/settings/access/users/page.tsx +++ b/src/app/[orgId]/settings/access/users/page.tsx @@ -2,13 +2,13 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { ListUsersResponse } from "@server/routers/user"; import { AxiosResponse } from "axios"; -import UsersTable, { UserRow } from "./UsersTable"; +import UsersTable, { UserRow } from "../../../../../components/UsersTable"; import { GetOrgResponse } from "@server/routers/org"; import { cache } from "react"; import OrgProvider from "@app/providers/OrgProvider"; import UserProvider from "@app/providers/UserProvider"; import { verifySession } from "@app/lib/auth/verifySession"; -import AccessPageHeaderAndNav from "../AccessPageHeaderAndNav"; +import AccessPageHeaderAndNav from "../../../../../components/AccessPageHeaderAndNav"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { getTranslations } from 'next-intl/server'; diff --git a/src/app/[orgId]/settings/api-keys/page.tsx b/src/app/[orgId]/settings/api-keys/page.tsx index 188921e5..ca526a7d 100644 --- a/src/app/[orgId]/settings/api-keys/page.tsx +++ b/src/app/[orgId]/settings/api-keys/page.tsx @@ -2,7 +2,7 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { AxiosResponse } from "axios"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import OrgApiKeysTable, { OrgApiKeyRow } from "./OrgApiKeysTable"; +import OrgApiKeysTable, { OrgApiKeyRow } from "../../../../components/OrgApiKeysTable"; import { ListOrgApiKeysResponse } from "@server/routers/apiKeys"; import { getTranslations } from 'next-intl/server'; @@ -15,7 +15,7 @@ export const dynamic = "force-dynamic"; export default async function ApiKeysPage(props: ApiKeyPageProps) { const params = await props.params; const t = await getTranslations(); - + let apiKeys: ListOrgApiKeysResponse["apiKeys"] = []; try { const res = await internal.get>( diff --git a/src/app/[orgId]/settings/clients/[clientId]/layout.tsx b/src/app/[orgId]/settings/clients/[clientId]/layout.tsx index d137b00c..84a5f78f 100644 --- a/src/app/[orgId]/settings/clients/[clientId]/layout.tsx +++ b/src/app/[orgId]/settings/clients/[clientId]/layout.tsx @@ -3,7 +3,7 @@ import { AxiosResponse } from "axios"; import { authCookieHeader } from "@app/lib/api/cookies"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { GetClientResponse } from "@server/routers/client"; -import ClientInfoCard from "./ClientInfoCard"; +import ClientInfoCard from "../../../../../components/ClientInfoCard"; import ClientProvider from "@app/providers/ClientProvider"; import { redirect } from "next/navigation"; import { HorizontalTabs } from "@app/components/HorizontalTabs"; diff --git a/src/app/[orgId]/settings/clients/page.tsx b/src/app/[orgId]/settings/clients/page.tsx index 83cc11e3..994b1d56 100644 --- a/src/app/[orgId]/settings/clients/page.tsx +++ b/src/app/[orgId]/settings/clients/page.tsx @@ -1,10 +1,10 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { AxiosResponse } from "axios"; -import { ClientRow } from "./ClientsTable"; +import { ClientRow } from "../../../../components/ClientsTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { ListClientsResponse } from "@server/routers/client"; -import ClientsTable from "./ClientsTable"; +import ClientsTable from "../../../../components/ClientsTable"; type ClientsPageProps = { params: Promise<{ orgId: string }>; diff --git a/src/app/[orgId]/settings/domains/page.tsx b/src/app/[orgId]/settings/domains/page.tsx index c85fe10d..cb587d92 100644 --- a/src/app/[orgId]/settings/domains/page.tsx +++ b/src/app/[orgId]/settings/domains/page.tsx @@ -2,7 +2,7 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { AxiosResponse } from "axios"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import DomainsTable, { DomainRow } from "./DomainsTable"; +import DomainsTable, { DomainRow } from "../../../../components/DomainsTable"; import { getTranslations } from "next-intl/server"; import { cache } from "react"; import { GetOrgResponse } from "@server/routers/org"; diff --git a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx index 9bb9919a..4705550e 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx @@ -27,8 +27,8 @@ import { } from "@app/components/ui/form"; import { ListUsersResponse } from "@server/routers/user"; import { Binary, Key } from "lucide-react"; -import SetResourcePasswordForm from "./SetResourcePasswordForm"; -import SetResourcePincodeForm from "./SetResourcePincodeForm"; +import SetResourcePasswordForm from "../../../../../../components/SetResourcePasswordForm"; +import SetResourcePincodeForm from "../../../../../../components/SetResourcePincodeForm"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { diff --git a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx index ce8f29a7..e2bab1cb 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx @@ -54,7 +54,7 @@ import DomainPicker from "@app/components/DomainPicker"; import { Globe } from "lucide-react"; import { build } from "@server/build"; import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils"; -import { DomainRow } from "../../../domains/DomainsTable"; +import { DomainRow } from "../../../../../../components/DomainsTable"; import { toASCII, toUnicode } from "punycode"; export default function GeneralForm() { @@ -160,7 +160,7 @@ export default function GeneralForm() { const rawDomains = res.data.data.domains as DomainRow[]; const domains = rawDomains.map((domain) => ({ ...domain, - baseDomain: toUnicode(domain.baseDomain), + baseDomain: toUnicode(domain.baseDomain), })); setBaseDomains(domains); setFormKey((key) => key + 1); diff --git a/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx b/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx index f6d4b3c0..8a5d0997 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/layout.tsx @@ -12,7 +12,7 @@ import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { GetOrgResponse } from "@server/routers/org"; import OrgProvider from "@app/providers/OrgProvider"; import { cache } from "react"; -import ResourceInfoBox from "./ResourceInfoBox"; +import ResourceInfoBox from "../../../../../components/ResourceInfoBox"; import { GetSiteResponse } from "@server/routers/site"; import { getTranslations } from 'next-intl/server'; diff --git a/src/app/[orgId]/settings/resources/create/page.tsx b/src/app/[orgId]/settings/resources/create/page.tsx index 782b3135..c28f5a5f 100644 --- a/src/app/[orgId]/settings/resources/create/page.tsx +++ b/src/app/[orgId]/settings/resources/create/page.tsx @@ -90,7 +90,7 @@ import { ListTargetsResponse } from "@server/routers/target"; import { DockerManager, DockerState } from "@app/lib/docker"; import { parseHostTarget } from "@app/lib/parseHostTarget"; import { toASCII, toUnicode } from 'punycode'; -import { DomainRow } from "../../domains/DomainsTable"; +import { DomainRow } from "../../../../../components/DomainsTable"; const baseResourceFormSchema = z.object({ name: z.string().min(1).max(255), diff --git a/src/app/[orgId]/settings/resources/page.tsx b/src/app/[orgId]/settings/resources/page.tsx index f8ef5397..b1b907e6 100644 --- a/src/app/[orgId]/settings/resources/page.tsx +++ b/src/app/[orgId]/settings/resources/page.tsx @@ -3,7 +3,7 @@ import { authCookieHeader } from "@app/lib/api/cookies"; import ResourcesTable, { ResourceRow, InternalResourceRow -} from "./ResourcesTable"; +} from "../../../../components/ResourcesTable"; import { AxiosResponse } from "axios"; import { ListResourcesResponse } from "@server/routers/resource"; import { ListAllSiteResourcesByOrgResponse } from "@server/routers/siteResource"; diff --git a/src/app/[orgId]/settings/share-links/CreateShareLinkForm.tsx b/src/app/[orgId]/settings/share-links/CreateShareLinkForm.tsx index 18c989ab..e3ba3f17 100644 --- a/src/app/[orgId]/settings/share-links/CreateShareLinkForm.tsx +++ b/src/app/[orgId]/settings/share-links/CreateShareLinkForm.tsx @@ -58,14 +58,14 @@ import { CheckIcon, ChevronsUpDown } from "lucide-react"; import { Checkbox } from "@app/components/ui/checkbox"; import { GenerateAccessTokenResponse } from "@server/routers/accessToken"; import { constructShareLink } from "@app/lib/shareLinks"; -import { ShareLinkRow } from "./ShareLinksTable"; +import { ShareLinkRow } from "@app/components/ShareLinksTable"; import { QRCodeCanvas, QRCodeSVG } from "qrcode.react"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@app/components/ui/collapsible"; -import AccessTokenSection from "./AccessTokenUsage"; +import AccessTokenSection from "@app/components/AccessTokenUsage"; import { useTranslations } from "next-intl"; import { toUnicode } from 'punycode'; diff --git a/src/app/[orgId]/settings/share-links/ShareLinksTable.tsx b/src/app/[orgId]/settings/share-links/ShareLinksTable.tsx index 41768255..2943311f 100644 --- a/src/app/[orgId]/settings/share-links/ShareLinksTable.tsx +++ b/src/app/[orgId]/settings/share-links/ShareLinksTable.tsx @@ -1,7 +1,7 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; -import { ShareLinksDataTable } from "./ShareLinksDataTable"; +import { ShareLinksDataTable } from "@app/components/ShareLinksDataTable"; import { DropdownMenu, DropdownMenuContent, @@ -31,7 +31,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { ArrayElement } from "@server/types/ArrayElement"; import { ListAccessTokensResponse } from "@server/routers/accessToken"; import moment from "moment"; -import CreateShareLinkForm from "./CreateShareLinkForm"; +import CreateShareLinkForm from "@app/components/CreateShareLinkForm"; import { constructShareLink } from "@app/lib/shareLinks"; import { useTranslations } from "next-intl"; diff --git a/src/app/[orgId]/settings/share-links/page.tsx b/src/app/[orgId]/settings/share-links/page.tsx index e4efabd9..be561acd 100644 --- a/src/app/[orgId]/settings/share-links/page.tsx +++ b/src/app/[orgId]/settings/share-links/page.tsx @@ -7,8 +7,8 @@ import { cache } from "react"; import { GetOrgResponse } from "@server/routers/org"; import OrgProvider from "@app/providers/OrgProvider"; import { ListAccessTokensResponse } from "@server/routers/accessToken"; -import ShareLinksTable, { ShareLinkRow } from "./ShareLinksTable"; -import ShareableLinksSplash from "./ShareLinksSplash"; +import ShareLinksTable, { ShareLinkRow } from "../../../../components/ShareLinksTable"; +import ShareableLinksSplash from "../../../../components/ShareLinksSplash"; import { getTranslations } from "next-intl/server"; type ShareLinksPageProps = { diff --git a/src/app/[orgId]/settings/sites/[niceId]/layout.tsx b/src/app/[orgId]/settings/sites/[niceId]/layout.tsx index 597cc852..039deebb 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/layout.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/layout.tsx @@ -6,7 +6,7 @@ import { redirect } from "next/navigation"; import { authCookieHeader } from "@app/lib/api/cookies"; import { HorizontalTabs } from "@app/components/HorizontalTabs"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import SiteInfoCard from "./SiteInfoCard"; +import SiteInfoCard from "../../../../../components/SiteInfoCard"; import { getTranslations } from "next-intl/server"; interface SettingsLayoutProps { diff --git a/src/app/[orgId]/settings/sites/page.tsx b/src/app/[orgId]/settings/sites/page.tsx index 10bcad53..a854083c 100644 --- a/src/app/[orgId]/settings/sites/page.tsx +++ b/src/app/[orgId]/settings/sites/page.tsx @@ -2,9 +2,9 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { ListSitesResponse } from "@server/routers/site"; import { AxiosResponse } from "axios"; -import SitesTable, { SiteRow } from "./SitesTable"; +import SitesTable, { SiteRow } from "../../../../components/SitesTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import SitesSplashCard from "./SitesSplashCard"; +import SitesSplashCard from "../../../../components/SitesSplashCard"; import { getTranslations } from "next-intl/server"; type SitesPageProps = { diff --git a/src/app/admin/api-keys/page.tsx b/src/app/admin/api-keys/page.tsx index 22607f2f..e518911f 100644 --- a/src/app/admin/api-keys/page.tsx +++ b/src/app/admin/api-keys/page.tsx @@ -3,7 +3,7 @@ import { authCookieHeader } from "@app/lib/api/cookies"; import { AxiosResponse } from "axios"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { ListRootApiKeysResponse } from "@server/routers/apiKeys"; -import ApiKeysTable, { ApiKeyRow } from "./ApiKeysTable"; +import ApiKeysTable, { ApiKeyRow } from "../../../components/ApiKeysTable"; import { getTranslations } from "next-intl/server"; type ApiKeyPageProps = {}; diff --git a/src/app/admin/idp/[idpId]/policies/page.tsx b/src/app/admin/idp/[idpId]/policies/page.tsx index aadd6eb8..01b186bf 100644 --- a/src/app/admin/idp/[idpId]/policies/page.tsx +++ b/src/app/admin/idp/[idpId]/policies/page.tsx @@ -31,7 +31,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { InfoIcon, ExternalLink, CheckIcon } from "lucide-react"; -import PolicyTable, { PolicyRow } from "./PolicyTable"; +import PolicyTable, { PolicyRow } from "../../../../../components/PolicyTable"; import { AxiosResponse } from "axios"; import { ListOrgsResponse } from "@server/routers/org"; import { diff --git a/src/app/admin/idp/page.tsx b/src/app/admin/idp/page.tsx index 4db77785..fef0990c 100644 --- a/src/app/admin/idp/page.tsx +++ b/src/app/admin/idp/page.tsx @@ -2,7 +2,7 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { AxiosResponse } from "axios"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import IdpTable, { IdpRow } from "./AdminIdpTable"; +import IdpTable, { IdpRow } from "../../../components/AdminIdpTable"; import { getTranslations } from "next-intl/server"; export default async function IdpPage() { @@ -16,7 +16,7 @@ export default async function IdpPage() { } catch (e) { console.error(e); } - + const t = await getTranslations(); return ( diff --git a/src/app/admin/license/page.tsx b/src/app/admin/license/page.tsx index 40570a9f..0e3526eb 100644 --- a/src/app/admin/license/page.tsx +++ b/src/app/admin/license/page.tsx @@ -6,7 +6,7 @@ import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { formatAxiosError } from "@app/lib/api"; -import { LicenseKeysDataTable } from "./LicenseKeysDataTable"; +import { LicenseKeysDataTable } from "../../../components/LicenseKeysDataTable"; import { AxiosResponse } from "axios"; import { Button } from "@app/components/ui/button"; import { @@ -49,7 +49,7 @@ import CopyTextBox from "@app/components/CopyTextBox"; import { Progress } from "@app/components/ui/progress"; import { MinusCircle, PlusCircle } from "lucide-react"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; -import { SitePriceCalculator } from "./components/SitePriceCalculator"; +import { SitePriceCalculator } from "../../../components/SitePriceCalculator"; import Link from "next/link"; import { Checkbox } from "@app/components/ui/checkbox"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; diff --git a/src/app/admin/users/AdminUsersTable.tsx b/src/app/admin/users/AdminUsersTable.tsx index 6c5e4613..8e75ff24 100644 --- a/src/app/admin/users/AdminUsersTable.tsx +++ b/src/app/admin/users/AdminUsersTable.tsx @@ -1,7 +1,7 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; -import { UsersDataTable } from "./AdminUsersDataTable"; +import { UsersDataTable } from "@app/components/AdminUsersDataTable"; import { Button } from "@app/components/ui/button"; import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react"; import { useRouter } from "next/navigation"; diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx index e9673374..bf6547a3 100644 --- a/src/app/admin/users/page.tsx +++ b/src/app/admin/users/page.tsx @@ -3,7 +3,7 @@ import { authCookieHeader } from "@app/lib/api/cookies"; import { AxiosResponse } from "axios"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { AdminListUsersResponse } from "@server/routers/user/adminListUsers"; -import UsersTable, { GlobalUserRow } from "./AdminUsersTable"; +import UsersTable, { GlobalUserRow } from "../../../components/AdminUsersTable"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { InfoIcon } from "lucide-react"; import { getTranslations } from "next-intl/server"; diff --git a/src/app/auth/idp/[idpId]/oidc/callback/page.tsx b/src/app/auth/idp/[idpId]/oidc/callback/page.tsx index 1c0f8125..2ff8d09a 100644 --- a/src/app/auth/idp/[idpId]/oidc/callback/page.tsx +++ b/src/app/auth/idp/[idpId]/oidc/callback/page.tsx @@ -1,5 +1,5 @@ import { cookies } from "next/headers"; -import ValidateOidcToken from "./ValidateOidcToken"; +import ValidateOidcToken from "@app/components/ValidateOidcToken"; import { cache } from "react"; import { priv } from "@app/lib/api"; import { AxiosResponse } from "axios"; diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index cad0ce58..e2505303 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -2,7 +2,7 @@ import { verifySession } from "@app/lib/auth/verifySession"; import Link from "next/link"; import { redirect } from "next/navigation"; import { cache } from "react"; -import DashboardLoginForm from "./DashboardLoginForm"; +import DashboardLoginForm from "@app/components/DashboardLoginForm"; import { Mail } from "lucide-react"; import { pullEnv } from "@app/lib/pullEnv"; import { cleanRedirect } from "@app/lib/cleanRedirect"; diff --git a/src/app/auth/reset-password/ResetPasswordForm.tsx b/src/app/auth/reset-password/ResetPasswordForm.tsx index 596afb99..3d456bd9 100644 --- a/src/app/auth/reset-password/ResetPasswordForm.tsx +++ b/src/app/auth/reset-password/ResetPasswordForm.tsx @@ -35,7 +35,7 @@ import { ResetPasswordResponse } from "@server/routers/auth"; import { Loader2 } from "lucide-react"; -import { Alert, AlertDescription } from "../../../components/ui/alert"; +import { Alert, AlertDescription } from "@app/components/ui/alert"; import { toast } from "@app/hooks/useToast"; import { useRouter } from "next/navigation"; import { formatAxiosError } from "@app/lib/api"; @@ -210,7 +210,7 @@ export default function ResetPasswordForm({ } catch (verificationError) { console.error("Failed to send verification code:", verificationError); } - + if (redirect) { router.push(`/auth/verify-email?redirect=${redirect}`); } else { @@ -254,8 +254,8 @@ export default function ResetPasswordForm({ {quickstart ? t('completeAccountSetup') : t('passwordReset')} - {quickstart - ? t('completeAccountSetupDescription') + {quickstart + ? t('completeAccountSetupDescription') : t('passwordResetDescription') } @@ -282,8 +282,8 @@ export default function ResetPasswordForm({ - {quickstart - ? t('accountSetupSent') + {quickstart + ? t('accountSetupSent') : t('passwordResetSent') } @@ -325,8 +325,8 @@ export default function ResetPasswordForm({ render={({ field }) => ( - {quickstart - ? t('accountSetupCode') + {quickstart + ? t('accountSetupCode') : t('passwordResetCode') } @@ -338,8 +338,8 @@ export default function ResetPasswordForm({ - {quickstart - ? t('accountSetupCodeDescription') + {quickstart + ? t('accountSetupCodeDescription') : t('passwordResetCodeDescription') } @@ -354,8 +354,8 @@ export default function ResetPasswordForm({ render={({ field }) => ( - {quickstart - ? t('passwordCreate') + {quickstart + ? t('passwordCreate') : t('passwordNew') } @@ -375,8 +375,8 @@ export default function ResetPasswordForm({ render={({ field }) => ( - {quickstart - ? t('passwordCreateConfirm') + {quickstart + ? t('passwordCreateConfirm') : t('passwordNewConfirm') } @@ -490,8 +490,8 @@ export default function ResetPasswordForm({ {isSubmitting && ( )} - {quickstart - ? t('accountSetupSubmit') + {quickstart + ? t('accountSetupSubmit') : t('passwordResetSubmit') } diff --git a/src/app/auth/reset-password/page.tsx b/src/app/auth/reset-password/page.tsx index f06c7c4c..490f89f7 100644 --- a/src/app/auth/reset-password/page.tsx +++ b/src/app/auth/reset-password/page.tsx @@ -1,7 +1,7 @@ import { verifySession } from "@app/lib/auth/verifySession"; import { redirect } from "next/navigation"; import { cache } from "react"; -import ResetPasswordForm from "./ResetPasswordForm"; +import ResetPasswordForm from "@app/components/ResetPasswordForm"; import Link from "next/link"; import { cleanRedirect } from "@app/lib/cleanRedirect"; import { getTranslations } from "next-intl/server"; diff --git a/src/app/auth/resource/[resourceId]/page.tsx b/src/app/auth/resource/[resourceId]/page.tsx index 347d3586..25580ee7 100644 --- a/src/app/auth/resource/[resourceId]/page.tsx +++ b/src/app/auth/resource/[resourceId]/page.tsx @@ -2,20 +2,20 @@ import { GetResourceAuthInfoResponse, GetExchangeTokenResponse } from "@server/routers/resource"; -import ResourceAuthPortal from "./ResourceAuthPortal"; +import ResourceAuthPortal from "@app/components/ResourceAuthPortal"; import { internal, priv } from "@app/lib/api"; import { AxiosResponse } from "axios"; import { authCookieHeader } from "@app/lib/api/cookies"; import { cache } from "react"; import { verifySession } from "@app/lib/auth/verifySession"; import { redirect } from "next/navigation"; -import ResourceNotFound from "./ResourceNotFound"; -import ResourceAccessDenied from "./ResourceAccessDenied"; -import AccessToken from "./AccessToken"; +import ResourceNotFound from "@app/components/ResourceNotFound"; +import ResourceAccessDenied from "@app/components/ResourceAccessDenied"; +import AccessToken from "@app/components/AccessToken"; import { pullEnv } from "@app/lib/pullEnv"; import { LoginFormIDP } from "@app/components/LoginForm"; import { ListIdpsResponse } from "@server/routers/idp"; -import AutoLoginHandler from "./AutoLoginHandler"; +import AutoLoginHandler from "@app/components/AutoLoginHandler"; export const dynamic = "force-dynamic"; diff --git a/src/app/auth/signup/page.tsx b/src/app/auth/signup/page.tsx index 673e69bf..b4f4fddd 100644 --- a/src/app/auth/signup/page.tsx +++ b/src/app/auth/signup/page.tsx @@ -1,4 +1,4 @@ -import SignupForm from "@app/app/auth/signup/SignupForm"; +import SignupForm from "@app/components/SignupForm"; import { verifySession } from "@app/lib/auth/verifySession"; import { cleanRedirect } from "@app/lib/cleanRedirect"; import { pullEnv } from "@app/lib/pullEnv"; @@ -11,7 +11,7 @@ import { getTranslations } from "next-intl/server"; export const dynamic = "force-dynamic"; export default async function Page(props: { - searchParams: Promise<{ + searchParams: Promise<{ redirect: string | undefined; email: string | undefined; }>; diff --git a/src/app/auth/verify-email/page.tsx b/src/app/auth/verify-email/page.tsx index 10ad809f..c549abf0 100644 --- a/src/app/auth/verify-email/page.tsx +++ b/src/app/auth/verify-email/page.tsx @@ -1,4 +1,4 @@ -import VerifyEmailForm from "@app/app/auth/verify-email/VerifyEmailForm"; +import VerifyEmailForm from "@app/components/VerifyEmailForm"; import { verifySession } from "@app/lib/auth/verifySession"; import { cleanRedirect } from "@app/lib/cleanRedirect"; import { pullEnv } from "@app/lib/pullEnv"; diff --git a/src/app/invite/page.tsx b/src/app/invite/page.tsx index 2e0c11e2..49c5a2c5 100644 --- a/src/app/invite/page.tsx +++ b/src/app/invite/page.tsx @@ -4,7 +4,7 @@ import { verifySession } from "@app/lib/auth/verifySession"; import { AcceptInviteResponse } from "@server/routers/user"; import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; -import InviteStatusCard from "./InviteStatusCard"; +import InviteStatusCard from "../../components/InviteStatusCard"; import { formatAxiosError } from "@app/lib/api"; import { getTranslations } from "next-intl/server"; @@ -72,14 +72,14 @@ export default async function InvitePage(props: { const type = cardType(); if (!user && type === "user_does_not_exist") { - const redirectUrl = emailParam + const redirectUrl = emailParam ? `/auth/signup?redirect=/invite?token=${params.token}&email=${encodeURIComponent(emailParam)}` : `/auth/signup?redirect=/invite?token=${params.token}`; redirect(redirectUrl); } if (!user && type === "not_logged_in") { - const redirectUrl = emailParam + const redirectUrl = emailParam ? `/auth/login?redirect=/invite?token=${params.token}&email=${encodeURIComponent(emailParam)}` : `/auth/login?redirect=/invite?token=${params.token}`; redirect(redirectUrl); diff --git a/src/app/s/[accessToken]/page.tsx b/src/app/s/[accessToken]/page.tsx index d28ff7be..61299366 100644 --- a/src/app/s/[accessToken]/page.tsx +++ b/src/app/s/[accessToken]/page.tsx @@ -1,4 +1,4 @@ -import AccessToken from "@app/app/auth/resource/[resourceId]/AccessToken"; +import AccessToken from "@app/components/AccessToken"; export default async function ResourceAuthPage(props: { params: Promise<{ accessToken: string }>; diff --git a/src/app/[orgId]/settings/access/AccessPageHeaderAndNav.tsx b/src/components/AccessPageHeaderAndNav.tsx similarity index 100% rename from src/app/[orgId]/settings/access/AccessPageHeaderAndNav.tsx rename to src/components/AccessPageHeaderAndNav.tsx diff --git a/src/app/auth/resource/[resourceId]/AccessToken.tsx b/src/components/AccessToken.tsx similarity index 100% rename from src/app/auth/resource/[resourceId]/AccessToken.tsx rename to src/components/AccessToken.tsx diff --git a/src/app/[orgId]/settings/share-links/AccessTokenUsage.tsx b/src/components/AccessTokenUsage.tsx similarity index 100% rename from src/app/[orgId]/settings/share-links/AccessTokenUsage.tsx rename to src/components/AccessTokenUsage.tsx diff --git a/src/app/admin/idp/AdminIdpDataTable.tsx b/src/components/AdminIdpDataTable.tsx similarity index 100% rename from src/app/admin/idp/AdminIdpDataTable.tsx rename to src/components/AdminIdpDataTable.tsx diff --git a/src/app/admin/idp/AdminIdpTable.tsx b/src/components/AdminIdpTable.tsx similarity index 99% rename from src/app/admin/idp/AdminIdpTable.tsx rename to src/components/AdminIdpTable.tsx index fa7de6da..9c6c3ac4 100644 --- a/src/app/admin/idp/AdminIdpTable.tsx +++ b/src/components/AdminIdpTable.tsx @@ -1,7 +1,7 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; -import { IdpDataTable } from "./AdminIdpDataTable"; +import { IdpDataTable } from "@app/components/AdminIdpDataTable"; import { Button } from "@app/components/ui/button"; import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react"; import { useState } from "react"; diff --git a/src/app/admin/users/AdminUsersDataTable.tsx b/src/components/AdminUsersDataTable.tsx similarity index 100% rename from src/app/admin/users/AdminUsersDataTable.tsx rename to src/components/AdminUsersDataTable.tsx diff --git a/src/components/AdminUsersTable.tsx b/src/components/AdminUsersTable.tsx new file mode 100644 index 00000000..8e75ff24 --- /dev/null +++ b/src/components/AdminUsersTable.tsx @@ -0,0 +1,269 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { UsersDataTable } from "@app/components/AdminUsersDataTable"; +import { Button } from "@app/components/ui/button"; +import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; +import { toast } from "@app/hooks/useToast"; +import { formatAxiosError } from "@app/lib/api"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useTranslations } from "next-intl"; +import { + DropdownMenu, + DropdownMenuItem, + DropdownMenuContent, + DropdownMenuTrigger +} from "@app/components/ui/dropdown-menu"; + +export type GlobalUserRow = { + id: string; + name: string | null; + username: string; + email: string | null; + type: string; + idpId: number | null; + idpName: string; + dateCreated: string; + twoFactorEnabled: boolean | null; + twoFactorSetupRequested: boolean | null; +}; + +type Props = { + users: GlobalUserRow[]; +}; + +export default function UsersTable({ users }: Props) { + const router = useRouter(); + const t = useTranslations(); + + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [selected, setSelected] = useState(null); + const [rows, setRows] = useState(users); + + const api = createApiClient(useEnvContext()); + + const deleteUser = (id: string) => { + api.delete(`/user/${id}`) + .catch((e) => { + console.error(t("userErrorDelete"), e); + toast({ + variant: "destructive", + title: t("userErrorDelete"), + description: formatAxiosError(e, t("userErrorDelete")) + }); + }) + .then(() => { + router.refresh(); + setIsDeleteModalOpen(false); + + const newRows = rows.filter((row) => row.id !== id); + + setRows(newRows); + }); + }; + + const columns: ColumnDef[] = [ + { + accessorKey: "id", + header: ({ column }) => { + return ( + + ); + } + }, + { + accessorKey: "username", + header: ({ column }) => { + return ( + + ); + } + }, + { + accessorKey: "email", + header: ({ column }) => { + return ( + + ); + } + }, + { + accessorKey: "name", + header: ({ column }) => { + return ( + + ); + } + }, + { + accessorKey: "idpName", + header: ({ column }) => { + return ( + + ); + } + }, + { + accessorKey: "twoFactorEnabled", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const userRow = row.original; + + return ( +
+ + {userRow.twoFactorEnabled || + userRow.twoFactorSetupRequested ? ( + + {t("enabled")} + + ) : ( + {t("disabled")} + )} + +
+ ); + } + }, + { + id: "actions", + cell: ({ row }) => { + const r = row.original; + return ( + <> +
+ + + + + + { + setSelected(r); + setIsDeleteModalOpen(true); + }} + > + {t("delete")} + + + + +
+ + ); + } + } + ]; + + return ( + <> + {selected && ( + { + setIsDeleteModalOpen(val); + setSelected(null); + }} + dialog={ +
+

+ {t("userQuestionRemove", { + selectedUser: + selected?.email || + selected?.name || + selected?.username + })} +

+ +

+ {t("userMessageRemove")} +

+ +

{t("userMessageConfirm")}

+
+ } + buttonText={t("userDeleteConfirm")} + onConfirm={async () => deleteUser(selected!.id)} + string={ + selected.email || selected.name || selected.username + } + title={t("userDeleteServer")} + /> + )} + + + + ); +} diff --git a/src/app/admin/api-keys/ApiKeysDataTable.tsx b/src/components/ApiKeysDataTable.tsx similarity index 100% rename from src/app/admin/api-keys/ApiKeysDataTable.tsx rename to src/components/ApiKeysDataTable.tsx diff --git a/src/app/admin/api-keys/ApiKeysTable.tsx b/src/components/ApiKeysTable.tsx similarity index 98% rename from src/app/admin/api-keys/ApiKeysTable.tsx rename to src/components/ApiKeysTable.tsx index 02aead9e..99094651 100644 --- a/src/app/admin/api-keys/ApiKeysTable.tsx +++ b/src/components/ApiKeysTable.tsx @@ -18,7 +18,7 @@ import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import moment from "moment"; -import { ApiKeysDataTable } from "./ApiKeysDataTable"; +import { ApiKeysDataTable } from "@app/components/ApiKeysDataTable"; import { useTranslations } from "next-intl"; export type ApiKeyRow = { diff --git a/src/app/auth/resource/[resourceId]/AutoLoginHandler.tsx b/src/components/AutoLoginHandler.tsx similarity index 100% rename from src/app/auth/resource/[resourceId]/AutoLoginHandler.tsx rename to src/components/AutoLoginHandler.tsx diff --git a/src/app/[orgId]/settings/clients/[clientId]/ClientInfoCard.tsx b/src/components/ClientInfoCard.tsx similarity index 100% rename from src/app/[orgId]/settings/clients/[clientId]/ClientInfoCard.tsx rename to src/components/ClientInfoCard.tsx diff --git a/src/app/[orgId]/settings/clients/ClientsDataTable.tsx b/src/components/ClientsDataTable.tsx similarity index 100% rename from src/app/[orgId]/settings/clients/ClientsDataTable.tsx rename to src/components/ClientsDataTable.tsx diff --git a/src/app/[orgId]/settings/clients/ClientsTable.tsx b/src/components/ClientsTable.tsx similarity index 99% rename from src/app/[orgId]/settings/clients/ClientsTable.tsx rename to src/components/ClientsTable.tsx index 7fa81622..fc7c7c84 100644 --- a/src/app/[orgId]/settings/clients/ClientsTable.tsx +++ b/src/components/ClientsTable.tsx @@ -1,7 +1,7 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; -import { ClientsDataTable } from "./ClientsDataTable"; +import { ClientsDataTable } from "@app/components/ClientsDataTable"; import { DropdownMenu, DropdownMenuContent, diff --git a/src/app/[orgId]/settings/domains/CreateDomainForm.tsx b/src/components/CreateDomainForm.tsx similarity index 100% rename from src/app/[orgId]/settings/domains/CreateDomainForm.tsx rename to src/components/CreateDomainForm.tsx diff --git a/src/app/[orgId]/settings/access/roles/CreateRoleForm.tsx b/src/components/CreateRoleForm.tsx similarity index 100% rename from src/app/[orgId]/settings/access/roles/CreateRoleForm.tsx rename to src/components/CreateRoleForm.tsx diff --git a/src/components/CreateShareLinkForm.tsx b/src/components/CreateShareLinkForm.tsx new file mode 100644 index 00000000..e3ba3f17 --- /dev/null +++ b/src/components/CreateShareLinkForm.tsx @@ -0,0 +1,549 @@ +"use client"; + +import { Button } from "@app/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { Input } from "@app/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@app/components/ui/select"; +import { toast } from "@app/hooks/useToast"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { AxiosResponse } from "axios"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import CopyTextBox from "@app/components/CopyTextBox"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import { useOrgContext } from "@app/hooks/useOrgContext"; +import { formatAxiosError } from "@app/lib/api"; +import { cn } from "@app/lib/cn"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { ListResourcesResponse } from "@server/routers/resource"; +import { + Popover, + PopoverContent, + PopoverTrigger +} from "@app/components/ui/popover"; +import { CaretSortIcon } from "@radix-ui/react-icons"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from "@app/components/ui/command"; +import { CheckIcon, ChevronsUpDown } from "lucide-react"; +import { Checkbox } from "@app/components/ui/checkbox"; +import { GenerateAccessTokenResponse } from "@server/routers/accessToken"; +import { constructShareLink } from "@app/lib/shareLinks"; +import { ShareLinkRow } from "@app/components/ShareLinksTable"; +import { QRCodeCanvas, QRCodeSVG } from "qrcode.react"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger +} from "@app/components/ui/collapsible"; +import AccessTokenSection from "@app/components/AccessTokenUsage"; +import { useTranslations } from "next-intl"; +import { toUnicode } from 'punycode'; + +type FormProps = { + open: boolean; + setOpen: (open: boolean) => void; + onCreated?: (result: ShareLinkRow) => void; +}; + +export default function CreateShareLinkForm({ + open, + setOpen, + onCreated +}: FormProps) { + const { org } = useOrgContext(); + + const { env } = useEnvContext(); + const api = createApiClient({ env }); + + const [link, setLink] = useState(null); + const [accessTokenId, setAccessTokenId] = useState(null); + const [accessToken, setAccessToken] = useState(null); + const [loading, setLoading] = useState(false); + const [neverExpire, setNeverExpire] = useState(false); + + const [isOpen, setIsOpen] = useState(false); + const t = useTranslations(); + + const [resources, setResources] = useState< + { + resourceId: number; + name: string; + resourceUrl: string; + }[] + >([]); + + const formSchema = z.object({ + resourceId: z.number({ message: t('shareErrorSelectResource') }), + resourceName: z.string(), + resourceUrl: z.string(), + timeUnit: z.string(), + timeValue: z.coerce.number().int().positive().min(1), + title: z.string().optional() + }); + + const timeUnits = [ + { unit: "minutes", name: t('minutes') }, + { unit: "hours", name: t('hours') }, + { unit: "days", name: t('days') }, + { unit: "weeks", name: t('weeks') }, + { unit: "months", name: t('months') }, + { unit: "years", name: t('years') } + ]; + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + timeUnit: "days", + timeValue: 30, + title: "" + } + }); + + useEffect(() => { + if (!open) { + return; + } + + async function fetchResources() { + const res = await api + .get< + AxiosResponse + >(`/org/${org?.org.orgId}/resources`) + .catch((e) => { + console.error(e); + toast({ + variant: "destructive", + title: t('shareErrorFetchResource'), + description: formatAxiosError( + e, + t('shareErrorFetchResourceDescription') + ) + }); + }); + + if (res?.status === 200) { + setResources( + res.data.data.resources + .filter((r) => { + return r.http; + }) + .map((r) => ({ + resourceId: r.resourceId, + name: r.name, + resourceUrl: `${r.ssl ? "https://" : "http://"}${toUnicode(r.fullDomain || "")}/` + })) + ); + } + } + + fetchResources(); + }, [open]); + + async function onSubmit(values: z.infer) { + setLoading(true); + + // convert time to seconds + let timeInSeconds = values.timeValue; + switch (values.timeUnit) { + case "minutes": + timeInSeconds *= 60; + break; + case "hours": + timeInSeconds *= 60 * 60; + break; + case "days": + timeInSeconds *= 60 * 60 * 24; + break; + case "weeks": + timeInSeconds *= 60 * 60 * 24 * 7; + break; + case "months": + timeInSeconds *= 60 * 60 * 24 * 30; + break; + case "years": + timeInSeconds *= 60 * 60 * 24 * 365; + break; + } + + const res = await api + .post>( + `/resource/${values.resourceId}/access-token`, + { + validForSeconds: neverExpire ? undefined : timeInSeconds, + title: + values.title || + t('shareLink', {resource: (values.resourceName || "Resource" + values.resourceId)}) + } + ) + .catch((e) => { + console.error(e); + toast({ + variant: "destructive", + title: t('shareErrorCreate'), + description: formatAxiosError( + e, + t('shareErrorCreateDescription') + ) + }); + }); + + if (res && res.data.data.accessTokenId) { + const token = res.data.data; + const link = constructShareLink(token.accessToken); + setLink(link); + + setAccessToken(token.accessToken); + setAccessTokenId(token.accessTokenId); + + const resource = resources.find( + (r) => r.resourceId === values.resourceId + ); + + onCreated?.({ + accessTokenId: token.accessTokenId, + resourceId: token.resourceId, + resourceName: values.resourceName, + title: token.title, + createdAt: token.createdAt, + expiresAt: token.expiresAt + }); + } + + setLoading(false); + } + + function getSelectedResourceName(id: number) { + const resource = resources.find((r) => r.resourceId === id); + return `${resource?.name}`; + } + + return ( + <> + { + setOpen(val); + setLink(null); + setLoading(false); + form.reset(); + }} + > + + + {t('shareCreate')} + + {t('shareCreateDescription')} + + + +
+ {!link && ( +
+ + ( + + + {t('resource')} + + + + + + + + + + + + + {t('resourcesNotFound')} + + + {resources.map( + ( + r + ) => ( + { + form.setValue( + "resourceId", + r.resourceId + ); + form.setValue( + "resourceName", + r.name + ); + form.setValue( + "resourceUrl", + r.resourceUrl + ); + }} + > + + {`${r.name}`} + + ) + )} + + + + + + + + )} + /> + + ( + + + {t('shareTitleOptional')} + + + + + + + )} + /> + +
+
+ {t('expireIn')} +
+ ( + + + + + )} + /> + + ( + + + + + + + )} + /> +
+
+ +
+ + setNeverExpire( + val as boolean + ) + } + /> + +
+ +

+ {t('shareExpireDescription')} +

+
+ + + )} + {link && ( +
+

+ {t('shareSeeOnce')} +

+

+ {t('shareAccessHint')} +

+ +
+ +
+ + +
+ +
+
+ + + +
+ + {accessTokenId && accessToken && ( +
+
+ +
+
+ )} +
+
+
+ )} +
+
+ + + + + + +
+
+ + ); +} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/CustomDomainInput.tsx b/src/components/CustomDomainInput.tsx similarity index 100% rename from src/app/[orgId]/settings/resources/[resourceId]/CustomDomainInput.tsx rename to src/components/CustomDomainInput.tsx diff --git a/src/app/auth/login/DashboardLoginForm.tsx b/src/components/DashboardLoginForm.tsx similarity index 100% rename from src/app/auth/login/DashboardLoginForm.tsx rename to src/components/DashboardLoginForm.tsx diff --git a/src/app/[orgId]/settings/access/roles/DeleteRoleForm.tsx b/src/components/DeleteRoleForm.tsx similarity index 99% rename from src/app/[orgId]/settings/access/roles/DeleteRoleForm.tsx rename to src/components/DeleteRoleForm.tsx index f3042f71..3516851b 100644 --- a/src/app/[orgId]/settings/access/roles/DeleteRoleForm.tsx +++ b/src/components/DeleteRoleForm.tsx @@ -34,7 +34,7 @@ import { SelectTrigger, SelectValue } from "@app/components/ui/select"; -import { RoleRow } from "./RolesTable"; +import { RoleRow } from "@app/components/RolesTable"; import { formatAxiosError } from "@app/lib/api"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; diff --git a/src/app/[orgId]/settings/domains/DomainsDataTable.tsx b/src/components/DomainsDataTable.tsx similarity index 100% rename from src/app/[orgId]/settings/domains/DomainsDataTable.tsx rename to src/components/DomainsDataTable.tsx diff --git a/src/app/[orgId]/settings/domains/DomainsTable.tsx b/src/components/DomainsTable.tsx similarity index 98% rename from src/app/[orgId]/settings/domains/DomainsTable.tsx rename to src/components/DomainsTable.tsx index 84bc8bc6..5bafe935 100644 --- a/src/app/[orgId]/settings/domains/DomainsTable.tsx +++ b/src/components/DomainsTable.tsx @@ -1,7 +1,7 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; -import { DomainsDataTable } from "./DomainsDataTable"; +import { DomainsDataTable } from "@app/components/DomainsDataTable"; import { Button } from "@app/components/ui/button"; import { ArrowUpDown } from "lucide-react"; import { useState } from "react"; @@ -12,7 +12,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { Badge } from "@app/components/ui/badge"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; -import CreateDomainForm from "./CreateDomainForm"; +import CreateDomainForm from "@app/components/CreateDomainForm"; import { useToast } from "@app/hooks/useToast"; import { useOrgContext } from "@app/hooks/useOrgContext"; diff --git a/src/components/IdpCreateWizard.tsx b/src/components/IdpCreateWizard.tsx new file mode 100644 index 00000000..beeeff1c --- /dev/null +++ b/src/components/IdpCreateWizard.tsx @@ -0,0 +1,429 @@ +"use client"; + +import { + SettingsContainer, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionForm, + SettingsSectionGrid, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { z } from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Input } from "@app/components/ui/input"; +import { Checkbox } from "@app/components/ui/checkbox"; +import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; +import { InfoIcon, ExternalLink } from "lucide-react"; +import { StrategySelect } from "@app/components/StrategySelect"; +import { SwitchInput } from "@app/components/SwitchInput"; +import { Badge } from "@app/components/ui/badge"; +import { useTranslations } from "next-intl"; + +type CreateIdpFormValues = { + name: string; + type: "oidc"; + clientId: string; + clientSecret: string; + authUrl: string; + tokenUrl: string; + identifierPath: string; + emailPath?: string; + namePath?: string; + scopes: string; + autoProvision: boolean; +}; + +type IdpCreateWizardProps = { + onSubmit: (data: CreateIdpFormValues) => void | Promise; + defaultValues?: Partial; + loading?: boolean; +}; + +export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: IdpCreateWizardProps) { + const t = useTranslations(); + + const createIdpFormSchema = z.object({ + name: z.string().min(2, { message: t('nameMin', {len: 2}) }), + type: z.enum(["oidc"]), + clientId: z.string().min(1, { message: t('idpClientIdRequired') }), + clientSecret: z.string().min(1, { message: t('idpClientSecretRequired') }), + authUrl: z.string().url({ message: t('idpErrorAuthUrlInvalid') }), + tokenUrl: z.string().url({ message: t('idpErrorTokenUrlInvalid') }), + identifierPath: z + .string() + .min(1, { message: t('idpPathRequired') }), + emailPath: z.string().optional(), + namePath: z.string().optional(), + scopes: z.string().min(1, { message: t('idpScopeRequired') }), + autoProvision: z.boolean().default(false) + }); + + interface ProviderTypeOption { + id: "oidc"; + title: string; + description: string; + } + + const providerTypes: ReadonlyArray = [ + { + id: "oidc", + title: "OAuth2/OIDC", + description: t('idpOidcDescription') + } + ]; + + const form = useForm({ + resolver: zodResolver(createIdpFormSchema), + defaultValues: { + name: "", + type: "oidc", + clientId: "", + clientSecret: "", + authUrl: "", + tokenUrl: "", + identifierPath: "sub", + namePath: "name", + emailPath: "email", + scopes: "openid profile email", + autoProvision: false, + ...defaultValues + } + }); + + const handleSubmit = (data: CreateIdpFormValues) => { + onSubmit(data); + }; + + return ( + + + + + {t('idpTitle')} + + + {t('idpCreateSettingsDescription')} + + + + +
+ + ( + + {t('name')} + + + + + {t('idpDisplayName')} + + + + )} + /> + +
+ { + form.setValue( + "autoProvision", + checked + ); + }} + disabled={loading} + /> +
+ + {t('idpAutoProvisionUsersDescription')} + + + +
+
+
+ + + + + {t('idpType')} + + + {t('idpTypeDescription')} + + + + { + form.setValue("type", value as "oidc"); + }} + cols={3} + /> + + + + {form.watch("type") === "oidc" && ( + + + + + {t('idpOidcConfigure')} + + + {t('idpOidcConfigureDescription')} + + + +
+ + ( + + + {t('idpClientId')} + + + + + + {t('idpClientIdDescription')} + + + + )} + /> + + ( + + + {t('idpClientSecret')} + + + + + + {t('idpClientSecretDescription')} + + + + )} + /> + + ( + + + {t('idpAuthUrl')} + + + + + + {t('idpAuthUrlDescription')} + + + + )} + /> + + ( + + + {t('idpTokenUrl')} + + + + + + {t('idpTokenUrlDescription')} + + + + )} + /> + + + + + + + {t('idpOidcConfigureAlert')} + + + {t('idpOidcConfigureAlertDescription')} + + +
+
+ + + + + {t('idpToken')} + + + {t('idpTokenDescription')} + + + +
+ + + + + {t('idpJmespathAbout')} + + + {t('idpJmespathAboutDescription')}{" "} + + {t('idpJmespathAboutDescriptionLink')}{" "} + + + + + + ( + + + {t('idpJmespathLabel')} + + + + + + {t('idpJmespathLabelDescription')} + + + + )} + /> + + ( + + + {t('idpJmespathEmailPathOptional')} + + + + + + {t('idpJmespathEmailPathOptionalDescription')} + + + + )} + /> + + ( + + + {t('idpJmespathNamePathOptional')} + + + + + + {t('idpJmespathNamePathOptionalDescription')} + + + + )} + /> + + ( + + + {t('idpOidcConfigureScopes')} + + + + + + {t('idpOidcConfigureScopesDescription')} + + + + )} + /> + + +
+
+
+ )} +
+ ); +} diff --git a/src/app/[orgId]/settings/access/invitations/InvitationsDataTable.tsx b/src/components/InvitationsDataTable.tsx similarity index 100% rename from src/app/[orgId]/settings/access/invitations/InvitationsDataTable.tsx rename to src/components/InvitationsDataTable.tsx diff --git a/src/app/[orgId]/settings/access/invitations/InvitationsTable.tsx b/src/components/InvitationsTable.tsx similarity index 97% rename from src/app/[orgId]/settings/access/invitations/InvitationsTable.tsx rename to src/components/InvitationsTable.tsx index dfb3d263..a97220f2 100644 --- a/src/app/[orgId]/settings/access/invitations/InvitationsTable.tsx +++ b/src/components/InvitationsTable.tsx @@ -9,10 +9,10 @@ import { } from "@app/components/ui/dropdown-menu"; import { Button } from "@app/components/ui/button"; import { MoreHorizontal } from "lucide-react"; -import { InvitationsDataTable } from "./InvitationsDataTable"; +import { InvitationsDataTable } from "@app/components/InvitationsDataTable"; import { useState } from "react"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; -import RegenerateInvitationForm from "./RegenerateInvitationForm"; +import RegenerateInvitationForm from "@app/components/RegenerateInvitationForm"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient } from "@app/lib/api"; diff --git a/src/app/invite/InviteStatusCard.tsx b/src/components/InviteStatusCard.tsx similarity index 100% rename from src/app/invite/InviteStatusCard.tsx rename to src/components/InviteStatusCard.tsx diff --git a/src/app/admin/license/LicenseKeysDataTable.tsx b/src/components/LicenseKeysDataTable.tsx similarity index 100% rename from src/app/admin/license/LicenseKeysDataTable.tsx rename to src/components/LicenseKeysDataTable.tsx diff --git a/src/app/[orgId]/MemberResourcesPortal.tsx b/src/components/MemberResourcesPortal.tsx similarity index 100% rename from src/app/[orgId]/MemberResourcesPortal.tsx rename to src/components/MemberResourcesPortal.tsx diff --git a/src/app/[orgId]/settings/api-keys/OrgApiKeysDataTable.tsx b/src/components/OrgApiKeysDataTable.tsx similarity index 100% rename from src/app/[orgId]/settings/api-keys/OrgApiKeysDataTable.tsx rename to src/components/OrgApiKeysDataTable.tsx diff --git a/src/app/[orgId]/settings/api-keys/OrgApiKeysTable.tsx b/src/components/OrgApiKeysTable.tsx similarity index 98% rename from src/app/[orgId]/settings/api-keys/OrgApiKeysTable.tsx rename to src/components/OrgApiKeysTable.tsx index b503241f..bfe8dfab 100644 --- a/src/app/[orgId]/settings/api-keys/OrgApiKeysTable.tsx +++ b/src/components/OrgApiKeysTable.tsx @@ -1,7 +1,7 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; -import { OrgApiKeysDataTable } from "./OrgApiKeysDataTable"; +import { OrgApiKeysDataTable } from "@app/components/OrgApiKeysDataTable"; import { DropdownMenu, DropdownMenuContent, diff --git a/src/app/[orgId]/OrganizationLandingCard.tsx b/src/components/OrganizationLandingCard.tsx similarity index 100% rename from src/app/[orgId]/OrganizationLandingCard.tsx rename to src/components/OrganizationLandingCard.tsx diff --git a/src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx b/src/components/PolicyDataTable.tsx similarity index 100% rename from src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx rename to src/components/PolicyDataTable.tsx diff --git a/src/app/admin/idp/[idpId]/policies/PolicyTable.tsx b/src/components/PolicyTable.tsx similarity index 98% rename from src/app/admin/idp/[idpId]/policies/PolicyTable.tsx rename to src/components/PolicyTable.tsx index f68a00c7..81fed28f 100644 --- a/src/app/admin/idp/[idpId]/policies/PolicyTable.tsx +++ b/src/components/PolicyTable.tsx @@ -9,7 +9,7 @@ import { Pencil, ArrowRight } from "lucide-react"; -import { PolicyDataTable } from "./PolicyDataTable"; +import { PolicyDataTable } from "@app/components/PolicyDataTable"; import { Badge } from "@app/components/ui/badge"; import { DropdownMenu, diff --git a/src/app/[orgId]/settings/access/invitations/RegenerateInvitationForm.tsx b/src/components/RegenerateInvitationForm.tsx similarity index 100% rename from src/app/[orgId]/settings/access/invitations/RegenerateInvitationForm.tsx rename to src/components/RegenerateInvitationForm.tsx diff --git a/src/components/ResetPasswordForm.tsx b/src/components/ResetPasswordForm.tsx new file mode 100644 index 00000000..faafccf4 --- /dev/null +++ b/src/components/ResetPasswordForm.tsx @@ -0,0 +1,533 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle +} from "@/components/ui/card"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + InputOTP, + InputOTPGroup, + InputOTPSlot +} from "@/components/ui/input-otp"; +import { AxiosResponse } from "axios"; +import { + RequestPasswordResetBody, + RequestPasswordResetResponse, + ResetPasswordBody, + ResetPasswordResponse +} from "@server/routers/auth"; +import { Loader2 } from "lucide-react"; +import { Alert, AlertDescription } from "./ui/alert"; +import { toast } from "@app/hooks/useToast"; +import { useRouter } from "next/navigation"; +import { formatAxiosError } from "@app/lib/api"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp"; +import { passwordSchema } from "@server/auth/passwordSchema"; +import { cleanRedirect } from "@app/lib/cleanRedirect"; +import { useTranslations } from "next-intl"; + +const requestSchema = z.object({ + email: z.string().email() +}); + +export type ResetPasswordFormProps = { + emailParam?: string; + tokenParam?: string; + redirect?: string; + quickstart?: boolean; +}; + +export default function ResetPasswordForm({ + emailParam, + tokenParam, + redirect, + quickstart +}: ResetPasswordFormProps) { + const router = useRouter(); + + const [error, setError] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + const t = useTranslations(); + + function getState() { + if (emailParam && !tokenParam) { + return "request"; + } + + if (emailParam && tokenParam) { + return "reset"; + } + + return "request"; + } + + const [state, setState] = useState<"request" | "reset" | "mfa">(getState()); + + const api = createApiClient(useEnvContext()); + + const formSchema = z + .object({ + email: z.string().email({ message: t('emailInvalid') }), + token: z.string().min(8, { message: t('tokenInvalid') }), + password: passwordSchema, + confirmPassword: passwordSchema + }) + .refine((data) => data.password === data.confirmPassword, { + path: ["confirmPassword"], + message: t('passwordNotMatch') + }); + + const mfaSchema = z.object({ + code: z.string().length(6, { message: t('pincodeInvalid') }) + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: emailParam || "", + token: tokenParam || "", + password: "", + confirmPassword: "" + } + }); + + const mfaForm = useForm>({ + resolver: zodResolver(mfaSchema), + defaultValues: { + code: "" + } + }); + + const requestForm = useForm>({ + resolver: zodResolver(requestSchema), + defaultValues: { + email: emailParam || "" + } + }); + + async function onRequest(data: z.infer) { + const { email } = data; + + setIsSubmitting(true); + + const res = await api + .post>( + "/auth/reset-password/request", + { + email + } as RequestPasswordResetBody + ) + .catch((e) => { + setError(formatAxiosError(e, t('errorOccurred'))); + console.error(t('passwordErrorRequestReset'), e); + setIsSubmitting(false); + }); + + if (res && res.data?.data) { + setError(null); + setState("reset"); + setIsSubmitting(false); + form.setValue("email", email); + } + } + + async function onReset(data: any) { + setIsSubmitting(true); + + const { password, email, token } = form.getValues(); + const { code } = mfaForm.getValues(); + + const res = await api + .post>( + "/auth/reset-password", + { + email, + token, + newPassword: password, + code + } as ResetPasswordBody + ) + .catch((e) => { + setError(formatAxiosError(e, t('errorOccurred'))); + console.error(t('passwordErrorReset'), e); + setIsSubmitting(false); + }); + + console.log(res); + + if (res) { + setError(null); + + if (res.data.data?.codeRequested) { + setState("mfa"); + setIsSubmitting(false); + mfaForm.reset(); + return; + } + + setSuccessMessage(quickstart ? t('accountSetupSuccess') : t('passwordResetSuccess')); + + // Auto-login after successful password reset + try { + const loginRes = await api.post("/auth/login", { + email: form.getValues("email"), + password: form.getValues("password") + }); + + if (loginRes.data.data?.codeRequested) { + if (redirect) { + router.push(`/auth/login?redirect=${redirect}`); + } else { + router.push("/auth/login"); + } + return; + } + + if (loginRes.data.data?.emailVerificationRequired) { + try { + await api.post("/auth/verify-email/request"); + } catch (verificationError) { + console.error("Failed to send verification code:", verificationError); + } + + if (redirect) { + router.push(`/auth/verify-email?redirect=${redirect}`); + } else { + router.push("/auth/verify-email"); + } + return; + } + + // Login successful, redirect + setTimeout(() => { + if (redirect) { + const safe = cleanRedirect(redirect); + router.push(safe); + } else { + router.push("/"); + } + setIsSubmitting(false); + }, 1500); + + } catch (loginError) { + // Auto-login failed, but password reset was successful + console.error("Auto-login failed:", loginError); + setTimeout(() => { + if (redirect) { + const safe = cleanRedirect(redirect); + router.push(safe); + } else { + router.push("/login"); + } + setIsSubmitting(false); + }, 1500); + } + } + } + + return ( +
+ + + + {quickstart ? t('completeAccountSetup') : t('passwordReset')} + + + {quickstart + ? t('completeAccountSetupDescription') + : t('passwordResetDescription') + } + + + +
+ {state === "request" && ( +
+ + ( + + {t('email')} + + + + + + {quickstart + ? t('accountSetupSent') + : t('passwordResetSent') + } + + + )} + /> + + + )} + + {state === "reset" && ( +
+ + ( + + {t('email')} + + + + + + )} + /> + + {!tokenParam && ( + ( + + + {quickstart + ? t('accountSetupCode') + : t('passwordResetCode') + } + + + + + + + {quickstart + ? t('accountSetupCodeDescription') + : t('passwordResetCodeDescription') + } + + + )} + /> + )} + + ( + + + {quickstart + ? t('passwordCreate') + : t('passwordNew') + } + + + + + + + )} + /> + ( + + + {quickstart + ? t('passwordCreateConfirm') + : t('passwordNewConfirm') + } + + + + + + + )} + /> + + + )} + + {state === "mfa" && ( +
+ + ( + + + {t('pincodeAuth')} + + +
+ + + + + + + + + + +
+
+ +
+ )} + /> + + + )} + + {error && ( + + {error} + + )} + + {successMessage && ( + + + {successMessage} + + + )} + +
+ {(state === "reset" || state === "mfa") && ( + + )} + + {state === "request" && ( + + )} + + {state === "mfa" && ( + + )} + + {(state === "mfa" || state === "reset") && ( + + )} +
+
+
+
+
+ ); +} diff --git a/src/app/auth/resource/[resourceId]/ResourceAccessDenied.tsx b/src/components/ResourceAccessDenied.tsx similarity index 100% rename from src/app/auth/resource/[resourceId]/ResourceAccessDenied.tsx rename to src/components/ResourceAccessDenied.tsx diff --git a/src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx b/src/components/ResourceAuthPortal.tsx similarity index 99% rename from src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx rename to src/components/ResourceAuthPortal.tsx index c77c9e0b..c9877857 100644 --- a/src/app/auth/resource/[resourceId]/ResourceAuthPortal.tsx +++ b/src/components/ResourceAuthPortal.tsx @@ -38,7 +38,7 @@ import { AuthWithPasswordResponse, AuthWithWhitelistResponse } from "@server/routers/resource"; -import ResourceAccessDenied from "./ResourceAccessDenied"; +import ResourceAccessDenied from "@app/components/ResourceAccessDenied"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; diff --git a/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx b/src/components/ResourceInfoBox.tsx similarity index 100% rename from src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx rename to src/components/ResourceInfoBox.tsx diff --git a/src/app/auth/resource/[resourceId]/ResourceNotFound.tsx b/src/components/ResourceNotFound.tsx similarity index 100% rename from src/app/auth/resource/[resourceId]/ResourceNotFound.tsx rename to src/components/ResourceNotFound.tsx diff --git a/src/app/[orgId]/settings/resources/ResourcesTable.tsx b/src/components/ResourcesTable.tsx similarity index 100% rename from src/app/[orgId]/settings/resources/ResourcesTable.tsx rename to src/components/ResourcesTable.tsx diff --git a/src/app/[orgId]/settings/access/roles/RolesDataTable.tsx b/src/components/RolesDataTable.tsx similarity index 100% rename from src/app/[orgId]/settings/access/roles/RolesDataTable.tsx rename to src/components/RolesDataTable.tsx diff --git a/src/app/[orgId]/settings/access/roles/RolesTable.tsx b/src/components/RolesTable.tsx similarity index 95% rename from src/app/[orgId]/settings/access/roles/RolesTable.tsx rename to src/components/RolesTable.tsx index 40260fb7..e92e71b6 100644 --- a/src/app/[orgId]/settings/access/roles/RolesTable.tsx +++ b/src/components/RolesTable.tsx @@ -13,10 +13,10 @@ import { useState } from "react"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { toast } from "@app/hooks/useToast"; -import { RolesDataTable } from "./RolesDataTable"; +import { RolesDataTable } from "@app/components/RolesDataTable"; import { Role } from "@server/db"; -import CreateRoleForm from "./CreateRoleForm"; -import DeleteRoleForm from "./DeleteRoleForm"; +import CreateRoleForm from "@app/components/CreateRoleForm"; +import DeleteRoleForm from "@app/components/DeleteRoleForm"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useTranslations } from "next-intl"; diff --git a/src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePasswordForm.tsx b/src/components/SetResourcePasswordForm.tsx similarity index 100% rename from src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePasswordForm.tsx rename to src/components/SetResourcePasswordForm.tsx diff --git a/src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePincodeForm.tsx b/src/components/SetResourcePincodeForm.tsx similarity index 100% rename from src/app/[orgId]/settings/resources/[resourceId]/authentication/SetResourcePincodeForm.tsx rename to src/components/SetResourcePincodeForm.tsx diff --git a/src/app/[orgId]/settings/share-links/ShareLinksDataTable.tsx b/src/components/ShareLinksDataTable.tsx similarity index 100% rename from src/app/[orgId]/settings/share-links/ShareLinksDataTable.tsx rename to src/components/ShareLinksDataTable.tsx diff --git a/src/app/[orgId]/settings/share-links/ShareLinksSplash.tsx b/src/components/ShareLinksSplash.tsx similarity index 100% rename from src/app/[orgId]/settings/share-links/ShareLinksSplash.tsx rename to src/components/ShareLinksSplash.tsx diff --git a/src/components/ShareLinksTable.tsx b/src/components/ShareLinksTable.tsx new file mode 100644 index 00000000..2943311f --- /dev/null +++ b/src/components/ShareLinksTable.tsx @@ -0,0 +1,298 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { ShareLinksDataTable } from "@app/components/ShareLinksDataTable"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from "@app/components/ui/dropdown-menu"; +import { Button } from "@app/components/ui/button"; +import { + Copy, + ArrowRight, + ArrowUpDown, + MoreHorizontal, + Check, + ArrowUpRight, + ShieldOff, + ShieldCheck +} from "lucide-react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +// import CreateResourceForm from "./CreateResourceForm"; +import { useState } from "react"; +import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; +import { formatAxiosError } from "@app/lib/api"; +import { toast } from "@app/hooks/useToast"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { ArrayElement } from "@server/types/ArrayElement"; +import { ListAccessTokensResponse } from "@server/routers/accessToken"; +import moment from "moment"; +import CreateShareLinkForm from "@app/components/CreateShareLinkForm"; +import { constructShareLink } from "@app/lib/shareLinks"; +import { useTranslations } from "next-intl"; + +export type ShareLinkRow = { + accessTokenId: string; + resourceId: number; + resourceName: string; + title: string | null; + createdAt: number; + expiresAt: number | null; +}; + +type ShareLinksTableProps = { + shareLinks: ShareLinkRow[]; + orgId: string; +}; + +export default function ShareLinksTable({ + shareLinks, + orgId +}: ShareLinksTableProps) { + const router = useRouter(); + const t = useTranslations(); + + const api = createApiClient(useEnvContext()); + + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + const [rows, setRows] = useState(shareLinks); + + function formatLink(link: string) { + return link.substring(0, 20) + "..." + link.substring(link.length - 20); + } + + async function deleteSharelink(id: string) { + await api.delete(`/access-token/${id}`).catch((e) => { + toast({ + title: t("shareErrorDelete"), + description: formatAxiosError(e, t("shareErrorDeleteMessage")) + }); + }); + + const newRows = rows.filter((r) => r.accessTokenId !== id); + setRows(newRows); + + toast({ + title: t("shareDeleted"), + description: t("shareDeletedDescription") + }); + } + + const columns: ColumnDef[] = [ + { + accessorKey: "resourceName", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const r = row.original; + return ( + + + + ); + } + }, + { + accessorKey: "title", + header: ({ column }) => { + return ( + + ); + } + }, + // { + // accessorKey: "domain", + // header: "Link", + // cell: ({ row }) => { + // const r = row.original; + // + // const link = constructShareLink( + // r.resourceId, + // r.accessTokenId, + // r.tokenHash + // ); + // + // return ( + //
+ // + // {formatLink(link)} + // + // + //
+ // ); + // } + // }, + { + accessorKey: "createdAt", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const r = row.original; + return moment(r.createdAt).format("lll"); + } + }, + { + accessorKey: "expiresAt", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const r = row.original; + if (r.expiresAt) { + return moment(r.expiresAt).format("lll"); + } + return t("never"); + } + }, + { + id: "delete", + cell: ({ row }) => { + const resourceRow = row.original; + return ( +
+ {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* { */} + {/* deleteSharelink( */} + {/* resourceRow.accessTokenId */} + {/* ); */} + {/* }} */} + {/* > */} + {/* */} + {/* */} + {/* */} + {/* */} + +
+ ); + } + } + ]; + + return ( + <> + { + setRows([val, ...rows]); + }} + /> + + { + setIsCreateModalOpen(true); + }} + /> + + ); +} diff --git a/src/app/auth/signup/SignupForm.tsx b/src/components/SignupForm.tsx similarity index 100% rename from src/app/auth/signup/SignupForm.tsx rename to src/components/SignupForm.tsx diff --git a/src/app/[orgId]/settings/sites/[niceId]/SiteInfoCard.tsx b/src/components/SiteInfoCard.tsx similarity index 100% rename from src/app/[orgId]/settings/sites/[niceId]/SiteInfoCard.tsx rename to src/components/SiteInfoCard.tsx diff --git a/src/app/admin/license/components/SitePriceCalculator.tsx b/src/components/SitePriceCalculator.tsx similarity index 100% rename from src/app/admin/license/components/SitePriceCalculator.tsx rename to src/components/SitePriceCalculator.tsx diff --git a/src/app/[orgId]/settings/sites/SitesDataTable.tsx b/src/components/SitesDataTable.tsx similarity index 100% rename from src/app/[orgId]/settings/sites/SitesDataTable.tsx rename to src/components/SitesDataTable.tsx diff --git a/src/app/[orgId]/settings/sites/SitesSplashCard.tsx b/src/components/SitesSplashCard.tsx similarity index 100% rename from src/app/[orgId]/settings/sites/SitesSplashCard.tsx rename to src/components/SitesSplashCard.tsx diff --git a/src/app/[orgId]/settings/sites/SitesTable.tsx b/src/components/SitesTable.tsx similarity index 99% rename from src/app/[orgId]/settings/sites/SitesTable.tsx rename to src/components/SitesTable.tsx index 8387ab7c..f9ac8c0d 100644 --- a/src/app/[orgId]/settings/sites/SitesTable.tsx +++ b/src/components/SitesTable.tsx @@ -1,7 +1,7 @@ "use client"; import { Column, ColumnDef } from "@tanstack/react-table"; -import { SitesDataTable } from "./SitesDataTable"; +import { SitesDataTable } from "@app/components/SitesDataTable"; import { DropdownMenu, DropdownMenuContent, diff --git a/src/app/[orgId]/settings/access/users/UsersDataTable.tsx b/src/components/UsersDataTable.tsx similarity index 100% rename from src/app/[orgId]/settings/access/users/UsersDataTable.tsx rename to src/components/UsersDataTable.tsx diff --git a/src/app/[orgId]/settings/access/users/UsersTable.tsx b/src/components/UsersTable.tsx similarity index 99% rename from src/app/[orgId]/settings/access/users/UsersTable.tsx rename to src/components/UsersTable.tsx index 6b8af509..dcfd5e20 100644 --- a/src/app/[orgId]/settings/access/users/UsersTable.tsx +++ b/src/components/UsersTable.tsx @@ -9,7 +9,7 @@ import { } from "@app/components/ui/dropdown-menu"; import { Button } from "@app/components/ui/button"; import { ArrowRight, ArrowUpDown, Crown, MoreHorizontal } from "lucide-react"; -import { UsersDataTable } from "./UsersDataTable"; +import { UsersDataTable } from "@app/components/UsersDataTable"; import { useState } from "react"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { useOrgContext } from "@app/hooks/useOrgContext"; diff --git a/src/app/auth/idp/[idpId]/oidc/callback/ValidateOidcToken.tsx b/src/components/ValidateOidcToken.tsx similarity index 100% rename from src/app/auth/idp/[idpId]/oidc/callback/ValidateOidcToken.tsx rename to src/components/ValidateOidcToken.tsx diff --git a/src/app/auth/verify-email/VerifyEmailForm.tsx b/src/components/VerifyEmailForm.tsx similarity index 99% rename from src/app/auth/verify-email/VerifyEmailForm.tsx rename to src/components/VerifyEmailForm.tsx index e9761eef..9cf48a2f 100644 --- a/src/app/auth/verify-email/VerifyEmailForm.tsx +++ b/src/components/VerifyEmailForm.tsx @@ -30,7 +30,7 @@ import { import { AxiosResponse } from "axios"; import { VerifyEmailResponse } from "@server/routers/auth"; import { ArrowRight, IdCard, Loader2 } from "lucide-react"; -import { Alert, AlertDescription } from "../../../components/ui/alert"; +import { Alert, AlertDescription } from "./ui/alert"; import { toast } from "@app/hooks/useToast"; import { useRouter } from "next/navigation"; import { formatAxiosError } from "@app/lib/api";