101 lines
3.1 KiB
TypeScript
101 lines
3.1 KiB
TypeScript
import type { VercelRequest, VercelResponse } from "@vercel/node";
|
|
import { createClient } from "@supabase/supabase-js";
|
|
import { verifyMessage } from "ethers";
|
|
|
|
const supabase = createClient(
|
|
process.env.SUPABASE_URL || "",
|
|
process.env.SUPABASE_SERVICE_ROLE || "",
|
|
);
|
|
|
|
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
if (req.method !== "POST") {
|
|
res.setHeader("Allow", "POST");
|
|
return res.status(405).json({ error: "Method not allowed" });
|
|
}
|
|
|
|
try {
|
|
const authHeader = req.headers.authorization;
|
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
return res.status(401).json({ error: "Missing authorization header" });
|
|
}
|
|
|
|
const token = authHeader.substring(7);
|
|
|
|
// Verify token with Supabase
|
|
const { data: userData, error: authError } =
|
|
await supabase.auth.getUser(token);
|
|
if (authError || !userData.user) {
|
|
return res.status(401).json({ error: "Invalid token" });
|
|
}
|
|
|
|
const { wallet_address, signature, message, nonce } = req.body;
|
|
|
|
if (!wallet_address || !signature || !message || !nonce) {
|
|
return res.status(400).json({ error: "Missing required fields" });
|
|
}
|
|
|
|
const normalizedAddress = wallet_address.toLowerCase();
|
|
|
|
// Verify the signature
|
|
let recoveredAddress: string;
|
|
try {
|
|
recoveredAddress = verifyMessage(message, signature).toLowerCase();
|
|
} catch (error) {
|
|
return res.status(401).json({ error: "Invalid signature" });
|
|
}
|
|
|
|
if (recoveredAddress !== normalizedAddress) {
|
|
return res.status(401).json({ error: "Signature does not match wallet" });
|
|
}
|
|
|
|
// Verify nonce
|
|
const { data: nonceData, error: nonceError } = await supabase
|
|
.from("web3_nonces")
|
|
.select("*")
|
|
.eq("wallet_address", normalizedAddress)
|
|
.eq("nonce", nonce)
|
|
.single();
|
|
|
|
if (nonceError || !nonceData) {
|
|
return res.status(401).json({ error: "Invalid or expired nonce" });
|
|
}
|
|
|
|
// Mark nonce as used
|
|
await supabase
|
|
.from("web3_nonces")
|
|
.update({ used_at: new Date().toISOString() })
|
|
.eq("nonce", nonce);
|
|
|
|
// Link wallet to existing user
|
|
const { error: linkError } = await supabase.from("web3_wallets").insert({
|
|
user_id: userData.user.id,
|
|
wallet_address: normalizedAddress,
|
|
chain_id: 1, // Ethereum mainnet
|
|
});
|
|
|
|
if (
|
|
linkError &&
|
|
!linkError.message.includes("violates unique constraint")
|
|
) {
|
|
console.error("Failed to link wallet:", linkError);
|
|
return res.status(500).json({ error: "Failed to link wallet" });
|
|
}
|
|
|
|
// Also update user_profiles wallet_address column if available
|
|
await supabase
|
|
.from("user_profiles")
|
|
.update({ wallet_address: normalizedAddress })
|
|
.eq("auth_id", userData.user.id);
|
|
|
|
return res.status(200).json({
|
|
success: true,
|
|
message: "Web3 wallet linked successfully",
|
|
wallet_address: normalizedAddress,
|
|
});
|
|
} catch (error: any) {
|
|
console.error("Link Web3 error:", error);
|
|
return res.status(500).json({
|
|
error: error?.message || "Failed to link wallet",
|
|
});
|
|
}
|
|
}
|