438 lines
13 KiB
TypeScript
438 lines
13 KiB
TypeScript
import * as vscode from "vscode";
|
||
import * as http from "http";
|
||
import * as path from "path";
|
||
import * as fs from "fs";
|
||
|
||
/**
|
||
* IC Coder Authentication Provider
|
||
* 集成到 VSCode 账户系统
|
||
*/
|
||
export class ICCoderAuthenticationProvider
|
||
implements vscode.AuthenticationProvider
|
||
{
|
||
private static readonly AUTH_TYPE = "iccoder";
|
||
private static readonly AUTH_NAME = "IC Coder";
|
||
private static readonly LOGIN_URL = "http://192.168.1.108:2005/login";
|
||
private static loginServer: http.Server | null = null;
|
||
private static currentPort: number | null = null;
|
||
|
||
private _onDidChangeSessions =
|
||
new vscode.EventEmitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
|
||
public readonly onDidChangeSessions = this._onDidChangeSessions.event;
|
||
|
||
private _sessions: vscode.AuthenticationSession[] = [];
|
||
|
||
constructor(private readonly context: vscode.ExtensionContext) {
|
||
// 从存储中恢复会话
|
||
this.loadSessions();
|
||
}
|
||
|
||
/**
|
||
* 从存储中加载会话
|
||
*/
|
||
private async loadSessions(): Promise<void> {
|
||
const storedSessions = this.context.globalState.get<
|
||
vscode.AuthenticationSession[]
|
||
>("icCoderSessions", []);
|
||
this._sessions = storedSessions;
|
||
}
|
||
|
||
/**
|
||
* 保存会话到存储
|
||
*/
|
||
private async saveSessions(): Promise<void> {
|
||
await this.context.globalState.update("icCoderSessions", this._sessions);
|
||
}
|
||
|
||
/**
|
||
* 获取会话列表
|
||
*/
|
||
async getSessions(
|
||
scopes?: readonly string[]
|
||
): Promise<vscode.AuthenticationSession[]> {
|
||
return [...this._sessions];
|
||
}
|
||
|
||
/**
|
||
* 创建会话(登录)
|
||
*/
|
||
async createSession(
|
||
scopes: readonly string[]
|
||
): Promise<vscode.AuthenticationSession> {
|
||
try {
|
||
const token = await this.login();
|
||
|
||
// 创建会话
|
||
const session: vscode.AuthenticationSession = {
|
||
id: this.generateSessionId(),
|
||
accessToken: token,
|
||
account: {
|
||
id: "iccoder-user",
|
||
label: "IC Coder 用户",
|
||
},
|
||
scopes: [...scopes],
|
||
};
|
||
|
||
this._sessions.push(session);
|
||
await this.saveSessions();
|
||
|
||
// 触发会话变化事件
|
||
this._onDidChangeSessions.fire({
|
||
added: [session],
|
||
removed: [],
|
||
changed: [],
|
||
});
|
||
|
||
vscode.window.showInformationMessage("登录成功!窗口将自动刷新...");
|
||
|
||
// 延迟 1 秒后重新加载窗口,让用户看到成功消息
|
||
setTimeout(() => {
|
||
vscode.commands.executeCommand("workbench.action.reloadWindow");
|
||
}, 1000);
|
||
|
||
return session;
|
||
} catch (error) {
|
||
vscode.window.showErrorMessage(
|
||
`登录失败: ${error instanceof Error ? error.message : String(error)}`
|
||
);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 删除会话(登出)
|
||
*/
|
||
async removeSession(sessionId: string): Promise<void> {
|
||
const sessionIndex = this._sessions.findIndex((s) => s.id === sessionId);
|
||
if (sessionIndex > -1) {
|
||
const session = this._sessions[sessionIndex];
|
||
this._sessions.splice(sessionIndex, 1);
|
||
await this.saveSessions();
|
||
|
||
// 触发会话变化事件
|
||
this._onDidChangeSessions.fire({
|
||
added: [],
|
||
removed: [session],
|
||
changed: [],
|
||
});
|
||
|
||
vscode.window.showInformationMessage("已退出登录!窗口将自动刷新...");
|
||
|
||
// 延迟 1 秒后重新加载窗口,让用户看到成功消息
|
||
setTimeout(() => {
|
||
vscode.commands.executeCommand("workbench.action.reloadWindow");
|
||
}, 1000);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成会话 ID
|
||
*/
|
||
private generateSessionId(): string {
|
||
return `iccoder-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
||
}
|
||
|
||
/**
|
||
* 登录逻辑(打开浏览器并等待回调)
|
||
*/
|
||
private async login(): Promise<string> {
|
||
// 如果已有服务器在运行,先关闭
|
||
if (ICCoderAuthenticationProvider.loginServer) {
|
||
ICCoderAuthenticationProvider.loginServer.close();
|
||
ICCoderAuthenticationProvider.loginServer = null;
|
||
}
|
||
|
||
// 创建本地服务器监听回调
|
||
const { server, port } = await this.createCallbackServer();
|
||
ICCoderAuthenticationProvider.loginServer = server;
|
||
ICCoderAuthenticationProvider.currentPort = port;
|
||
|
||
// 构建登录 URL
|
||
const callbackUrl = `http://localhost:${port}/callback`;
|
||
const loginUrl = `${
|
||
ICCoderAuthenticationProvider.LOGIN_URL
|
||
}?redirect_uri=${encodeURIComponent(callbackUrl)}`;
|
||
|
||
console.log("🔐 登录服务器已启动,监听端口:", port);
|
||
console.log("🌐 登录 URL:", loginUrl);
|
||
|
||
// 打开浏览器登录
|
||
await vscode.env.openExternal(vscode.Uri.parse(loginUrl));
|
||
|
||
vscode.window.showInformationMessage(
|
||
"请在浏览器中完成登录,登录成功后将自动返回..."
|
||
);
|
||
|
||
// 等待 token(通过 Promise)
|
||
return new Promise((resolve, reject) => {
|
||
const timeout = setTimeout(() => {
|
||
if (ICCoderAuthenticationProvider.loginServer) {
|
||
ICCoderAuthenticationProvider.loginServer.close();
|
||
ICCoderAuthenticationProvider.loginServer = null;
|
||
reject(new Error("登录超时"));
|
||
}
|
||
}, 5 * 60 * 1000);
|
||
|
||
// 将 resolve 和 reject 保存到服务器上下文
|
||
(server as any)._loginResolve = resolve;
|
||
(server as any)._loginReject = reject;
|
||
(server as any)._loginTimeout = timeout;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 创建本地回调服务器
|
||
*/
|
||
private createCallbackServer(): Promise<{
|
||
server: http.Server;
|
||
port: number;
|
||
}> {
|
||
return new Promise((resolve, reject) => {
|
||
// 读取 icon.png 并转换为 Base64
|
||
const iconPath = path.join(
|
||
this.context.extensionPath,
|
||
"media",
|
||
"icon.png"
|
||
);
|
||
let iconBase64 = "";
|
||
try {
|
||
const iconBuffer = fs.readFileSync(iconPath);
|
||
iconBase64 = `data:image/png;base64,${iconBuffer.toString("base64")}`;
|
||
} catch (error) {
|
||
console.warn("无法读取 icon.png:", error);
|
||
}
|
||
|
||
const server = http.createServer(async (req, res) => {
|
||
try {
|
||
console.log("📥 收到回调请求:", req.url);
|
||
|
||
const url = new URL(
|
||
req.url!,
|
||
`http://localhost:${ICCoderAuthenticationProvider.currentPort}`
|
||
);
|
||
console.log("📍 路径:", url.pathname);
|
||
console.log("📋 所有参数:", Object.fromEntries(url.searchParams));
|
||
|
||
if (url.pathname === "/callback") {
|
||
const token = url.searchParams.get("token");
|
||
console.log("🔑 Token:", token ? "已获取" : "未找到");
|
||
|
||
if (token) {
|
||
// 返回成功页面
|
||
res.writeHead(200, {
|
||
"Content-Type": "text/html; charset=utf-8",
|
||
});
|
||
res.end(this.getSuccessPage(iconBase64));
|
||
|
||
// 关闭服务器
|
||
server.close();
|
||
ICCoderAuthenticationProvider.loginServer = null;
|
||
|
||
// 清除超时
|
||
if ((server as any)._loginTimeout) {
|
||
clearTimeout((server as any)._loginTimeout);
|
||
}
|
||
|
||
// 返回 token
|
||
if ((server as any)._loginResolve) {
|
||
(server as any)._loginResolve(token);
|
||
}
|
||
} else {
|
||
res.writeHead(400, {
|
||
"Content-Type": "text/html; charset=utf-8",
|
||
});
|
||
res.end(`
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head><meta charset="utf-8"><title>登录失败</title></head>
|
||
<body><h1>❌ 登录失败</h1><p>未获取到有效的 Token</p></body>
|
||
</html>
|
||
`);
|
||
|
||
if ((server as any)._loginReject) {
|
||
(server as any)._loginReject(new Error("未获取到有效的 Token"));
|
||
}
|
||
}
|
||
} else {
|
||
res.writeHead(404);
|
||
res.end("Not Found");
|
||
}
|
||
} catch (error) {
|
||
res.writeHead(500);
|
||
res.end("Internal Server Error");
|
||
if ((server as any)._loginReject) {
|
||
(server as any)._loginReject(error);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 监听端口(使用 0 表示自动分配可用端口)
|
||
server.listen(0, () => {
|
||
const address = server.address();
|
||
const port =
|
||
typeof address === "object" && address ? address.port : 3000;
|
||
resolve({ server, port });
|
||
});
|
||
|
||
// 处理错误
|
||
server.on("error", (error: NodeJS.ErrnoException) => {
|
||
reject(error);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 获取登录成功页面 HTML
|
||
*/
|
||
private getSuccessPage(iconBase64: string): string {
|
||
return `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>登录成功 - IC Coder</title>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
min-height: 100vh;
|
||
background: linear-gradient(135deg, #dbeafe 0%, #93c5fd 100%);
|
||
overflow: hidden;
|
||
position: relative;
|
||
}
|
||
.bg-circle {
|
||
position: absolute;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
animation: float 20s infinite ease-in-out;
|
||
}
|
||
.bg-circle:nth-child(1) { width: 300px; height: 300px; top: -150px; left: -150px; }
|
||
.bg-circle:nth-child(2) { width: 200px; height: 200px; bottom: -100px; right: -100px; animation-delay: 5s; }
|
||
.bg-circle:nth-child(3) { width: 150px; height: 150px; top: 50%; right: 10%; animation-delay: 10s; }
|
||
@keyframes float {
|
||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||
33% { transform: translate(30px, -30px) scale(1.1); }
|
||
66% { transform: translate(-20px, 20px) scale(0.9); }
|
||
}
|
||
.container {
|
||
position: relative;
|
||
z-index: 10;
|
||
text-align: center;
|
||
background: rgba(255, 255, 255, 0.95);
|
||
backdrop-filter: blur(10px);
|
||
padding: 60px 50px;
|
||
border-radius: 24px;
|
||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||
max-width: 500px;
|
||
animation: slideUp 0.6s ease-out;
|
||
}
|
||
@keyframes slideUp {
|
||
from { opacity: 0; transform: translateY(30px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
.success-icon {
|
||
width: 80px;
|
||
height: 80px;
|
||
margin: 0 auto 30px;
|
||
background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
animation: scaleIn 0.5s ease-out 0.2s both;
|
||
}
|
||
@keyframes scaleIn {
|
||
from { transform: scale(0); opacity: 0; }
|
||
to { transform: scale(1); opacity: 1; }
|
||
}
|
||
.checkmark {
|
||
width: 40px;
|
||
height: 40px;
|
||
border: 4px solid white;
|
||
border-radius: 50%;
|
||
position: relative;
|
||
}
|
||
.checkmark::after {
|
||
content: '';
|
||
position: absolute;
|
||
left: 8px;
|
||
top: 3px;
|
||
width: 12px;
|
||
height: 20px;
|
||
border: solid white;
|
||
border-width: 0 4px 4px 0;
|
||
transform: rotate(45deg);
|
||
}
|
||
h1 {
|
||
color: #2d3748;
|
||
font-size: 32px;
|
||
font-weight: 700;
|
||
margin-bottom: 16px;
|
||
animation: fadeIn 0.6s ease-out 0.3s both;
|
||
}
|
||
p {
|
||
color: #718096;
|
||
font-size: 18px;
|
||
line-height: 1.6;
|
||
margin-bottom: 30px;
|
||
animation: fadeIn 0.6s ease-out 0.4s both;
|
||
}
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; }
|
||
to { opacity: 1; }
|
||
}
|
||
.brand {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 10px;
|
||
margin-top: 20px;
|
||
padding-top: 20px;
|
||
border-top: 1px solid #e2e8f0;
|
||
animation: fadeIn 0.6s ease-out 0.5s both;
|
||
}
|
||
.brand-logo {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
}
|
||
.brand-logo img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
.brand-text {
|
||
color: #4a5568;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="bg-circle"></div>
|
||
<div class="bg-circle"></div>
|
||
<div class="bg-circle"></div>
|
||
<div class="container">
|
||
<div class="success-icon">
|
||
<div class="checkmark"></div>
|
||
</div>
|
||
<h1>登录成功!</h1>
|
||
<p>您已成功登录 IC Coder<br>现在可以返回 VSCode 继续使用</p>
|
||
<div class="brand">
|
||
<div class="brand-logo">
|
||
<img src="${iconBase64}" alt="IC Coder" />
|
||
</div>
|
||
<span class="brand-text">IC Coder</span>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
`;
|
||
}
|
||
}
|