mirror of
https://github.com/System-End/scraps.git
synced 2026-04-19 16:28:20 +00:00
slack id fallback
This commit is contained in:
parent
8bfdcc7d52
commit
a3feec9263
7 changed files with 180 additions and 56 deletions
108
backend/dist/index.js
vendored
108
backend/dist/index.js
vendored
|
|
@ -31556,11 +31556,13 @@ var SCRAPS_START_DATE = "2026-02-03";
|
|||
var SYNC_INTERVAL_MS = 2 * 60 * 1000;
|
||||
var hackatimeUserCache = new Map;
|
||||
var hackatimeUserIdCache = new Map;
|
||||
async function getHackatimeUser(email) {
|
||||
const cached = hackatimeUserCache.get(email);
|
||||
async function getHackatimeUser(email, slackId) {
|
||||
const cacheKey = email || slackId || "";
|
||||
const cached = hackatimeUserCache.get(cacheKey);
|
||||
if (cached !== undefined)
|
||||
return cached;
|
||||
try {
|
||||
let user_id = null;
|
||||
const emailResponse = await fetch(`${HACKATIME_API}/user/get_user_by_email`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
|
@ -31570,11 +31572,30 @@ async function getHackatimeUser(email) {
|
|||
},
|
||||
body: JSON.stringify({ email })
|
||||
});
|
||||
if (!emailResponse.ok)
|
||||
return null;
|
||||
const emailData = await emailResponse.json();
|
||||
const user_id = parseInt(String(emailData.user_id), 10);
|
||||
if (isNaN(user_id))
|
||||
if (emailResponse.ok) {
|
||||
const emailData = await emailResponse.json();
|
||||
user_id = parseInt(String(emailData.user_id), 10);
|
||||
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;
|
||||
const infoResponse = await fetch(`${HACKATIME_API}/user/info?user_id=${user_id}`, {
|
||||
headers: {
|
||||
|
|
@ -31593,7 +31614,7 @@ async function getHackatimeUser(email) {
|
|||
banned: userObj.banned || false,
|
||||
suspected: userObj.suspected || false
|
||||
};
|
||||
hackatimeUserCache.set(email, user);
|
||||
hackatimeUserCache.set(cacheKey, user);
|
||||
return user;
|
||||
} catch {
|
||||
return null;
|
||||
|
|
@ -31670,7 +31691,8 @@ async function syncSingleProject(projectId) {
|
|||
hackatimeProject: projectsTable.hackatimeProject,
|
||||
hours: projectsTable.hours,
|
||||
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);
|
||||
if (!project)
|
||||
return { hours: 0, updated: false, error: "Project not found" };
|
||||
|
|
@ -31679,7 +31701,7 @@ async function syncSingleProject(projectId) {
|
|||
const entries = parseHackatimeProjects(project.hackatimeProject);
|
||||
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) {
|
||||
await db.delete(sessionsTable).where(eq(sessionsTable.userId, project.userId));
|
||||
return { hours: 0, updated: false, error: "User is banned on Hackatime" };
|
||||
|
|
@ -31899,7 +31921,7 @@ async function computeEffectiveHoursForProject(project) {
|
|||
|
||||
// src/routes/projects.ts
|
||||
var ALLOWED_IMAGE_DOMAIN = "cdn.hackclub.com";
|
||||
async function prefixHackatimeIds(hackatimeProject, email) {
|
||||
async function prefixHackatimeIds(hackatimeProject, email, slackId) {
|
||||
if (!hackatimeProject)
|
||||
return null;
|
||||
const names = hackatimeProject.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
|
||||
|
|
@ -31911,7 +31933,7 @@ async function prefixHackatimeIds(hackatimeProject, email) {
|
|||
});
|
||||
if (alreadyPrefixed)
|
||||
return hackatimeProject;
|
||||
const hackatimeUser = await getHackatimeUser(email);
|
||||
const hackatimeUser = await getHackatimeUser(email, slackId);
|
||||
if (!hackatimeUser || typeof hackatimeUser.user_id !== "number")
|
||||
return hackatimeProject;
|
||||
return names.map((name) => {
|
||||
|
|
@ -32205,7 +32227,7 @@ projects.post("/", async ({ body, headers }) => {
|
|||
if (!validateImageUrl(data.image)) {
|
||||
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 newProject = await db.insert(projectsTable).values({
|
||||
userId: user.id,
|
||||
|
|
@ -32251,7 +32273,7 @@ projects.put("/:id", async ({ params, body, headers }) => {
|
|||
if (!playableCheck.valid) {
|
||||
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 updated = await db.update(projectsTable).set({
|
||||
name: data.name,
|
||||
|
|
@ -32848,7 +32870,7 @@ authRoutes.get("/callback", async ({ query, redirect: redirect2 }) => {
|
|||
return redirect2("https://fraud.land");
|
||||
}
|
||||
try {
|
||||
const hackatimeUser = await getHackatimeUser(identity.primary_email);
|
||||
const hackatimeUser = await getHackatimeUser(identity.primary_email, identity.slack_id);
|
||||
if (hackatimeUser?.banned) {
|
||||
console.log("[AUTH] Hackatime-banned user attempted login:", { userId: user.id, username: user.username, hackatimeUserId: hackatimeUser.user_id });
|
||||
return redirect2("https://fraud.land");
|
||||
|
|
@ -33985,7 +34007,7 @@ async function filterHackatimeBanned(users) {
|
|||
const filtered = [];
|
||||
for (const user2 of users) {
|
||||
try {
|
||||
const htUser = await getHackatimeUser(user2.email);
|
||||
const htUser = await getHackatimeUser(user2.email, user2.slackId);
|
||||
if (htUser?.banned)
|
||||
continue;
|
||||
} catch {}
|
||||
|
|
@ -34001,6 +34023,7 @@ leaderboard.get("/", async ({ query }) => {
|
|||
username: usersTable.username,
|
||||
avatar: usersTable.avatar,
|
||||
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"),
|
||||
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"),
|
||||
|
|
@ -34025,6 +34048,7 @@ leaderboard.get("/", async ({ query }) => {
|
|||
username: usersTable.username,
|
||||
avatar: usersTable.avatar,
|
||||
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"),
|
||||
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"),
|
||||
|
|
@ -34056,12 +34080,13 @@ leaderboard.get("/views", async () => {
|
|||
views: projectsTable.views,
|
||||
userId: projectsTable.userId,
|
||||
userEmail: usersTable.email,
|
||||
userSlackId: usersTable.slackId,
|
||||
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);
|
||||
const filtered = [];
|
||||
for (const project of results) {
|
||||
try {
|
||||
const htUser = await getHackatimeUser(project.userEmail);
|
||||
const htUser = await getHackatimeUser(project.userEmail, project.userSlackId);
|
||||
if (htUser?.banned)
|
||||
continue;
|
||||
} catch {}
|
||||
|
|
@ -34161,6 +34186,7 @@ hackatime.get("/projects", async ({ headers }) => {
|
|||
return { error: "No email found for user", projects: [] };
|
||||
}
|
||||
try {
|
||||
let hackatimeUserId = null;
|
||||
const emailResponse = await fetch(`${HACKATIME_ADMIN_API}/user/get_user_by_email`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
|
@ -34170,13 +34196,35 @@ hackatime.get("/projects", async ({ headers }) => {
|
|||
},
|
||||
body: JSON.stringify({ email: user2.email })
|
||||
});
|
||||
if (!emailResponse.ok) {
|
||||
const errorText = await emailResponse.text();
|
||||
console.log("[HACKATIME] Email lookup error:", { status: emailResponse.status, body: errorText });
|
||||
if (emailResponse.ok) {
|
||||
const emailData = await emailResponse.json();
|
||||
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: [] };
|
||||
}
|
||||
const { user_id: hackatimeUserId } = await emailResponse.json();
|
||||
console.log("[HACKATIME] Found hackatime user_id:", hackatimeUserId, "for email:", user2.email);
|
||||
const projectsParams = new URLSearchParams({
|
||||
user_id: String(hackatimeUserId),
|
||||
start_date: SCRAPS_START_DATE2
|
||||
|
|
@ -35038,7 +35086,7 @@ admin.get("/users/:id", async ({ params, headers, status: status2 }) => {
|
|||
let hackatimeBanned = false;
|
||||
if (targetUser[0].email) {
|
||||
try {
|
||||
const htUser = await getHackatimeUser(targetUser[0].email);
|
||||
const htUser = await getHackatimeUser(targetUser[0].email, targetUser[0].slackId);
|
||||
if (htUser) {
|
||||
hackatimeSuspected = htUser.suspected || false;
|
||||
hackatimeBanned = htUser.banned || false;
|
||||
|
|
@ -35270,6 +35318,7 @@ admin.get("/reviews/:id", async ({ params, headers }) => {
|
|||
id: usersTable.id,
|
||||
username: usersTable.username,
|
||||
email: usersTable.email,
|
||||
slackId: usersTable.slackId,
|
||||
avatar: usersTable.avatar,
|
||||
internalNotes: usersTable.internalNotes
|
||||
}).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;
|
||||
if (projectUser[0]?.email) {
|
||||
try {
|
||||
const htUser = await getHackatimeUser(projectUser[0].email);
|
||||
const htUser = await getHackatimeUser(projectUser[0].email, projectUser[0].slackId);
|
||||
if (htUser) {
|
||||
hackatimeUserId = htUser.user_id;
|
||||
hackatimeSuspected = htUser.suspected || false;
|
||||
|
|
@ -35573,6 +35622,7 @@ admin.get("/second-pass/:id", async ({ params, headers }) => {
|
|||
id: usersTable.id,
|
||||
username: usersTable.username,
|
||||
email: usersTable.email,
|
||||
slackId: usersTable.slackId,
|
||||
avatar: usersTable.avatar,
|
||||
internalNotes: usersTable.internalNotes
|
||||
}).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;
|
||||
if (projectUser[0]?.email) {
|
||||
try {
|
||||
const htUser = await getHackatimeUser(projectUser[0].email);
|
||||
const htUser = await getHackatimeUser(projectUser[0].email, projectUser[0].slackId);
|
||||
if (htUser) {
|
||||
hackatimeUserId = htUser.user_id;
|
||||
hackatimeSuspected = htUser.suspected || false;
|
||||
|
|
@ -36198,10 +36248,16 @@ admin.get("/orders", async ({ headers, query, status: status2 }) => {
|
|||
}
|
||||
const rows = await ordersQuery;
|
||||
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;
|
||||
await Promise.all(uniqueEmails.map(async (email) => {
|
||||
try {
|
||||
const htUser = await getHackatimeUser(email);
|
||||
const htUser = await getHackatimeUser(email, emailToSlackId.get(email));
|
||||
banMap.set(email, htUser?.banned ?? false);
|
||||
} catch {
|
||||
banMap.set(email, false);
|
||||
|
|
|
|||
|
|
@ -42,12 +42,15 @@ const hackatimeUserCache = new Map<string, HackatimeUser>()
|
|||
// Cache of hackatime user_id -> hackatime user
|
||||
const hackatimeUserIdCache = new Map<number, HackatimeUser>()
|
||||
|
||||
export async function getHackatimeUser(email: string): Promise<HackatimeUser | null> {
|
||||
const cached = hackatimeUserCache.get(email)
|
||||
export async function getHackatimeUser(email: string, slackId?: string | null): Promise<HackatimeUser | null> {
|
||||
const cacheKey = email || slackId || ''
|
||||
const cached = hackatimeUserCache.get(cacheKey)
|
||||
if (cached !== undefined) return cached
|
||||
|
||||
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`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
|
@ -57,13 +60,34 @@ export async function getHackatimeUser(email: string): Promise<HackatimeUser | n
|
|||
},
|
||||
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 }
|
||||
const user_id = parseInt(String(emailData.user_id), 10)
|
||||
if (isNaN(user_id)) return null
|
||||
// If email lookup failed and we have a slack ID, try fuzzy search by slack ID
|
||||
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() 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}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${config.hackatimeAdminKey}`,
|
||||
|
|
@ -81,7 +105,7 @@ export async function getHackatimeUser(email: string): Promise<HackatimeUser | n
|
|||
banned: userObj.banned || false,
|
||||
suspected: userObj.suspected || false
|
||||
}
|
||||
hackatimeUserCache.set(email, user)
|
||||
hackatimeUserCache.set(cacheKey, user)
|
||||
return user
|
||||
} catch {
|
||||
return null
|
||||
|
|
@ -213,14 +237,15 @@ async function syncAllProjects(): Promise<void> {
|
|||
adminProjectsCache.clear()
|
||||
|
||||
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
|
||||
.select({
|
||||
id: projectsTable.id,
|
||||
hackatimeProject: projectsTable.hackatimeProject,
|
||||
hours: projectsTable.hours,
|
||||
userId: projectsTable.userId,
|
||||
userEmail: usersTable.email
|
||||
userEmail: usersTable.email,
|
||||
userSlackId: usersTable.slackId
|
||||
})
|
||||
.from(projectsTable)
|
||||
.innerJoin(usersTable, eq(projectsTable.userId, usersTable.id))
|
||||
|
|
@ -243,7 +268,8 @@ async function syncAllProjects(): Promise<void> {
|
|||
let migrated = 0
|
||||
|
||||
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) {
|
||||
const userId = userProjects[0].userId
|
||||
|
|
@ -414,7 +440,8 @@ export async function syncSingleProject(projectId: number): Promise<{ hours: num
|
|||
hackatimeProject: projectsTable.hackatimeProject,
|
||||
hours: projectsTable.hours,
|
||||
userId: projectsTable.userId,
|
||||
userEmail: usersTable.email
|
||||
userEmail: usersTable.email,
|
||||
userSlackId: usersTable.slackId
|
||||
})
|
||||
.from(projectsTable)
|
||||
.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)
|
||||
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) {
|
||||
await db.delete(sessionsTable).where(eq(sessionsTable.userId, project.userId))
|
||||
|
|
|
|||
|
|
@ -514,7 +514,7 @@ admin.get("/users/:id", async ({ params, headers, status }) => {
|
|||
let hackatimeBanned = false;
|
||||
if (targetUser[0].email) {
|
||||
try {
|
||||
const htUser = await getHackatimeUser(targetUser[0].email);
|
||||
const htUser = await getHackatimeUser(targetUser[0].email, targetUser[0].slackId);
|
||||
if (htUser) {
|
||||
hackatimeSuspected = htUser.suspected || false;
|
||||
hackatimeBanned = htUser.banned || false;
|
||||
|
|
@ -840,6 +840,7 @@ admin.get("/reviews/:id", async ({ params, headers }) => {
|
|||
id: usersTable.id,
|
||||
username: usersTable.username,
|
||||
email: usersTable.email,
|
||||
slackId: usersTable.slackId,
|
||||
avatar: usersTable.avatar,
|
||||
internalNotes: usersTable.internalNotes,
|
||||
})
|
||||
|
|
@ -875,7 +876,7 @@ admin.get("/reviews/:id", async ({ params, headers }) => {
|
|||
let hackatimeBanned = false;
|
||||
if (projectUser[0]?.email) {
|
||||
try {
|
||||
const htUser = await getHackatimeUser(projectUser[0].email);
|
||||
const htUser = await getHackatimeUser(projectUser[0].email, projectUser[0].slackId);
|
||||
if (htUser) {
|
||||
hackatimeUserId = htUser.user_id;
|
||||
hackatimeSuspected = htUser.suspected || false;
|
||||
|
|
@ -1312,6 +1313,7 @@ admin.get("/second-pass/:id", async ({ params, headers }) => {
|
|||
id: usersTable.id,
|
||||
username: usersTable.username,
|
||||
email: usersTable.email,
|
||||
slackId: usersTable.slackId,
|
||||
avatar: usersTable.avatar,
|
||||
internalNotes: usersTable.internalNotes,
|
||||
})
|
||||
|
|
@ -1347,7 +1349,7 @@ admin.get("/second-pass/:id", async ({ params, headers }) => {
|
|||
let hackatimeBanned = false;
|
||||
if (projectUser[0]?.email) {
|
||||
try {
|
||||
const htUser = await getHackatimeUser(projectUser[0].email);
|
||||
const htUser = await getHackatimeUser(projectUser[0].email, projectUser[0].slackId);
|
||||
if (htUser) {
|
||||
hackatimeUserId = htUser.user_id;
|
||||
hackatimeSuspected = htUser.suspected || false;
|
||||
|
|
@ -2331,11 +2333,17 @@ admin.get("/orders", async ({ headers, query, status }) => {
|
|||
|
||||
// Batch-check Hackatime ban status for unique user emails
|
||||
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>();
|
||||
await Promise.all(
|
||||
uniqueEmails.map(async (email) => {
|
||||
try {
|
||||
const htUser = await getHackatimeUser(email);
|
||||
const htUser = await getHackatimeUser(email, emailToSlackId.get(email));
|
||||
banMap.set(email, htUser?.banned ?? false);
|
||||
} catch {
|
||||
banMap.set(email, false);
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ authRoutes.get("/callback", async ({ query, redirect }) => {
|
|||
|
||||
// Check if user is banned on Hackatime
|
||||
try {
|
||||
const hackatimeUser = await getHackatimeUser(identity.primary_email)
|
||||
const hackatimeUser = await getHackatimeUser(identity.primary_email, identity.slack_id)
|
||||
if (hackatimeUser?.banned) {
|
||||
console.log("[AUTH] Hackatime-banned user attempted login:", { userId: user.id, username: user.username, hackatimeUserId: hackatimeUser.user_id })
|
||||
return redirect('https://fraud.land')
|
||||
|
|
|
|||
|
|
@ -35,7 +35,9 @@ hackatime.get('/projects', async ({ headers }) => {
|
|||
}
|
||||
|
||||
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`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
|
@ -46,14 +48,39 @@ hackatime.get('/projects', async ({ headers }) => {
|
|||
body: JSON.stringify({ email: user.email })
|
||||
})
|
||||
|
||||
if (!emailResponse.ok) {
|
||||
const errorText = await emailResponse.text()
|
||||
console.log('[HACKATIME] Email lookup error:', { status: emailResponse.status, body: errorText })
|
||||
return { projects: [] }
|
||||
if (emailResponse.ok) {
|
||||
const emailData = await emailResponse.json() as { user_id: number }
|
||||
hackatimeUserId = emailData.user_id
|
||||
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 }
|
||||
console.log('[HACKATIME] Found hackatime user_id:', hackatimeUserId, 'for email:', user.email)
|
||||
// Fallback: fuzzy search by slack ID if email lookup failed
|
||||
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
|
||||
const projectsParams = new URLSearchParams({
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ import { getHackatimeUser } from '../lib/hackatime-sync'
|
|||
|
||||
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[] = []
|
||||
for (const user of users) {
|
||||
try {
|
||||
const htUser = await getHackatimeUser(user.email)
|
||||
const htUser = await getHackatimeUser(user.email, user.slackId)
|
||||
if (htUser?.banned) continue
|
||||
} catch {
|
||||
// If lookup fails, don't exclude the user
|
||||
|
|
@ -32,6 +32,7 @@ leaderboard.get('/', async ({ query }) => {
|
|||
username: usersTable.username,
|
||||
avatar: usersTable.avatar,
|
||||
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'),
|
||||
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'),
|
||||
|
|
@ -70,6 +71,7 @@ leaderboard.get('/', async ({ query }) => {
|
|||
username: usersTable.username,
|
||||
avatar: usersTable.avatar,
|
||||
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'),
|
||||
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'),
|
||||
|
|
@ -115,6 +117,7 @@ leaderboard.get('/views', async () => {
|
|||
views: projectsTable.views,
|
||||
userId: projectsTable.userId,
|
||||
userEmail: usersTable.email,
|
||||
userSlackId: usersTable.slackId,
|
||||
userRole: usersTable.role
|
||||
})
|
||||
.from(projectsTable)
|
||||
|
|
@ -131,7 +134,7 @@ leaderboard.get('/views', async () => {
|
|||
const filtered: typeof results = []
|
||||
for (const project of results) {
|
||||
try {
|
||||
const htUser = await getHackatimeUser(project.userEmail)
|
||||
const htUser = await getHackatimeUser(project.userEmail, project.userSlackId)
|
||||
if (htUser?.banned) continue
|
||||
} catch {
|
||||
// If lookup fails, don't exclude
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ function parseHackatimeProject(hackatimeProject: string | null): string | null {
|
|||
async function prefixHackatimeIds(
|
||||
hackatimeProject: string | null,
|
||||
email: string,
|
||||
slackId?: string | null,
|
||||
): Promise<string | null> {
|
||||
if (!hackatimeProject) return null;
|
||||
const names = hackatimeProject
|
||||
|
|
@ -51,7 +52,7 @@ async function prefixHackatimeIds(
|
|||
});
|
||||
if (alreadyPrefixed) return hackatimeProject;
|
||||
|
||||
const hackatimeUser = await getHackatimeUser(email);
|
||||
const hackatimeUser = await getHackatimeUser(email, slackId);
|
||||
if (!hackatimeUser || typeof hackatimeUser.user_id !== "number")
|
||||
return hackatimeProject;
|
||||
|
||||
|
|
@ -558,6 +559,7 @@ projects.post("/", async ({ body, headers }) => {
|
|||
const projectName = await prefixHackatimeIds(
|
||||
parseHackatimeProjects(data.hackatimeProject || null),
|
||||
user.email,
|
||||
user.slackId,
|
||||
);
|
||||
const tier =
|
||||
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(
|
||||
parseHackatimeProjects(data.hackatimeProject || null),
|
||||
user.email,
|
||||
user.slackId,
|
||||
);
|
||||
const tier =
|
||||
data.tier !== undefined ? Math.max(1, Math.min(4, data.tier)) : undefined;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue