getcert-backend / api /v2 /_services /publicProfileSystem.ts
ggop213's picture
feat: add free starter courses, Google OAuth integration, and resilient profile sync
6f30bfd
Raw History Blame Contribute Delete
9.78 kB
import { getSupabaseAdmin } from '../_shared/supabaseAdmin.js';
import type {
AggregatedSkillScore,
CertificationTrack,
ProjectEvidence,
PublicProfile,
ValidationSummary,
} from '../_types.js';
type JsonRecord = Record<string, unknown>;
type UserRow = {
id: string;
username: string;
referral_code: string | null;
};
type CertificateRow = {
id: string;
topic: string;
project_title: string | null;
issued_at: string;
metadata: JsonRecord | null;
};
const CANONICAL_PUBLIC_DOMAIN = 'https://getcert.eu.cc';
function appBaseUrl(): string {
const configured = (process.env.CERTIFICATE_VERIFY_BASE_URL || process.env.APP_ORIGIN || '').trim();
if (
!configured ||
configured.includes('hf.space') ||
configured.includes('huggingface.co') ||
configured.includes('vercel.app') ||
configured.includes('localhost')
) {
return CANONICAL_PUBLIC_DOMAIN;
}
return configured.replace(/\/$/, '');
}
function toStringValue(value: unknown): string | null {
if (typeof value !== 'string') {
return null;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
function toNumberValue(value: unknown): number | null {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (typeof value === 'string' && value.trim()) {
const parsed = Number(value);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return null;
}
function getObject(value: unknown): JsonRecord | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null;
}
return value as JsonRecord;
}
function parseValidationSummary(metadata: JsonRecord): ValidationSummary | null {
const nested = getObject(metadata.validation_summary);
if (!nested) {
return null;
}
const suspiciousSignals = Array.isArray(nested.suspicious_signals)
? nested.suspicious_signals.filter((item): item is string => typeof item === 'string')
: [];
return {
overallPassed: Boolean(nested.overall_passed),
commitPassed: Boolean(nested.commit_passed),
readmePassed: Boolean(nested.readme_passed),
proofPassed: Boolean(nested.proof_passed),
commitCount: Math.round(toNumberValue(nested.commit_count) ?? 0),
fileCount: Math.round(toNumberValue(nested.file_count) ?? 0),
meaningfulFileCount: Math.round(toNumberValue(nested.meaningful_file_count) ?? 0),
readmeRichness: Math.round(toNumberValue(nested.readme_richness) ?? 0),
suspiciousSignals,
};
}
function getDifficultyWeight(projectType: string | null): number {
if (projectType === 'data') {
return 1.3;
}
if (projectType === 'python') {
return 1.15;
}
if (projectType === 'web' || projectType === 'fullstack') {
return 1.2;
}
return 1;
}
function getDifficultyLevel(weight: number): 'beginner' | 'intermediate' | 'advanced' {
if (weight >= 1.26) {
return 'advanced';
}
if (weight >= 1.16) {
return 'intermediate';
}
return 'beginner';
}
function roundValue(value: number): number {
return Math.round(value * 100) / 100;
}
function buildPassConfidence(finalScore: number, summary: ValidationSummary | null): number {
if (!summary) {
return Math.max(0, Math.min(100, Math.round(finalScore * 0.8)));
}
let confidence = finalScore * 0.6;
confidence += summary.overallPassed ? 22 : 10;
confidence += Math.min(10, summary.commitCount * 0.6);
confidence += summary.readmePassed ? 8 : 2;
return Math.max(0, Math.min(100, Math.round(confidence)));
}
export async function getPublicProfile(userId: string): Promise<PublicProfile> {
const supabase = getSupabaseAdmin();
const base = appBaseUrl();
const { data: user, error: userError } = await supabase
.from('users')
.select('id, username, referral_code')
.eq('id', userId)
.single();
if (userError || !user) {
throw new Error('User not found');
}
const { data: certificates, error: certError } = await supabase
.from('certificates')
.select('id, topic, project_title, issued_at, metadata')
.eq('user_id', userId)
.order('issued_at', { ascending: false });
const { count: referredUsersCount } = await supabase
.from('users')
.select('id', { count: 'exact', head: true })
.eq('referred_by', userId);
if (certError) {
throw new Error(`Failed to fetch certificates: ${certError.message}`);
}
const rows = (certificates || []) as CertificateRow[];
const allProjects: ProjectEvidence[] = rows.map((item) => {
const metadata = item.metadata || {};
const validationSummary = parseValidationSummary(metadata);
const projectType = toStringValue(metadata.project_type);
const difficultyWeight = getDifficultyWeight(projectType);
const finalScore = Math.round(toNumberValue(metadata.final_score) ?? 0);
const commitCount = Math.round(
toNumberValue(metadata.commit_count) ??
toNumberValue(validationSummary?.commitCount) ??
0
);
const fileCount = Math.round(toNumberValue(validationSummary?.fileCount) ?? 0);
const readmeScore = Math.round(toNumberValue(validationSummary?.readmeRichness) ?? 0);
const track = (toStringValue(metadata.certification_track) || 'verifiedcert') as CertificationTrack;
return {
certificateId: toStringValue(metadata.certificate_id),
certificateDbId: item.id,
skill: item.topic,
projectTitle: item.project_title,
projectType,
difficultyLevel: getDifficultyLevel(difficultyWeight),
difficultyWeight,
githubRepoUrl: toStringValue(metadata.github_repo_url) || toStringValue(metadata.github_url),
liveDemoUrl: toStringValue(metadata.live_demo_url) || toStringValue(metadata.proof_url),
finalScore,
commitCount,
fileCount,
validationSummary,
passConfidence: buildPassConfidence(finalScore, validationSummary),
issuedAt: item.issued_at,
track,
trustSignals: {
commitCount,
validationPassed: validationSummary ? validationSummary.overallPassed : finalScore >= 60,
readmeScore,
projectType,
},
};
});
const projects = allProjects.filter((p) => p.track === 'verifiedcert');
const skillAccumulator = new Map<string, {
totalScore: number;
weightedScore: number;
totalWeight: number;
count: number;
}>();
for (const project of projects) {
const current = skillAccumulator.get(project.skill) || {
totalScore: 0,
weightedScore: 0,
totalWeight: 0,
count: 0,
};
current.totalScore += project.finalScore;
current.weightedScore += project.finalScore * project.difficultyWeight;
current.totalWeight += project.difficultyWeight;
current.count += 1;
skillAccumulator.set(project.skill, current);
}
const aggregatedSkills: AggregatedSkillScore[] = Array.from(skillAccumulator.entries())
.map(([skill, aggregate]) => ({
skill,
averageScore: roundValue(aggregate.totalScore / Math.max(1, aggregate.count)),
weightedScore: roundValue(aggregate.weightedScore / Math.max(1, aggregate.totalWeight)),
projectCount: aggregate.count,
totalWeight: roundValue(aggregate.totalWeight),
}))
.sort((left, right) => right.weightedScore - left.weightedScore);
const totalCertificates = rows.length;
const verifiedProjects = projects;
const verifiedCount = verifiedProjects.length;
const totalCommitCount = verifiedProjects.reduce((sum, project) => sum + project.commitCount, 0);
const averageFinalScore = verifiedCount
? roundValue(verifiedProjects.reduce((sum, project) => sum + project.finalScore, 0) / verifiedCount)
: 0;
const averageReadmeScore = verifiedCount
? roundValue(verifiedProjects.reduce((sum, project) => sum + project.trustSignals.readmeScore, 0) / verifiedCount)
: 0;
const validationPassRate = verifiedCount
? roundValue(
(verifiedProjects.filter((project) => project.trustSignals.validationPassed).length / verifiedCount) * 100
)
: 0;
const certificatesPayload = rows.map((item) => {
const metadata = item.metadata || {};
const projectType = toStringValue(metadata.project_type);
const track = (toStringValue(metadata.certification_track) || 'verifiedcert') as CertificationTrack;
return {
id: item.id,
skill: item.topic,
projectTitle: item.project_title,
issuedAt: item.issued_at,
githubUrl: toStringValue(metadata.github_repo_url) || toStringValue(metadata.github_url),
proofUrl: toStringValue(metadata.live_demo_url) || toStringValue(metadata.proof_url),
certificateId: toStringValue(metadata.certificate_id),
finalScore: Math.round(toNumberValue(metadata.final_score) ?? 0),
difficultyLevel: getDifficultyLevel(getDifficultyWeight(projectType)),
track,
};
});
const quickCertCount = allProjects.filter((p) => p.track === 'quickcert').length;
const verifiedCertCount = verifiedCount;
return {
userId,
username: (user as UserRow).username,
referralCode: (user as UserRow).referral_code,
referredUsersCount: referredUsersCount || 0,
certificates: certificatesPayload,
aggregatedSkills,
projects,
trustMetrics: {
totalCertificates,
averageFinalScore,
totalCommitCount,
validationPassRate,
averageReadmeScore,
topSkill: aggregatedSkills[0]?.skill || null,
quickCertCount,
verifiedCertCount,
},
shareLinks: {
profile: `${base}/user/${encodeURIComponent(userId)}`,
recruiter: `${base}/recruiter/${encodeURIComponent(userId)}`,
referral: (user as UserRow).referral_code
? `${base}/?ref=${encodeURIComponent((user as UserRow).referral_code as string)}`
: null,
},
};
}