slack id fallback

This commit is contained in:
NotARoomba 2026-03-04 14:17:18 -05:00
parent 8bfdcc7d52
commit a3feec9263
7 changed files with 180 additions and 56 deletions

108
backend/dist/index.js vendored
View file

@ -31556,11 +31556,13 @@ var SCRAPS_START_DATE = "2026-02-03";
var SYNC_INTERVAL_MS = 2 * 60 * 1000; var SYNC_INTERVAL_MS = 2 * 60 * 1000;
var hackatimeUserCache = new Map; var hackatimeUserCache = new Map;
var hackatimeUserIdCache = new Map; var hackatimeUserIdCache = new Map;
async function getHackatimeUser(email) { async function getHackatimeUser(email, slackId) {
const cached = hackatimeUserCache.get(email); const cacheKey = email || slackId || "";
const cached = hackatimeUserCache.get(cacheKey);
if (cached !== undefined) if (cached !== undefined)
return cached; return cached;
try { try {
let user_id = null;
const emailResponse = await fetch(`${HACKATIME_API}/user/get_user_by_email`, { const emailResponse = await fetch(`${HACKATIME_API}/user/get_user_by_email`, {
method: "POST", method: "POST",
headers: { headers: {
@ -31570,11 +31572,30 @@ async function getHackatimeUser(email) {
}, },
body: JSON.stringify({ email }) body: JSON.stringify({ email })
}); });
if (!emailResponse.ok) if (emailResponse.ok) {
return null; const emailData = await emailResponse.json();
const emailData = await emailResponse.json(); user_id = parseInt(String(emailData.user_id), 10);
const user_id = parseInt(String(emailData.user_id), 10); if (isNaN(user_id))
if (isNaN(user_id)) user_id = null;
}
if (user_id === null && slackId) {
const fuzzyResponse = await fetch(`${HACKATIME_API}/user/search_fuzzy`, {
method: "POST",
headers: {
Authorization: `Bearer ${config.hackatimeAdminKey}`,
"Content-Type": "application/json",
Accept: "application/json"
},
body: JSON.stringify({ query: slackId })
});
if (fuzzyResponse.ok) {
const fuzzyData = await fuzzyResponse.json();
if (fuzzyData.users?.length === 1) {
user_id = fuzzyData.users[0].id;
}
}
}
if (user_id === null)
return null; return null;
const infoResponse = await fetch(`${HACKATIME_API}/user/info?user_id=${user_id}`, { const infoResponse = await fetch(`${HACKATIME_API}/user/info?user_id=${user_id}`, {
headers: { headers: {
@ -31593,7 +31614,7 @@ async function getHackatimeUser(email) {
banned: userObj.banned || false, banned: userObj.banned || false,
suspected: userObj.suspected || false suspected: userObj.suspected || false
}; };
hackatimeUserCache.set(email, user); hackatimeUserCache.set(cacheKey, user);
return user; return user;
} catch { } catch {
return null; return null;
@ -31670,7 +31691,8 @@ async function syncSingleProject(projectId) {
hackatimeProject: projectsTable.hackatimeProject, hackatimeProject: projectsTable.hackatimeProject,
hours: projectsTable.hours, hours: projectsTable.hours,
userId: projectsTable.userId, userId: projectsTable.userId,
userEmail: usersTable.email userEmail: usersTable.email,
userSlackId: usersTable.slackId
}).from(projectsTable).innerJoin(usersTable, eq(projectsTable.userId, usersTable.id)).where(eq(projectsTable.id, projectId)).limit(1); }).from(projectsTable).innerJoin(usersTable, eq(projectsTable.userId, usersTable.id)).where(eq(projectsTable.id, projectId)).limit(1);
if (!project) if (!project)
return { hours: 0, updated: false, error: "Project not found" }; return { hours: 0, updated: false, error: "Project not found" };
@ -31679,7 +31701,7 @@ async function syncSingleProject(projectId) {
const entries = parseHackatimeProjects(project.hackatimeProject); const entries = parseHackatimeProjects(project.hackatimeProject);
if (entries.length === 0) if (entries.length === 0)
return { hours: project.hours ?? 0, updated: false, error: "Invalid Hackatime project format" }; return { hours: project.hours ?? 0, updated: false, error: "Invalid Hackatime project format" };
const hackatimeUser = await getHackatimeUser(project.userEmail); const hackatimeUser = await getHackatimeUser(project.userEmail, project.userSlackId);
if (hackatimeUser?.banned) { if (hackatimeUser?.banned) {
await db.delete(sessionsTable).where(eq(sessionsTable.userId, project.userId)); await db.delete(sessionsTable).where(eq(sessionsTable.userId, project.userId));
return { hours: 0, updated: false, error: "User is banned on Hackatime" }; return { hours: 0, updated: false, error: "User is banned on Hackatime" };
@ -31899,7 +31921,7 @@ async function computeEffectiveHoursForProject(project) {
// src/routes/projects.ts // src/routes/projects.ts
var ALLOWED_IMAGE_DOMAIN = "cdn.hackclub.com"; var ALLOWED_IMAGE_DOMAIN = "cdn.hackclub.com";
async function prefixHackatimeIds(hackatimeProject, email) { async function prefixHackatimeIds(hackatimeProject, email, slackId) {
if (!hackatimeProject) if (!hackatimeProject)
return null; return null;
const names = hackatimeProject.split(",").map((p) => p.trim()).filter((p) => p.length > 0); const names = hackatimeProject.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
@ -31911,7 +31933,7 @@ async function prefixHackatimeIds(hackatimeProject, email) {
}); });
if (alreadyPrefixed) if (alreadyPrefixed)
return hackatimeProject; return hackatimeProject;
const hackatimeUser = await getHackatimeUser(email); const hackatimeUser = await getHackatimeUser(email, slackId);
if (!hackatimeUser || typeof hackatimeUser.user_id !== "number") if (!hackatimeUser || typeof hackatimeUser.user_id !== "number")
return hackatimeProject; return hackatimeProject;
return names.map((name) => { return names.map((name) => {
@ -32205,7 +32227,7 @@ projects.post("/", async ({ body, headers }) => {
if (!validateImageUrl(data.image)) { if (!validateImageUrl(data.image)) {
return { error: "Image must be from cdn.hackclub.com" }; return { error: "Image must be from cdn.hackclub.com" };
} }
const projectName = await prefixHackatimeIds(parseHackatimeProjects2(data.hackatimeProject || null), user.email); const projectName = await prefixHackatimeIds(parseHackatimeProjects2(data.hackatimeProject || null), user.email, user.slackId);
const tier = data.tier !== undefined ? Math.max(1, Math.min(4, data.tier)) : 1; const tier = data.tier !== undefined ? Math.max(1, Math.min(4, data.tier)) : 1;
const newProject = await db.insert(projectsTable).values({ const newProject = await db.insert(projectsTable).values({
userId: user.id, userId: user.id,
@ -32251,7 +32273,7 @@ projects.put("/:id", async ({ params, body, headers }) => {
if (!playableCheck.valid) { if (!playableCheck.valid) {
return { error: playableCheck.error }; return { error: playableCheck.error };
} }
const projectName = await prefixHackatimeIds(parseHackatimeProjects2(data.hackatimeProject || null), user.email); const projectName = await prefixHackatimeIds(parseHackatimeProjects2(data.hackatimeProject || null), user.email, user.slackId);
const tier = data.tier !== undefined ? Math.max(1, Math.min(4, data.tier)) : undefined; const tier = data.tier !== undefined ? Math.max(1, Math.min(4, data.tier)) : undefined;
const updated = await db.update(projectsTable).set({ const updated = await db.update(projectsTable).set({
name: data.name, name: data.name,
@ -32848,7 +32870,7 @@ authRoutes.get("/callback", async ({ query, redirect: redirect2 }) => {
return redirect2("https://fraud.land"); return redirect2("https://fraud.land");
} }
try { try {
const hackatimeUser = await getHackatimeUser(identity.primary_email); const hackatimeUser = await getHackatimeUser(identity.primary_email, identity.slack_id);
if (hackatimeUser?.banned) { if (hackatimeUser?.banned) {
console.log("[AUTH] Hackatime-banned user attempted login:", { userId: user.id, username: user.username, hackatimeUserId: hackatimeUser.user_id }); console.log("[AUTH] Hackatime-banned user attempted login:", { userId: user.id, username: user.username, hackatimeUserId: hackatimeUser.user_id });
return redirect2("https://fraud.land"); return redirect2("https://fraud.land");
@ -33985,7 +34007,7 @@ async function filterHackatimeBanned(users) {
const filtered = []; const filtered = [];
for (const user2 of users) { for (const user2 of users) {
try { try {
const htUser = await getHackatimeUser(user2.email); const htUser = await getHackatimeUser(user2.email, user2.slackId);
if (htUser?.banned) if (htUser?.banned)
continue; continue;
} catch {} } catch {}
@ -34001,6 +34023,7 @@ leaderboard.get("/", async ({ query }) => {
username: usersTable.username, username: usersTable.username,
avatar: usersTable.avatar, avatar: usersTable.avatar,
email: usersTable.email, email: usersTable.email,
slackId: usersTable.slackId,
scrapsEarned: sql`COALESCE((SELECT SUM(scraps_awarded) FROM projects WHERE user_id = ${usersTable.id} AND scraps_paid_at IS NOT NULL AND status != 'permanently_rejected'), 0)`.as("scraps_earned"), scrapsEarned: sql`COALESCE((SELECT SUM(scraps_awarded) FROM projects WHERE user_id = ${usersTable.id} AND scraps_paid_at IS NOT NULL AND status != 'permanently_rejected'), 0)`.as("scraps_earned"),
scrapsBonus: sql`COALESCE((SELECT SUM(amount) FROM user_bonuses WHERE user_id = ${usersTable.id}), 0)`.as("scraps_bonus"), scrapsBonus: sql`COALESCE((SELECT SUM(amount) FROM user_bonuses WHERE user_id = ${usersTable.id}), 0)`.as("scraps_bonus"),
scrapsShopSpent: sql`COALESCE((SELECT SUM(total_price) FROM shop_orders WHERE user_id = ${usersTable.id}), 0)`.as("scraps_shop_spent"), scrapsShopSpent: sql`COALESCE((SELECT SUM(total_price) FROM shop_orders WHERE user_id = ${usersTable.id}), 0)`.as("scraps_shop_spent"),
@ -34025,6 +34048,7 @@ leaderboard.get("/", async ({ query }) => {
username: usersTable.username, username: usersTable.username,
avatar: usersTable.avatar, avatar: usersTable.avatar,
email: usersTable.email, email: usersTable.email,
slackId: usersTable.slackId,
scrapsEarned: sql`COALESCE((SELECT SUM(scraps_awarded) FROM projects WHERE user_id = ${usersTable.id} AND scraps_paid_at IS NOT NULL AND status != 'permanently_rejected'), 0)`.as("scraps_earned"), scrapsEarned: sql`COALESCE((SELECT SUM(scraps_awarded) FROM projects WHERE user_id = ${usersTable.id} AND scraps_paid_at IS NOT NULL AND status != 'permanently_rejected'), 0)`.as("scraps_earned"),
scrapsBonus: sql`COALESCE((SELECT SUM(amount) FROM user_bonuses WHERE user_id = ${usersTable.id}), 0)`.as("scraps_bonus"), scrapsBonus: sql`COALESCE((SELECT SUM(amount) FROM user_bonuses WHERE user_id = ${usersTable.id}), 0)`.as("scraps_bonus"),
scrapsShopSpent: sql`COALESCE((SELECT SUM(total_price) FROM shop_orders WHERE user_id = ${usersTable.id}), 0)`.as("scraps_shop_spent"), scrapsShopSpent: sql`COALESCE((SELECT SUM(total_price) FROM shop_orders WHERE user_id = ${usersTable.id}), 0)`.as("scraps_shop_spent"),
@ -34056,12 +34080,13 @@ leaderboard.get("/views", async () => {
views: projectsTable.views, views: projectsTable.views,
userId: projectsTable.userId, userId: projectsTable.userId,
userEmail: usersTable.email, userEmail: usersTable.email,
userSlackId: usersTable.slackId,
userRole: usersTable.role userRole: usersTable.role
}).from(projectsTable).innerJoin(usersTable, eq(projectsTable.userId, usersTable.id)).where(and(eq(projectsTable.status, "shipped"), or(eq(projectsTable.deleted, 0), isNull(projectsTable.deleted)), ne(usersTable.role, "banned"))).orderBy(desc(projectsTable.views)).limit(20); }).from(projectsTable).innerJoin(usersTable, eq(projectsTable.userId, usersTable.id)).where(and(eq(projectsTable.status, "shipped"), or(eq(projectsTable.deleted, 0), isNull(projectsTable.deleted)), ne(usersTable.role, "banned"))).orderBy(desc(projectsTable.views)).limit(20);
const filtered = []; const filtered = [];
for (const project of results) { for (const project of results) {
try { try {
const htUser = await getHackatimeUser(project.userEmail); const htUser = await getHackatimeUser(project.userEmail, project.userSlackId);
if (htUser?.banned) if (htUser?.banned)
continue; continue;
} catch {} } catch {}
@ -34161,6 +34186,7 @@ hackatime.get("/projects", async ({ headers }) => {
return { error: "No email found for user", projects: [] }; return { error: "No email found for user", projects: [] };
} }
try { try {
let hackatimeUserId = null;
const emailResponse = await fetch(`${HACKATIME_ADMIN_API}/user/get_user_by_email`, { const emailResponse = await fetch(`${HACKATIME_ADMIN_API}/user/get_user_by_email`, {
method: "POST", method: "POST",
headers: { headers: {
@ -34170,13 +34196,35 @@ hackatime.get("/projects", async ({ headers }) => {
}, },
body: JSON.stringify({ email: user2.email }) body: JSON.stringify({ email: user2.email })
}); });
if (!emailResponse.ok) { if (emailResponse.ok) {
const errorText = await emailResponse.text(); const emailData = await emailResponse.json();
console.log("[HACKATIME] Email lookup error:", { status: emailResponse.status, body: errorText }); hackatimeUserId = emailData.user_id;
console.log("[HACKATIME] Found hackatime user_id:", hackatimeUserId, "for email:", user2.email);
} else {
console.log("[HACKATIME] Email lookup failed for:", user2.email, "- trying slack ID fallback");
}
if (hackatimeUserId === null && user2.slackId) {
const fuzzyResponse = await fetch(`${HACKATIME_ADMIN_API}/user/search_fuzzy`, {
method: "POST",
headers: {
Authorization: `Bearer ${config.hackatimeAdminKey}`,
"Content-Type": "application/json",
Accept: "application/json"
},
body: JSON.stringify({ query: user2.slackId })
});
if (fuzzyResponse.ok) {
const fuzzyData = await fuzzyResponse.json();
if (fuzzyData.users?.length === 1) {
hackatimeUserId = fuzzyData.users[0].id;
console.log("[HACKATIME] Found hackatime user_id:", hackatimeUserId, "via slack ID:", user2.slackId);
}
}
}
if (hackatimeUserId === null) {
console.log("[HACKATIME] Could not find hackatime user for:", user2.email, user2.slackId);
return { projects: [] }; return { projects: [] };
} }
const { user_id: hackatimeUserId } = await emailResponse.json();
console.log("[HACKATIME] Found hackatime user_id:", hackatimeUserId, "for email:", user2.email);
const projectsParams = new URLSearchParams({ const projectsParams = new URLSearchParams({
user_id: String(hackatimeUserId), user_id: String(hackatimeUserId),
start_date: SCRAPS_START_DATE2 start_date: SCRAPS_START_DATE2
@ -35038,7 +35086,7 @@ admin.get("/users/:id", async ({ params, headers, status: status2 }) => {
let hackatimeBanned = false; let hackatimeBanned = false;
if (targetUser[0].email) { if (targetUser[0].email) {
try { try {
const htUser = await getHackatimeUser(targetUser[0].email); const htUser = await getHackatimeUser(targetUser[0].email, targetUser[0].slackId);
if (htUser) { if (htUser) {
hackatimeSuspected = htUser.suspected || false; hackatimeSuspected = htUser.suspected || false;
hackatimeBanned = htUser.banned || false; hackatimeBanned = htUser.banned || false;
@ -35270,6 +35318,7 @@ admin.get("/reviews/:id", async ({ params, headers }) => {
id: usersTable.id, id: usersTable.id,
username: usersTable.username, username: usersTable.username,
email: usersTable.email, email: usersTable.email,
slackId: usersTable.slackId,
avatar: usersTable.avatar, avatar: usersTable.avatar,
internalNotes: usersTable.internalNotes internalNotes: usersTable.internalNotes
}).from(usersTable).where(eq(usersTable.id, project[0].userId)).limit(1); }).from(usersTable).where(eq(usersTable.id, project[0].userId)).limit(1);
@ -35288,7 +35337,7 @@ admin.get("/reviews/:id", async ({ params, headers }) => {
let hackatimeBanned = false; let hackatimeBanned = false;
if (projectUser[0]?.email) { if (projectUser[0]?.email) {
try { try {
const htUser = await getHackatimeUser(projectUser[0].email); const htUser = await getHackatimeUser(projectUser[0].email, projectUser[0].slackId);
if (htUser) { if (htUser) {
hackatimeUserId = htUser.user_id; hackatimeUserId = htUser.user_id;
hackatimeSuspected = htUser.suspected || false; hackatimeSuspected = htUser.suspected || false;
@ -35573,6 +35622,7 @@ admin.get("/second-pass/:id", async ({ params, headers }) => {
id: usersTable.id, id: usersTable.id,
username: usersTable.username, username: usersTable.username,
email: usersTable.email, email: usersTable.email,
slackId: usersTable.slackId,
avatar: usersTable.avatar, avatar: usersTable.avatar,
internalNotes: usersTable.internalNotes internalNotes: usersTable.internalNotes
}).from(usersTable).where(eq(usersTable.id, project[0].userId)).limit(1); }).from(usersTable).where(eq(usersTable.id, project[0].userId)).limit(1);
@ -35591,7 +35641,7 @@ admin.get("/second-pass/:id", async ({ params, headers }) => {
let hackatimeBanned = false; let hackatimeBanned = false;
if (projectUser[0]?.email) { if (projectUser[0]?.email) {
try { try {
const htUser = await getHackatimeUser(projectUser[0].email); const htUser = await getHackatimeUser(projectUser[0].email, projectUser[0].slackId);
if (htUser) { if (htUser) {
hackatimeUserId = htUser.user_id; hackatimeUserId = htUser.user_id;
hackatimeSuspected = htUser.suspected || false; hackatimeSuspected = htUser.suspected || false;
@ -36198,10 +36248,16 @@ admin.get("/orders", async ({ headers, query, status: status2 }) => {
} }
const rows = await ordersQuery; const rows = await ordersQuery;
const uniqueEmails = [...new Set(rows.map((r) => r.userEmail).filter(Boolean))]; const uniqueEmails = [...new Set(rows.map((r) => r.userEmail).filter(Boolean))];
const emailToSlackId = new Map;
for (const row of rows) {
if (row.userEmail && !emailToSlackId.has(row.userEmail)) {
emailToSlackId.set(row.userEmail, row.slackId);
}
}
const banMap = new Map; const banMap = new Map;
await Promise.all(uniqueEmails.map(async (email) => { await Promise.all(uniqueEmails.map(async (email) => {
try { try {
const htUser = await getHackatimeUser(email); const htUser = await getHackatimeUser(email, emailToSlackId.get(email));
banMap.set(email, htUser?.banned ?? false); banMap.set(email, htUser?.banned ?? false);
} catch { } catch {
banMap.set(email, false); banMap.set(email, false);

View file

@ -42,12 +42,15 @@ const hackatimeUserCache = new Map<string, HackatimeUser>()
// Cache of hackatime user_id -> hackatime user // Cache of hackatime user_id -> hackatime user
const hackatimeUserIdCache = new Map<number, HackatimeUser>() const hackatimeUserIdCache = new Map<number, HackatimeUser>()
export async function getHackatimeUser(email: string): Promise<HackatimeUser | null> { export async function getHackatimeUser(email: string, slackId?: string | null): Promise<HackatimeUser | null> {
const cached = hackatimeUserCache.get(email) const cacheKey = email || slackId || ''
const cached = hackatimeUserCache.get(cacheKey)
if (cached !== undefined) return cached if (cached !== undefined) return cached
try { try {
// First get user_id by email let user_id: number | null = null
// Try email lookup first
const emailResponse = await fetch(`${HACKATIME_API}/user/get_user_by_email`, { const emailResponse = await fetch(`${HACKATIME_API}/user/get_user_by_email`, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -57,13 +60,34 @@ export async function getHackatimeUser(email: string): Promise<HackatimeUser | n
}, },
body: JSON.stringify({ email }) body: JSON.stringify({ email })
}) })
if (!emailResponse.ok) return null if (emailResponse.ok) {
const emailData = await emailResponse.json() as { user_id: number }
user_id = parseInt(String(emailData.user_id), 10)
if (isNaN(user_id)) user_id = null
}
const emailData = await emailResponse.json() as { user_id: number } // If email lookup failed and we have a slack ID, try fuzzy search by slack ID
const user_id = parseInt(String(emailData.user_id), 10) if (user_id === null && slackId) {
if (isNaN(user_id)) return null const fuzzyResponse = await fetch(`${HACKATIME_API}/user/search_fuzzy`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${config.hackatimeAdminKey}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ query: slackId })
})
if (fuzzyResponse.ok) {
const fuzzyData = await fuzzyResponse.json() as { users: { id: number }[] }
if (fuzzyData.users?.length === 1) {
user_id = fuzzyData.users[0].id
}
}
}
// Then get username and slack_uid by user_id if (user_id === null) return null
// Get full user info by user_id
const infoResponse = await fetch(`${HACKATIME_API}/user/info?user_id=${user_id}`, { const infoResponse = await fetch(`${HACKATIME_API}/user/info?user_id=${user_id}`, {
headers: { headers: {
'Authorization': `Bearer ${config.hackatimeAdminKey}`, 'Authorization': `Bearer ${config.hackatimeAdminKey}`,
@ -81,7 +105,7 @@ export async function getHackatimeUser(email: string): Promise<HackatimeUser | n
banned: userObj.banned || false, banned: userObj.banned || false,
suspected: userObj.suspected || false suspected: userObj.suspected || false
} }
hackatimeUserCache.set(email, user) hackatimeUserCache.set(cacheKey, user)
return user return user
} catch { } catch {
return null return null
@ -213,14 +237,15 @@ async function syncAllProjects(): Promise<void> {
adminProjectsCache.clear() adminProjectsCache.clear()
try { try {
// Get all projects with hackatime projects that are not deleted and not shipped, joined with user email // Get all projects with hackatime projects that are not deleted and not shipped, joined with user email and slackId
const projects = await db const projects = await db
.select({ .select({
id: projectsTable.id, id: projectsTable.id,
hackatimeProject: projectsTable.hackatimeProject, hackatimeProject: projectsTable.hackatimeProject,
hours: projectsTable.hours, hours: projectsTable.hours,
userId: projectsTable.userId, userId: projectsTable.userId,
userEmail: usersTable.email userEmail: usersTable.email,
userSlackId: usersTable.slackId
}) })
.from(projectsTable) .from(projectsTable)
.innerJoin(usersTable, eq(projectsTable.userId, usersTable.id)) .innerJoin(usersTable, eq(projectsTable.userId, usersTable.id))
@ -243,7 +268,8 @@ async function syncAllProjects(): Promise<void> {
let migrated = 0 let migrated = 0
for (const [email, userProjects] of projectsByEmail) { for (const [email, userProjects] of projectsByEmail) {
const hackatimeUser = await getHackatimeUser(email) const slackId = userProjects[0].userSlackId
const hackatimeUser = await getHackatimeUser(email, slackId)
if (hackatimeUser?.banned) { if (hackatimeUser?.banned) {
const userId = userProjects[0].userId const userId = userProjects[0].userId
@ -414,7 +440,8 @@ export async function syncSingleProject(projectId: number): Promise<{ hours: num
hackatimeProject: projectsTable.hackatimeProject, hackatimeProject: projectsTable.hackatimeProject,
hours: projectsTable.hours, hours: projectsTable.hours,
userId: projectsTable.userId, userId: projectsTable.userId,
userEmail: usersTable.email userEmail: usersTable.email,
userSlackId: usersTable.slackId
}) })
.from(projectsTable) .from(projectsTable)
.innerJoin(usersTable, eq(projectsTable.userId, usersTable.id)) .innerJoin(usersTable, eq(projectsTable.userId, usersTable.id))
@ -427,7 +454,7 @@ export async function syncSingleProject(projectId: number): Promise<{ hours: num
const entries = parseHackatimeProjects(project.hackatimeProject) const entries = parseHackatimeProjects(project.hackatimeProject)
if (entries.length === 0) return { hours: project.hours ?? 0, updated: false, error: 'Invalid Hackatime project format' } if (entries.length === 0) return { hours: project.hours ?? 0, updated: false, error: 'Invalid Hackatime project format' }
const hackatimeUser = await getHackatimeUser(project.userEmail) const hackatimeUser = await getHackatimeUser(project.userEmail, project.userSlackId)
if (hackatimeUser?.banned) { if (hackatimeUser?.banned) {
await db.delete(sessionsTable).where(eq(sessionsTable.userId, project.userId)) await db.delete(sessionsTable).where(eq(sessionsTable.userId, project.userId))

View file

@ -514,7 +514,7 @@ admin.get("/users/:id", async ({ params, headers, status }) => {
let hackatimeBanned = false; let hackatimeBanned = false;
if (targetUser[0].email) { if (targetUser[0].email) {
try { try {
const htUser = await getHackatimeUser(targetUser[0].email); const htUser = await getHackatimeUser(targetUser[0].email, targetUser[0].slackId);
if (htUser) { if (htUser) {
hackatimeSuspected = htUser.suspected || false; hackatimeSuspected = htUser.suspected || false;
hackatimeBanned = htUser.banned || false; hackatimeBanned = htUser.banned || false;
@ -840,6 +840,7 @@ admin.get("/reviews/:id", async ({ params, headers }) => {
id: usersTable.id, id: usersTable.id,
username: usersTable.username, username: usersTable.username,
email: usersTable.email, email: usersTable.email,
slackId: usersTable.slackId,
avatar: usersTable.avatar, avatar: usersTable.avatar,
internalNotes: usersTable.internalNotes, internalNotes: usersTable.internalNotes,
}) })
@ -875,7 +876,7 @@ admin.get("/reviews/:id", async ({ params, headers }) => {
let hackatimeBanned = false; let hackatimeBanned = false;
if (projectUser[0]?.email) { if (projectUser[0]?.email) {
try { try {
const htUser = await getHackatimeUser(projectUser[0].email); const htUser = await getHackatimeUser(projectUser[0].email, projectUser[0].slackId);
if (htUser) { if (htUser) {
hackatimeUserId = htUser.user_id; hackatimeUserId = htUser.user_id;
hackatimeSuspected = htUser.suspected || false; hackatimeSuspected = htUser.suspected || false;
@ -1312,6 +1313,7 @@ admin.get("/second-pass/:id", async ({ params, headers }) => {
id: usersTable.id, id: usersTable.id,
username: usersTable.username, username: usersTable.username,
email: usersTable.email, email: usersTable.email,
slackId: usersTable.slackId,
avatar: usersTable.avatar, avatar: usersTable.avatar,
internalNotes: usersTable.internalNotes, internalNotes: usersTable.internalNotes,
}) })
@ -1347,7 +1349,7 @@ admin.get("/second-pass/:id", async ({ params, headers }) => {
let hackatimeBanned = false; let hackatimeBanned = false;
if (projectUser[0]?.email) { if (projectUser[0]?.email) {
try { try {
const htUser = await getHackatimeUser(projectUser[0].email); const htUser = await getHackatimeUser(projectUser[0].email, projectUser[0].slackId);
if (htUser) { if (htUser) {
hackatimeUserId = htUser.user_id; hackatimeUserId = htUser.user_id;
hackatimeSuspected = htUser.suspected || false; hackatimeSuspected = htUser.suspected || false;
@ -2331,11 +2333,17 @@ admin.get("/orders", async ({ headers, query, status }) => {
// Batch-check Hackatime ban status for unique user emails // Batch-check Hackatime ban status for unique user emails
const uniqueEmails = [...new Set(rows.map((r) => r.userEmail).filter(Boolean))] as string[]; const uniqueEmails = [...new Set(rows.map((r) => r.userEmail).filter(Boolean))] as string[];
const emailToSlackId = new Map<string, string | null>();
for (const row of rows) {
if (row.userEmail && !emailToSlackId.has(row.userEmail)) {
emailToSlackId.set(row.userEmail, row.slackId);
}
}
const banMap = new Map<string, boolean>(); const banMap = new Map<string, boolean>();
await Promise.all( await Promise.all(
uniqueEmails.map(async (email) => { uniqueEmails.map(async (email) => {
try { try {
const htUser = await getHackatimeUser(email); const htUser = await getHackatimeUser(email, emailToSlackId.get(email));
banMap.set(email, htUser?.banned ?? false); banMap.set(email, htUser?.banned ?? false);
} catch { } catch {
banMap.set(email, false); banMap.set(email, false);

View file

@ -101,7 +101,7 @@ authRoutes.get("/callback", async ({ query, redirect }) => {
// Check if user is banned on Hackatime // Check if user is banned on Hackatime
try { try {
const hackatimeUser = await getHackatimeUser(identity.primary_email) const hackatimeUser = await getHackatimeUser(identity.primary_email, identity.slack_id)
if (hackatimeUser?.banned) { if (hackatimeUser?.banned) {
console.log("[AUTH] Hackatime-banned user attempted login:", { userId: user.id, username: user.username, hackatimeUserId: hackatimeUser.user_id }) console.log("[AUTH] Hackatime-banned user attempted login:", { userId: user.id, username: user.username, hackatimeUserId: hackatimeUser.user_id })
return redirect('https://fraud.land') return redirect('https://fraud.land')

View file

@ -35,7 +35,9 @@ hackatime.get('/projects', async ({ headers }) => {
} }
try { try {
// Step 1: Get hackatime user_id by email // Step 1: Get hackatime user_id by email, with slack ID fallback
let hackatimeUserId: number | null = null
const emailResponse = await fetch(`${HACKATIME_ADMIN_API}/user/get_user_by_email`, { const emailResponse = await fetch(`${HACKATIME_ADMIN_API}/user/get_user_by_email`, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -46,14 +48,39 @@ hackatime.get('/projects', async ({ headers }) => {
body: JSON.stringify({ email: user.email }) body: JSON.stringify({ email: user.email })
}) })
if (!emailResponse.ok) { if (emailResponse.ok) {
const errorText = await emailResponse.text() const emailData = await emailResponse.json() as { user_id: number }
console.log('[HACKATIME] Email lookup error:', { status: emailResponse.status, body: errorText }) hackatimeUserId = emailData.user_id
return { projects: [] } console.log('[HACKATIME] Found hackatime user_id:', hackatimeUserId, 'for email:', user.email)
} else {
console.log('[HACKATIME] Email lookup failed for:', user.email, '- trying slack ID fallback')
} }
const { user_id: hackatimeUserId } = await emailResponse.json() as { user_id: number } // Fallback: fuzzy search by slack ID if email lookup failed
console.log('[HACKATIME] Found hackatime user_id:', hackatimeUserId, 'for email:', user.email) if (hackatimeUserId === null && user.slackId) {
const fuzzyResponse = await fetch(`${HACKATIME_ADMIN_API}/user/search_fuzzy`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${config.hackatimeAdminKey}`,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ query: user.slackId })
})
if (fuzzyResponse.ok) {
const fuzzyData = await fuzzyResponse.json() as { users: { id: number }[] }
if (fuzzyData.users?.length === 1) {
hackatimeUserId = fuzzyData.users[0].id
console.log('[HACKATIME] Found hackatime user_id:', hackatimeUserId, 'via slack ID:', user.slackId)
}
}
}
if (hackatimeUserId === null) {
console.log('[HACKATIME] Could not find hackatime user for:', user.email, user.slackId)
return { projects: [] }
}
// Step 2: Get projects via admin endpoint with start_date // Step 2: Get projects via admin endpoint with start_date
const projectsParams = new URLSearchParams({ const projectsParams = new URLSearchParams({

View file

@ -8,11 +8,11 @@ import { getHackatimeUser } from '../lib/hackatime-sync'
const leaderboard = new Elysia({ prefix: '/leaderboard' }) const leaderboard = new Elysia({ prefix: '/leaderboard' })
async function filterHackatimeBanned<T extends { email: string }>(users: T[]): Promise<T[]> { async function filterHackatimeBanned<T extends { email: string; slackId?: string | null }>(users: T[]): Promise<T[]> {
const filtered: T[] = [] const filtered: T[] = []
for (const user of users) { for (const user of users) {
try { try {
const htUser = await getHackatimeUser(user.email) const htUser = await getHackatimeUser(user.email, user.slackId)
if (htUser?.banned) continue if (htUser?.banned) continue
} catch { } catch {
// If lookup fails, don't exclude the user // If lookup fails, don't exclude the user
@ -32,6 +32,7 @@ leaderboard.get('/', async ({ query }) => {
username: usersTable.username, username: usersTable.username,
avatar: usersTable.avatar, avatar: usersTable.avatar,
email: usersTable.email, email: usersTable.email,
slackId: usersTable.slackId,
scrapsEarned: sql<number>`COALESCE((SELECT SUM(scraps_awarded) FROM projects WHERE user_id = ${usersTable.id} AND scraps_paid_at IS NOT NULL AND status != 'permanently_rejected'), 0)`.as('scraps_earned'), scrapsEarned: sql<number>`COALESCE((SELECT SUM(scraps_awarded) FROM projects WHERE user_id = ${usersTable.id} AND scraps_paid_at IS NOT NULL AND status != 'permanently_rejected'), 0)`.as('scraps_earned'),
scrapsBonus: sql<number>`COALESCE((SELECT SUM(amount) FROM user_bonuses WHERE user_id = ${usersTable.id}), 0)`.as('scraps_bonus'), scrapsBonus: sql<number>`COALESCE((SELECT SUM(amount) FROM user_bonuses WHERE user_id = ${usersTable.id}), 0)`.as('scraps_bonus'),
scrapsShopSpent: sql<number>`COALESCE((SELECT SUM(total_price) FROM shop_orders WHERE user_id = ${usersTable.id}), 0)`.as('scraps_shop_spent'), scrapsShopSpent: sql<number>`COALESCE((SELECT SUM(total_price) FROM shop_orders WHERE user_id = ${usersTable.id}), 0)`.as('scraps_shop_spent'),
@ -70,6 +71,7 @@ leaderboard.get('/', async ({ query }) => {
username: usersTable.username, username: usersTable.username,
avatar: usersTable.avatar, avatar: usersTable.avatar,
email: usersTable.email, email: usersTable.email,
slackId: usersTable.slackId,
scrapsEarned: sql<number>`COALESCE((SELECT SUM(scraps_awarded) FROM projects WHERE user_id = ${usersTable.id} AND scraps_paid_at IS NOT NULL AND status != 'permanently_rejected'), 0)`.as('scraps_earned'), scrapsEarned: sql<number>`COALESCE((SELECT SUM(scraps_awarded) FROM projects WHERE user_id = ${usersTable.id} AND scraps_paid_at IS NOT NULL AND status != 'permanently_rejected'), 0)`.as('scraps_earned'),
scrapsBonus: sql<number>`COALESCE((SELECT SUM(amount) FROM user_bonuses WHERE user_id = ${usersTable.id}), 0)`.as('scraps_bonus'), scrapsBonus: sql<number>`COALESCE((SELECT SUM(amount) FROM user_bonuses WHERE user_id = ${usersTable.id}), 0)`.as('scraps_bonus'),
scrapsShopSpent: sql<number>`COALESCE((SELECT SUM(total_price) FROM shop_orders WHERE user_id = ${usersTable.id}), 0)`.as('scraps_shop_spent'), scrapsShopSpent: sql<number>`COALESCE((SELECT SUM(total_price) FROM shop_orders WHERE user_id = ${usersTable.id}), 0)`.as('scraps_shop_spent'),
@ -115,6 +117,7 @@ leaderboard.get('/views', async () => {
views: projectsTable.views, views: projectsTable.views,
userId: projectsTable.userId, userId: projectsTable.userId,
userEmail: usersTable.email, userEmail: usersTable.email,
userSlackId: usersTable.slackId,
userRole: usersTable.role userRole: usersTable.role
}) })
.from(projectsTable) .from(projectsTable)
@ -131,7 +134,7 @@ leaderboard.get('/views', async () => {
const filtered: typeof results = [] const filtered: typeof results = []
for (const project of results) { for (const project of results) {
try { try {
const htUser = await getHackatimeUser(project.userEmail) const htUser = await getHackatimeUser(project.userEmail, project.userSlackId)
if (htUser?.banned) continue if (htUser?.banned) continue
} catch { } catch {
// If lookup fails, don't exclude // If lookup fails, don't exclude

View file

@ -32,6 +32,7 @@ function parseHackatimeProject(hackatimeProject: string | null): string | null {
async function prefixHackatimeIds( async function prefixHackatimeIds(
hackatimeProject: string | null, hackatimeProject: string | null,
email: string, email: string,
slackId?: string | null,
): Promise<string | null> { ): Promise<string | null> {
if (!hackatimeProject) return null; if (!hackatimeProject) return null;
const names = hackatimeProject const names = hackatimeProject
@ -51,7 +52,7 @@ async function prefixHackatimeIds(
}); });
if (alreadyPrefixed) return hackatimeProject; if (alreadyPrefixed) return hackatimeProject;
const hackatimeUser = await getHackatimeUser(email); const hackatimeUser = await getHackatimeUser(email, slackId);
if (!hackatimeUser || typeof hackatimeUser.user_id !== "number") if (!hackatimeUser || typeof hackatimeUser.user_id !== "number")
return hackatimeProject; return hackatimeProject;
@ -558,6 +559,7 @@ projects.post("/", async ({ body, headers }) => {
const projectName = await prefixHackatimeIds( const projectName = await prefixHackatimeIds(
parseHackatimeProjects(data.hackatimeProject || null), parseHackatimeProjects(data.hackatimeProject || null),
user.email, user.email,
user.slackId,
); );
const tier = const tier =
data.tier !== undefined ? Math.max(1, Math.min(4, data.tier)) : 1; data.tier !== undefined ? Math.max(1, Math.min(4, data.tier)) : 1;
@ -647,6 +649,7 @@ projects.put("/:id", async ({ params, body, headers }) => {
const projectName = await prefixHackatimeIds( const projectName = await prefixHackatimeIds(
parseHackatimeProjects(data.hackatimeProject || null), parseHackatimeProjects(data.hackatimeProject || null),
user.email, user.email,
user.slackId,
); );
const tier = const tier =
data.tier !== undefined ? Math.max(1, Math.min(4, data.tier)) : undefined; data.tier !== undefined ? Math.max(1, Math.min(4, data.tier)) : undefined;