oauth.ts 2.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
  1. import { COOKIE_NAME, ONE_YEAR_MS } from "@shared/const";
  2. import type { Express, Request, Response } from "express";
  3. import * as db from "../db";
  4. import { getSessionCookieOptions } from "./cookies";
  5. import { sdk } from "./sdk";
  6. function getQueryParam(req: Request, key: string): string | undefined {
  7. const value = req.query[key];
  8. return typeof value === "string" ? value : undefined;
  9. }
  10. export function registerOAuthRoutes(app: Express) {
  11. app.get("/api/oauth/callback", async (req: Request, res: Response) => {
  12. const code = getQueryParam(req, "code");
  13. const state = getQueryParam(req, "state");
  14. if (!code || !state) {
  15. res.status(400).json({ error: "code and state are required" });
  16. return;
  17. }
  18. try {
  19. const tokenResponse = await sdk.exchangeCodeForToken(code, state);
  20. const userInfo = await sdk.getUserInfo(tokenResponse.accessToken);
  21. if (!userInfo.openId) {
  22. res.status(400).json({ error: "openId missing from user info" });
  23. return;
  24. }
  25. await db.upsertUser({
  26. openId: userInfo.openId,
  27. name: userInfo.name || null,
  28. email: userInfo.email ?? null,
  29. loginMethod: userInfo.loginMethod ?? userInfo.platform ?? null,
  30. lastSignedIn: new Date(),
  31. });
  32. const sessionToken = await sdk.createSessionToken(userInfo.openId, {
  33. name: userInfo.name || "",
  34. expiresInMs: ONE_YEAR_MS,
  35. });
  36. const cookieOptions = getSessionCookieOptions(req);
  37. res.cookie(COOKIE_NAME, sessionToken, { ...cookieOptions, maxAge: ONE_YEAR_MS });
  38. // Parse state to check for returnPath
  39. let redirectTo = "/";
  40. try {
  41. const decoded = Buffer.from(state, "base64").toString("utf-8");
  42. const parsed = JSON.parse(decoded);
  43. if (parsed.returnPath && typeof parsed.returnPath === "string" && parsed.returnPath.startsWith("/")) {
  44. redirectTo = parsed.returnPath;
  45. }
  46. } catch {
  47. // state is just the redirectUri string, redirect to /
  48. }
  49. res.redirect(302, redirectTo);
  50. } catch (error) {
  51. console.error("[OAuth] Callback failed", error);
  52. res.status(500).json({ error: "OAuth callback failed" });
  53. }
  54. });
  55. }