Skip to content
api.ts 3.04 KiB
Newer Older
Grant's avatar
Grant committed
import { Router } from "express";
import { prisma } from "./lib/prisma";
Grant's avatar
Grant committed
import { OpenID } from "./lib/oidc";
import { TokenSet, errors as OIDC_Errors } from "openid-client";
import { Logger } from "./lib/Logger";
Grant's avatar
Grant committed

const app = Router();

app.get("/me", (req, res) => {
  res.json(req.session);
});

Grant's avatar
Grant committed
app.get("/login", (req, res) => {
Grant's avatar
Grant committed
  res.redirect(
    OpenID.client.authorizationUrl({
      prompt: "consent",
      scope: "openid instance",
    })
  );
Grant's avatar
Grant committed
});

Grant's avatar
Grant committed
// TODO: logout endpoint
Grant's avatar
Grant committed

Grant's avatar
Grant committed
app.get("/callback", async (req, res) => {
  // TODO: return proper UIs for errors intead of raw JSON (#35)
Grant's avatar
Grant committed
  // const { code } = req.query;
Grant's avatar
Grant committed

  let exchange: TokenSet;

  try {
    const params = OpenID.client.callbackParams(req);
    exchange = await OpenID.client.callback(OpenID.getRedirectUrl(), params);
  } catch (e) {
    if (e instanceof OIDC_Errors.RPError) {
      // client error

      res.status(400).json({
        success: false,
        error: e.name,
        error_description: e.message,
      });
      return;
    }

    if (e instanceof OIDC_Errors.OPError) {
      // server error

      switch (e.error) {
        case "invalid_client":
          // this happens when we're configured with invalid credentials
          Logger.error(
            "OpenID is improperly configured. Cannot exchange tokens, do I have valid credetials?"
          );
          res.status(500).json({
            success: false,
            error: "internal server error",
            error_description: "I'm misconfigured.",
          });
          return;
        case "invalid_grant":
          res.status(400).json({
            success: false,
            error: "invalid_grant",
            error_description: "retry /api/login",
          });
          return;
      }

      res.status(400).json({
        success: false,
        error: e.error,
        error_description: e.error_description,
      });
      return;
    }

    res.status(500).json({
      success: false,
      error: "unknown error",
      error_description: "report this",
    });
    return;
  }

Grant's avatar
Grant committed
  if (!exchange || !exchange.access_token) {
    return res.status(400).json({
      success: false,
      error: "FAILED TOKEN EXCHANGE",
Grant's avatar
Grant committed
  const whoami = await OpenID.client.userinfo<{
    instance: {
      software: {
        name: string;
        version: string;
        logo_uri?: string;
        repository?: string;
        homepage?: string;
      };
      instance: {
        logo_uri?: string;
        banner_uri?: string;
        name?: string;
      };
    };
  }>(exchange.access_token);

  const [username, hostname] = whoami.sub.split("@");
Grant's avatar
Grant committed

  await prisma.user.upsert({
    where: {
      sub: [username, hostname].join("@"),
    },
    update: {},
    create: {
      sub: [username, hostname].join("@"),
    },
  });

  req.session.user = {
    service: {
Grant's avatar
Grant committed
      ...whoami.instance,
Grant's avatar
Grant committed
      instance: {
Grant's avatar
Grant committed
        ...whoami.instance.instance,
Grant's avatar
Grant committed
        hostname,
      },
    },
    user: {
Grant's avatar
Grant committed
      picture_url: whoami.picture,
Grant's avatar
Grant committed
      username,
    },
  };
  req.session.save();
  res.redirect("/");
});

export default app;