fix: save verified email addresses during GitHub registration

This commit is contained in:
Thomas Durieux committed 2026-10-09 13:24:21 +00:00
1 parent 2d21448a77
commit 444e5ab248
8 files changed
+229 -10

No files matched your search

+41
View File
@@ -0,0 +1,41 @@
import UserModel from "./model/users/users.model";
import { IUserDocument } from "./model/users/users.types";
import { normalizeEmail, notificationEmail } from "./owner-notifications";
function usableEmail(value: unknown): string | null {
const email = normalizeEmail(value);
if (!email) return null;
const domain = email.split("@")[1].toLowerCase();
return ["users.noreply.github.com", "noreply.github.com"].includes(domain) ? null : email;
}
/** Fill a missing address after the GitHub identity has been authenticated. */
export async function saveRegistrationEmail(user: IUserDocument, token: string): Promise<void> {
if (notificationEmail(user.emails, user.notificationEmail)) return;
// Compare the original fields so a concurrent settings save always wins.
const previousEmails = user.toObject().emails;
const previousPreference = user.notificationEmail ?? null;
try {
const response = await fetch("https://api.github.com/user/emails?per_page=100", {
headers: { Accept: "application/vnd.github+json", Authorization: `Bearer ${token}`,
"X-GitHub-Api-Version": "2022-11-28", "User-Agent": "Anonymous-GitHub" },
signal: AbortSignal.timeout(5000),
});
if (!response.ok) return;
const data: unknown = await response.json();
if (!Array.isArray(data)) return;
const verified = data.filter((entry): entry is { email: string; primary?: boolean } =>
!!entry && entry.verified === true && !!usableEmail(entry.email));
const email = usableEmail((verified.find(entry => entry.primary === true) || verified[0])?.email);
if (!email) return;
const emails = [{ email, default: true }];
const result = await UserModel.updateOne({ _id: user._id, notificationEmail: previousPreference,
$or: [{ emails: previousEmails ?? [] },
...(!previousEmails?.length ? [{ emails: { $exists: false } }] : [])],
}, { $set: { emails } });
if (result.modifiedCount) user.emails = emails;
} catch {
// Missing App email permission or an upstream outage must not prevent login.
// The existing email form remains available when automatic collection fails.
}
}
+4 -5
View File
@@ -1,4 +1,5 @@
import { resetOwnerAccessAlerts } from "../../core/owner-notifications";
import { saveRegistrationEmail } from "../../core/registration-email";
import { createClient } from "redis";
import * as passport from "passport";
import * as session from "express-session";
@@ -100,12 +101,9 @@ export const verify = async (
externalIDs: {
github: profile.id,
},
emails: profile.emails?.map((email) => {
return { email: email.value, default: false };
}),
emails: [],
photo,
});
if (user.emails?.length) user.emails[0].default = true;
await user.save();
}
}
@@ -121,6 +119,7 @@ export const verify = async (
return;
}
await setCredential(String(user!._id), accessToken);
await saveRegistrationEmail(user!, accessToken);
await resetOwnerAccessAlerts(String(user!._id));
done(null, { username: user!.username, user });
} catch (error) {
@@ -208,7 +207,7 @@ router.get(
returnTo: safeAuthReturnTo(req.query.returnTo, current ? "/connections" : "/dashboard"), expires: Date.now() + 10 * 60000 };
next();
},
passport.authenticate("github", { scope: ["repo"] }), // Note the scope here
passport.authenticate("github", { scope: ["repo", "user:email"] }),
function (req: express.Request, res: express.Response) {
res.redirect("/");
}
+2
View File
@@ -1,4 +1,5 @@
import { notificationEmail, normalizeEmail, resetOwnerAccessAlerts } from "../../core/owner-notifications";
import { saveRegistrationEmail } from "../../core/registration-email";
import * as express from "express";
import { createHmac, randomBytes, randomUUID, timingSafeEqual } from "crypto";
import config from "../../config";
@@ -94,6 +95,7 @@ router.get("/app/callback", enabled, async (req, res) => {
}
if (isDisabledAccount(user.status)) throw appError("not_connected", 403);
await saveAppGrant(user.id, tokens);
await saveRegistrationEmail(user, tokens.access_token);
await resetOwnerAccessAlerts(user.id);
await new Promise<void>((resolve, reject) => req.login({ username: user!.username, user }, err => err ? reject(err) : resolve()));
res.redirect(flow.install ? `/github/app/install?returnTo=${encodeURIComponent(flow.returnTo)}&repository=${encodeURIComponent(flow.repository || "")}` : flow.returnTo);