import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.1";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers":
"authorization, x-client-info, apikey, content-type, x-supabase-client-platform",
};
const log = (step: string, details?: unknown) => {
const str = details ? ` - ${JSON.stringify(details)}` : "";
console.log(`[wechat-miniapp-auth] ${step}${str}`);
};
async function generatePassword(openid: string): Promise<string> {
const secret = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const key = await crypto.subtle.importKey(
"raw", new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" }, false, ["sign"]
);
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(openid));
return Array.from(new Uint8Array(sig))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders });
}
try {
const { wxCode, phoneCode } = await req.json();
if (!wxCode || !phoneCode) {
return new Response(
JSON.stringify({ error: "wxCode and phoneCode are required" }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 400 }
);
}
const appId = Deno.env.get("SUPERUN_WECHAT_APP_ID");
const appSecret = Deno.env.get("SUPERUN_WECHAT_APP_SECRET");
if (!appId || !appSecret) {
log("ERROR", "Missing WECHAT credentials");
return new Response(
JSON.stringify({ error: "Server configuration error" }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 500 }
);
}
log("Step 1: Exchange wx code for openid");
const sessionRes = await fetch(
`https://api.weixin.qq.com/sns/jscode2session?appid=${appId}&secret=${appSecret}&js_code=${wxCode}&grant_type=authorization_code`
);
const sessionData = await sessionRes.json();
if (sessionData.errcode) {
log("ERROR jscode2session", { errcode: sessionData.errcode, errmsg: sessionData.errmsg });
return new Response(
JSON.stringify({ error: sessionData.errmsg || "WeChat session error" }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 400 }
);
}
const openid: string = sessionData.openid;
log("Got openid", { openid: openid.substring(0, 8) + "..." });
log("Step 2: Get access_token");
const tokenRes = await fetch(
`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}`
);
const tokenData = await tokenRes.json();
if (tokenData.errcode) {
log("ERROR get_token", { errcode: tokenData.errcode, errmsg: tokenData.errmsg });
return new Response(
JSON.stringify({ error: tokenData.errmsg || "WeChat token error" }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 400 }
);
}
log("Step 3: Get phone number");
const phoneRes = await fetch(
`https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=${tokenData.access_token}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ code: phoneCode }),
}
);
const phoneData = await phoneRes.json();
if (phoneData.errcode !== 0) {
log("ERROR getPhoneNumber", { errcode: phoneData.errcode, errmsg: phoneData.errmsg });
return new Response(
JSON.stringify({ error: phoneData.errmsg || "Phone number error" }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 400 }
);
}
const purePhone = phoneData.phone_info.purePhoneNumber;
const countryCode = phoneData.phone_info.countryCode;
const fullPhone = `+${countryCode}${purePhone}`;
log("Got phone", { phone: purePhone.substring(0, 3) + "****" });
log("Step 4: Supabase auth");
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
const serviceRoleKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const anonKey = Deno.env.get("SUPABASE_ANON_KEY")!;
const adminClient = createClient(supabaseUrl, serviceRoleKey);
const anonClient = createClient(supabaseUrl, anonKey);
const syntheticEmail = `wx_${openid}@miniapp.local`;
const password = await generatePassword(openid);
let signInResult = await anonClient.auth.signInWithPassword({ email: syntheticEmail, password });
if (signInResult.error) {
log("User not found, creating");
const { error: createError } = await adminClient.auth.admin.createUser({
email: syntheticEmail, email_confirm: true, password,
user_metadata: { openid, phone: fullPhone, nickname: "{{DEFAULT_NICKNAME}}" },
});
if (createError) {
log("ERROR createUser", { message: createError.message });
return new Response(
JSON.stringify({ error: createError.message }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 500 }
);
}
signInResult = await anonClient.auth.signInWithPassword({ email: syntheticEmail, password });
if (signInResult.error) {
log("ERROR signIn after create", { message: signInResult.error.message });
return new Response(
JSON.stringify({ error: "登录失败,请重试" }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 500 }
);
}
}
log("Login success", { userId: signInResult.data.user?.id?.substring(0, 8) + "..." });
return new Response(
JSON.stringify({
session: {
access_token: signInResult.data.session!.access_token,
refresh_token: signInResult.data.session!.refresh_token,
},
openid,
}),
{ headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 200 }
);
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
log("ERROR", { message: msg });
return new Response(JSON.stringify({ error: msg }), {
headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 500,
});
}
});