aboutsummaryrefslogtreecommitdiffstats
path: root/packages/trpc
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-11-30 00:01:07 +0000
committerMohamed Bassem <me@mbassem.com>2025-11-30 00:01:07 +0000
commitb12c1c3a82941f2767ade8f497db56933415b94d (patch)
treeb1fa65f111c21bfb996c8b99ebff2d60c11a5876 /packages/trpc
parent4898b6be87c6edec8c74d69317899ce918c550ad (diff)
downloadkarakeep-b12c1c3a82941f2767ade8f497db56933415b94d.tar.zst
feat: add support for turnstile on signup
Diffstat (limited to 'packages/trpc')
-rw-r--r--packages/trpc/lib/turnstile.ts71
-rw-r--r--packages/trpc/routers/users.ts13
2 files changed, 84 insertions, 0 deletions
diff --git a/packages/trpc/lib/turnstile.ts b/packages/trpc/lib/turnstile.ts
new file mode 100644
index 00000000..3ba25f05
--- /dev/null
+++ b/packages/trpc/lib/turnstile.ts
@@ -0,0 +1,71 @@
+import { z } from "zod";
+
+import serverConfig from "@karakeep/shared/config";
+import logger from "@karakeep/shared/logger";
+
+const TurnstileVerifyResponseSchema = z.object({
+ success: z.boolean(),
+ challenge_ts: z.string().optional(),
+ hostname: z.string().optional(),
+ "error-codes": z.array(z.string()).optional(),
+});
+
+export async function verifyTurnstileToken(
+ token: string,
+ remoteIp?: string | null,
+) {
+ if (!serverConfig.auth.turnstile.enabled) {
+ return { success: true };
+ }
+
+ if (!token) {
+ return { success: false, "error-codes": ["missing-input-response"] };
+ }
+
+ const body = new URLSearchParams();
+ body.append("secret", serverConfig.auth.turnstile.secretKey!);
+ body.append("response", token);
+ if (remoteIp) {
+ body.append("remoteip", remoteIp);
+ }
+
+ try {
+ const response = await fetch(
+ "https://challenges.cloudflare.com/turnstile/v0/siteverify",
+ {
+ method: "POST",
+ body,
+ },
+ );
+
+ if (!response.ok) {
+ logger.warn(
+ `[Turnstile] Verification request failed with status ${response.status}`,
+ );
+ return { success: false, "error-codes": ["request-not-ok"] };
+ }
+
+ const json = await response.json();
+ const parseResult = TurnstileVerifyResponseSchema.safeParse(json);
+
+ if (!parseResult.success) {
+ logger.warn("[Turnstile] Invalid response format", {
+ error: parseResult.error,
+ remoteIp,
+ });
+ return { success: false, "error-codes": ["invalid-response"] };
+ }
+
+ const parsed = parseResult.data;
+ if (!parsed.success) {
+ logger.warn("[Turnstile] Verification failed", {
+ errorCodes: parsed["error-codes"],
+ remoteIp,
+ });
+ }
+ return parsed;
+ } catch (error) {
+ logger.warn("[Turnstile] Verification threw", { error, remoteIp });
+ return { success: false, "error-codes": ["internal-error"] };
+ }
+}
diff --git a/packages/trpc/routers/users.ts b/packages/trpc/routers/users.ts
index 5ce9c67e..d3bc06d9 100644
--- a/packages/trpc/routers/users.ts
+++ b/packages/trpc/routers/users.ts
@@ -18,6 +18,7 @@ import {
publicProcedure,
router,
} from "../index";
+import { verifyTurnstileToken } from "../lib/turnstile";
import { User } from "../models/users";
export const usersAppRouter = router({
@@ -51,6 +52,18 @@ export const usersAppRouter = router({
message: errorMessage,
});
}
+ if (serverConfig.auth.turnstile.enabled) {
+ const result = await verifyTurnstileToken(
+ input.turnstileToken ?? "",
+ ctx.req.ip,
+ );
+ if (!result.success) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: "Turnstile verification failed",
+ });
+ }
+ }
const user = await User.create(ctx, input);
return {
id: user.id,