Add achievement activation API
cgen-878a1b949b5f4973be29e9040d53ed7f
This commit is contained in:
parent
c8f34e51c4
commit
4e0af56bff
1 changed files with 204 additions and 0 deletions
204
api/achievements/activate.ts
Normal file
204
api/achievements/activate.ts
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
import type { VercelRequest, VercelResponse } from "@vercel/node";
|
||||
import { randomUUID } from "crypto";
|
||||
import { getAdminClient } from "../../api/_supabase";
|
||||
|
||||
const CORE_ACHIEVEMENTS = [
|
||||
{
|
||||
id: "welcome-to-aethex",
|
||||
name: "Welcome to AeThex",
|
||||
description: "Completed onboarding and joined the AeThex network.",
|
||||
icon: "🎉",
|
||||
badgeColor: "#7C3AED",
|
||||
xpReward: 250,
|
||||
},
|
||||
{
|
||||
id: "aethex-explorer",
|
||||
name: "AeThex Explorer",
|
||||
description: "Engaged with community initiatives and posted first update.",
|
||||
icon: "🧭",
|
||||
badgeColor: "#0EA5E9",
|
||||
xpReward: 400,
|
||||
},
|
||||
{
|
||||
id: "community-champion",
|
||||
name: "Community Champion",
|
||||
description: "Contributed feedback, resolved bugs, and mentored squads.",
|
||||
icon: "🏆",
|
||||
badgeColor: "#22C55E",
|
||||
xpReward: 750,
|
||||
},
|
||||
{
|
||||
id: "workshop-architect",
|
||||
name: "Workshop Architect",
|
||||
description: "Published a high-impact mod or toolkit adopted by teams.",
|
||||
icon: "🛠️",
|
||||
badgeColor: "#F97316",
|
||||
xpReward: 1200,
|
||||
},
|
||||
{
|
||||
id: "god-mode",
|
||||
name: "GOD Mode",
|
||||
description: "Legendary status awarded by AeThex studio leadership.",
|
||||
icon: "⚡",
|
||||
badgeColor: "#FACC15",
|
||||
xpReward: 5000,
|
||||
},
|
||||
] as const;
|
||||
|
||||
const DEFAULT_TARGET_EMAIL = "mrpiglr@gmail.com";
|
||||
const DEFAULT_TARGET_USERNAME = "mrpiglr";
|
||||
|
||||
export default async function handler(
|
||||
req: VercelRequest,
|
||||
res: VercelResponse,
|
||||
) {
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
const { targetEmail, targetUsername } = (req.body || {}) as {
|
||||
targetEmail?: string;
|
||||
targetUsername?: string;
|
||||
};
|
||||
|
||||
try {
|
||||
const admin = getAdminClient();
|
||||
const nowIso = new Date().toISOString();
|
||||
|
||||
// Ensure core achievements exist
|
||||
const achievementResults = await Promise.all(
|
||||
CORE_ACHIEVEMENTS.map(async (achievement) => {
|
||||
const { error } = await admin
|
||||
.from("achievements")
|
||||
.upsert(
|
||||
{
|
||||
id: achievement.id,
|
||||
name: achievement.name,
|
||||
description: achievement.description,
|
||||
icon: achievement.icon,
|
||||
badge_color: achievement.badgeColor,
|
||||
xp_reward: achievement.xpReward,
|
||||
created_at: nowIso,
|
||||
},
|
||||
{ onConflict: "id" },
|
||||
);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
return achievement.id;
|
||||
}),
|
||||
);
|
||||
|
||||
// Normalise profile progression defaults
|
||||
await Promise.all([
|
||||
admin
|
||||
.from("user_profiles")
|
||||
.update({ level: 1 })
|
||||
.is("level", null),
|
||||
admin
|
||||
.from("user_profiles")
|
||||
.update({ total_xp: 0 })
|
||||
.is("total_xp", null),
|
||||
admin
|
||||
.from("user_profiles")
|
||||
.update({ loyalty_points: 0 })
|
||||
.is("loyalty_points", null),
|
||||
admin
|
||||
.from("user_profiles")
|
||||
.update({ user_type: "game_developer" })
|
||||
.neq("user_type", "game_developer"),
|
||||
]);
|
||||
|
||||
// Locate target user
|
||||
const normalizedEmail = (targetEmail || DEFAULT_TARGET_EMAIL).toLowerCase();
|
||||
const normalizedUsername = (targetUsername || DEFAULT_TARGET_USERNAME).toLowerCase();
|
||||
|
||||
let targetUserId: string | null = null;
|
||||
|
||||
try {
|
||||
const { data, error } = await (admin.auth as any).admin.listUsers({
|
||||
email: normalizedEmail,
|
||||
});
|
||||
if (!error && data?.users?.length) {
|
||||
const match = data.users.find(
|
||||
(user: any) => user.email?.toLowerCase() === normalizedEmail,
|
||||
);
|
||||
if (match) {
|
||||
targetUserId = match.id;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to query auth users for GOD mode activation", error);
|
||||
}
|
||||
|
||||
if (!targetUserId) {
|
||||
const { data: profileRow } = await admin
|
||||
.from("user_profiles")
|
||||
.select("id, username")
|
||||
.ilike("username", `${normalizedUsername}%`)
|
||||
.maybeSingle();
|
||||
|
||||
if (profileRow?.id) {
|
||||
targetUserId = profileRow.id;
|
||||
}
|
||||
}
|
||||
|
||||
let godModeAwarded = false;
|
||||
if (targetUserId) {
|
||||
const progressStats = {
|
||||
level: 100,
|
||||
total_xp: 99000,
|
||||
loyalty_points: 99000,
|
||||
current_streak: 365,
|
||||
longest_streak: 365,
|
||||
};
|
||||
|
||||
await admin
|
||||
.from("user_profiles")
|
||||
.update({
|
||||
user_type: "game_developer",
|
||||
...progressStats,
|
||||
last_streak_at: nowIso.split("T")[0],
|
||||
updated_at: nowIso,
|
||||
})
|
||||
.eq("id", targetUserId);
|
||||
|
||||
const { data: existingGodMode } = await admin
|
||||
.from("user_achievements")
|
||||
.select("id")
|
||||
.eq("user_id", targetUserId)
|
||||
.eq("achievement_id", "god-mode")
|
||||
.maybeSingle();
|
||||
|
||||
if (!existingGodMode) {
|
||||
const { error: insertError } = await admin
|
||||
.from("user_achievements")
|
||||
.insert({
|
||||
id: randomUUID(),
|
||||
user_id: targetUserId,
|
||||
achievement_id: "god-mode",
|
||||
earned_at: nowIso,
|
||||
});
|
||||
|
||||
if (insertError) {
|
||||
throw insertError;
|
||||
}
|
||||
}
|
||||
|
||||
godModeAwarded = true;
|
||||
}
|
||||
|
||||
return res.json({
|
||||
ok: true,
|
||||
achievementsSeeded: achievementResults.length,
|
||||
godModeAwarded,
|
||||
targetUserId,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("activate achievements error", error);
|
||||
return res.status(500).json({
|
||||
error: error?.message || "Failed to activate achievements",
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue