Merge branch 'dev' into clients-pops

This commit is contained in:
Owen
2025-06-10 13:00:20 -04:00
282 changed files with 6489 additions and 3540 deletions

View File

@@ -14,7 +14,7 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { useOrgContext } from "@app/hooks/useOrgContext";
import { toast } from "@app/hooks/useToast";
import { RolesDataTable } from "./RolesDataTable";
import { Role } from "@server/db/schemas";
import { Role } from "@server/db";
import CreateRoleForm from "./CreateRoleForm";
import DeleteRoleForm from "./DeleteRoleForm";
import { createApiClient } from "@app/lib/api";

View File

@@ -44,7 +44,6 @@ import {
CreateOrgApiKeyBody,
CreateOrgApiKeyResponse
} from "@server/routers/apiKeys";
import { ApiKey } from "@server/db/schemas";
import {
InfoSection,
InfoSectionContent,

View File

@@ -1,9 +1,8 @@
"use client";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { ArrowRight, InfoIcon, ShieldCheck, ShieldOff } from "lucide-react";
import { InfoIcon, ShieldCheck, ShieldOff } from "lucide-react";
import { useResourceContext } from "@app/hooks/useResourceContext";
import { Separator } from "@app/components/ui/separator";
import CopyToClipboard from "@app/components/CopyToClipboard";
import {
InfoSection,
@@ -11,13 +10,17 @@ import {
InfoSections,
InfoSectionTitle
} from "@app/components/InfoSection";
import Link from "next/link";
import { Switch } from "@app/components/ui/switch";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useDockerSocket } from "@app/hooks/useDockerSocket";
type ResourceInfoBoxType = {};
export default function ResourceInfoBox({}: ResourceInfoBoxType) {
const { resource, authInfo } = useResourceContext();
const { resource, authInfo, site } = useResourceContext();
const api = createApiClient(useEnvContext());
const { isEnabled, isAvailable } = useDockerSocket(site!);
let fullUrl = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
@@ -67,6 +70,24 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
{resource.siteName}
</InfoSectionContent>
</InfoSection>
{/* {isEnabled && (
<InfoSection>
<InfoSectionTitle>Socket</InfoSectionTitle>
<InfoSectionContent>
{isAvailable ? (
<span className="text-green-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>Online</span>
</span>
) : (
<span className="text-neutral-500 flex items-center space-x-2">
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
<span>Offline</span>
</span>
)}
</InfoSectionContent>
</InfoSection>
)} */}
</>
) : (
<>
@@ -92,7 +113,9 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
<InfoSection>
<InfoSectionTitle>Visibility</InfoSectionTitle>
<InfoSectionContent>
<span>{resource.enabled ? "Enabled" : "Disabled"}</span>
<span>
{resource.enabled ? "Enabled" : "Disabled"}
</span>
</InfoSectionContent>
</InfoSection>
</InfoSections>

View File

@@ -28,7 +28,7 @@ import {
} from "@app/components/Credenza";
import { formatAxiosError } from "@app/lib/api";
import { AxiosResponse } from "axios";
import { Resource } from "@server/db/schemas";
import { Resource } from "@server/db";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";

View File

@@ -28,7 +28,7 @@ import {
} from "@app/components/Credenza";
import { formatAxiosError } from "@app/lib/api";
import { AxiosResponse } from "axios";
import { Resource } from "@server/db/schemas";
import { Resource } from "@server/db";
import {
InputOTP,
InputOTPGroup,

View File

@@ -13,15 +13,7 @@ import { GetOrgResponse } from "@server/routers/org";
import OrgProvider from "@app/providers/OrgProvider";
import { cache } from "react";
import ResourceInfoBox from "./ResourceInfoBox";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from "@app/components/ui/breadcrumb";
import Link from "next/link";
import { GetSiteResponse } from "@server/routers/site";
interface ResourceLayoutProps {
children: React.ReactNode;
@@ -35,6 +27,7 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
let authInfo = null;
let resource = null;
let site = null;
try {
const res = await internal.get<AxiosResponse<GetResourceResponse>>(
`/resource/${params.resourceId}`,
@@ -49,6 +42,19 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
redirect(`/${params.orgId}/settings/resources`);
}
// Fetch site info
if (resource.siteId) {
try {
const res = await internal.get<AxiosResponse<GetSiteResponse>>(
`/site/${resource.siteId}`,
await authCookieHeader()
);
site = res.data.data;
} catch {
redirect(`/${params.orgId}/settings/resources`);
}
}
try {
const res = await internal.get<
AxiosResponse<GetResourceAuthInfoResponse>
@@ -110,7 +116,11 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
/>
<OrgProvider org={org}>
<ResourceProvider resource={resource} authInfo={authInfo}>
<ResourceProvider
site={site}
resource={resource}
authInfo={authInfo}
>
<div className="space-y-6">
<ResourceInfoBox />
<HorizontalTabs items={navItems}>

View File

@@ -41,7 +41,6 @@ import {
TableBody,
TableCaption,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow
@@ -61,7 +60,8 @@ import {
SettingsSectionDescription,
SettingsSectionBody,
SettingsSectionFooter,
SettingsSectionForm
SettingsSectionForm,
SettingsSectionGrid
} from "@app/components/Settings";
import { SwitchInput } from "@app/components/SwitchInput";
import { useRouter } from "next/navigation";
@@ -73,6 +73,8 @@ import {
CollapsibleContent,
CollapsibleTrigger
} from "@app/components/ui/collapsible";
import { ContainersSelector } from "@app/components/ContainersSelector";
import { FaDocker } from "react-icons/fa";
const addTargetSchema = z.object({
ip: z.string().refine(isTargetValid),
@@ -163,6 +165,9 @@ export default function ReverseProxyTargets(props: {
} as z.infer<typeof addTargetSchema>
});
const watchedIp = addTargetForm.watch("ip");
const watchedPort = addTargetForm.watch("port");
const tlsSettingsForm = useForm<TlsSettingsValues>({
resolver: zodResolver(tlsSettingsSchema),
defaultValues: {
@@ -556,115 +561,6 @@ export default function ReverseProxyTargets(props: {
return (
<SettingsContainer>
{resource.http && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
HTTPS & TLS Settings
</SettingsSectionTitle>
<SettingsSectionDescription>
Configure TLS settings for your resource
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...tlsSettingsForm}>
<form
onSubmit={tlsSettingsForm.handleSubmit(
saveTlsSettings
)}
className="space-y-4"
id="tls-settings-form"
>
<FormField
control={tlsSettingsForm.control}
name="ssl"
render={({ field }) => (
<FormItem>
<FormControl>
<SwitchInput
id="ssl-toggle"
label="Enable SSL (https)"
defaultChecked={
field.value
}
onCheckedChange={(
val
) => {
field.onChange(val);
}}
/>
</FormControl>
</FormItem>
)}
/>
<Collapsible
open={isAdvancedOpen}
onOpenChange={setIsAdvancedOpen}
className="space-y-2"
>
<div className="flex items-center justify-between space-x-4">
<CollapsibleTrigger asChild>
<Button
variant="text"
size="sm"
className="p-0 flex items-center justify-start gap-2 w-full"
>
<h4 className="text-sm font-semibold">
Advanced TLS Settings
</h4>
<div>
<ChevronsUpDown className="h-4 w-4" />
<span className="sr-only">
Toggle
</span>
</div>
</Button>
</CollapsibleTrigger>
</div>
<CollapsibleContent className="space-y-2">
<FormField
control={
tlsSettingsForm.control
}
name="tlsServerName"
render={({ field }) => (
<FormItem>
<FormLabel>
TLS Server Name
(SNI)
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The TLS Server Name
to use for SNI.
Leave empty to use
the default.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</CollapsibleContent>
</Collapsible>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
type="submit"
loading={httpsTlsLoading}
form="tls-settings-form"
>
Save Settings
</Button>
</SettingsSectionFooter>
</SettingsSection>
)}
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
@@ -767,11 +663,31 @@ export default function ReverseProxyTargets(props: {
control={addTargetForm.control}
name="ip"
render={({ field }) => (
<FormItem>
<FormItem className="relative">
<FormLabel>IP / Hostname</FormLabel>
<FormControl>
<Input id="ip" {...field} />
</FormControl>
{site && site.type == "newt" && (
<ContainersSelector
site={site}
onContainerSelect={(
hostname,
port
) => {
addTargetForm.setValue(
"ip",
hostname
);
if (port) {
addTargetForm.setValue(
"port",
port
);
}
}}
/>
)}
<FormMessage />
</FormItem>
)}
@@ -798,12 +714,7 @@ export default function ReverseProxyTargets(props: {
type="submit"
variant="outlinePrimary"
className="mt-6"
disabled={
!(
addTargetForm.getValues("ip") &&
addTargetForm.getValues("port")
)
}
disabled={!(watchedIp && watchedPort)}
>
Add Target
</Button>
@@ -873,59 +784,175 @@ export default function ReverseProxyTargets(props: {
</SettingsSection>
{resource.http && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
Additional Proxy Settings
</SettingsSectionTitle>
<SettingsSectionDescription>
Configure how your resource handles proxy settings
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...proxySettingsForm}>
<form
onSubmit={proxySettingsForm.handleSubmit(
saveProxySettings
)}
className="space-y-4"
id="proxy-settings-form"
>
<FormField
control={proxySettingsForm.control}
name="setHostHeader"
render={({ field }) => (
<FormItem>
<FormLabel>
Custom Host Header
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The host header to set when
proxying requests. Leave
empty to use the default.
</FormDescription>
<FormMessage />
</FormItem>
<SettingsSectionGrid cols={2}>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
Secure Connection Configuration
</SettingsSectionTitle>
<SettingsSectionDescription>
Configure SSL/TLS settings for your resource
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...tlsSettingsForm}>
<form
onSubmit={tlsSettingsForm.handleSubmit(
saveTlsSettings
)}
/>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
type="submit"
loading={proxySettingsLoading}
form="proxy-settings-form"
>
Save Proxy Settings
</Button>
</SettingsSectionFooter>
</SettingsSection>
className="space-y-4"
id="tls-settings-form"
>
<FormField
control={tlsSettingsForm.control}
name="ssl"
render={({ field }) => (
<FormItem>
<FormControl>
<SwitchInput
id="ssl-toggle"
label="Enable SSL (https)"
defaultChecked={
field.value
}
onCheckedChange={(
val
) => {
field.onChange(
val
);
}}
/>
</FormControl>
</FormItem>
)}
/>
<Collapsible
open={isAdvancedOpen}
onOpenChange={setIsAdvancedOpen}
className="space-y-2"
>
<div className="flex items-center justify-between space-x-4">
<CollapsibleTrigger asChild>
<Button
variant="text"
size="sm"
className="p-0 flex items-center justify-start gap-2 w-full"
>
<p className="text-sm text-muted-foreground">
Advanced TLS
Settings
</p>
<div>
<ChevronsUpDown className="h-4 w-4" />
<span className="sr-only">
Toggle
</span>
</div>
</Button>
</CollapsibleTrigger>
</div>
<CollapsibleContent className="space-y-2">
<FormField
control={
tlsSettingsForm.control
}
name="tlsServerName"
render={({ field }) => (
<FormItem>
<FormLabel>
TLS Server Name
(SNI)
</FormLabel>
<FormControl>
<Input
{...field}
/>
</FormControl>
<FormDescription>
The TLS Server
Name to use for
SNI. Leave empty
to use the
default.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</CollapsibleContent>
</Collapsible>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
type="submit"
loading={httpsTlsLoading}
form="tls-settings-form"
>
Save Settings
</Button>
</SettingsSectionFooter>
</SettingsSection>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
Additional Proxy Settings
</SettingsSectionTitle>
<SettingsSectionDescription>
Configure how your resource handles proxy
settings
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SettingsSectionForm>
<Form {...proxySettingsForm}>
<form
onSubmit={proxySettingsForm.handleSubmit(
saveProxySettings
)}
className="space-y-4"
id="proxy-settings-form"
>
<FormField
control={proxySettingsForm.control}
name="setHostHeader"
render={({ field }) => (
<FormItem>
<FormLabel>
Custom Host Header
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
The host header to set
when proxying requests.
Leave empty to use the
default.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
type="submit"
loading={proxySettingsLoading}
form="proxy-settings-form"
>
Save Settings
</Button>
</SettingsSectionFooter>
</SettingsSection>
</SettingsSectionGrid>
)}
</SettingsContainer>
);

View File

@@ -32,7 +32,7 @@ import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast";
import { AxiosResponse } from "axios";
import { Resource } from "@server/db/schemas";
import { Resource } from "@server/db";
import { StrategySelect } from "@app/components/StrategySelect";
import {
Select,

View File

@@ -269,7 +269,7 @@ PersistentKeepalive = 5`
- NEWT_ID=${siteDefaults?.newtId}
- NEWT_SECRET=${siteDefaults?.newtSecret}`;
const newtConfigDockerRun = `docker run -it fosrl/newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${env.app.dashboardUrl}`;
const newtConfigDockerRun = `docker run -dit fosrl/newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${env.app.dashboardUrl}`;
return loadingPage ? (
<LoaderPlaceholder height="300px" />

View File

@@ -27,6 +27,7 @@ import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import CreateSiteFormModal from "./CreateSiteModal";
import { parseDataSize } from '@app/lib/dataSize';
export type SiteRow = {
id: number;
@@ -198,7 +199,9 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
}
},
sortingFn: (rowA, rowB) =>
parseDataSize(rowA.original.mbIn) - parseDataSize(rowB.original.mbIn)
},
{
accessorKey: "mbOut",
@@ -214,7 +217,9 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
}
},
sortingFn: (rowA, rowB) =>
parseDataSize(rowA.original.mbOut) - parseDataSize(rowB.original.mbOut),
},
{
accessorKey: "type",

View File

@@ -31,9 +31,13 @@ import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useState } from "react";
import { SwitchInput } from "@app/components/SwitchInput";
import Link from "next/link";
import { ArrowRight, ExternalLink } from "lucide-react";
const GeneralFormSchema = z.object({
name: z.string().nonempty("Name is required")
name: z.string().nonempty("Name is required"),
dockerSocketEnabled: z.boolean().optional()
});
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
@@ -50,7 +54,8 @@ export default function GeneralPage() {
const form = useForm<GeneralFormValues>({
resolver: zodResolver(GeneralFormSchema),
defaultValues: {
name: site?.name
name: site?.name,
dockerSocketEnabled: site?.dockerSocketEnabled ?? false
},
mode: "onChange"
});
@@ -60,7 +65,8 @@ export default function GeneralPage() {
await api
.post(`/site/${site?.siteId}`, {
name: data.name
name: data.name,
dockerSocketEnabled: data.dockerSocketEnabled
})
.catch((e) => {
toast({
@@ -73,7 +79,10 @@ export default function GeneralPage() {
});
});
updateSite({ name: data.name });
updateSite({
name: data.name,
dockerSocketEnabled: data.dockerSocketEnabled
});
toast({
title: "Site updated",
@@ -102,7 +111,7 @@ export default function GeneralPage() {
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
className="space-y-6"
id="general-settings-form"
>
<FormField
@@ -122,6 +131,45 @@ export default function GeneralPage() {
</FormItem>
)}
/>
{site && site.type === "newt" && (
<FormField
control={form.control}
name="dockerSocketEnabled"
render={({ field }) => (
<FormItem>
<FormControl>
<SwitchInput
id="docker-socket-enabled"
label="Enable Docker Socket"
defaultChecked={
field.value
}
onCheckedChange={
field.onChange
}
/>
</FormControl>
<FormMessage />
<FormDescription>
Enable Docker Socket
discovery for populating
container information.
Socket path must be provided
to Newt.{" "}
<a
href="https://docs.fossorial.io/Newt/overview#docker-socket-integration"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline inline-flex items-center"
>
Learn more
<ExternalLink className="ml-1 h-4 w-4" />
</a>
</FormDescription>
</FormItem>
)}
/>
)}
</form>
</Form>
</SettingsSectionForm>

View File

@@ -258,7 +258,7 @@ PersistentKeepalive = 5`;
- NEWT_SECRET=${secret}`
],
"Docker Run": [
`docker run -it fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}`
`docker run -dit fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}`
]
},
podman: {
@@ -281,7 +281,7 @@ Restart=always
WantedBy=default.target`
],
"Podman Run": [
`podman run -it docker.io/fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}`
`podman run -dit docker.io/fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}`
]
}
};

View File

@@ -395,7 +395,7 @@ export default function PoliciesPage() {
</FormControl>
<FormDescription>
This expression must return
thr org ID or true for the
the org ID or true for the
user to be allowed to access
the organization.
</FormDescription>

View File

@@ -289,17 +289,17 @@ export default function LicensePage() {
terms corresponding to the
tier associated with your
license key.
<br />
<Link
href="https://fossorial.io/license.html"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
View Fossorial
Commercial License &
Subscription Terms
</Link>
{/* <br /> */}
{/* <Link */}
{/* href="https://fossorial.io/license.html" */}
{/* target="_blank" */}
{/* rel="noopener noreferrer" */}
{/* className="text-primary hover:underline" */}
{/* > */}
{/* View Fossorial */}
{/* Commercial License & */}
{/* Subscription Terms */}
{/* </Link> */}
</FormLabel>
<FormMessage />
</div>
@@ -503,32 +503,32 @@ export default function LicensePage() {
</div>
)}
</div>
<SettingsSectionFooter>
{!licenseStatus?.isHostLicensed ? (
<>
<Button
onClick={() => {
setPurchaseMode("license");
setIsPurchaseModalOpen(true);
}}
>
Purchase License
</Button>
</>
) : (
<>
<Button
variant="outline"
onClick={() => {
setPurchaseMode("additional-sites");
setIsPurchaseModalOpen(true);
}}
>
Purchase Additional Sites
</Button>
</>
)}
</SettingsSectionFooter>
{/* <SettingsSectionFooter> */}
{/* {!licenseStatus?.isHostLicensed ? ( */}
{/* <> */}
{/* <Button */}
{/* onClick={() => { */}
{/* setPurchaseMode("license"); */}
{/* setIsPurchaseModalOpen(true); */}
{/* }} */}
{/* > */}
{/* Purchase License */}
{/* </Button> */}
{/* </> */}
{/* ) : ( */}
{/* <> */}
{/* <Button */}
{/* variant="outline" */}
{/* onClick={() => { */}
{/* setPurchaseMode("additional-sites"); */}
{/* setIsPurchaseModalOpen(true); */}
{/* }} */}
{/* > */}
{/* Purchase Additional Sites */}
{/* </Button> */}
{/* </> */}
{/* )} */}
{/* </SettingsSectionFooter> */}
</SettingsSection>
</SettingsSectionGrid>
<LicenseKeysDataTable

View File

@@ -1,8 +1,11 @@
import { cookies } from "next/headers";
import ValidateOidcToken from "./ValidateOidcToken";
import { idp } from "@server/db/schemas";
import db from "@server/db";
import { eq } from "drizzle-orm";
import { cache } from "react";
import { priv } from "@app/lib/api";
import { AxiosResponse } from "axios";
import { GetIdpResponse } from "@server/routers/idp";
export const dynamic = "force-dynamic";
export default async function Page(props: {
params: Promise<{ orgId: string; idpId: string }>;
@@ -17,13 +20,14 @@ export default async function Page(props: {
const allCookies = await cookies();
const stateCookie = allCookies.get("p_oidc_state")?.value;
// query db directly in server component because just need the name
const [idpRes] = await db
.select({ name: idp.name })
.from(idp)
.where(eq(idp.idpId, parseInt(params.idpId!)));
if (!idpRes) {
const idpRes = await cache(
async () => await priv.get<AxiosResponse<GetIdpResponse>>(`/idp/${params.idpId}`)
)();
const foundIdp = idpRes.data?.data?.idp;
if (!foundIdp) {
return <div>IdP not found</div>;
}
@@ -35,7 +39,7 @@ export default async function Page(props: {
code={searchParams.code}
expectedState={searchParams.state}
stateCookie={stateCookie}
idp={{ name: idpRes.name }}
idp={{ name: foundIdp.name }}
/>
</>
);

View File

@@ -45,8 +45,8 @@ export default function DashboardLoginForm({
<Image
src={`/logo/pangolin_orange.svg`}
alt="Pangolin Logo"
width="100"
height="100"
width={100}
height={100}
/>
</div>
<div className="text-center space-y-1">

View File

@@ -6,9 +6,11 @@ import DashboardLoginForm from "./DashboardLoginForm";
import { Mail } from "lucide-react";
import { pullEnv } from "@app/lib/pullEnv";
import { cleanRedirect } from "@app/lib/cleanRedirect";
import db from "@server/db";
import { idp } from "@server/db/schemas";
import { idp } from "@server/db";
import { LoginFormIDP } from "@app/components/LoginForm";
import { priv } from "@app/lib/api";
import { AxiosResponse } from "axios";
import { ListIdpsResponse } from "@server/routers/idp";
export const dynamic = "force-dynamic";
@@ -34,8 +36,10 @@ export default async function Page(props: {
redirectUrl = cleanRedirect(searchParams.redirect as string);
}
const idps = await db.select().from(idp);
const loginIdps = idps.map((idp) => ({
const idpsRes = await cache(
async () => await priv.get<AxiosResponse<ListIdpsResponse>>("/idp")
)();
const loginIdps = idpsRes.data.data.idps.map((idp) => ({
idpId: idp.idpId,
name: idp.name
})) as LoginFormIDP[];

View File

@@ -14,8 +14,9 @@ import ResourceAccessDenied from "./ResourceAccessDenied";
import AccessToken from "./AccessToken";
import { pullEnv } from "@app/lib/pullEnv";
import { LoginFormIDP } from "@app/components/LoginForm";
import db from "@server/db";
import { idp } from "@server/db/schemas";
import { ListIdpsResponse } from "@server/routers/idp";
export const dynamic = "force-dynamic";
export default async function ResourceAuthPage(props: {
params: Promise<{ resourceId: number }>;
@@ -130,8 +131,10 @@ export default async function ResourceAuthPage(props: {
);
}
const idps = await db.select().from(idp);
const loginIdps = idps.map((idp) => ({
const idpsRes = await cache(
async () => await priv.get<AxiosResponse<ListIdpsResponse>>("/idp")
)();
const loginIdps = idpsRes.data.data.idps.map((idp) => ({
idpId: idp.idpId,
name: idp.name
})) as LoginFormIDP[];

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -101,7 +101,14 @@ export default function StepperForm() {
);
const generateId = (name: string) => {
return name.toLowerCase().replace(/\s+/g, "-");
// Replace any character that is not a letter, number, space, or hyphen with a hyphen
// Also collapse multiple hyphens and trim
return name
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, "-")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.replace(/^-+|-+$/g, "");
};
async function orgSubmit(values: z.infer<typeof orgSchema>) {
@@ -227,23 +234,22 @@ export default function StepperForm() {
type="text"
{...field}
onChange={(e) => {
const orgId =
generateId(
e.target
.value
);
// Prevent "/" in orgName input
const sanitizedValue = e.target.value.replace(/\//g, "-");
const orgId = generateId(sanitizedValue);
orgForm.setValue(
"orgId",
orgId
);
orgForm.setValue(
"orgName",
e.target.value
sanitizedValue
);
debouncedCheckOrgIdAvailability(
orgId
);
}}
value={field.value.replace(/\//g, "-")}
/>
</FormControl>
<FormMessage />