Compare commits

...

2 Commits

Author SHA1 Message Date
miloschwartz
b1bcdadf80 fix invite flow 2025-10-07 21:15:14 -07:00
Owen
dcb4ae71b8 Add postgres pool info to config 2025-10-07 15:11:10 -07:00
5 changed files with 146 additions and 108 deletions

View File

@@ -1725,5 +1725,6 @@
"healthCheckNotAvailable": "Local",
"rewritePath": "Rewrite Path",
"rewritePathDescription": "Optionally rewrite the path before forwarding to the target.",
"continueToApplication": "Continue to application"
"continueToApplication": "Continue to application",
"checkingInvite": "Checking Invite"
}

View File

@@ -35,11 +35,12 @@ function createDb() {
}
// Create connection pools instead of individual connections
const poolConfig = config.postgres.pool;
const primaryPool = new Pool({
connectionString,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
max: poolConfig.max_connections,
idleTimeoutMillis: poolConfig.idle_timeout_ms,
connectionTimeoutMillis: poolConfig.connection_timeout_ms,
});
const replicas = [];
@@ -50,9 +51,9 @@ function createDb() {
for (const conn of replicaConnections) {
const replicaPool = new Pool({
connectionString: conn.connection_string,
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
max: poolConfig.max_replica_connections,
idleTimeoutMillis: poolConfig.idle_timeout_ms,
connectionTimeoutMillis: poolConfig.connection_timeout_ms,
});
replicas.push(DrizzlePostgres(replicaPool));
}

View File

@@ -158,7 +158,21 @@ export const configSchema = z
connection_string: z.string()
})
)
.optional(),
pool: z
.object({
max_connections: z.number().positive().optional().default(20),
max_replica_connections: z.number().positive().optional().default(10),
idle_timeout_ms: z.number().positive().optional().default(30000),
connection_timeout_ms: z.number().positive().optional().default(5000)
})
.optional()
.default({
max_connections: 20,
max_replica_connections: 10,
idle_timeout_ms: 30000,
connection_timeout_ms: 5000
})
})
.optional(),
traefik: z

View File

@@ -1,11 +1,6 @@
import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies";
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 "../../components/InviteStatusCard";
import { formatAxiosError } from "@app/lib/api";
import { getTranslations } from "next-intl/server";
export default async function InvitePage(props: {
@@ -27,8 +22,8 @@ export default async function InvitePage(props: {
if (parts.length !== 2) {
return (
<>
<h1>{t('inviteInvalid')}</h1>
<p>{t('inviteInvalidDescription')}</p>
<h1>{t("inviteInvalid")}</h1>
<p>{t("inviteInvalidDescription")}</p>
</>
);
}
@@ -36,58 +31,15 @@ export default async function InvitePage(props: {
const inviteId = parts[0];
const token = parts[1];
let error = "";
const res = await internal
.post<AxiosResponse<AcceptInviteResponse>>(
`/invite/accept`,
{
inviteId,
token,
},
await authCookieHeader()
)
.catch((e) => {
error = formatAxiosError(e);
console.error(error);
});
if (res && res.status === 200) {
redirect(`/${res.data.data.orgId}`);
}
function cardType() {
if (error.includes("Invite is not for this user")) {
return "wrong_user";
} else if (
error.includes("User does not exist. Please create an account first.")
) {
return "user_does_not_exist";
} else if (error.includes("You must be logged in to accept an invite")) {
return "not_logged_in";
} else {
return "rejected";
}
}
const type = cardType();
if (!user && type === "user_does_not_exist") {
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
? `/auth/login?redirect=/invite?token=${params.token}&email=${encodeURIComponent(emailParam)}`
: `/auth/login?redirect=/invite?token=${params.token}`;
redirect(redirectUrl);
}
return (
<>
<InviteStatusCard type={type} token={tokenParam} email={emailParam} />
<InviteStatusCard
tokenParam={tokenParam}
inviteToken={token}
inviteId={inviteId}
user={user}
email={emailParam}
/>
</>
);
}

View File

@@ -1,47 +1,119 @@
"use client";
import { createApiClient } from "@app/lib/api";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { Button } from "@app/components/ui/button";
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
CardTitle
} from "@app/components/ui/card";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { XCircle } from "lucide-react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import { AxiosResponse } from "axios";
import { AcceptInviteResponse, GetUserResponse } from "@server/routers/user";
import { Loader2 } from "lucide-react";
type InviteStatusCardProps = {
type: "rejected" | "wrong_user" | "user_does_not_exist" | "not_logged_in";
token: string;
user: GetUserResponse | null;
tokenParam: string;
inviteId: string;
inviteToken: string;
email?: string;
};
export default function InviteStatusCard({
type,
token,
inviteId,
email,
user,
tokenParam,
inviteToken
}: InviteStatusCardProps) {
const router = useRouter();
const api = createApiClient(useEnvContext());
const t = useTranslations();
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [type, setType] = useState<
"rejected" | "wrong_user" | "user_does_not_exist" | "not_logged_in"
>("rejected");
useEffect(() => {
async function init() {
let error = "";
const res = await api
.post<AxiosResponse<AcceptInviteResponse>>(`/invite/accept`, {
inviteId,
token: inviteToken
})
.catch((e) => {
error = formatAxiosError(e);
console.log("Error accepting invite:", error);
setError(error);
// console.error(e);
});
if (res && res.status === 200) {
router.push(`/${res.data.data.orgId}`);
return;
}
function cardType() {
if (error.includes("Invite is not for this user")) {
return "wrong_user";
} else if (
error.includes(
"User does not exist. Please create an account first."
)
) {
return "user_does_not_exist";
} else if (
error.includes("You must be logged in to accept an invite")
) {
return "not_logged_in";
} else {
return "rejected";
}
}
const type = cardType();
setType(type);
if (!user && type === "user_does_not_exist") {
const redirectUrl = email
? `/auth/signup?redirect=/invite?token=${tokenParam}&email=${encodeURIComponent(email)}`
: `/auth/signup?redirect=/invite?token=${tokenParam}`;
router.push(redirectUrl);
} else if (!user && type === "not_logged_in") {
const redirectUrl = email
? `/auth/login?redirect=/invite?token=${tokenParam}&email=${encodeURIComponent(email)}`
: `/auth/login?redirect=/invite?token=${tokenParam}`;
router.push(redirectUrl);
} else {
setLoading(false);
}
}
init();
}, []);
async function goToLogin() {
await api.post("/auth/logout", {});
const redirectUrl = email
? `/auth/login?redirect=/invite?token=${token}&email=${encodeURIComponent(email)}`
: `/auth/login?redirect=/invite?token=${token}`;
const redirectUrl = email
? `/auth/login?redirect=/invite?token=${tokenParam}&email=${encodeURIComponent(email)}`
: `/auth/login?redirect=/invite?token=${tokenParam}`;
router.push(redirectUrl);
}
async function goToSignup() {
await api.post("/auth/logout", {});
const redirectUrl = email
? `/auth/signup?redirect=/invite?token=${token}&email=${encodeURIComponent(email)}`
: `/auth/signup?redirect=/invite?token=${token}`;
const redirectUrl = email
? `/auth/signup?redirect=/invite?token=${tokenParam}&email=${encodeURIComponent(email)}`
: `/auth/signup?redirect=/invite?token=${tokenParam}`;
router.push(redirectUrl);
}
@@ -50,35 +122,27 @@ export default function InviteStatusCard({
return (
<div>
<p className="text-center mb-4">
{t('inviteErrorNotValid')}
{t("inviteErrorNotValid")}
</p>
<ul className="list-disc list-inside text-sm space-y-2">
<li>{t('inviteErrorExpired')}</li>
<li>{t('inviteErrorRevoked')}</li>
<li>{t('inviteErrorTypo')}</li>
<li>{t("inviteErrorExpired")}</li>
<li>{t("inviteErrorRevoked")}</li>
<li>{t("inviteErrorTypo")}</li>
</ul>
</div>
);
} else if (type === "wrong_user") {
return (
<div>
<p className="text-center mb-4">
{t('inviteErrorUser')}
</p>
<p className="text-center">
{t('inviteLoginUser')}
</p>
<p className="text-center mb-4">{t("inviteErrorUser")}</p>
<p className="text-center">{t("inviteLoginUser")}</p>
</div>
);
} else if (type === "user_does_not_exist") {
return (
<div>
<p className="text-center mb-4">
{t('inviteErrorNoUser')}
</p>
<p className="text-center">
{t('inviteCreateUser')}
</p>
<p className="text-center mb-4">{t("inviteErrorNoUser")}</p>
<p className="text-center">{t("inviteCreateUser")}</p>
</div>
);
}
@@ -92,37 +156,43 @@ export default function InviteStatusCard({
router.push("/");
}}
>
{t('goHome')}
{t("goHome")}
</Button>
);
} else if (type === "wrong_user") {
return (
<Button onClick={goToLogin}>{t('inviteLogInOtherUser')}</Button>
<Button onClick={goToLogin}>{t("inviteLogInOtherUser")}</Button>
);
} else if (type === "user_does_not_exist") {
return <Button onClick={goToSignup}>{t('createAnAccount')}</Button>;
return <Button onClick={goToSignup}>{t("createAnAccount")}</Button>;
}
}
return (
<div className="p-3 md:mt-32 flex items-center justify-center">
<div className="flex items-center justify-center min-h-screen">
<Card className="w-full max-w-md">
<CardHeader>
{/* <div className="flex items-center justify-center w-20 h-20 rounded-full bg-red-100 mx-auto mb-4">
<XCircle
className="w-10 h-10 text-red-600"
aria-hidden="true"
/>
</div> */}
<CardTitle className="text-center text-2xl font-bold">
{t('inviteNotAccepted')}
{loading ? t("checkingInvite") : t("inviteNotAccepted")}
</CardTitle>
</CardHeader>
<CardContent>{renderBody()}</CardContent>
<CardContent>
{loading && (
<div className="flex flex-col items-center space-y-4">
<div className="flex items-center space-x-2">
<Loader2 className="h-5 w-5 animate-spin" />
<span>{t("loading")}</span>
</div>
</div>
)}
{!loading && renderBody()}
</CardContent>
<CardFooter className="flex justify-center space-x-4">
{renderFooter()}
</CardFooter>
{!loading && (
<CardFooter className="flex justify-center space-x-4">
{renderFooter()}
</CardFooter>
)}
</Card>
</div>
);