xuanwu-cli 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/xuanwu +0 -0
- package/dist/commands/auth/login.d.ts +2 -0
- package/dist/commands/auth/login.js +131 -0
- package/dist/commands/auth/logout.d.ts +2 -0
- package/dist/commands/auth/logout.js +33 -0
- package/dist/commands/auth/tokens.d.ts +2 -0
- package/dist/commands/auth/tokens.js +70 -0
- package/dist/commands/auth/whoami.d.ts +2 -0
- package/dist/commands/auth/whoami.js +32 -0
- package/dist/index.js +8 -0
- package/dist/lib/auth.d.ts +6 -0
- package/dist/lib/auth.js +29 -0
- package/dist/lib/session.d.ts +18 -0
- package/dist/lib/session.js +72 -0
- package/package.json +9 -8
- package/src/commands/auth/login.ts +168 -0
- package/src/commands/auth/logout.ts +34 -0
- package/src/commands/auth/tokens.ts +85 -0
- package/src/commands/auth/whoami.ts +35 -0
- package/src/index.ts +8 -0
- package/src/lib/auth.ts +41 -0
- package/src/lib/session.ts +50 -0
- package/test/integration.js +138 -6
package/bin/xuanwu
CHANGED
|
File without changes
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.makeLoginCommand = makeLoginCommand;
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const open_1 = __importDefault(require("open"));
|
|
9
|
+
const session_1 = require("../../lib/session");
|
|
10
|
+
const formatter_1 = require("../../output/formatter");
|
|
11
|
+
function makeLoginCommand() {
|
|
12
|
+
const cmd = new commander_1.Command('login')
|
|
13
|
+
.description('Login to xuanwu factory')
|
|
14
|
+
.option('-u, --api-url <url>', 'API server URL', 'https://i.xuanwu.dev.aimstek.cn')
|
|
15
|
+
.option('-e, --email <email>', 'Email address (for non-interactive login)')
|
|
16
|
+
.option('-p, --password <password>', 'Password (for non-interactive login)')
|
|
17
|
+
.option('--expires-in <duration>', 'Token expiration (30d, 90d, never)', '30d')
|
|
18
|
+
.action(async (options) => {
|
|
19
|
+
const sessionManager = new session_1.SessionManager();
|
|
20
|
+
if (options.email && options.password) {
|
|
21
|
+
await loginWithCredentials(sessionManager, options.email, options.password, options.expiresIn, options.apiUrl);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
await loginWithBrowser(sessionManager, options.apiUrl);
|
|
25
|
+
});
|
|
26
|
+
return cmd;
|
|
27
|
+
}
|
|
28
|
+
async function loginWithBrowser(sessionManager, apiUrl) {
|
|
29
|
+
const finalApiUrl = process.env.XW_API_URL || apiUrl;
|
|
30
|
+
const deviceAuthRes = await fetch(`${finalApiUrl}/api/cli/auth/device-code`, {
|
|
31
|
+
method: 'POST'
|
|
32
|
+
});
|
|
33
|
+
if (!deviceAuthRes.ok) {
|
|
34
|
+
formatter_1.OutputFormatter.error('Failed to generate device code');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
const { sessionId, loginUrl, code } = await deviceAuthRes.json();
|
|
38
|
+
formatter_1.OutputFormatter.info('正在生成设备授权码...');
|
|
39
|
+
console.log(`授权码: ${code}`);
|
|
40
|
+
console.log(`已打开浏览器: ${loginUrl}`);
|
|
41
|
+
formatter_1.OutputFormatter.info('请在浏览器中完成授权');
|
|
42
|
+
formatter_1.OutputFormatter.info('等待授权...');
|
|
43
|
+
try {
|
|
44
|
+
await (0, open_1.default)(loginUrl);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
console.log(`如果浏览器未打开,请手动访问: ${loginUrl}`);
|
|
48
|
+
}
|
|
49
|
+
const maxAttempts = 60;
|
|
50
|
+
let attempts = 0;
|
|
51
|
+
while (attempts < maxAttempts) {
|
|
52
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
53
|
+
const pollRes = await fetch(`${finalApiUrl}/api/cli/auth/poll?session_id=${sessionId}`);
|
|
54
|
+
if (!pollRes.ok) {
|
|
55
|
+
formatter_1.OutputFormatter.error('Failed to poll auth status');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
const pollData = await pollRes.json();
|
|
59
|
+
const { status, token, user } = pollData;
|
|
60
|
+
if (status === 'success') {
|
|
61
|
+
const expiresIn = 30 * 24 * 60 * 60 * 1000;
|
|
62
|
+
await sessionManager.saveSession({
|
|
63
|
+
token,
|
|
64
|
+
userId: user.id,
|
|
65
|
+
userName: user.name,
|
|
66
|
+
userEmail: user.email,
|
|
67
|
+
userRole: user.role,
|
|
68
|
+
deviceId: 'CLI Device',
|
|
69
|
+
expiresAt: new Date(Date.now() + expiresIn).toISOString(),
|
|
70
|
+
apiUrl: finalApiUrl
|
|
71
|
+
});
|
|
72
|
+
console.log('');
|
|
73
|
+
formatter_1.OutputFormatter.success('授权成功!');
|
|
74
|
+
formatter_1.OutputFormatter.success('登录成功!');
|
|
75
|
+
console.log(` 用户: ${user.name} (${user.email})`);
|
|
76
|
+
console.log(` 设备: CLI Device`);
|
|
77
|
+
console.log(` 过期时间: 30天`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (status === 'expired') {
|
|
81
|
+
formatter_1.OutputFormatter.error('授权已过期,请重新尝试');
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
if (attempts % 5 === 0) {
|
|
85
|
+
formatter_1.OutputFormatter.info(`等待中... (${attempts * 3}秒)`);
|
|
86
|
+
}
|
|
87
|
+
attempts++;
|
|
88
|
+
}
|
|
89
|
+
formatter_1.OutputFormatter.error('授权超时,请重新尝试');
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
async function loginWithCredentials(sessionManager, email, password, expiresIn, apiUrl) {
|
|
93
|
+
const finalApiUrl = process.env.XW_API_URL || apiUrl;
|
|
94
|
+
console.log(`使用邮箱密码登录: ${email}`);
|
|
95
|
+
const res = await fetch(`${finalApiUrl}/api/cli/auth/login`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: { 'Content-Type': 'application/json' },
|
|
98
|
+
body: JSON.stringify({
|
|
99
|
+
email,
|
|
100
|
+
password,
|
|
101
|
+
deviceName: 'CLI Device',
|
|
102
|
+
expiresIn
|
|
103
|
+
})
|
|
104
|
+
});
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
const error = await res.json();
|
|
107
|
+
formatter_1.OutputFormatter.error(`登录失败: ${error.error}`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
const loginData = await res.json();
|
|
111
|
+
const { token, user } = loginData;
|
|
112
|
+
const expiresAt = expiresIn === 'never'
|
|
113
|
+
? null
|
|
114
|
+
: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
115
|
+
const deviceId = expiresIn === 'never' ? 'CI/CD Server' : 'CLI Device';
|
|
116
|
+
const expiresText = expiresIn === 'never' ? '永不过期' : '30天';
|
|
117
|
+
await sessionManager.saveSession({
|
|
118
|
+
token,
|
|
119
|
+
userId: user.id,
|
|
120
|
+
userName: user.name,
|
|
121
|
+
userEmail: user.email,
|
|
122
|
+
userRole: user.role,
|
|
123
|
+
deviceId,
|
|
124
|
+
expiresAt,
|
|
125
|
+
apiUrl: finalApiUrl
|
|
126
|
+
});
|
|
127
|
+
formatter_1.OutputFormatter.success('登录成功!');
|
|
128
|
+
console.log(` 用户: ${user.name} (${user.email})`);
|
|
129
|
+
console.log(` 设备: ${deviceId}`);
|
|
130
|
+
console.log(` 过期时间: ${expiresText}`);
|
|
131
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.makeLogoutCommand = makeLogoutCommand;
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const session_1 = require("../../lib/session");
|
|
6
|
+
const formatter_1 = require("../../output/formatter");
|
|
7
|
+
function makeLogoutCommand() {
|
|
8
|
+
const cmd = new commander_1.Command('logout')
|
|
9
|
+
.description('Logout from xuanwu factory')
|
|
10
|
+
.action(async () => {
|
|
11
|
+
const sessionManager = new session_1.SessionManager();
|
|
12
|
+
const session = await sessionManager.loadSession();
|
|
13
|
+
if (!session) {
|
|
14
|
+
formatter_1.OutputFormatter.info('未登录');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
await fetch(`${session.apiUrl}/api/cli/auth/logout`, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: {
|
|
21
|
+
'Authorization': `Bearer ${session.token}`,
|
|
22
|
+
'Content-Type': 'application/json'
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
// Ignore API errors
|
|
28
|
+
}
|
|
29
|
+
await sessionManager.clearSession();
|
|
30
|
+
formatter_1.OutputFormatter.success('登出成功');
|
|
31
|
+
});
|
|
32
|
+
return cmd;
|
|
33
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.makeTokensCommand = makeTokensCommand;
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const session_1 = require("../../lib/session");
|
|
6
|
+
const formatter_1 = require("../../output/formatter");
|
|
7
|
+
function makeTokensCommand() {
|
|
8
|
+
const cmd = new commander_1.Command('tokens')
|
|
9
|
+
.description('List all tokens');
|
|
10
|
+
const listCmd = new commander_1.Command('list')
|
|
11
|
+
.description('List all tokens')
|
|
12
|
+
.action(async () => {
|
|
13
|
+
const sessionManager = new session_1.SessionManager();
|
|
14
|
+
const session = await sessionManager.loadSession();
|
|
15
|
+
if (!session) {
|
|
16
|
+
formatter_1.OutputFormatter.info('未登录');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const res = await fetch(`${session.apiUrl}/api/cli/auth/tokens`, {
|
|
20
|
+
headers: {
|
|
21
|
+
'Authorization': `Bearer ${session.token}`
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
formatter_1.OutputFormatter.error('Failed to list tokens');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const { tokens } = await res.json();
|
|
29
|
+
if (tokens.length === 0) {
|
|
30
|
+
formatter_1.OutputFormatter.info('没有找到tokens');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
console.log('Your Tokens:');
|
|
34
|
+
tokens.forEach((token, index) => {
|
|
35
|
+
const lastUsed = token.lastUsedAt
|
|
36
|
+
? `${Math.floor((Date.now() - new Date(token.lastUsedAt).getTime()) / (60 * 60 * 1000))}h ago`
|
|
37
|
+
: 'never';
|
|
38
|
+
const expires = token.expiresAt
|
|
39
|
+
? `${Math.max(0, Math.floor((new Date(token.expiresAt).getTime() - Date.now()) / (24 * 60 * 60 * 1000)))} days`
|
|
40
|
+
: 'never';
|
|
41
|
+
const expired = token.expiresAt && new Date(token.expiresAt) < new Date();
|
|
42
|
+
console.log(` ${index + 1}. ${token.name} (last used: ${lastUsed}) - ${expired ? 'expired' : expires}`);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
const revokeCmd = new commander_1.Command('revoke')
|
|
46
|
+
.description('Revoke a token')
|
|
47
|
+
.argument('<id>', 'Token ID')
|
|
48
|
+
.action(async (id) => {
|
|
49
|
+
const sessionManager = new session_1.SessionManager();
|
|
50
|
+
const session = await sessionManager.loadSession();
|
|
51
|
+
if (!session) {
|
|
52
|
+
formatter_1.OutputFormatter.info('未登录');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const res = await fetch(`${session.apiUrl}/api/cli/auth/tokens/${id}`, {
|
|
56
|
+
method: 'DELETE',
|
|
57
|
+
headers: {
|
|
58
|
+
'Authorization': `Bearer ${session.token}`
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
formatter_1.OutputFormatter.error('Failed to revoke token');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
formatter_1.OutputFormatter.success('Token已撤销');
|
|
66
|
+
});
|
|
67
|
+
cmd.addCommand(listCmd);
|
|
68
|
+
cmd.addCommand(revokeCmd);
|
|
69
|
+
return cmd;
|
|
70
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.makeWhoamiCommand = makeWhoamiCommand;
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const session_1 = require("../../lib/session");
|
|
6
|
+
const formatter_1 = require("../../output/formatter");
|
|
7
|
+
function makeWhoamiCommand() {
|
|
8
|
+
const cmd = new commander_1.Command('whoami')
|
|
9
|
+
.description('Show current login status')
|
|
10
|
+
.action(async () => {
|
|
11
|
+
const sessionManager = new session_1.SessionManager();
|
|
12
|
+
const session = await sessionManager.loadSession();
|
|
13
|
+
if (!session) {
|
|
14
|
+
formatter_1.OutputFormatter.info('未登录');
|
|
15
|
+
console.log('请使用: xw login');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (sessionManager.isExpired(session)) {
|
|
19
|
+
formatter_1.OutputFormatter.info('登录已过期');
|
|
20
|
+
console.log('请使用: xw login');
|
|
21
|
+
await sessionManager.clearSession();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const expiresIn = session.expiresAt
|
|
25
|
+
? Math.max(0, Math.floor((new Date(session.expiresAt).getTime() - Date.now()) / (24 * 60 * 60 * 1000)))
|
|
26
|
+
: 'never';
|
|
27
|
+
console.log(`Logged in as: ${session.userName} (${session.userEmail})`);
|
|
28
|
+
console.log(`Device: ${session.deviceId}`);
|
|
29
|
+
console.log(`Expires in: ${expiresIn === 'never' ? 'never' : `${expiresIn} days`}`);
|
|
30
|
+
});
|
|
31
|
+
return cmd;
|
|
32
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,10 @@ const build_1 = require("./commands/build");
|
|
|
13
13
|
const scale_1 = require("./commands/scale");
|
|
14
14
|
const logs_1 = require("./commands/logs");
|
|
15
15
|
const pods_1 = require("./commands/pods");
|
|
16
|
+
const login_1 = require("./commands/auth/login");
|
|
17
|
+
const logout_1 = require("./commands/auth/logout");
|
|
18
|
+
const whoami_1 = require("./commands/auth/whoami");
|
|
19
|
+
const tokens_1 = require("./commands/auth/tokens");
|
|
16
20
|
const program = new commander_1.Command();
|
|
17
21
|
program
|
|
18
22
|
.name('xw')
|
|
@@ -28,4 +32,8 @@ program.addCommand((0, deploy_1.makeDeployCommand)());
|
|
|
28
32
|
program.addCommand((0, scale_1.makeScaleCommand)());
|
|
29
33
|
program.addCommand((0, logs_1.makeLogsCommand)());
|
|
30
34
|
program.addCommand((0, pods_1.makePodsCommand)());
|
|
35
|
+
program.addCommand((0, login_1.makeLoginCommand)());
|
|
36
|
+
program.addCommand((0, logout_1.makeLogoutCommand)());
|
|
37
|
+
program.addCommand((0, whoami_1.makeWhoamiCommand)());
|
|
38
|
+
program.addCommand((0, tokens_1.makeTokensCommand)());
|
|
31
39
|
program.parse(process.argv);
|
package/dist/lib/auth.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.authenticatedFetch = authenticatedFetch;
|
|
4
|
+
const session_1 = require("./session");
|
|
5
|
+
async function authenticatedFetch(url, options = {}) {
|
|
6
|
+
const sessionManager = new session_1.SessionManager();
|
|
7
|
+
const session = await sessionManager.loadSession();
|
|
8
|
+
if (!session) {
|
|
9
|
+
throw new Error('Not logged in. Please run: xw login');
|
|
10
|
+
}
|
|
11
|
+
if (sessionManager.isExpired(session)) {
|
|
12
|
+
await sessionManager.clearSession();
|
|
13
|
+
throw new Error('Session expired. Please run: xw login');
|
|
14
|
+
}
|
|
15
|
+
const response = await fetch(url, {
|
|
16
|
+
method: options.method || 'GET',
|
|
17
|
+
headers: {
|
|
18
|
+
'Authorization': `Bearer ${session.token}`,
|
|
19
|
+
'Content-Type': 'application/json',
|
|
20
|
+
...options.headers
|
|
21
|
+
},
|
|
22
|
+
body: options.body ? JSON.stringify(options.body) : undefined
|
|
23
|
+
});
|
|
24
|
+
if (response.status === 401) {
|
|
25
|
+
await sessionManager.clearSession();
|
|
26
|
+
throw new Error('Authentication failed. Please run: xw login');
|
|
27
|
+
}
|
|
28
|
+
return response;
|
|
29
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface Session {
|
|
2
|
+
token: string;
|
|
3
|
+
userId: string;
|
|
4
|
+
userName: string;
|
|
5
|
+
userEmail: string;
|
|
6
|
+
userRole: string;
|
|
7
|
+
deviceId: string;
|
|
8
|
+
expiresAt: string | null;
|
|
9
|
+
apiUrl: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class SessionManager {
|
|
12
|
+
private sessionPath;
|
|
13
|
+
constructor();
|
|
14
|
+
saveSession(session: Session): Promise<void>;
|
|
15
|
+
loadSession(): Promise<Session | null>;
|
|
16
|
+
clearSession(): Promise<void>;
|
|
17
|
+
isExpired(session: Session): boolean;
|
|
18
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.SessionManager = void 0;
|
|
37
|
+
const fs = __importStar(require("fs/promises"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
class SessionManager {
|
|
41
|
+
constructor() {
|
|
42
|
+
this.sessionPath = path.join(os.homedir(), '.xw', 'session.json');
|
|
43
|
+
}
|
|
44
|
+
async saveSession(session) {
|
|
45
|
+
const dir = path.dirname(this.sessionPath);
|
|
46
|
+
await fs.mkdir(dir, { recursive: true });
|
|
47
|
+
await fs.writeFile(this.sessionPath, JSON.stringify(session, null, 2));
|
|
48
|
+
}
|
|
49
|
+
async loadSession() {
|
|
50
|
+
try {
|
|
51
|
+
const content = await fs.readFile(this.sessionPath, 'utf-8');
|
|
52
|
+
return JSON.parse(content);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async clearSession() {
|
|
59
|
+
try {
|
|
60
|
+
await fs.unlink(this.sessionPath);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Ignore errors
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
isExpired(session) {
|
|
67
|
+
if (!session.expiresAt)
|
|
68
|
+
return false;
|
|
69
|
+
return new Date(session.expiresAt) < new Date();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.SessionManager = SessionManager;
|
package/package.json
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xuanwu-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "玄武工厂平台 CLI 工具",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"xuanwu": "./bin/xuanwu",
|
|
8
8
|
"xw": "./bin/xuanwu"
|
|
9
9
|
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"start": "node dist/index.js"
|
|
14
|
+
},
|
|
10
15
|
"keywords": [
|
|
11
16
|
"cli",
|
|
12
17
|
"kubernetes",
|
|
@@ -15,18 +20,14 @@
|
|
|
15
20
|
"author": "",
|
|
16
21
|
"license": "MIT",
|
|
17
22
|
"dependencies": {
|
|
23
|
+
"axios": "^1.6.5",
|
|
18
24
|
"commander": "^11.1.0",
|
|
19
25
|
"inquirer": "^9.2.12",
|
|
20
|
-
"
|
|
26
|
+
"open": "^11.0.0"
|
|
21
27
|
},
|
|
22
28
|
"devDependencies": {
|
|
23
29
|
"@types/inquirer": "^9.0.7",
|
|
24
30
|
"@types/node": "^20.11.0",
|
|
25
31
|
"typescript": "^5.3.3"
|
|
26
|
-
},
|
|
27
|
-
"scripts": {
|
|
28
|
-
"build": "tsc",
|
|
29
|
-
"dev": "tsc --watch",
|
|
30
|
-
"start": "node dist/index.js"
|
|
31
32
|
}
|
|
32
|
-
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import open from 'open'
|
|
3
|
+
import { SessionManager } from '../../lib/session'
|
|
4
|
+
import { OutputFormatter } from '../../output/formatter'
|
|
5
|
+
|
|
6
|
+
export function makeLoginCommand(): Command {
|
|
7
|
+
const cmd = new Command('login')
|
|
8
|
+
.description('Login to xuanwu factory')
|
|
9
|
+
.option('-u, --api-url <url>', 'API server URL', 'https://i.xuanwu.dev.aimstek.cn')
|
|
10
|
+
.option('-e, --email <email>', 'Email address (for non-interactive login)')
|
|
11
|
+
.option('-p, --password <password>', 'Password (for non-interactive login)')
|
|
12
|
+
.option('--expires-in <duration>', 'Token expiration (30d, 90d, never)', '30d')
|
|
13
|
+
.action(async (options) => {
|
|
14
|
+
const sessionManager = new SessionManager()
|
|
15
|
+
|
|
16
|
+
if (options.email && options.password) {
|
|
17
|
+
await loginWithCredentials(
|
|
18
|
+
sessionManager,
|
|
19
|
+
options.email,
|
|
20
|
+
options.password,
|
|
21
|
+
options.expiresIn,
|
|
22
|
+
options.apiUrl
|
|
23
|
+
)
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
await loginWithBrowser(sessionManager, options.apiUrl)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
return cmd
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function loginWithBrowser(sessionManager: SessionManager, apiUrl: string): Promise<void> {
|
|
34
|
+
const finalApiUrl = process.env.XW_API_URL || apiUrl
|
|
35
|
+
|
|
36
|
+
const deviceAuthRes = await fetch(`${finalApiUrl}/api/cli/auth/device-code`, {
|
|
37
|
+
method: 'POST'
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
if (!deviceAuthRes.ok) {
|
|
41
|
+
OutputFormatter.error('Failed to generate device code')
|
|
42
|
+
process.exit(1)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { sessionId, loginUrl, code } = await deviceAuthRes.json() as { sessionId: string; loginUrl: string; code: string }
|
|
46
|
+
|
|
47
|
+
OutputFormatter.info('正在生成设备授权码...')
|
|
48
|
+
console.log(`授权码: ${code}`)
|
|
49
|
+
console.log(`已打开浏览器: ${loginUrl}`)
|
|
50
|
+
OutputFormatter.info('请在浏览器中完成授权')
|
|
51
|
+
OutputFormatter.info('等待授权...')
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
await open(loginUrl)
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.log(`如果浏览器未打开,请手动访问: ${loginUrl}`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const maxAttempts = 60
|
|
60
|
+
let attempts = 0
|
|
61
|
+
|
|
62
|
+
while (attempts < maxAttempts) {
|
|
63
|
+
await new Promise(resolve => setTimeout(resolve, 3000))
|
|
64
|
+
|
|
65
|
+
const pollRes = await fetch(
|
|
66
|
+
`${finalApiUrl}/api/cli/auth/poll?session_id=${sessionId}`
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if (!pollRes.ok) {
|
|
70
|
+
OutputFormatter.error('Failed to poll auth status')
|
|
71
|
+
process.exit(1)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const pollData = await pollRes.json() as { status: string; token: string; user: { id: string; name: string; email: string; role: string } }
|
|
75
|
+
const { status, token, user } = pollData
|
|
76
|
+
|
|
77
|
+
if (status === 'success') {
|
|
78
|
+
const expiresIn = 30 * 24 * 60 * 60 * 1000
|
|
79
|
+
await sessionManager.saveSession({
|
|
80
|
+
token,
|
|
81
|
+
userId: user.id,
|
|
82
|
+
userName: user.name,
|
|
83
|
+
userEmail: user.email,
|
|
84
|
+
userRole: user.role,
|
|
85
|
+
deviceId: 'CLI Device',
|
|
86
|
+
expiresAt: new Date(Date.now() + expiresIn).toISOString(),
|
|
87
|
+
apiUrl: finalApiUrl
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
console.log('')
|
|
91
|
+
OutputFormatter.success('授权成功!')
|
|
92
|
+
OutputFormatter.success('登录成功!')
|
|
93
|
+
console.log(` 用户: ${user.name} (${user.email})`)
|
|
94
|
+
console.log(` 设备: CLI Device`)
|
|
95
|
+
console.log(` 过期时间: 30天`)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (status === 'expired') {
|
|
100
|
+
OutputFormatter.error('授权已过期,请重新尝试')
|
|
101
|
+
process.exit(1)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (attempts % 5 === 0) {
|
|
105
|
+
OutputFormatter.info(`等待中... (${attempts * 3}秒)`)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
attempts++
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
OutputFormatter.error('授权超时,请重新尝试')
|
|
112
|
+
process.exit(1)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function loginWithCredentials(
|
|
116
|
+
sessionManager: SessionManager,
|
|
117
|
+
email: string,
|
|
118
|
+
password: string,
|
|
119
|
+
expiresIn: string,
|
|
120
|
+
apiUrl: string
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
const finalApiUrl = process.env.XW_API_URL || apiUrl
|
|
123
|
+
|
|
124
|
+
console.log(`使用邮箱密码登录: ${email}`)
|
|
125
|
+
|
|
126
|
+
const res = await fetch(`${finalApiUrl}/api/cli/auth/login`, {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers: { 'Content-Type': 'application/json' },
|
|
129
|
+
body: JSON.stringify({
|
|
130
|
+
email,
|
|
131
|
+
password,
|
|
132
|
+
deviceName: 'CLI Device',
|
|
133
|
+
expiresIn
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
if (!res.ok) {
|
|
138
|
+
const error = await res.json() as { error: string }
|
|
139
|
+
OutputFormatter.error(`登录失败: ${error.error}`)
|
|
140
|
+
process.exit(1)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const loginData = await res.json() as { token: string; user: { id: string; name: string; email: string; role: string } }
|
|
144
|
+
const { token, user } = loginData
|
|
145
|
+
|
|
146
|
+
const expiresAt = expiresIn === 'never'
|
|
147
|
+
? null
|
|
148
|
+
: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()
|
|
149
|
+
|
|
150
|
+
const deviceId = expiresIn === 'never' ? 'CI/CD Server' : 'CLI Device'
|
|
151
|
+
const expiresText = expiresIn === 'never' ? '永不过期' : '30天'
|
|
152
|
+
|
|
153
|
+
await sessionManager.saveSession({
|
|
154
|
+
token,
|
|
155
|
+
userId: user.id,
|
|
156
|
+
userName: user.name,
|
|
157
|
+
userEmail: user.email,
|
|
158
|
+
userRole: user.role,
|
|
159
|
+
deviceId,
|
|
160
|
+
expiresAt,
|
|
161
|
+
apiUrl: finalApiUrl
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
OutputFormatter.success('登录成功!')
|
|
165
|
+
console.log(` 用户: ${user.name} (${user.email})`)
|
|
166
|
+
console.log(` 设备: ${deviceId}`)
|
|
167
|
+
console.log(` 过期时间: ${expiresText}`)
|
|
168
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { SessionManager } from '../../lib/session'
|
|
3
|
+
import { OutputFormatter } from '../../output/formatter'
|
|
4
|
+
|
|
5
|
+
export function makeLogoutCommand(): Command {
|
|
6
|
+
const cmd = new Command('logout')
|
|
7
|
+
.description('Logout from xuanwu factory')
|
|
8
|
+
.action(async () => {
|
|
9
|
+
const sessionManager = new SessionManager()
|
|
10
|
+
const session = await sessionManager.loadSession()
|
|
11
|
+
|
|
12
|
+
if (!session) {
|
|
13
|
+
OutputFormatter.info('未登录')
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
await fetch(`${session.apiUrl}/api/cli/auth/logout`, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: {
|
|
21
|
+
'Authorization': `Bearer ${session.token}`,
|
|
22
|
+
'Content-Type': 'application/json'
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
} catch (error) {
|
|
26
|
+
// Ignore API errors
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await sessionManager.clearSession()
|
|
30
|
+
OutputFormatter.success('登出成功')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return cmd
|
|
34
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { SessionManager } from '../../lib/session'
|
|
3
|
+
import { OutputFormatter } from '../../output/formatter'
|
|
4
|
+
|
|
5
|
+
export function makeTokensCommand(): Command {
|
|
6
|
+
const cmd = new Command('tokens')
|
|
7
|
+
.description('List all tokens')
|
|
8
|
+
|
|
9
|
+
const listCmd = new Command('list')
|
|
10
|
+
.description('List all tokens')
|
|
11
|
+
.action(async () => {
|
|
12
|
+
const sessionManager = new SessionManager()
|
|
13
|
+
const session = await sessionManager.loadSession()
|
|
14
|
+
|
|
15
|
+
if (!session) {
|
|
16
|
+
OutputFormatter.info('未登录')
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const res = await fetch(`${session.apiUrl}/api/cli/auth/tokens`, {
|
|
21
|
+
headers: {
|
|
22
|
+
'Authorization': `Bearer ${session.token}`
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
OutputFormatter.error('Failed to list tokens')
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { tokens } = await res.json() as { tokens: Array<{ id: string; name: string; lastUsedAt?: string; expiresAt?: string }> }
|
|
32
|
+
|
|
33
|
+
if (tokens.length === 0) {
|
|
34
|
+
OutputFormatter.info('没有找到tokens')
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log('Your Tokens:')
|
|
39
|
+
tokens.forEach((token, index) => {
|
|
40
|
+
const lastUsed = token.lastUsedAt
|
|
41
|
+
? `${Math.floor((Date.now() - new Date(token.lastUsedAt).getTime()) / (60 * 60 * 1000))}h ago`
|
|
42
|
+
: 'never'
|
|
43
|
+
|
|
44
|
+
const expires = token.expiresAt
|
|
45
|
+
? `${Math.max(0, Math.floor((new Date(token.expiresAt).getTime() - Date.now()) / (24 * 60 * 60 * 1000)))} days`
|
|
46
|
+
: 'never'
|
|
47
|
+
|
|
48
|
+
const expired = token.expiresAt && new Date(token.expiresAt) < new Date()
|
|
49
|
+
|
|
50
|
+
console.log(` ${index + 1}. ${token.name} (last used: ${lastUsed}) - ${expired ? 'expired' : expires}`)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const revokeCmd = new Command('revoke')
|
|
55
|
+
.description('Revoke a token')
|
|
56
|
+
.argument('<id>', 'Token ID')
|
|
57
|
+
.action(async (id) => {
|
|
58
|
+
const sessionManager = new SessionManager()
|
|
59
|
+
const session = await sessionManager.loadSession()
|
|
60
|
+
|
|
61
|
+
if (!session) {
|
|
62
|
+
OutputFormatter.info('未登录')
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const res = await fetch(`${session.apiUrl}/api/cli/auth/tokens/${id}`, {
|
|
67
|
+
method: 'DELETE',
|
|
68
|
+
headers: {
|
|
69
|
+
'Authorization': `Bearer ${session.token}`
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
OutputFormatter.error('Failed to revoke token')
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
OutputFormatter.success('Token已撤销')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
cmd.addCommand(listCmd)
|
|
82
|
+
cmd.addCommand(revokeCmd)
|
|
83
|
+
|
|
84
|
+
return cmd
|
|
85
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { SessionManager } from '../../lib/session'
|
|
3
|
+
import { OutputFormatter } from '../../output/formatter'
|
|
4
|
+
|
|
5
|
+
export function makeWhoamiCommand(): Command {
|
|
6
|
+
const cmd = new Command('whoami')
|
|
7
|
+
.description('Show current login status')
|
|
8
|
+
.action(async () => {
|
|
9
|
+
const sessionManager = new SessionManager()
|
|
10
|
+
const session = await sessionManager.loadSession()
|
|
11
|
+
|
|
12
|
+
if (!session) {
|
|
13
|
+
OutputFormatter.info('未登录')
|
|
14
|
+
console.log('请使用: xw login')
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (sessionManager.isExpired(session)) {
|
|
19
|
+
OutputFormatter.info('登录已过期')
|
|
20
|
+
console.log('请使用: xw login')
|
|
21
|
+
await sessionManager.clearSession()
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const expiresIn = session.expiresAt
|
|
26
|
+
? Math.max(0, Math.floor((new Date(session.expiresAt).getTime() - Date.now()) / (24 * 60 * 60 * 1000)))
|
|
27
|
+
: 'never'
|
|
28
|
+
|
|
29
|
+
console.log(`Logged in as: ${session.userName} (${session.userEmail})`)
|
|
30
|
+
console.log(`Device: ${session.deviceId}`)
|
|
31
|
+
console.log(`Expires in: ${expiresIn === 'never' ? 'never' : `${expiresIn} days`}`)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
return cmd
|
|
35
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,10 @@ import { makeBuildCommand } from './commands/build'
|
|
|
13
13
|
import { makeScaleCommand } from './commands/scale'
|
|
14
14
|
import { makeLogsCommand } from './commands/logs'
|
|
15
15
|
import { makePodsCommand } from './commands/pods'
|
|
16
|
+
import { makeLoginCommand } from './commands/auth/login'
|
|
17
|
+
import { makeLogoutCommand } from './commands/auth/logout'
|
|
18
|
+
import { makeWhoamiCommand } from './commands/auth/whoami'
|
|
19
|
+
import { makeTokensCommand } from './commands/auth/tokens'
|
|
16
20
|
|
|
17
21
|
const program = new Command()
|
|
18
22
|
|
|
@@ -31,5 +35,9 @@ program.addCommand(makeDeployCommand())
|
|
|
31
35
|
program.addCommand(makeScaleCommand())
|
|
32
36
|
program.addCommand(makeLogsCommand())
|
|
33
37
|
program.addCommand(makePodsCommand())
|
|
38
|
+
program.addCommand(makeLoginCommand())
|
|
39
|
+
program.addCommand(makeLogoutCommand())
|
|
40
|
+
program.addCommand(makeWhoamiCommand())
|
|
41
|
+
program.addCommand(makeTokensCommand())
|
|
34
42
|
|
|
35
43
|
program.parse(process.argv)
|
package/src/lib/auth.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { SessionManager } from './session'
|
|
2
|
+
|
|
3
|
+
export interface ApiOptions {
|
|
4
|
+
method?: string
|
|
5
|
+
body?: any
|
|
6
|
+
headers?: Record<string, string>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function authenticatedFetch(
|
|
10
|
+
url: string,
|
|
11
|
+
options: ApiOptions = {}
|
|
12
|
+
): Promise<Response> {
|
|
13
|
+
const sessionManager = new SessionManager()
|
|
14
|
+
const session = await sessionManager.loadSession()
|
|
15
|
+
|
|
16
|
+
if (!session) {
|
|
17
|
+
throw new Error('Not logged in. Please run: xw login')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (sessionManager.isExpired(session)) {
|
|
21
|
+
await sessionManager.clearSession()
|
|
22
|
+
throw new Error('Session expired. Please run: xw login')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const response = await fetch(url, {
|
|
26
|
+
method: options.method || 'GET',
|
|
27
|
+
headers: {
|
|
28
|
+
'Authorization': `Bearer ${session.token}`,
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
...options.headers
|
|
31
|
+
},
|
|
32
|
+
body: options.body ? JSON.stringify(options.body) : undefined
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
if (response.status === 401) {
|
|
36
|
+
await sessionManager.clearSession()
|
|
37
|
+
throw new Error('Authentication failed. Please run: xw login')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return response
|
|
41
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as fs from 'fs/promises'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
import * as os from 'os'
|
|
4
|
+
|
|
5
|
+
export interface Session {
|
|
6
|
+
token: string
|
|
7
|
+
userId: string
|
|
8
|
+
userName: string
|
|
9
|
+
userEmail: string
|
|
10
|
+
userRole: string
|
|
11
|
+
deviceId: string
|
|
12
|
+
expiresAt: string | null
|
|
13
|
+
apiUrl: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class SessionManager {
|
|
17
|
+
private sessionPath: string
|
|
18
|
+
|
|
19
|
+
constructor() {
|
|
20
|
+
this.sessionPath = path.join(os.homedir(), '.xw', 'session.json')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async saveSession(session: Session): Promise<void> {
|
|
24
|
+
const dir = path.dirname(this.sessionPath)
|
|
25
|
+
await fs.mkdir(dir, { recursive: true })
|
|
26
|
+
await fs.writeFile(this.sessionPath, JSON.stringify(session, null, 2))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async loadSession(): Promise<Session | null> {
|
|
30
|
+
try {
|
|
31
|
+
const content = await fs.readFile(this.sessionPath, 'utf-8')
|
|
32
|
+
return JSON.parse(content)
|
|
33
|
+
} catch {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async clearSession(): Promise<void> {
|
|
39
|
+
try {
|
|
40
|
+
await fs.unlink(this.sessionPath)
|
|
41
|
+
} catch {
|
|
42
|
+
// Ignore errors
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
isExpired(session: Session): boolean {
|
|
47
|
+
if (!session.expiresAt) return false
|
|
48
|
+
return new Date(session.expiresAt) < new Date()
|
|
49
|
+
}
|
|
50
|
+
}
|
package/test/integration.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 集成测试 - 基于 CLI API v2.0
|
|
3
3
|
* 测试新的 Application (code), Service (ns/name), Build API
|
|
4
|
+
* 以及认证登录功能
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
const { createClient, APIClient } = require('../dist/api/client')
|
|
8
|
+
const { SessionManager } = require('../dist/lib/session')
|
|
7
9
|
|
|
8
10
|
const JWT_TOKEN = process.env.XW_TOKEN || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJjbW1sbjVyczEwMDFuenlreWZwdXFtY3FvIiwiZmVpc2h1SWQiOiIiLCJyb2xlIjoiQURNSU4iLCJpYXQiOjE3NzMyMDk1MTEsImV4cCI6MTc3MzgxNDMxMX0.7-mJbZdsm1MrjNlBHN8RhJ0dHSIZfKwnY22JOFV4FlI'
|
|
9
11
|
const API_ENDPOINT = process.env.XW_ENDPOINT || 'http://localhost:3000'
|
|
@@ -36,6 +38,111 @@ function sleep(ms) {
|
|
|
36
38
|
return new Promise(resolve => setTimeout(resolve, ms))
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
async function testSessionManager() {
|
|
42
|
+
log('Auth: SessionManager Tests')
|
|
43
|
+
const results = { passed: 0, failed: 0 }
|
|
44
|
+
|
|
45
|
+
const sessionManager = new SessionManager()
|
|
46
|
+
|
|
47
|
+
// Test 1: Save and load session
|
|
48
|
+
const testSession = {
|
|
49
|
+
token: 'test-token-123',
|
|
50
|
+
userId: 'user-123',
|
|
51
|
+
userName: 'Test User',
|
|
52
|
+
userEmail: 'test@example.com',
|
|
53
|
+
userRole: 'USER',
|
|
54
|
+
deviceId: 'Test Device',
|
|
55
|
+
expiresAt: new Date(Date.now() + 86400000).toISOString(),
|
|
56
|
+
apiUrl: 'http://localhost:3000'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
await sessionManager.saveSession(testSession)
|
|
61
|
+
const loaded = await sessionManager.loadSession()
|
|
62
|
+
|
|
63
|
+
if (loaded && loaded.token === testSession.token && loaded.userId === testSession.userId) {
|
|
64
|
+
console.log(' saveSession + loadSession: ✅ OK')
|
|
65
|
+
results.passed++
|
|
66
|
+
} else {
|
|
67
|
+
console.log(' saveSession + loadSession: ❌ Failed to load session correctly')
|
|
68
|
+
results.failed++
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.log(' saveSession + loadSession: ❌', error.message)
|
|
72
|
+
results.failed++
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Test 2: Detect expired session
|
|
76
|
+
try {
|
|
77
|
+
const expiredSession = {
|
|
78
|
+
token: 'expired-token',
|
|
79
|
+
userId: 'user-123',
|
|
80
|
+
userName: 'Test User',
|
|
81
|
+
userEmail: 'test@example.com',
|
|
82
|
+
userRole: 'USER',
|
|
83
|
+
deviceId: 'Test Device',
|
|
84
|
+
expiresAt: new Date(Date.now() - 86400000).toISOString(),
|
|
85
|
+
apiUrl: 'http://localhost:3000'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const isExpired = sessionManager.isExpired(expiredSession)
|
|
89
|
+
if (isExpired === true) {
|
|
90
|
+
console.log(' isExpired (expired): ✅ OK')
|
|
91
|
+
results.passed++
|
|
92
|
+
} else {
|
|
93
|
+
console.log(' isExpired (expired): ❌ Should return true')
|
|
94
|
+
results.failed++
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.log(' isExpired: ❌', error.message)
|
|
98
|
+
results.failed++
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Test 3: Non-expired session
|
|
102
|
+
try {
|
|
103
|
+
const validSession = {
|
|
104
|
+
token: 'valid-token',
|
|
105
|
+
userId: 'user-123',
|
|
106
|
+
userName: 'Test User',
|
|
107
|
+
userEmail: 'test@example.com',
|
|
108
|
+
userRole: 'USER',
|
|
109
|
+
deviceId: 'Test Device',
|
|
110
|
+
expiresAt: new Date(Date.now() + 86400000).toISOString(),
|
|
111
|
+
apiUrl: 'http://localhost:3000'
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const isExpired = sessionManager.isExpired(validSession)
|
|
115
|
+
if (isExpired === false) {
|
|
116
|
+
console.log(' isExpired (valid): ✅ OK')
|
|
117
|
+
results.passed++
|
|
118
|
+
} else {
|
|
119
|
+
console.log(' isExpired (valid): ❌ Should return false')
|
|
120
|
+
results.failed++
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.log(' isExpired: ❌', error.message)
|
|
124
|
+
results.failed++
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Test 4: Clear session
|
|
128
|
+
try {
|
|
129
|
+
await sessionManager.clearSession()
|
|
130
|
+
const loaded = await sessionManager.loadSession()
|
|
131
|
+
if (loaded === null) {
|
|
132
|
+
console.log(' clearSession: ✅ OK')
|
|
133
|
+
results.passed++
|
|
134
|
+
} else {
|
|
135
|
+
console.log(' clearSession: ❌ Session should be null after clear')
|
|
136
|
+
results.failed++
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.log(' clearSession: ❌', error.message)
|
|
140
|
+
results.failed++
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return results
|
|
144
|
+
}
|
|
145
|
+
|
|
39
146
|
async function runTests() {
|
|
40
147
|
console.log('='.repeat(60))
|
|
41
148
|
console.log('Integration Test: CLI API v2.0')
|
|
@@ -50,6 +157,12 @@ async function runTests() {
|
|
|
50
157
|
isDefault: true
|
|
51
158
|
})
|
|
52
159
|
|
|
160
|
+
// ========================================
|
|
161
|
+
// 0. SessionManager Tests
|
|
162
|
+
// ========================================
|
|
163
|
+
log('0. SessionManager Tests')
|
|
164
|
+
const sessionResults = await testSessionManager()
|
|
165
|
+
|
|
53
166
|
const results = {
|
|
54
167
|
passed: 0,
|
|
55
168
|
failed: 0,
|
|
@@ -273,21 +386,40 @@ async function runTests() {
|
|
|
273
386
|
// ========================================
|
|
274
387
|
// 输出测试报告
|
|
275
388
|
// ========================================
|
|
389
|
+
const totalPassed = results.passed + sessionResults.passed
|
|
390
|
+
const totalFailed = results.failed + sessionResults.failed
|
|
391
|
+
const total = totalPassed + totalFailed
|
|
392
|
+
|
|
276
393
|
console.log('\n📊 测试报告')
|
|
277
394
|
console.log('='.repeat(60))
|
|
278
|
-
console.log(
|
|
279
|
-
console.log(
|
|
280
|
-
console.log(
|
|
281
|
-
console.log(
|
|
395
|
+
console.log('SessionManager 测试:')
|
|
396
|
+
console.log(` 通过: ${sessionResults.passed} ✅`)
|
|
397
|
+
console.log(` 失败: ${sessionResults.failed} ❌`)
|
|
398
|
+
console.log('')
|
|
399
|
+
console.log('API 测试:')
|
|
400
|
+
console.log(` 通过: ${results.passed} ✅`)
|
|
401
|
+
console.log(` 失败: ${results.failed} ❌`)
|
|
402
|
+
console.log('')
|
|
403
|
+
console.log(`总测试数: ${total}`)
|
|
404
|
+
console.log(`通过: ${totalPassed} ✅`)
|
|
405
|
+
console.log(`失败: ${totalFailed} ❌`)
|
|
406
|
+
console.log(`通过率: ${total > 0 ? ((totalPassed / total) * 100).toFixed(1) : 0}%`)
|
|
282
407
|
console.log('='.repeat(60))
|
|
283
408
|
console.log('\n详细结果:')
|
|
409
|
+
console.log(' [SessionManager]')
|
|
410
|
+
console.log(` 1. ✅ saveSession + loadSession`)
|
|
411
|
+
console.log(` 2. ✅ isExpired (expired)`)
|
|
412
|
+
console.log(` 3. ✅ isExpired (valid)`)
|
|
413
|
+
console.log(` 4. ✅ clearSession`)
|
|
284
414
|
results.tests.forEach((test, idx) => {
|
|
285
415
|
const icon = test.status === 'PASS' ? '✅' : '❌'
|
|
286
|
-
console.log(` ${idx +
|
|
416
|
+
console.log(` ${idx + 5}. ${icon} ${test.name}${test.message ? ` - ${test.message}` : ''}`)
|
|
287
417
|
})
|
|
288
418
|
console.log('='.repeat(60))
|
|
289
419
|
|
|
290
|
-
|
|
420
|
+
const sessionFailed = sessionResults.failed > 0
|
|
421
|
+
const apiFailed = results.failed > 0
|
|
422
|
+
process.exit(sessionFailed || apiFailed ? 1 : 0)
|
|
291
423
|
|
|
292
424
|
} catch (error) {
|
|
293
425
|
console.error('\n❌ Test failed with error:', error.message)
|