Add keybase integration (#661)
This commit is contained in:
parent
75750213da
commit
4fc391e97d
|
@ -38,6 +38,7 @@ public/apple-touch-icon*
|
|||
# files added in production
|
||||
lib/tasks/deploy.rake
|
||||
config/initializers/production.rb
|
||||
config/initializers/development.rb
|
||||
config/database.yml
|
||||
config/initializers/secret_token.rb
|
||||
config/*.sphinx.conf
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M0,0c0,5.33,0,10.67,0,16c5.33,0,10.67,0,16,0c0-5.33,0-10.67,0-16C10.67,0,5.33,0,0,0z M13.14,13.45c-3.2,0-6.43,0-9.63,0
|
||||
c0-0.1,0-0.2,0-0.3c0-0.11,0-0.22,0-0.33c0-0.11,0-0.1,0.1-0.11c0.23-0.02,0.46-0.03,0.69-0.07c0.46-0.08,0.67-0.31,0.74-0.78
|
||||
c0.02-0.14,0.03-0.29,0.03-0.43c0-2.25,0-4.5,0-6.74c0-0.18-0.02-0.36-0.04-0.53C4.99,3.85,4.79,3.66,4.5,3.55
|
||||
c-0.27-0.1-0.55-0.11-0.83-0.13c-0.05,0-0.1,0-0.15,0c0-0.03,0-0.06,0-0.08c0-0.19,0-0.38,0-0.57c0-0.07,0.02-0.09,0.09-0.09
|
||||
c1.04,0,2.07,0,3.11,0c0.71,0,1.42,0,2.13,0c0.07,0,0.1,0.01,0.1,0.09c-0.01,0.19,0,0.37,0,0.56c0,0.03,0,0.05,0,0.08
|
||||
c-0.14,0.01-0.28,0-0.41,0.02c-0.2,0.03-0.4,0.06-0.59,0.11C7.6,3.65,7.42,3.89,7.38,4.22c-0.02,0.15-0.03,0.3-0.03,0.45
|
||||
c0,2.13,0,4.26,0,6.39c0,0.27,0.03,0.54,0.06,0.8c0.03,0.29,0.22,0.48,0.49,0.58c0.23,0.09,0.48,0.11,0.72,0.12
|
||||
c0.63,0.02,1.25,0.01,1.86-0.15c0.88-0.22,1.47-0.77,1.76-1.64c0.09-0.28,0.13-0.57,0.2-0.86c0.01-0.02,0.03-0.06,0.05-0.06
|
||||
c0.23,0,0.42,0,0.66,0C13.11,11.06,13.16,12.25,13.14,13.45z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,13 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<g>
|
||||
<path fill="#AC130D" d="M16,16c-5.33,0-10.67,0-16,0C0,10.67,0,5.33,0,0c5.33,0,10.67,0,16,0C16,5.33,16,10.67,16,16z"/>
|
||||
<path fill="#FFFFFF" d="M3.5,13.45c0-0.1,0-0.2,0-0.3c0-0.11,0-0.22,0-0.33c0-0.11,0-0.1,0.1-0.11c0.23-0.02,0.46-0.03,0.69-0.07
|
||||
c0.46-0.08,0.67-0.31,0.74-0.78c0.02-0.14,0.03-0.29,0.03-0.43c0-2.25,0-4.5,0-6.74c0-0.18-0.02-0.36-0.04-0.53
|
||||
C4.99,3.85,4.79,3.66,4.5,3.55c-0.27-0.1-0.55-0.11-0.83-0.13c-0.05,0-0.1,0-0.15,0c0-0.03,0-0.06,0-0.08c0-0.19,0-0.38,0-0.57
|
||||
c0-0.07,0.02-0.09,0.09-0.09c1.04,0,2.07,0,3.11,0c0.71,0,1.42,0,2.13,0c0.07,0,0.1,0.01,0.1,0.09c-0.01,0.19,0,0.37,0,0.56
|
||||
c0,0.03,0,0.05,0,0.08c-0.14,0.01-0.28,0-0.41,0.02c-0.2,0.03-0.4,0.06-0.59,0.11C7.6,3.65,7.42,3.89,7.38,4.22
|
||||
c-0.02,0.15-0.03,0.3-0.03,0.45c0,2.13,0,4.26,0,6.39c0,0.27,0.03,0.54,0.06,0.8c0.03,0.29,0.22,0.48,0.49,0.58
|
||||
c0.23,0.09,0.48,0.11,0.72,0.12c0.63,0.02,1.25,0.01,1.86-0.15c0.88-0.22,1.47-0.77,1.76-1.64c0.09-0.28,0.13-0.57,0.2-0.86
|
||||
c0.01-0.02,0.03-0.06,0.05-0.06c0.23,0,0.42,0,0.66,0c-0.02,1.2,0.02,2.4,0,3.6C9.94,13.45,6.71,13.45,3.5,13.45z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,73 @@
|
|||
class KeybaseProofsController < ApplicationController
|
||||
before_action :require_logged_in_user, only: [:new, :create]
|
||||
before_action :check_new_params, only: :new
|
||||
before_action :check_user_matches, only: :new
|
||||
before_action :force_to_json, only: [:kbconfig]
|
||||
|
||||
def new
|
||||
@kb_username = params[:kb_username]
|
||||
@kb_signature = params[:kb_signature]
|
||||
@kb_ua = params[:kb_ua]
|
||||
@kb_avatar = Keybase.avatar_url(@kb_username)
|
||||
end
|
||||
|
||||
def create
|
||||
kb_username = post_params[:kb_username]
|
||||
kb_signature = post_params[:kb_signature]
|
||||
kb_ua = post_params[:kb_ua]
|
||||
if Keybase.proof_valid?(kb_username, kb_signature, @user.username)
|
||||
@user.add_or_update_keybase_proof(kb_username, kb_signature)
|
||||
@user.save!
|
||||
return redirect_to Keybase.success_url(kb_username, kb_signature, kb_ua, @user.username)
|
||||
else
|
||||
flash[:error] = "Failed to connect your account to Keybase. Try again from Keybase."
|
||||
return redirect_to settings_path
|
||||
end
|
||||
end
|
||||
|
||||
def kbconfig
|
||||
return render json: {} unless Keybase.enabled?
|
||||
@domain = Keybase.DOMAIN
|
||||
@name = Rails.application.name
|
||||
@brand_color = "#AC130D"
|
||||
@description = "Computing-focused community centered around link aggregation and discussion"
|
||||
@contacts = ["admin@#{Keybase.DOMAIN}"]
|
||||
# rubocop:disable Style/FormatStringToken
|
||||
@prefill_url = "#{new_keybase_proof_url}?kb_username=%{kb_username}&" \
|
||||
"kb_signature=%{sig_hash}&kb_ua=%{kb_ua}&username=%{username}"
|
||||
@profile_url = "#{u_url}/%{username}"
|
||||
@check_url = "#{u_url}/%{username}.json"
|
||||
# rubocop:enable Style/FormatStringToken
|
||||
@logo_black = "https://lobste.rs/small-black-logo.svg"
|
||||
@logo_full = "https://lobste.rs/full-color.logo.svg"
|
||||
@user_re = User.username_regex_s[1...-1]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def force_to_json
|
||||
request.format = :json
|
||||
end
|
||||
|
||||
def check_user_matches
|
||||
unless case_insensitive_match?(@user.username, params[:username])
|
||||
flash[:error] = "not logged in as the correct user"
|
||||
return redirect_to settings_path
|
||||
end
|
||||
end
|
||||
|
||||
def case_insensitive_match?(first_string, second_string)
|
||||
# can replace this with first_string.casecmp?(second_string) when ruby >= 2.4.6
|
||||
first_string.casecmp(second_string).zero?
|
||||
end
|
||||
|
||||
def post_params
|
||||
params.require(:keybase_proof).permit(:kb_username, :kb_signature, :kb_ua, :username)
|
||||
end
|
||||
|
||||
def check_new_params
|
||||
redirect_to settings_path unless [:kb_username, :kb_signature, :kb_ua, :username].all? do |k|
|
||||
params[k].present?
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
module KeybaseProofsHelper
|
||||
def keybase_user_link(kb_sig)
|
||||
File.join Keybase.BASE_URL, kb_sig[:kb_username]
|
||||
end
|
||||
|
||||
def keybase_proof_link(kb_sig)
|
||||
File.join Keybase.BASE_URL, kb_sig[:kb_username], "sigchain\##{kb_sig[:sig_hash]}"
|
||||
end
|
||||
end
|
|
@ -67,6 +67,7 @@ class User < ApplicationRecord
|
|||
s.string :twitter_oauth_token
|
||||
s.string :twitter_oauth_token_secret
|
||||
s.string :twitter_username
|
||||
s.any :keybase_signatures, array: true
|
||||
s.string :homepage
|
||||
end
|
||||
|
||||
|
@ -169,6 +170,10 @@ class User < ApplicationRecord
|
|||
h[:twitter_username] = self.twitter_username
|
||||
end
|
||||
|
||||
if self.keybase_signatures.present?
|
||||
h[:keybase_signatures] = self.keybase_signatures
|
||||
end
|
||||
|
||||
h
|
||||
end
|
||||
|
||||
|
@ -433,6 +438,12 @@ class User < ApplicationRecord
|
|||
Time.current - self.created_at <= NEW_USER_DAYS.days
|
||||
end
|
||||
|
||||
def add_or_update_keybase_proof(kb_username, kb_signature)
|
||||
self.keybase_signatures ||= []
|
||||
self.keybase_signatures.reject! {|kbsig| kbsig['kb_username'] == kb_username }
|
||||
self.keybase_signatures.push('kb_username' => kb_username, 'sig_hash' => kb_signature)
|
||||
end
|
||||
|
||||
def is_heavy_self_promoter?
|
||||
total_count = self.stories_submitted_count
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<% if user.keybase_signatures? %>
|
||||
Linked to
|
||||
<% user.keybase_signatures.each do |kbs| %>
|
||||
<strong><%= link_to "@#{kbs[:kb_username]}", keybase_user_link(kbs) %></strong>
|
||||
<strong><%= link_to "✔", keybase_proof_link(kbs) %></strong>
|
||||
|
||||
<% end %>
|
||||
<% if for_self %>
|
||||
(<%= link_to "Disconnect on Keybase", "https://keybase.io/download" %>)
|
||||
<% end %>
|
||||
<% elsif for_self %>
|
||||
Install <%= link_to "Keybase", "https://keybase.io/download" %> to prove a cryptographic connection.
|
||||
<% end %>
|
|
@ -0,0 +1,24 @@
|
|||
# see https://keybase.io/docs/proof_integration_guide#1-config
|
||||
|
||||
JSON.pretty_generate({
|
||||
"version": 1,
|
||||
"domain": @domain,
|
||||
"display_name": @name,
|
||||
"description": @description,
|
||||
"brand_color": @brand_color,
|
||||
"logo": {
|
||||
"svg_black": @logo_black,
|
||||
"svg_full": @logo_full
|
||||
},
|
||||
"username": {
|
||||
"re": @user_re,
|
||||
"min": 1,
|
||||
"max": 25
|
||||
},
|
||||
"prefill_url": @prefill_url,
|
||||
"profile_url": @profile_url,
|
||||
"check_url": @check_url,
|
||||
"check_path": ["keybase_signatures"],
|
||||
"avatar_path": ["avatar_url"],
|
||||
"contact": @contacts
|
||||
})
|
|
@ -0,0 +1,25 @@
|
|||
<div class="box wide">
|
||||
<div class="legend">
|
||||
<span>
|
||||
Connect Your Account (<%= @user.username %>) to Keybase
|
||||
</span>
|
||||
</div>
|
||||
<div id="gravatar">
|
||||
<%= avatar_img(@user, 100) %>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<%= image_tag @kb_avatar, width: 100 %>
|
||||
</p>
|
||||
|
||||
<%= form_with(url: keybase_proofs_url, scope: :keybase_proof) do |f| %>
|
||||
Is this you on Keybase? <b><i><%= @kb_username %></b></i>
|
||||
<%= f.hidden_field "kb_username", value: @kb_username %>
|
||||
<%= f.hidden_field "kb_signature", value: @kb_signature %>
|
||||
<%= f.hidden_field "kb_ua", value: @kb_ua %>
|
||||
<%= f.hidden_field "username", value: @user.username %>
|
||||
<p>
|
||||
<%= f.submit "Confirm" %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
|
@ -144,6 +144,17 @@
|
|||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if Keybase.enabled? %>
|
||||
<div class="boxline">
|
||||
<%= f.label :kb_username,
|
||||
raw("<a href=\"https://keybase.io/\">Keybase</a>:"),
|
||||
:class => "required" %>
|
||||
<span>
|
||||
<%= render :partial => "keybase_proofs/proofs", locals: {user: @edit_user, for_self: true} %>
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="legend">
|
||||
|
|
|
@ -149,9 +149,20 @@
|
|||
<% else %>
|
||||
<span class="na">A mystery...</span>
|
||||
<% end %>
|
||||
<br>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if Keybase.enabled? %>
|
||||
<% for_self = (@showing_user == @user) %>
|
||||
<% if @showing_user.keybase_signatures? || for_self %>
|
||||
<label class="required">Keybase:</label>
|
||||
<span class="d">
|
||||
<%= render :partial => "keybase_proofs/proofs", locals: {user: @showing_user, for_self: for_self} %>
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if @user && @user.is_moderator? && !@showing_user.is_moderator? %>
|
||||
<h2>Moderator Information</h2>
|
||||
|
||||
|
|
|
@ -35,5 +35,8 @@ if Rails.env.production?
|
|||
|
||||
BCrypt::Engine.cost = 12
|
||||
|
||||
Keybase.DOMAIN = Rails.application.domain
|
||||
Keybase.BASE_URL = ENV.fetch('KEYBASE_BASE_URL') { 'https://keybase.io' }
|
||||
|
||||
ActionMailer::Base.delivery_method = :sendmail
|
||||
end
|
||||
|
|
|
@ -147,6 +147,9 @@ Rails.application.routes.draw do
|
|||
get "/settings/twitter_callback" => "settings#twitter_callback"
|
||||
post "/settings/twitter_disconnect" => "settings#twitter_disconnect"
|
||||
|
||||
resources :keybase_proofs, only: [:new, :create]
|
||||
get "/.well-known/keybase-proof-config" => "keybase_proofs#kbconfig", :as => "keybase_config"
|
||||
|
||||
get "/filters" => "filters#index"
|
||||
post "/filters" => "filters#update"
|
||||
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
class Keybase
|
||||
cattr_accessor :DOMAIN
|
||||
cattr_accessor :BASE_URL
|
||||
|
||||
# these need to be overridden in config/initializers/production.rb
|
||||
@@DOMAIN = nil
|
||||
@@BASE_URL = nil
|
||||
|
||||
def self.enabled?
|
||||
@@DOMAIN.present? || ENV['KEYBASE_BASE_URL']
|
||||
end
|
||||
|
||||
def self.avatar_url(kb_username)
|
||||
s = Sponge.new
|
||||
url = [
|
||||
File.join(base_url, '/_/api/1.0/user/pic_url.json?'),
|
||||
"username=#{kb_username}",
|
||||
].join('')
|
||||
res = s.fetch(url, :get).body
|
||||
return JSON.parse(res).fetch('pic_url', default_keybase_avatar_url)
|
||||
rescue ::DNSError, ::JSON::ParserError
|
||||
default_keybase_avatar_url
|
||||
end
|
||||
|
||||
def self.proof_valid?(kb_username, kb_signature, username)
|
||||
s = Sponge.new
|
||||
url = [
|
||||
File.join(base_url, '/_/api/1.0/sig/proof_valid.json?'),
|
||||
"domain=#{@@DOMAIN}&",
|
||||
"kb_username=#{kb_username}&",
|
||||
"sig_hash=#{kb_signature}&",
|
||||
"username=#{username}",
|
||||
].join('')
|
||||
res = s.fetch(url, :get).body
|
||||
js = JSON.parse(res)
|
||||
return js && js["proof_valid"].present? && js["proof_valid"]
|
||||
end
|
||||
|
||||
def self.success_url(kb_username, kb_signature, kb_ua, username)
|
||||
File.join(base_url, "/_/proof_creation_success?domain=#{@@DOMAIN}&" \
|
||||
"kb_username=#{kb_username}&username=#{username}&" \
|
||||
"sig_hash=#{kb_signature}&kb_ua=#{kb_ua}")
|
||||
end
|
||||
|
||||
def self.default_keybase_avatar_url
|
||||
"https://keybase.io/images/icons/icon-keybase-logo-48@2x.png"
|
||||
end
|
||||
|
||||
def self.base_url
|
||||
@@BASE_URL || "https://keybase.io"
|
||||
end
|
||||
end
|
|
@ -141,6 +141,8 @@ class Sponge
|
|||
end
|
||||
|
||||
if BAD_NETS.select {|n| IPAddr.new(n).include?(ip) }.any?
|
||||
# This blocks all requests to localhost, so you might need to comment
|
||||
# it out if you're building an end-to-end integration locally.
|
||||
raise BadIPsError.new("refusing to talk to IP #{ip}")
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
require "rails_helper"
|
||||
|
||||
describe KeybaseProofsController do
|
||||
render_views
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:kb_username) { "cryptojim" }
|
||||
let(:kb_sig) { "1"*66 }
|
||||
let(:valid_kb_params) do
|
||||
{ kb_username: kb_username, kb_signature: kb_sig,
|
||||
kb_ua: "sega-genesis", username: user.username, }
|
||||
end
|
||||
let(:new_params) { valid_kb_params }
|
||||
let(:create_params) { { keybase_proof: valid_kb_params } }
|
||||
|
||||
before do
|
||||
stub_login_as user
|
||||
end
|
||||
|
||||
context 'new' do
|
||||
it 'renders the expected kb_username' do
|
||||
get :new, params: new_params
|
||||
expect(response.body).to include(kb_username)
|
||||
end
|
||||
end
|
||||
|
||||
context 'create' do
|
||||
context 'when the user does not already have a proof' do
|
||||
it 'saves the signature to the user settings' do
|
||||
expect(Keybase).to receive(:proof_valid?)
|
||||
.with(kb_username, kb_sig, user.username).and_return(true)
|
||||
|
||||
post :create, params: create_params
|
||||
|
||||
expect(user.reload.keybase_signatures).to eq [
|
||||
{ 'kb_username' => kb_username, 'sig_hash' => kb_sig },
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user already has proofs' do
|
||||
let(:other_kb_username) { 'somethingelse' }
|
||||
let(:other_kb_sig) { '3'*66 }
|
||||
let(:expected_keybase_signatures) do
|
||||
[
|
||||
{ 'kb_username' => kb_username, 'sig_hash' => kb_sig },
|
||||
{ 'kb_username' => other_kb_username, 'sig_hash' => other_kb_sig },
|
||||
]
|
||||
end
|
||||
|
||||
before do
|
||||
user.add_or_update_keybase_proof(kb_username, '2'*66)
|
||||
user.add_or_update_keybase_proof(other_kb_username, other_kb_sig)
|
||||
user.save!
|
||||
end
|
||||
|
||||
it 'updates the signature for the matching user and retains any others' do
|
||||
expect(Keybase).to receive(:proof_valid?)
|
||||
.with(kb_username, kb_sig, user.username).and_return(true)
|
||||
|
||||
post :create, params: create_params
|
||||
|
||||
expect(user.reload.keybase_signatures).to match_array expected_keybase_signatures
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue