mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-05-15 14:38:03 +02:00
224 lines
6.4 KiB
TypeScript
224 lines
6.4 KiB
TypeScript
import { createClient } from "redis";
|
|
import * as passport from "passport";
|
|
import * as session from "express-session";
|
|
import RedisStore from "connect-redis";
|
|
import * as OAuth2Strategy from "passport-oauth2";
|
|
import { Profile, Strategy } from "passport-github2";
|
|
import * as express from "express";
|
|
|
|
import config from "../../config";
|
|
import UserModel from "../../core/model/users/users.model";
|
|
import { IUserDocument } from "../../core/model/users/users.types";
|
|
import AnonymousError from "../../core/AnonymousError";
|
|
import AnonymizedPullRequestModel from "../../core/model/anonymizedPullRequests/anonymizedPullRequests.model";
|
|
import { hashToken } from "./token-auth";
|
|
import { createLogger, serializeError } from "../../core/logger";
|
|
|
|
const logger = createLogger("auth");
|
|
|
|
export function ensureAuthenticated(
|
|
req: express.Request,
|
|
res: express.Response,
|
|
next: express.NextFunction
|
|
) {
|
|
if (req.isAuthenticated()) {
|
|
return next();
|
|
}
|
|
res.status(401).json({ error: "not_connected" });
|
|
}
|
|
|
|
const verify = async (
|
|
accessToken: string,
|
|
refreshToken: string,
|
|
profile: Profile,
|
|
done: OAuth2Strategy.VerifyCallback
|
|
): Promise<void> => {
|
|
let user: IUserDocument | null;
|
|
try {
|
|
const now = new Date();
|
|
user = await UserModel.findOne({ "externalIDs.github": profile.id });
|
|
if (user) {
|
|
await UserModel.updateOne(
|
|
{ _id: user._id },
|
|
{
|
|
$set: {
|
|
"accessTokens.github": accessToken,
|
|
"accessTokenDates.github": now,
|
|
},
|
|
}
|
|
);
|
|
await AnonymizedPullRequestModel.updateMany(
|
|
{ owner: user._id },
|
|
{ "source.accessToken": accessToken }
|
|
);
|
|
user = await UserModel.findById(user._id);
|
|
} else {
|
|
// Check if a user with this username already exists (e.g. created
|
|
// manually without externalIDs.github). Link the GitHub ID to the
|
|
// existing account instead of creating a duplicate that would lose
|
|
// the isAdmin flag.
|
|
user = await UserModel.findOne({ username: profile.username });
|
|
if (user) {
|
|
await UserModel.updateOne(
|
|
{ _id: user._id },
|
|
{
|
|
$set: {
|
|
"externalIDs.github": profile.id,
|
|
"accessTokens.github": accessToken,
|
|
"accessTokenDates.github": now,
|
|
},
|
|
}
|
|
);
|
|
user = await UserModel.findById(user._id);
|
|
} else {
|
|
const photo = profile.photos ? profile.photos[0]?.value : null;
|
|
user = new UserModel({
|
|
username: profile.username,
|
|
accessTokens: {
|
|
github: accessToken,
|
|
},
|
|
accessTokenDates: {
|
|
github: now,
|
|
},
|
|
externalIDs: {
|
|
github: profile.id,
|
|
},
|
|
emails: profile.emails?.map((email) => {
|
|
return { email: email.value, default: false };
|
|
}),
|
|
photo,
|
|
});
|
|
if (user.emails?.length) user.emails[0].default = true;
|
|
await user.save();
|
|
}
|
|
}
|
|
done(null, {
|
|
username: profile.username,
|
|
accessToken,
|
|
refreshToken,
|
|
profile,
|
|
user,
|
|
});
|
|
} catch (error) {
|
|
logger.error("verify failed", serializeError(error));
|
|
done(
|
|
new AnonymousError("unable_to_connect_user", {
|
|
httpStatus: 500,
|
|
object: profile,
|
|
cause: error as Error,
|
|
})
|
|
);
|
|
}
|
|
};
|
|
|
|
passport.use(
|
|
new Strategy(
|
|
{
|
|
clientID: config.CLIENT_ID,
|
|
clientSecret: config.CLIENT_SECRET,
|
|
callbackURL: config.AUTH_CALLBACK,
|
|
},
|
|
verify
|
|
)
|
|
);
|
|
|
|
passport.serializeUser((user: Express.User, done) => {
|
|
done(null, user);
|
|
});
|
|
|
|
passport.deserializeUser((user: Express.User, done) => {
|
|
done(null, user);
|
|
});
|
|
|
|
export function initSession() {
|
|
const redisClient = createClient({
|
|
legacyMode: false,
|
|
socket: {
|
|
port: config.REDIS_PORT,
|
|
host: config.REDIS_HOSTNAME,
|
|
},
|
|
});
|
|
redisClient.on("error", (err) =>
|
|
logger.error("redis client error", serializeError(err))
|
|
);
|
|
redisClient.connect();
|
|
const redisStore = new RedisStore({
|
|
client: redisClient,
|
|
prefix: "anoGH_session:",
|
|
});
|
|
|
|
return session({
|
|
secret: config.SESSION_SECRET,
|
|
store: redisStore,
|
|
saveUninitialized: false,
|
|
resave: false,
|
|
});
|
|
}
|
|
|
|
export const router = express.Router();
|
|
|
|
router.get(
|
|
"/login",
|
|
passport.authenticate("github", { scope: ["repo"] }), // Note the scope here
|
|
function (req: express.Request, res: express.Response) {
|
|
res.redirect("/");
|
|
}
|
|
);
|
|
|
|
router.get(
|
|
"/auth",
|
|
passport.authenticate("github", { failureRedirect: "/" }),
|
|
function (req: express.Request, res: express.Response) {
|
|
res.redirect("/");
|
|
}
|
|
);
|
|
|
|
// Dev-friendly login: accept an admin API token and establish a session
|
|
// cookie so the web UI is reachable without going through GitHub OAuth.
|
|
// Token may come from `Authorization: Bearer …`, `?token=…`, or JSON body.
|
|
router.all(
|
|
"/login-token",
|
|
async function (req: express.Request, res: express.Response) {
|
|
const fromHeader = (() => {
|
|
const h = req.headers["authorization"];
|
|
if (typeof h !== "string") return null;
|
|
const m = h.match(/^Bearer\s+(.+)$/i);
|
|
return m ? m[1].trim() : null;
|
|
})();
|
|
const token =
|
|
fromHeader ||
|
|
(typeof req.query.token === "string" ? req.query.token : null) ||
|
|
(req.body && typeof req.body.token === "string" ? req.body.token : null);
|
|
if (!token) {
|
|
return res.status(400).json({ error: "missing_token" });
|
|
}
|
|
try {
|
|
const model = await UserModel.findOne({
|
|
"apiTokens.tokenHash": hashToken(token),
|
|
});
|
|
if (!model) return res.status(401).json({ error: "invalid_token" });
|
|
const synthUser = {
|
|
username: model.username,
|
|
accessToken: model.accessTokens?.github,
|
|
profile: undefined,
|
|
user: model,
|
|
};
|
|
req.login(synthUser, (err) => {
|
|
if (err) {
|
|
logger.error("login-token req.login failed", serializeError(err));
|
|
return res.status(500).json({ error: "login_failed" });
|
|
}
|
|
UserModel.updateOne(
|
|
{ _id: model._id, "apiTokens.tokenHash": hashToken(token) },
|
|
{ $set: { "apiTokens.$.lastUsedAt": new Date() } }
|
|
).catch(() => undefined);
|
|
if (req.method === "GET") return res.redirect("/");
|
|
return res.json({ ok: true, username: model.username });
|
|
});
|
|
} catch (err) {
|
|
logger.error("login-token failed", serializeError(err));
|
|
res.status(500).json({ error: "server_error" });
|
|
}
|
|
}
|
|
);
|