516 lines
18 KiB
JavaScript
516 lines
18 KiB
JavaScript
/**
|
||
* 认证相关前端封装:注册 / 登录 / 游客登录走本站 BFF,token 等写入 cookie。
|
||
* 未注册用户自动游客登录(POST /api/auth/guest → 上游 /api/app/auth/guest)。
|
||
*/
|
||
(function (global) {
|
||
var UUID_KEY = "uuid";
|
||
var DEV_ID_KEY = "guest_dev_id";
|
||
var CHANNEL_KEY = "channel";
|
||
var GUEST_LOGOUT_FLAG_KEY = "guest_logout_manual";
|
||
var guestLoginPromise = null;
|
||
var volatileDevID = "";
|
||
/** 上游约定:token 失效需重新登录 */
|
||
var TOKEN_EXPIRED_CODE = 1000;
|
||
var tokenRecoveryPromise = null;
|
||
|
||
function toast(message, type) {
|
||
if (typeof global.showToast === "function") {
|
||
global.showToast(message, type || "info");
|
||
return;
|
||
}
|
||
// 调试兜底:无 toast 组件时用简易提示
|
||
try {
|
||
var tip = String(message || "").trim();
|
||
if (!tip) return;
|
||
var el = document.getElementById("auth-debug-toast");
|
||
if (!el) {
|
||
el = document.createElement("div");
|
||
el.id = "auth-debug-toast";
|
||
el.setAttribute("role", "status");
|
||
el.style.cssText =
|
||
"position:fixed;left:50%;bottom:48px;transform:translateX(-50%);z-index:99999;" +
|
||
"max-width:86vw;padding:10px 16px;border-radius:8px;font-size:14px;line-height:1.4;" +
|
||
"color:#fff;background:rgba(20,20,20,.88);box-shadow:0 8px 24px rgba(0,0,0,.25);";
|
||
document.body.appendChild(el);
|
||
}
|
||
el.textContent = tip;
|
||
el.style.background =
|
||
type === "error"
|
||
? "rgba(180,40,40,.92)"
|
||
: type === "success"
|
||
? "rgba(30,120,70,.92)"
|
||
: "rgba(20,20,20,.88)";
|
||
el.style.display = "block";
|
||
clearTimeout(el._hideTimer);
|
||
el._hideTimer = setTimeout(function () {
|
||
el.style.display = "none";
|
||
}, 2600);
|
||
} catch (e) {}
|
||
}
|
||
|
||
function getInviteCode() {
|
||
try {
|
||
var params = new URLSearchParams(global.location.search || "");
|
||
return (
|
||
params.get("inviteCode") ||
|
||
params.get("invite") ||
|
||
params.get("code") ||
|
||
""
|
||
).trim();
|
||
} catch (e) {
|
||
return "";
|
||
}
|
||
}
|
||
|
||
function hasSessionToken() {
|
||
return !!(global.CookieUtil && global.CookieUtil.getToken && global.CookieUtil.getToken());
|
||
}
|
||
|
||
// 与 91porn-h5-client generateUid 保持一致:16 位十六进制随机串 + 毫秒时间戳。
|
||
function generateUUID() {
|
||
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||
var uuid = [];
|
||
for (var i = 0; i < 16; i++) {
|
||
uuid[i] = chars.charAt(Math.floor(Math.random() * 16));
|
||
}
|
||
return uuid.join("") + Date.now();
|
||
}
|
||
|
||
function syncDevID(devID) {
|
||
if (!devID) return;
|
||
try {
|
||
localStorage.setItem(UUID_KEY, devID);
|
||
// 保留旧 key,兼容已上线页面和服务端游客 cookie。
|
||
localStorage.setItem(DEV_ID_KEY, devID);
|
||
} catch (e) {}
|
||
try {
|
||
if (global.CookieUtil && typeof global.CookieUtil.set === "function") {
|
||
global.CookieUtil.set(DEV_ID_KEY, devID, { days: 365 });
|
||
}
|
||
} catch (e2) {}
|
||
}
|
||
|
||
function getOrCreateDevID() {
|
||
var devID = "";
|
||
try {
|
||
devID = (localStorage.getItem(UUID_KEY) || "").trim();
|
||
} catch (e) {}
|
||
if (devID) {
|
||
syncDevID(devID);
|
||
return devID;
|
||
}
|
||
|
||
// 首屏 SSR 可能已生成同规则 UUID 并写入 cookie,优先复用以保持游客身份一致。
|
||
var fromCookie =
|
||
global.CookieUtil && typeof global.CookieUtil.get === "function"
|
||
? global.CookieUtil.get(DEV_ID_KEY)
|
||
: "";
|
||
if (fromCookie) {
|
||
syncDevID(fromCookie);
|
||
return fromCookie;
|
||
}
|
||
|
||
if (!volatileDevID) {
|
||
volatileDevID = generateUUID();
|
||
}
|
||
syncDevID(volatileDevID);
|
||
return volatileDevID;
|
||
}
|
||
|
||
function getChannel() {
|
||
if (
|
||
global.ChannelAttribution &&
|
||
typeof global.ChannelAttribution.get === "function"
|
||
) {
|
||
return global.ChannelAttribution.get();
|
||
}
|
||
|
||
var channel = "";
|
||
try {
|
||
channel = (localStorage.getItem(CHANNEL_KEY) || "").trim();
|
||
if (channel) {
|
||
if (global.CookieUtil && typeof global.CookieUtil.set === "function") {
|
||
global.CookieUtil.set(CHANNEL_KEY, channel, { days: 365 });
|
||
}
|
||
return channel;
|
||
}
|
||
} catch (e) {}
|
||
try {
|
||
channel =
|
||
global.CookieUtil && typeof global.CookieUtil.get === "function"
|
||
? String(global.CookieUtil.get(CHANNEL_KEY) || "").trim()
|
||
: "";
|
||
if (channel) {
|
||
try {
|
||
localStorage.setItem(CHANNEL_KEY, channel);
|
||
} catch (e2) {}
|
||
return channel;
|
||
}
|
||
} catch (e3) {}
|
||
try {
|
||
var params = new URLSearchParams(global.location.search || "");
|
||
channel = (params.get("dc") || params.get("ch") || params.get("pc") || "").trim();
|
||
if (channel) {
|
||
localStorage.setItem(CHANNEL_KEY, channel);
|
||
if (global.CookieUtil && typeof global.CookieUtil.set === "function") {
|
||
global.CookieUtil.set(CHANNEL_KEY, channel, { days: 365 });
|
||
}
|
||
}
|
||
} catch (e4) {}
|
||
return channel;
|
||
}
|
||
|
||
function clearGuestLogoutFlag() {
|
||
try {
|
||
sessionStorage.removeItem(GUEST_LOGOUT_FLAG_KEY);
|
||
} catch (e) {}
|
||
try {
|
||
if (global.CookieUtil && typeof global.CookieUtil.remove === "function") {
|
||
global.CookieUtil.remove("guest_skip_auto");
|
||
}
|
||
} catch (e2) {}
|
||
}
|
||
|
||
function markGuestLogout() {
|
||
try {
|
||
sessionStorage.setItem(GUEST_LOGOUT_FLAG_KEY, "1");
|
||
} catch (e) {}
|
||
try {
|
||
if (global.CookieUtil && typeof global.CookieUtil.set === "function") {
|
||
// 不设 days → session cookie,配合 SSR 跳过本次自动游客
|
||
global.CookieUtil.set("guest_skip_auto", "1");
|
||
}
|
||
} catch (e2) {}
|
||
}
|
||
|
||
function isGuestLogoutMarked() {
|
||
try {
|
||
if (sessionStorage.getItem(GUEST_LOGOUT_FLAG_KEY) === "1") return true;
|
||
} catch (e) {}
|
||
try {
|
||
if (
|
||
global.CookieUtil &&
|
||
typeof global.CookieUtil.get === "function" &&
|
||
global.CookieUtil.get("guest_skip_auto") === "1"
|
||
) {
|
||
return true;
|
||
}
|
||
} catch (e2) {}
|
||
return false;
|
||
}
|
||
|
||
function syncTrackerUid(user) {
|
||
if (!user || !global.tracker || typeof global.tracker.setUid !== "function") return;
|
||
var uid = user.userId != null ? user.userId : user.uid != null ? user.uid : user.id;
|
||
if (uid == null || String(uid) === "") return;
|
||
global.tracker.setUid(String(uid));
|
||
}
|
||
|
||
function applyAuthSession(res, failTip) {
|
||
if (!res || res.code !== 200 || !res.data) {
|
||
var err = new Error((res && (res.tip || res.msg)) || failTip || "请求失败");
|
||
err.code = res && res.code;
|
||
err.data = res;
|
||
throw err;
|
||
}
|
||
if (global.CookieUtil && typeof global.CookieUtil.setAuthSession === "function") {
|
||
global.CookieUtil.setAuthSession(res.data);
|
||
}
|
||
clearGuestLogoutFlag();
|
||
syncTrackerUid(res.data);
|
||
try {
|
||
document.dispatchEvent(
|
||
new CustomEvent("auth:login", { detail: { user: res.data } })
|
||
);
|
||
} catch (e) {}
|
||
return res;
|
||
}
|
||
|
||
function register(payload) {
|
||
var channel = getChannel();
|
||
var body = {
|
||
accountNo: (payload.accountNo || "").trim(),
|
||
password: payload.password || "",
|
||
devID: getOrCreateDevID(),
|
||
channel: channel,
|
||
};
|
||
var invite = (payload.agentInviteCode || getInviteCode() || "").trim();
|
||
if (invite) body.agentInviteCode = invite;
|
||
|
||
// 游客升级注册:携带当前游客 token,上游按 Authorization 绑定原游客账号。
|
||
var options = {};
|
||
var token =
|
||
global.CookieUtil && typeof global.CookieUtil.getToken === "function"
|
||
? global.CookieUtil.getToken()
|
||
: "";
|
||
var registered =
|
||
global.CookieUtil && typeof global.CookieUtil.isRegistered === "function"
|
||
? global.CookieUtil.isRegistered()
|
||
: false;
|
||
if (token && !registered) {
|
||
options.headers = { Authorization: token };
|
||
}
|
||
|
||
return global.HttpRequest.post("/api/auth/register", body, options).then(function (res) {
|
||
return applyAuthSession(res, "注册失败");
|
||
});
|
||
}
|
||
|
||
function login(payload) {
|
||
var body = {
|
||
account: (payload.account || "").trim(),
|
||
password: payload.password || "",
|
||
devID: getOrCreateDevID(),
|
||
channel: getChannel(),
|
||
};
|
||
return global.HttpRequest.post("/api/auth/login", body).then(function (res) {
|
||
return applyAuthSession(res, "登录失败");
|
||
});
|
||
}
|
||
|
||
function resetPassword(payload) {
|
||
return global.HttpRequest.post("/api/auth/resetPassword", {
|
||
accountNo: (payload.accountNo || "").trim(),
|
||
email: (payload.email || "").trim(),
|
||
newPassword: payload.newPassword || "",
|
||
});
|
||
}
|
||
|
||
function guestLogin() {
|
||
var devId = getOrCreateDevID();
|
||
return global.HttpRequest.post("/api/auth/guest", {
|
||
devID: devId,
|
||
deviceId: devId,
|
||
channel: getChannel(),
|
||
mac: "",
|
||
}).then(function (res) {
|
||
return applyAuthSession(res, "游客登录失败");
|
||
});
|
||
}
|
||
|
||
function isAuthEndpoint(url) {
|
||
return /\/api\/auth\/(guest|login|register|resetPassword)(\?|$)/.test(String(url || ""));
|
||
}
|
||
|
||
function isSafeRedirect(path) {
|
||
return typeof path === "string" && path.charAt(0) === "/" && path.charAt(1) !== "/";
|
||
}
|
||
|
||
/** PC 打开登录弹窗;H5 跳转登录页 */
|
||
function promptRegisteredRelogin() {
|
||
toast("登录已过期,请重新登录", "warning");
|
||
var isMobile =
|
||
global.matchMedia && global.matchMedia("(max-width: 768px)").matches;
|
||
if (!isMobile && typeof global.openLoginPop === "function") {
|
||
try {
|
||
global.openLoginPop();
|
||
return;
|
||
} catch (e) {}
|
||
}
|
||
if (typeof global.openLoginDialog === "function") {
|
||
try {
|
||
var opened = global.openLoginDialog();
|
||
if (opened && typeof opened.then === "function") {
|
||
opened.catch(function () {
|
||
global.location.href = "/login";
|
||
});
|
||
return;
|
||
}
|
||
return;
|
||
} catch (e2) {}
|
||
}
|
||
global.location.href = "/login";
|
||
}
|
||
|
||
/**
|
||
* 接口返回 code=1000(token 失效)时的统一处理。
|
||
* - 游客:清会话后走游客登录,成功后可重试原请求
|
||
* - 注册账号:清会话并打开登录弹窗 / 跳转登录页
|
||
* @returns {Promise<{ok:boolean, guest?:boolean, needLogin?:boolean, res?:any}>}
|
||
*/
|
||
function handleTokenExpired() {
|
||
if (tokenRecoveryPromise) {
|
||
return tokenRecoveryPromise;
|
||
}
|
||
|
||
var wasRegistered =
|
||
global.CookieUtil && typeof global.CookieUtil.isRegistered === "function"
|
||
? global.CookieUtil.isRegistered()
|
||
: false;
|
||
|
||
if (global.CookieUtil && typeof global.CookieUtil.clearAuth === "function") {
|
||
global.CookieUtil.clearAuth();
|
||
}
|
||
try {
|
||
document.dispatchEvent(
|
||
new CustomEvent("auth:logout", { detail: { reason: "token-expired" } })
|
||
);
|
||
} catch (e) {}
|
||
|
||
if (wasRegistered) {
|
||
var registeredJob = Promise.resolve().then(function () {
|
||
promptRegisteredRelogin();
|
||
return { ok: false, needLogin: true };
|
||
});
|
||
tokenRecoveryPromise = registeredJob;
|
||
registeredJob.finally(function () {
|
||
setTimeout(function () {
|
||
if (tokenRecoveryPromise === registeredJob) {
|
||
tokenRecoveryPromise = null;
|
||
}
|
||
}, 1200);
|
||
});
|
||
return registeredJob;
|
||
}
|
||
|
||
clearGuestLogoutFlag();
|
||
var guestJob = guestLogin()
|
||
.then(function (res) {
|
||
return { ok: true, guest: true, res: res };
|
||
})
|
||
.catch(function (err) {
|
||
toast("会话已失效,请刷新页面重试", "error");
|
||
throw err;
|
||
})
|
||
.finally(function () {
|
||
if (tokenRecoveryPromise === guestJob) {
|
||
tokenRecoveryPromise = null;
|
||
}
|
||
});
|
||
tokenRecoveryPromise = guestJob;
|
||
return guestJob;
|
||
}
|
||
|
||
/**
|
||
* 无 token(未自己登录)时自动游客登录。
|
||
* SSR 也会在首屏补游客;此处作客户端兜底。
|
||
* 手动退出后跳过本次;清掉标记后,下次进页再自动游客。
|
||
* BFF: POST /api/auth/guest → 上游 POST /api/app/auth/guest
|
||
*/
|
||
function ensureGuestSession() {
|
||
if (hasSessionToken()) {
|
||
return Promise.resolve({ ok: true, skipped: true });
|
||
}
|
||
if (isGuestLogoutMarked()) {
|
||
// 本次因手动退出跳过;清标记,下次进页恢复自动游客
|
||
clearGuestLogoutFlag();
|
||
return Promise.resolve({ ok: true, skipped: true, reason: "manual-logout" });
|
||
}
|
||
if (!global.HttpRequest) {
|
||
return Promise.resolve({ ok: false, skipped: true, reason: "no-http" });
|
||
}
|
||
if (!guestLoginPromise) {
|
||
guestLoginPromise = guestLogin()
|
||
.then(function (res) {
|
||
return { ok: true, res: res };
|
||
})
|
||
.catch(function (err) {
|
||
return { ok: false, error: err };
|
||
})
|
||
.finally(function () {
|
||
guestLoginPromise = null;
|
||
});
|
||
}
|
||
return guestLoginPromise;
|
||
}
|
||
|
||
function uploadImage(file) {
|
||
var fd = new FormData();
|
||
fd.append("file", file);
|
||
return global.HttpRequest.post("/api/upload/image", fd).then(function (res) {
|
||
if (!res || res.code !== 200 || !res.data || !res.data.url) {
|
||
var err = new Error((res && (res.tip || res.msg)) || "上传失败");
|
||
err.code = res && res.code;
|
||
err.data = res;
|
||
throw err;
|
||
}
|
||
return res.data;
|
||
});
|
||
}
|
||
|
||
function updateProfile(payload) {
|
||
var body = {};
|
||
if (payload.nickName != null && String(payload.nickName).trim() !== "") {
|
||
body.nickName = String(payload.nickName).trim();
|
||
}
|
||
if (payload.introduction != null) {
|
||
body.introduction = String(payload.introduction).trim();
|
||
}
|
||
if (payload.avatar) body.avatar = payload.avatar;
|
||
if (payload.oldPassword) body.oldPassword = payload.oldPassword;
|
||
if (payload.newPassword) body.newPassword = payload.newPassword;
|
||
if (typeof payload.privacyMode === "boolean") body.privacyMode = payload.privacyMode;
|
||
|
||
return global.HttpRequest.post("/api/auth/profile", body).then(function (res) {
|
||
if (!res || res.code !== 200) {
|
||
var err = new Error((res && (res.tip || res.msg)) || "保存失败");
|
||
err.code = res && res.code;
|
||
err.data = res;
|
||
throw err;
|
||
}
|
||
if (res.data && global.CookieUtil && typeof global.CookieUtil.setAuthSession === "function") {
|
||
global.CookieUtil.setAuthSession(res.data);
|
||
}
|
||
if (res.data) syncTrackerUid(res.data);
|
||
return res;
|
||
});
|
||
}
|
||
|
||
function isValidEmail(email) {
|
||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(email || "").trim());
|
||
}
|
||
|
||
/** 登录凭证:账号 4–16 位,或合法邮箱 */
|
||
function isValidLoginAccount(account) {
|
||
var v = String(account || "").trim();
|
||
if (!v) return false;
|
||
if (v.indexOf("@") >= 0) return isValidEmail(v);
|
||
return v.length >= 4 && v.length <= 16;
|
||
}
|
||
|
||
function logout() {
|
||
markGuestLogout();
|
||
if (global.CookieUtil && typeof global.CookieUtil.clearAuth === "function") {
|
||
global.CookieUtil.clearAuth();
|
||
}
|
||
try {
|
||
document.dispatchEvent(new CustomEvent("auth:logout", { detail: { reason: "manual" } }));
|
||
} catch (e) {}
|
||
if (!global.HttpRequest) {
|
||
return Promise.resolve({ code: 200 });
|
||
}
|
||
return global.HttpRequest.post("/api/auth/logout", {}).catch(function () {
|
||
return { code: 200 };
|
||
});
|
||
}
|
||
|
||
global.AuthAPI = {
|
||
TOKEN_EXPIRED_CODE: TOKEN_EXPIRED_CODE,
|
||
register: register,
|
||
login: login,
|
||
logout: logout,
|
||
resetPassword: resetPassword,
|
||
guestLogin: guestLogin,
|
||
ensureGuestSession: ensureGuestSession,
|
||
handleTokenExpired: handleTokenExpired,
|
||
isAuthEndpoint: isAuthEndpoint,
|
||
markGuestLogout: markGuestLogout,
|
||
hasSessionToken: hasSessionToken,
|
||
uploadImage: uploadImage,
|
||
updateProfile: updateProfile,
|
||
getInviteCode: getInviteCode,
|
||
getOrCreateDevID: getOrCreateDevID,
|
||
getChannel: getChannel,
|
||
isValidEmail: isValidEmail,
|
||
isValidLoginAccount: isValidLoginAccount,
|
||
toast: toast,
|
||
};
|
||
|
||
// 每次页面初始化都准备好持久设备 UUID,并捕获首次渠道。
|
||
getOrCreateDevID();
|
||
getChannel();
|
||
|
||
// 首屏无 token → 自动游客登录
|
||
ensureGuestSession();
|
||
})(typeof window !== "undefined" ? window : this);
|