refactor(integrations): start and end every OAuth connection through the kit

Minting the state, building the consent URL and dropping a grant each had a
copy per provider route. The routes keep their auth, rate limits and
redirects and hand the rest to one implementation, which the managed-profile
routes reuse.
This commit is contained in:
Marc-André Bombeck committed 2026-10-21 19:49:06 +02:00
1 parent b60fa0123b
commit 65e0cba0bf
14 files changed
+483 -344

No files matched your search

+11 -23
View File
@@ -1,14 +1,10 @@
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { shouldEmitSecureCookie } from "@/lib/auth/secure-cookie";
import { checkRateLimit } from "@/lib/rate-limit";
import { getAuthorizationUrl } from "@/lib/strava/client";
import { getStravaClientCredentials } from "@/lib/strava/credentials";
import {
OAUTH_STATE_TTL_MS,
mintSignedState,
oauthStateCookieName,
} from "@/lib/oauth/signed-state";
setConnectStateCookie,
startOAuthConnect,
} from "@/lib/integrations/oauth-kit/connect-start";
import { annotate } from "@/lib/logging/context";
import { checkRateLimit } from "@/lib/rate-limit";
import { NextResponse } from "next/server";
/**
@@ -45,8 +41,8 @@ export const GET = apiHandler(async () => {
);
}
const creds = await getStravaClientCredentials(user.id);
if (!creds) {
const started = await startOAuthConnect("strava", user.id);
if (!started.ok) {
annotate({ action: { name: "strava.connect.no_credentials" } });
return NextResponse.redirect(
new URL(
@@ -56,16 +52,8 @@ export const GET = apiHandler(async () => {
);
}
const state = mintSignedState("strava", user.id);
const url = getAuthorizationUrl(state, creds);
const response = NextResponse.redirect(url);
response.cookies.set(oauthStateCookieName("strava"), state, {
httpOnly: true,
secure: shouldEmitSecureCookie(),
sameSite: "lax",
maxAge: Math.floor(OAUTH_STATE_TTL_MS / 1000),
path: "/",
});
return response;
return setConnectStateCookie(
NextResponse.redirect(started.authorizationUrl),
started.cookie,
);
});
+3 -27
View File
@@ -1,15 +1,8 @@
import { prisma } from "@/lib/db";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { auditLog } from "@/lib/auth/audit";
import { disconnectOAuth } from "@/lib/integrations/oauth-kit/disconnect";
import { apiSuccess, apiError } from "@/lib/api-response";
import { annotate, getEvent } from "@/lib/logging/context";
import { annotate } from "@/lib/logging/context";
import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
import { markDisconnected } from "@/lib/integrations/status";
import { deauthorize } from "@/lib/strava/client";
import {
getStravaConnection,
STRAVA_DISCONNECTED,
} from "@/lib/strava/credentials";
/**
* v1.28.x — disconnect Strava for the current user.
@@ -30,26 +23,9 @@ export const POST = apiHandler(async () => {
});
}
const connection = await getStravaConnection(user.id);
if (!connection) {
if ((await disconnectOAuth("strava", user.id)) === "not_connected") {
return apiError("No Strava connection", 404);
}
// Best-effort revoke at Strava; a Strava-side outage must never block the
// local disconnect.
try {
await deauthorize(connection.accessToken);
} catch (err) {
getEvent()?.addWarning(`strava: deauthorize failed on disconnect: ${err}`);
}
await prisma.user.update({
where: { id: user.id },
data: STRAVA_DISCONNECTED,
});
await auditLog("strava.disconnect", { userId: user.id });
await markDisconnected(user.id, "strava");
return apiSuccess({ disconnected: true });
});