pindo 5.0.4 → 5.0.6

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e7508977271b8a9092068bffb6c429402f3f4f3ddf9d9c8fdd3e4c7b7580522
4
- data.tar.gz: 912474cf184b1621c147f5c713f374e48f40deeff97306504fa5745fe3648cd2
3
+ metadata.gz: da7db67cae593054d94da87ec555ccd0d6ed2691ca9d534729a386b4cc55f360
4
+ data.tar.gz: 25b281165febd17ac56dfcadd64dcf2e55674e326b28575ff3952007b7dbdc2d
5
5
  SHA512:
6
- metadata.gz: efc6aeff8688764f201c3723e8b8e391ab02c9abef5d7b9bee68d30a821903549b7343b62e02a02aa8c66918b048c3cc4652523380ab4e08e6a34d96ce8eca6a
7
- data.tar.gz: 92671cf31234549e38c59c2a673ff8bc181ef580b0ae5698c69b3179384346d03d3b8966f4b932348c23fd6cbfd1e7af3516e5431da9bdc841969f274327ad7b
6
+ metadata.gz: '089d77424e3f53c88de5d9839b6849a11c868e6bf637b485403cec677a0e320cc2292e09fe8c868ecec3950a28e21b060a0867eadcd9b01dd1cab7aaaf8b878d'
7
+ data.tar.gz: 34a13c1bf0691285cd4d7c76c284c288c445ec2f3e67650d504ccdf1d3c2baa599c91492943ddee6078245372e5ae42746d2cd2bb881f9da0075305f7bd03d66
@@ -545,7 +545,7 @@ module Pindo
545
545
  end
546
546
 
547
547
  git!(%W(-C #{project_dir} reset --hard))
548
- git!(%W(-C #{project_dir} clean -dfx))
548
+ git!(%W(-C #{project_dir} clean -df))
549
549
  git!(%W(-C #{project_dir} branch --set-upstream-to=origin/#{branch} #{branch}))
550
550
  git!(%W(-C #{project_dir} pull))
551
551
  end
@@ -0,0 +1,313 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'webrick'
4
+ require 'json'
5
+ require 'net/http'
6
+ require 'uri'
7
+ require 'securerandom'
8
+ require 'digest'
9
+ require 'base64'
10
+ require 'fileutils'
11
+ require 'pindo/base/aeshelper'
12
+ require 'pindo/config/pindouserlocalconfig'
13
+
14
+ module Pindo
15
+
16
+ class PgyerFeishuOAuthCLI
17
+ attr_reader :access_token, :refresh_token, :user_info
18
+
19
+ def initialize(client_id)
20
+ @client_id = client_id
21
+ @feishu_auth_url = 'https://open.feishu.cn/open-apis/authen/v1/index'
22
+ @redirect_uri = 'https://jps-new.devtestapp.com/auth/jwt/login'
23
+ # @state = SecureRandom.hex(16)
24
+ @state = 'success_login'
25
+ @larkScopeList = [
26
+ 'offline_access',
27
+ 'task:task:write',
28
+ 'task:section:write',
29
+ 'task:custom_field:write',
30
+ 'task:tasklist:write',
31
+ ];
32
+
33
+ @access_token = nil
34
+ @refresh_token = nil
35
+ @token_expired_at = nil
36
+
37
+ @pgyer_token_file = File.join(File::expand_path(Pindoconfig.instance.pindo_dir), ".pgyer_token")
38
+
39
+ # 尝试读取配置文件中的AES密钥
40
+ begin
41
+ config_file = File.join(File::expand_path(Pindoconfig.instance.pindo_common_configdir), "pgyer_client_config.json")
42
+ if File.exist?(config_file)
43
+ config_json = JSON.parse(File.read(config_file))
44
+ @pgyer_aes_key = config_json["pgyerapps_aes_key"] if config_json["pgyerapps_aes_key"]
45
+ end
46
+ rescue => e
47
+ puts "读取配置文件失败: #{e.message}"
48
+ end
49
+
50
+ # 如果配置文件中没有找到密钥,则使用默认密钥
51
+ @pgyer_aes_key ||= ENV['PGYER_AES_KEY'] || "pgyerOauthToken2024"
52
+
53
+ # Pgyer API endpoints
54
+ @pgyer_api_endpoint = 'https://jps-api.devtestapp.com/api/lark_login'
55
+ end
56
+
57
+ # 启动授权流程
58
+ def authorize
59
+ # 先检查是否有存储的令牌
60
+ if load_token
61
+ puts "找到已保存的Pgyer令牌,尝试验证..."
62
+ if validate_pgyer_token
63
+ puts "已有令牌验证成功!"
64
+ return true
65
+ else
66
+ puts "已有令牌已失效,需要重新授权。"
67
+ end
68
+ end
69
+
70
+ authorization_uri = build_authorization_uri
71
+ puts "正在打开浏览器进行飞书OAuth授权..."
72
+ puts "授权URI: #{authorization_uri}"
73
+
74
+ # 在浏览器中打开授权URL
75
+ open_browser(authorization_uri)
76
+
77
+ # 启动本地服务器处理回调
78
+ code = start_callback_server
79
+
80
+ if code
81
+ puts "正在使用飞书身份登录Pgyer..."
82
+ if login_pgyer_with_feishu(code:code)
83
+ store_token
84
+ return true
85
+ end
86
+ return false
87
+ else
88
+ puts "授权失败"
89
+ return false
90
+ end
91
+ end
92
+
93
+ # 验证Pgyer令牌
94
+ def validate_pgyer_token
95
+ return false unless @access_token
96
+
97
+ # 这里应该调用Pgyer的API验证令牌有效性
98
+ # 由于没有具体API文档,这里仅做示例
99
+ uri = URI("https://www.pgyer.com/api/user/profile")
100
+ request = Net::HTTP::Get.new(uri)
101
+ request['Authorization'] = "Bearer #{@access_token}"
102
+
103
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
104
+ http.request(request)
105
+ end
106
+
107
+ return response.code == '200'
108
+ end
109
+
110
+ # 使用code登录Pgyer, 并且Pgyer返回token
111
+ def login_pgyer_with_feishu(code: nil)
112
+ return false unless code
113
+
114
+ uri = URI(@pgyer_api_endpoint)
115
+ request = Net::HTTP::Post.new(uri)
116
+ request['Content-Type'] = 'application/json'
117
+
118
+ # 根据Pgyer的API要求构造请求体
119
+ request.body = {
120
+ code: code,
121
+ redirectUri: @redirect_uri,
122
+ scope: @larkScopeList.join(' ')
123
+ }.to_json
124
+
125
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
126
+ http.request(request)
127
+ end
128
+
129
+ if response.code == '200'
130
+ begin
131
+ result = JSON.parse(response.body)
132
+ if !result['meta'].nil? && result['meta']['code'] == 200 && !result['data'].nil? && !result['data']['token'].nil?
133
+ @access_token = result['data']['token']
134
+ @username = result['data']['token']
135
+ @user_id = result['data']['user_id']
136
+
137
+ return true
138
+ else
139
+ puts "Pgyer登录失败: #{result['message'] || '未知错误'}"
140
+ return false
141
+ end
142
+ rescue JSON::ParserError
143
+ puts "解析Pgyer响应失败"
144
+ return false
145
+ end
146
+ else
147
+ puts "Pgyer登录请求失败: HTTP #{response.code}"
148
+ return false
149
+ end
150
+
151
+ end
152
+
153
+
154
+ # 加载存储的令牌
155
+ def load_token
156
+ return false unless File.exist?(@pgyer_token_file)
157
+
158
+ begin
159
+ encrypted_data = File.read(@pgyer_token_file)
160
+ json_data = aes_decrypt(encrypted_data, @pgyer_aes_key)
161
+ token_data = JSON.parse(json_data)
162
+
163
+ @access_token = token_data['token']
164
+ @user_info = token_data['user_info'] if token_data['user_info']
165
+ @token_expired_at = Time.at(token_data['expires_at']) if token_data['expires_at']
166
+
167
+ # 检查令牌是否过期
168
+ if @token_expired_at && Time.now >= @token_expired_at
169
+ puts "令牌已过期,需要重新授权"
170
+ return false
171
+ end
172
+
173
+ return true
174
+ rescue => e
175
+ puts "加载令牌失败: #{e.message}"
176
+ return false
177
+ end
178
+ end
179
+
180
+ # 存储令牌
181
+ def store_token
182
+ return false unless @access_token
183
+
184
+ token_data = {
185
+ 'token' => @access_token,
186
+ 'user_info' => @user_info,
187
+ 'expires_at' => Time.now.to_i + 7*24*60*60 # 假设令牌有效期为7天
188
+ }
189
+
190
+ encrypted_data = aes_encrypt(token_data.to_json, @pgyer_aes_key)
191
+
192
+ begin
193
+ File.open(@pgyer_token_file, 'w') do |f|
194
+ f.write(encrypted_data)
195
+ end
196
+ puts "令牌已安全存储"
197
+ return true
198
+ rescue => e
199
+ puts "存储令牌失败: #{e.message}"
200
+ return false
201
+ end
202
+ end
203
+
204
+ private
205
+
206
+ # AES加密
207
+ def aes_encrypt(data, key)
208
+ # 使用Pindo的AESHelper类进行真正的AES加密
209
+ AESHelper::aes_128_ecb_encrypt(key, data)
210
+ end
211
+
212
+ # AES解密
213
+ def aes_decrypt(encrypted_data, key)
214
+ # 使用Pindo的AESHelper类进行真正的AES解密
215
+ AESHelper::aes_128_ecb_decrypt(key, encrypted_data)
216
+ end
217
+
218
+ # 构建授权URI
219
+ def build_authorization_uri
220
+ uri = URI(@feishu_auth_url)
221
+
222
+
223
+
224
+ params = {
225
+ 'app_id' => @client_id,
226
+ 'redirect_uri' => @redirect_uri,
227
+ 'response_type' => 'code',
228
+ 'state' => @state,
229
+ 'scope' => @larkScopeList.join(' ')
230
+ }
231
+
232
+ uri.query = URI.encode_www_form(params)
233
+ uri.to_s
234
+ end
235
+
236
+ # 在浏览器中打开URL
237
+ def open_browser(url)
238
+ if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
239
+ system("start", url)
240
+ elsif RbConfig::CONFIG['host_os'] =~ /darwin/
241
+ system("open", url)
242
+ elsif RbConfig::CONFIG['host_os'] =~ /linux|bsd/
243
+ system("xdg-open", url)
244
+ else
245
+ puts "无法自动打开浏览器,请手动访问: #{url}"
246
+ end
247
+ end
248
+
249
+ # 启动本地服务器处理回调
250
+ def start_callback_server
251
+ code = nil
252
+ uri = URI(@redirect_uri)
253
+ server = WEBrick::HTTPServer.new(
254
+ Port: uri.port,
255
+ Logger: WEBrick::Log.new("/dev/null"),
256
+ AccessLog: []
257
+ )
258
+
259
+ path = uri.path.empty? ? '/' : uri.path
260
+ server.mount_proc path do |req, res|
261
+ query_params = URI.decode_www_form(req.query_string || '').to_h
262
+
263
+ if query_params['state'] != @state
264
+ res.body = "状态不匹配,可能存在CSRF攻击风险"
265
+ elsif query_params['error']
266
+ res.body = "授权错误: #{query_params['error']}"
267
+ elsif query_params['code']
268
+ code = query_params['code']
269
+ res.body = <<-HTML
270
+ <!DOCTYPE html>
271
+ <html>
272
+ <head>
273
+ <title>飞书授权成功</title>
274
+ <style>
275
+ body { font-family: Arial, sans-serif; text-align: center; padding: 40px; }
276
+ .container { max-width: 600px; margin: 0 auto; }
277
+ .success { color: #4CAF50; }
278
+ </style>
279
+ </head>
280
+ <body>
281
+ <div class="container">
282
+ <h1 class="success">授权成功!</h1>
283
+ <p>飞书OAuth授权已完成,您现在可以关闭此窗口并返回命令行。</p>
284
+ </div>
285
+ </body>
286
+ </html>
287
+ HTML
288
+ else
289
+ res.body = "未获取到授权码"
290
+ end
291
+
292
+ server.shutdown
293
+ end
294
+
295
+ # 捕获Ctrl+C以允许用户中断
296
+ trap('INT') { server.shutdown }
297
+
298
+ # 在线程中运行服务器,最多等待2分钟
299
+ thread = Thread.new { server.start }
300
+ begin
301
+ thread.join(120) # 最多等待2分钟
302
+ rescue Timeout::Error
303
+ puts "授权超时"
304
+ server.shutdown
305
+ end
306
+
307
+ code
308
+ end
309
+
310
+
311
+ end
312
+
313
+ end