trails/apps/journal/app/routes/api.sync.komoot.connect.ts
Ullrich Schäfer 03304c354b
Add Komoot import with public (bio verification) and authenticated modes
Two-mode import: public mode verifies Komoot account ownership by checking
that the user's trails.cool profile URL appears in their Komoot bio — no
credentials stored. Authenticated mode uses email + password (AES-256-GCM
encrypted) to import private tours as well.

Includes unit tests for crypto/komoot client and E2E tests for the full
connect + import flow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 10:18:46 +02:00

48 lines
1.4 KiB
TypeScript

// POST /api/sync/komoot/connect
// Validates Komoot email/password credentials and stores them encrypted.
import { data, redirect } from "react-router";
import type { Route } from "./+types/api.sync.komoot.connect";
import { getSessionUser } from "~/lib/auth/session.server";
import { loginKomoot } from "~/lib/komoot.server";
import { encrypt } from "~/lib/crypto.server";
import { link } from "~/lib/connected-services/manager";
export async function action({ request }: Route.ActionArgs) {
const user = await getSessionUser(request);
if (!user) return redirect("/auth/login");
const body = (await request.json()) as { email?: string; password?: string };
const email = body.email?.trim() ?? "";
const password = body.password ?? "";
if (!email || !password) {
return data({ error: "missing_fields" }, { status: 400 });
}
let username: string;
try {
const result = await loginKomoot(email, password);
username = result.username;
} catch {
return data({ error: "invalid_credentials" }, { status: 401 });
}
const encryptedPassword = encrypt(password);
await link({
userId: user.id,
provider: "komoot",
credentialKind: "web-login",
credentials: {
mode: "authenticated",
email,
encryptedPassword,
komootUserId: username,
},
providerUserId: username,
grantedScopes: [],
});
return data({ success: true });
}