// ==UserScript==
// @name Caixin Auto Login
// @name:zh-CN 财新网自动登录
// @license MIT
// @namespace https://www.caixin.com/
// @version 1.4
// @description Automatic login script for Caixin.com with credential management
// @description:zh-CN 自动登录财新网账号
// @author https://github.com/hxueh
// @match *://*.caixin.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js
// @connect gateway.caixin.com
// @icon https://www.caixin.com/favicon.ico
// ==/UserScript==
(function () {
"use strict";
// Configuration constants
const CONFIG = {
API: {
LOGIN: "https://gateway.caixin.com/api/ucenter/user/v1/loginJsonp",
USER_INFO: "https://gateway.caixin.com/api/ucenter/userinfo/get",
},
ENCRYPTION: {
KEY: "G3JH98Y8MY9GWKWG",
MODE: CryptoJS.mode.ECB,
PADDING: CryptoJS.pad.Pkcs7,
},
COOKIE: {
DOMAIN: ".caixin.com",
MAX_AGE: 604800, // 7 days in seconds
},
LOGIN_PARAMS: {
DEVICE_TYPE: getDeviceType(),
UNIT: "1",
AREA_CODE: "+86",
},
};
/**
* Determines the device type based on user agent
* @returns {string} - "3" for mobile devices, "5" for desktop
*/
function getDeviceType() {
const userAgent = navigator.userAgent.toLowerCase();
const isMobile = /android|iphone|ipad|ipod|webos|windows phone/i.test(
userAgent
);
return isMobile ? "3" : "5";
}
/**
* Encrypts the password using AES encryption
* @param {string} password - The password to encrypt
* @returns {string} - URL encoded encrypted password
*/
function encrypt(password) {
const keyWordArray = CryptoJS.enc.Utf8.parse(CONFIG.ENCRYPTION.KEY);
const encrypted = CryptoJS.AES.encrypt(password, keyWordArray, {
mode: CONFIG.ENCRYPTION.MODE,
padding: CONFIG.ENCRYPTION.PADDING,
});
return encodeURIComponent(encrypted.toString());
}
/**
* Sets a cookie with standard Caixin parameters
* @param {string} name - Cookie name
* @param {string} value - Cookie value
*/
function setCaixinCookie(name, value) {
const cookieOptions = [
`${name}=${value}`,
`Path=/`,
`Domain=${CONFIG.COOKIE.DOMAIN}`,
"Secure=true",
`max-age=${CONFIG.COOKIE.MAX_AGE}`,
].join("; ");
document.cookie = cookieOptions;
}
/**
* Checks if the user is currently logged in
* @returns {Promise<boolean>} - True if logged in, false otherwise
*/
async function isLogin() {
try {
const response = await makeRequest({
method: "GET",
url: CONFIG.API.USER_INFO,
});
return response.code === 0;
} catch (error) {
console.error("Login check failed:", error);
return false;
}
}
/**
* Makes an XMLHttpRequest using GM_xmlhttpRequest
* @param {Object} options - Request options
* @returns {Promise} - Resolves with parsed JSON response
*/
function makeRequest(options) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
...options,
onload: (response) => resolve(JSON.parse(response.responseText)),
onerror: reject,
});
});
}
/**
* Performs the login process using stored credentials
* @returns {Promise<void>}
*/
async function performLogin() {
const credentials = {
phoneNumber: GM_getValue("phoneNumber"),
password: GM_getValue("password"),
};
if (!credentials.phoneNumber || !credentials.password) {
console.log(
"Credentials not found. Please set phone number and password."
);
return;
}
const loginUrl = new URL(CONFIG.API.LOGIN);
const params = {
account: credentials.phoneNumber,
password: encrypt(credentials.password),
deviceType: CONFIG.LOGIN_PARAMS.DEVICE_TYPE,
unit: CONFIG.LOGIN_PARAMS.UNIT,
areaCode: CONFIG.LOGIN_PARAMS.AREA_CODE,
};
Object.entries(params).forEach(([key, value]) => {
loginUrl.searchParams.append(key, value);
});
try {
const response = await makeRequest({
method: "GET",
url: loginUrl.toString(),
});
if (response.code !== 0) {
throw new Error(`Login failed with code: ${response.code}`);
}
// Set authentication cookies
const { uid, code, deviceType, unit, userAuth } = response.data;
const cookies = {
SA_USER_auth: userAuth,
SA_USER_DEVICE_TYPE: deviceType,
SA_USER_UID: uid,
SA_USER_UNIT: unit,
USER_LOGIN_CODE: code,
};
Object.entries(cookies).forEach(([name, value]) => {
setCaixinCookie(name, value);
});
location.reload();
} catch (error) {
console.error("Login process failed:", error);
}
}
/**
* Creates and displays the settings window UI
*/
function showSettingsWindow() {
const styles = `
.caixin-settings-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999999;
display: flex;
justify-content: center;
align-items: center;
}
.caixin-settings-window {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
width: 300px;
}
.caixin-settings-window h2 {
margin: 0 0 20px 0;
font-size: 18px;
color: #333;
}
.caixin-settings-window .form-group {
margin-bottom: 15px;
}
.caixin-settings-window label {
display: block;
margin-bottom: 5px;
color: #666;
}
.caixin-settings-window input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}
.caixin-settings-window .buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
}
.caixin-settings-window button {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.caixin-settings-window .save-btn {
background: #4CAF50;
color: white;
}
.caixin-settings-window .cancel-btn {
background: #f5f5f5;
color: #333;
}
.caixin-settings-window button:hover {
opacity: 0.9;
}
`;
GM_addStyle(styles);
const overlay = document.createElement("div");
overlay.className = "caixin-settings-overlay";
const currentSettings = {
phone: GM_getValue("phoneNumber", ""),
password: GM_getValue("password", ""),
};
const window = document.createElement("div");
window.className = "caixin-settings-window";
window.innerHTML = `
<h2>Caixin Login Settings</h2>
<div class="form-group">
<label for="caixin-phone">Phone Number:</label>
<input type="text" id="caixin-phone" value="${currentSettings.phone}" placeholder="+86 Phone Number">
</div>
<div class="form-group">
<label for="caixin-password">Password:</label>
<input type="password" id="caixin-password" value="${currentSettings.password}" placeholder="Password">
</div>
<div class="buttons">
<button class="cancel-btn">Cancel</button>
<button class="save-btn">Save</button>
</div>
`;
function closeSettings() {
document.body.removeChild(overlay);
}
// Event Handlers
window.querySelector(".save-btn").addEventListener("click", () => {
const phone = window.querySelector("#caixin-phone").value;
const password = window.querySelector("#caixin-password").value;
if (phone && password) {
GM_setValue("phoneNumber", phone);
GM_setValue("password", password);
closeSettings();
performLogin();
} else {
console.log("Please fill in both fields.");
}
});
window
.querySelector(".cancel-btn")
.addEventListener("click", closeSettings);
overlay.addEventListener("click", (e) => {
if (e.target === overlay) closeSettings();
});
overlay.appendChild(window);
document.body.appendChild(overlay);
}
// Initialize the script
async function init() {
const loggedIn = await isLogin();
if (!loggedIn) {
console.log("Not logged in. Attempting login...");
performLogin();
}
}
// Register settings menu command
GM_registerMenuCommand("⚙️ Caixin Login Settings", showSettingsWindow);
// Start the script
init();
})();