Class: JPSClient::Auth

Inherits:
Object
  • Object
show all
Defined in:
lib/jpsclient/auth/auth.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config = nil) ⇒ Auth

Returns a new instance of Auth.



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/jpsclient/auth/auth.rb', line 69

def initialize(config = nil)
  # 只接受 LoginConfig 对象或默认配置
  @config = config || LoginConfig.new

  # 从配置对象获取值
  @client_id = @config.client_id
  @feishu_auth_url = @config.feishu_auth_url
  @redirect_uri = @config.redirect_uri
  @api_endpoint = @config.api_endpoint
  @state = @config.state
  @server_port = @config.server_port

  @scope_list = [
    'offline_access',
    'task:task:write',
    'task:section:write',
    'task:custom_field:write',
    'task:tasklist:write',
    'drive:drive',
    'wiki:wiki',
    'docx:document',
    'bitable:app',
    'contact:user.employee_id:readonly',
    'docs:document.content:read',
    'im:chat',
    'base:app:copy',
    'base:record:update',
    'task:comment:write',
    'task:comment',
    'task:attachment:write',
    'vc:room:readonly',
    'vc:meeting:readonly'
  ]

  @access_token = nil
  @username = nil
  @user_id = nil
  @permissions = nil
  @lark_user_id = nil
  @tenant_manager = false

  # 调试模式,通过环境变量控制
  @verbose = ENV['PINDO_DEBUG'] == 'true'
end

Instance Attribute Details

#access_tokenObject (readonly)

Returns the value of attribute access_token.



66
67
68
# File 'lib/jpsclient/auth/auth.rb', line 66

def access_token
  @access_token
end

#lark_user_idObject (readonly)

Returns the value of attribute lark_user_id.



67
68
69
# File 'lib/jpsclient/auth/auth.rb', line 67

def lark_user_id
  @lark_user_id
end

#permissionsObject (readonly)

Returns the value of attribute permissions.



67
68
69
# File 'lib/jpsclient/auth/auth.rb', line 67

def permissions
  @permissions
end

#tenant_managerObject (readonly)

Returns the value of attribute tenant_manager.



67
68
69
# File 'lib/jpsclient/auth/auth.rb', line 67

def tenant_manager
  @tenant_manager
end

#user_idObject (readonly)

Returns the value of attribute user_id.



67
68
69
# File 'lib/jpsclient/auth/auth.rb', line 67

def user_id
  @user_id
end

#usernameObject (readonly)

Returns the value of attribute username.



66
67
68
# File 'lib/jpsclient/auth/auth.rb', line 66

def username
  @username
end

Instance Method Details

#authorizeObject

启动授权流程(保留兼容性)



126
127
128
# File 'lib/jpsclient/auth/auth.rb', line 126

def authorize
  
end

#authorize_and_loginObject

完整的授权和登录流程



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/jpsclient/auth/auth.rb', line 150

def 
  # 构建授权 URL
  authorization_uri = build_authorization_uri
  puts "正在打开浏览器进行飞书 OAuth 授权..."
  puts "\n授权 URL(如自动打开失败,请手动复制下面的链接到浏览器):"
  puts "=" * 80
  puts authorization_uri
  puts "=" * 80
  puts ""

  # 在浏览器中打开授权 URL
  open_browser(authorization_uri)

  # 启动本地服务器处理回调
  code = start_callback_server

  # 如果自动获取失败,提示用户手动输入
  if code.nil?
    loop do
      puts "\n自动获取授权码失败,请选择:"
      puts "1. 输入授权码 (直接复制 'code=' 后面的内容)"
      puts "2. 输入完整回调 URL"
      puts "3. 重新打开授权网页"
      puts "4. 退出"
      print "> "

      begin
        choice = STDIN.gets&.chomp

        # 处理 Ctrl+C 中断
        if choice.nil?
          puts "\n用户中断操作"
          return :user_cancelled
        end

        case choice
        when "1"
          puts "请输入授权码:"
          print "> "
          code_input = STDIN.gets&.chomp
          if code_input.nil?
            puts "用户中断操作"
            return :user_cancelled
          elsif !code_input.empty?
            code = code_input
            break
          else
            puts "授权码不能为空,请重新选择"
          end
        when "2"
          puts "请输入完整回调 URL:"
          print "> "
          url_input = STDIN.gets&.chomp
          if url_input.nil?
            puts "用户中断操作"
            return :user_cancelled
          elsif url_input.start_with?("http")
            # 尝试从 URL 中提取 code
            begin
              uri = URI(url_input)
              query_params = URI.decode_www_form(uri.query || '').to_h
              code = query_params['code']
              if code
                puts "✓ 从 URL 中成功提取授权码"
                break
              else
                puts "✗ URL 中没有找到授权码,请重新选择"
              end
            rescue => e
              puts "✗ 无法从 URL 中提取授权码: #{e.message}"
            end
          else
            puts "✗ 无效的 URL 格式,请重新选择"
          end
        when "3"
          # 重新打开授权网页
          puts "正在重新打开授权网页..."
          open_browser(authorization_uri)
          # 重新启动服务器尝试获取授权码
          code = start_callback_server
          if code
            break
          end
          # 如果还是失败,继续循环让用户选择
        when "4"
          puts "已退出授权流程"
          return :user_cancelled
        else
          puts "无效的选择,请输入 1-4 之间的数字"
        end
      rescue Interrupt
        puts "\n\n用户中断操作"
        return :user_cancelled
      end
    end
  end

  if code
    if exchange_code_for_token(code)
      puts "✓ JPS 登录成功!用户名: #{@username}"
      return true
    end
    return false
  else
    puts "✗ 授权失败"
    return false
  end
end

#exchange_code_for_token(code) ⇒ Object

使用授权码换取 token



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/jpsclient/auth/auth.rb', line 260

def exchange_code_for_token(code)
  begin
    puts "🔄 获取授权码成功: #{code[0..10]}..."
    puts "🔄 正在换取访问令牌..."

    request_data = {
      'code' => code,
      'redirectUri' => @redirect_uri,
      'scope' => @scope_list.join(' ')
    }

    uri = URI(@api_endpoint)
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = (uri.scheme == 'https')

    # 配置 SSL 以兼容 OpenSSL 3.x(避免 CRL 检查失败)
    if http.use_ssl?
      http.verify_mode = OpenSSL::SSL::VERIFY_PEER
      # 创建 SSL 上下文并禁用 CRL 检查
      http.cert_store = OpenSSL::X509::Store.new
      http.cert_store.set_default_paths
      # verify_flags = 0 表示不检查 CRL
      http.cert_store.flags = 0
    end

    request = Net::HTTP::Post.new(uri)
    request['Content-Type'] = 'application/json'
    request.body = request_data.to_json

    puts "正在请求 JPS API: #{@api_endpoint}" if @verbose
    response = http.request(request)

    puts "API 响应状态码: #{response.code}" if @verbose

    if response.body
      result = JSON.parse(response.body)
      puts "API 响应: #{result.inspect}" if @verbose

      if result['meta'] && result['meta']['code'] == 0 && result['data']
        data = result['data']
        # 兼容 snake_case 和 camelCase 两种响应格式
        @access_token = data['token'] if data['token']
        @username = data['username'] if data['username']
        @user_id = data['user_id'] || data['userId']
        @permissions = data['permissions']
        @lark_user_id = data['lark_user_id'] || data['larkUserId']
        @tenant_manager = data['tenant_manager'] || data['tenantManager'] || false

        return true if @access_token
      else
        error_msg = result['meta'] && result['meta']['message'] ? result['meta']['message'] : '未知错误'
        puts "JPS 登录失败: #{error_msg}"
      end
    else
      puts "API 返回空响应" if @verbose
    end

    return false
  rescue => e
    puts "请求 JPS API 失败: #{e.class} - #{e.message}"
    return false
  end
end

#get_token_dataObject

获取 token 数据(供 Client 使用)



136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/jpsclient/auth/auth.rb', line 136

def get_token_data
  return nil unless @access_token

  {
    'token' => @access_token,
    'username' => @username,
    'user_id' => @user_id,
    'permissions' => @permissions,
    'lark_user_id' => @lark_user_id,
    'tenant_manager' => @tenant_manager
  }
end

#get_usernameObject

获取用户名(登录后可用)



131
132
133
# File 'lib/jpsclient/auth/auth.rb', line 131

def get_username
  @username
end

#loginObject

主登录入口



115
116
117
118
119
120
121
122
123
# File 'lib/jpsclient/auth/auth.rb', line 115

def 
  puts "🔐 需要登录 JPS..."
  result = 

  # 如果用户主动取消,返回特殊标识
  return :user_cancelled if result == :user_cancelled

  return result
end