add fraud check and airlock

This commit is contained in:
Nathan 2026-02-18 10:59:51 -05:00
parent c46fd4d92e
commit e737cf7648
4 changed files with 85 additions and 4 deletions

View file

@ -41,7 +41,7 @@ interface HackatimeStatsResponse {
// Cache of email -> hackatime user to avoid repeated lookups
const hackatimeUserCache = new Map<string, HackatimeUser>()
async function getHackatimeUser(email: string): Promise<HackatimeUser | null> {
export async function getHackatimeUser(email: string): Promise<HackatimeUser | null> {
const cached = hackatimeUserCache.get(email)
if (cached !== undefined) return cached

View file

@ -10,7 +10,7 @@ import { projectActivityTable } from '../schemas/activity'
import { getUserFromSession } from '../lib/auth'
import { calculateScrapsFromHours, getUserScrapsBalance } from '../lib/scraps'
import { payoutPendingScraps, getNextPayoutDate } from '../lib/scraps-payout'
import { syncSingleProject } from '../lib/hackatime-sync'
import { syncSingleProject, getHackatimeUser } from '../lib/hackatime-sync'
import { notifyProjectReview } from '../lib/slack'
import { config } from '../config'
import { computeEffectiveHours, getProjectShippedDates, hasProjectBeenShipped, computeEffectiveHoursForProject } from '../lib/effective-hours'
@ -480,6 +480,7 @@ admin.get('/reviews/:id', async ({ params, headers }) => {
.select({
id: usersTable.id,
username: usersTable.username,
email: usersTable.email,
avatar: usersTable.avatar,
internalNotes: usersTable.internalNotes
})
@ -501,6 +502,15 @@ admin.get('/reviews/:id', async ({ params, headers }) => {
.where(inArray(usersTable.id, reviewerIds))
}
// Look up Hackatime user ID
let hackatimeUserId: number | null = null
if (projectUser[0]?.email) {
console.log('[ADMIN] Looking up hackatime user for:', projectUser[0].email)
const htUser = await getHackatimeUser(projectUser[0].email)
console.log('[ADMIN] Hackatime user result:', htUser)
if (htUser) hackatimeUserId = htUser.user_id
}
const isAdmin = user.role === 'admin'
// Hide pending_admin_approval from non-admin reviewers
const maskedProject = (!isAdmin && project[0].status === 'pending_admin_approval')
@ -514,9 +524,11 @@ admin.get('/reviews/:id', async ({ params, headers }) => {
return {
project: maskedProject,
hackatimeUserId,
user: projectUser[0] ? {
id: projectUser[0].id,
username: projectUser[0].username,
email: projectUser[0].email,
avatar: projectUser[0].avatar,
internalNotes: projectUser[0].internalNotes
} : null,
@ -869,6 +881,7 @@ admin.get('/second-pass/:id', async ({ params, headers }) => {
.select({
id: usersTable.id,
username: usersTable.username,
email: usersTable.email,
avatar: usersTable.avatar,
internalNotes: usersTable.internalNotes
})
@ -890,14 +903,23 @@ admin.get('/second-pass/:id', async ({ params, headers }) => {
.where(inArray(usersTable.id, reviewerIds))
}
// Look up Hackatime user ID
let hackatimeUserId: number | null = null
if (projectUser[0]?.email) {
const htUser = await getHackatimeUser(projectUser[0].email)
if (htUser) hackatimeUserId = htUser.user_id
}
// Calculate effective hours and overlapping projects
const effectiveHoursData = await computeEffectiveHoursForProject(project[0])
return {
project: project[0],
hackatimeUserId,
user: projectUser[0] ? {
id: projectUser[0].id,
username: projectUser[0].username,
email: projectUser[0].email,
avatar: projectUser[0].avatar,
internalNotes: projectUser[0].internalNotes
} : null,

View file

@ -16,7 +16,10 @@
Bot,
Loader,
ArrowLeft,
MessageSquare
MessageSquare,
ShieldAlert,
Clipboard,
Lock
} from '@lucide/svelte';
import ProjectPlaceholder from '$lib/components/ProjectPlaceholder.svelte';
import { getUser } from '$lib/auth-client';
@ -87,6 +90,7 @@
let projectUser = $state<ProjectUser | null>(null);
let reviews = $state<Review[]>([]);
let overlappingProjects = $state<OverlappingProject[]>([]);
let hackatimeUserId = $state<number | null>(null);
let loading = $state(true);
let submitting = $state(false);
let savingNotes = $state(false);
@ -140,6 +144,7 @@
projectUser = data.user;
reviews = data.reviews || [];
overlappingProjects = data.overlappingProjects || [];
hackatimeUserId = data.hackatimeUserId ?? null;
userInternalNotes = data.user?.internalNotes || '';
// Check if project is deleted
@ -535,6 +540,31 @@
<span>{syncing ? 'syncing...' : 'sync hours'}</span>
</button>
{/if}
{#if hackatimeUserId}
<a
href="https://joe.fraud.hackclub.com/profile/{hackatimeUserId}"
target="_blank"
rel="noopener noreferrer"
class="inline-flex cursor-pointer items-center gap-2 rounded-full border-4 border-black px-4 py-2 font-bold transition-all duration-200 hover:border-dashed"
>
<ShieldAlert size={18} />
<span>fraud check</span>
</a>
{/if}
{#if project.githubUrl}
<button
onclick={async () => {
if (project?.githubUrl) {
await navigator.clipboard.writeText(project.githubUrl);
window.open('https://airlock.hackclub.com/', '_blank');
}
}}
class="inline-flex cursor-pointer items-center gap-2 rounded-full border-4 border-black px-4 py-2 font-bold transition-all duration-200 hover:border-dashed"
>
<Lock size={18} />
<span>airlock</span>
</button>
{/if}
</div>
<!-- User Info (clickable) -->

View file

@ -14,7 +14,9 @@
ArrowLeft,
RefreshCw,
Bot,
MessageSquare
MessageSquare,
ShieldAlert,
Lock
} from '@lucide/svelte';
import ProjectPlaceholder from '$lib/components/ProjectPlaceholder.svelte';
import { getUser } from '$lib/auth-client';
@ -82,6 +84,7 @@
let projectUser = $state<ProjectUser | null>(null);
let reviews = $state<Review[]>([]);
let overlappingProjects = $state<OverlappingProject[]>([]);
let hackatimeUserId = $state<number | null>(null);
let loading = $state(true);
let submitting = $state(false);
let error = $state<string | null>(null);
@ -129,6 +132,7 @@
projectUser = data.user;
reviews = data.reviews || [];
overlappingProjects = data.overlappingProjects || [];
hackatimeUserId = data.hackatimeUserId ?? null;
if (data.project?.hoursOverride != null) {
hoursOverride = data.project.hoursOverride;
}
@ -412,6 +416,31 @@
<span>try it out</span>
</span>
{/if}
{#if hackatimeUserId}
<a
href="https://joe.fraud.hackclub.com/profile/{hackatimeUserId}"
target="_blank"
rel="noopener noreferrer"
class="inline-flex cursor-pointer items-center gap-2 rounded-full border-4 border-black px-4 py-2 font-bold transition-all duration-200 hover:border-dashed"
>
<ShieldAlert size={18} />
<span>fraud check</span>
</a>
{/if}
{#if project.githubUrl}
<button
onclick={async () => {
if (project?.githubUrl) {
await navigator.clipboard.writeText(project.githubUrl);
window.open('https://airlock.hackclub.com/', '_blank');
}
}}
class="inline-flex cursor-pointer items-center gap-2 rounded-full border-4 border-black px-4 py-2 font-bold transition-all duration-200 hover:border-dashed"
>
<Lock size={18} />
<span>airlock</span>
</button>
{/if}
</div>
<!-- User Info (clickable) -->