Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Active Storage for Profile Images #115

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
9 changes: 6 additions & 3 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,17 @@ gem 'jbuilder', '~> 2.7'
# gem 'bcrypt', '~> 3.1.7'

# Use Active Storage variant
# gem 'image_processing', '~> 1.2'
gem 'image_processing', '~> 1.2'


gem 'twitter', '~> 6.2.0'
gem 'sentry-raven', '~> 2.11'

gem "devise", "~> 4.7.3"
gem "omniauth", "~>1.9.0"
gem 'devise', '~> 4.9', '>= 4.9.2'
gem 'omniauth', '~> 2.1', '>= 2.1.1'
gem "omniauth-twitter", "~> 1.4.0"
gem 'omniauth-github', '~> 2.0.0'
gem "omniauth-rails_csrf_protection"
gem "haml-rails", "~> 2.0.1"
gem "twitter-bootstrap-rails", "~> 5.0.0"
gem "redcarpet", "~> 1.17.2"
Expand Down Expand Up @@ -80,3 +82,4 @@ gem 'simplecov', require: false, group: :test
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

gem 'mimemagic', github: 'mimemagicrb/mimemagic', ref: '01f92d86d15d85cfd0f20dabd025dcbd36a8a60f'

44 changes: 39 additions & 5 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ GEM
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
date (3.3.3)
devise (4.7.3)
devise (4.9.3)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
Expand Down Expand Up @@ -171,6 +171,9 @@ GEM
http_parser.rb (0.6.0)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
inherited_resources (1.13.1)
actionpack (>= 5.2, < 7.1)
has_scope (~> 0.6)
Expand All @@ -183,6 +186,7 @@ GEM
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
jwt (2.7.1)
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
Expand Down Expand Up @@ -217,10 +221,12 @@ GEM
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
method_source (1.0.0)
mini_magick (4.12.0)
mini_mime (1.1.5)
mini_portile2 (2.6.1)
minitest (5.19.0)
msgpack (1.7.1)
multi_xml (0.6.0)
multipart-post (2.1.1)
naught (1.1.0)
net-imap (0.3.7)
Expand All @@ -238,12 +244,29 @@ GEM
mini_portile2 (~> 2.6.1)
racc (~> 1.4)
oauth (0.5.8)
omniauth (1.9.2)
oauth2 (2.0.9)
faraday (>= 0.17.3, < 3.0)
jwt (>= 1.0, < 3.0)
multi_xml (~> 0.5)
rack (>= 1.2, < 4)
snaky_hash (~> 2.0)
version_gem (~> 1.1)
omniauth (2.1.1)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
rack (>= 2.2.3)
rack-protection
omniauth-github (2.0.1)
omniauth (~> 2.0)
omniauth-oauth2 (~> 1.8)
omniauth-oauth (1.2.0)
oauth
omniauth (>= 1.0, < 3)
omniauth-oauth2 (1.8.0)
oauth2 (>= 1.4, < 3)
omniauth (~> 2.0)
omniauth-rails_csrf_protection (1.0.1)
actionpack (>= 4.2)
omniauth (~> 2.0)
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
rack
Expand All @@ -260,6 +283,8 @@ GEM
rack (2.2.8)
rack-mini-profiler (2.3.3)
rack (>= 1.2.0)
rack-protection (3.1.0)
rack (~> 2.2, >= 2.2.4)
rack-proxy (0.7.2)
rack
rack-test (2.1.0)
Expand Down Expand Up @@ -322,6 +347,8 @@ GEM
rspec-mocks (~> 3.12)
rspec-support (~> 3.12)
rspec-support (3.12.1)
ruby-vips (2.2.0)
ffi (~> 1.12)
ruby2_keywords (0.0.5)
ruby_parser (3.18.1)
sexp_processor (~> 4.16)
Expand Down Expand Up @@ -353,6 +380,9 @@ GEM
simplecov_json_formatter (~> 0.1)
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.3)
snaky_hash (2.0.1)
hashie
version_gem (~> 1.1, >= 1.1.1)
spring (4.0.0)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)
Expand Down Expand Up @@ -392,6 +422,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.8)
version_gem (1.1.3)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.2.0)
Expand Down Expand Up @@ -426,17 +457,20 @@ DEPENDENCIES
byebug
capybara (>= 3.26)
database_cleaner
devise (~> 4.7.3)
devise (~> 4.9, >= 4.9.2)
dotenv-rails (~> 2.7, >= 2.7.4)
factory_bot_rails
faker (~> 2.19)
haml-rails (~> 2.0.1)
image_processing (~> 1.2)
jbuilder (~> 2.7)
listen (~> 3.3)
mimemagic!
newrelic_rpm
nokogiri (~> 1.12.5)
omniauth (~> 1.9.0)
omniauth (~> 2.1, >= 2.1.1)
omniauth-github (~> 2.0.0)
omniauth-rails_csrf_protection
omniauth-twitter (~> 1.4.0)
pg
pry (~> 0.14.2)
Expand Down
26 changes: 26 additions & 0 deletions app/admin/external_identities.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
ActiveAdmin.register ExternalIdentity do

# See permitted parameters documentation:
# https://github.com/activeadmin/activeadmin/blob/master/docs/2-resource-customization.md#setting-up-strong-parameters
#
# Uncomment all parameters which should be permitted for assignment
#
permit_params :user_id, :provider, :uid, :oauth, :handle, :description, :website, :name, :email, :image
#
# or
#
# permit_params do
# permitted = [:user_id, :provider, :uid, :oauth, :handle, :description, :website, :name, :email, :image]
# permitted << :other if params[:action] == 'create' && current_user.admin?
# permitted
# end

index do
column :id
column :name
column :handle
actions
end


end
2 changes: 1 addition & 1 deletion app/admin/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

index do
column :id
column :twitter_handle
column :name
column :last_sign_in_at
column :created_at
column :sign_in_count
column :letters_count do |user|
user.letters.count
end
column :external_identities
column :likes do |user|
user.likes.count
end
Expand Down
12 changes: 12 additions & 0 deletions app/controllers/users/omniauth_callbacks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,16 @@ def twitter
redirect_to root_path
end
end

def github
@user = User.find_for_github_oauth(request.env["omniauth.auth"])
if @user.persisted?
flash[:notice] = t("flash.successful_sign_in_github")
sign_in_and_redirect @user, :event => :authentication
else
session["devise.github_data"] = request.env["omniauth.auth"]
flash[:error] = t('flash.error_signing_in')
redirect_to root_path
end
end
end
27 changes: 9 additions & 18 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
@@ -1,27 +1,18 @@
module ApplicationHelper
def get_twitter_image(user)
image_tag image_for_twitter_handle(user), onerror: 'this.error=null;this.src="https://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png"'
def get_profile_image(user)
image_tag image_for_handle(user), onerror: 'this.error=null;this.src="https://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png"'
end

private

RubyConfIndia2023 = Date.parse('2023-08-26')
private

def image_for_twitter_handle(user)
if user.updated_at.after?(RubyConfIndia2023) && user.image.present?
# this is a quick workaround for being unable to fetch images via Cloudinary for new twitter sign_ins due to Twitter API policy changes.
# one problem with continuing to use this long term would be:
# - if a user changes their profile picture, the url saved on our model will become invalid
# we do have a fallback to default image in place, but it's not ideal
# - As an improvement, we can try to cache it (more like a permanent snapshot until their next login)
# - But a proper solution would be to fetch their current profile picture via a service or to periodically refresh it ourself
user.image
def image_for_handle(user)
if user.twitter_identity.present? && user.twitter_identity.profile_image.attached?
url_for(user.twitter_identity.profile_image)
elsif user.github_identity.present? && user.github_identity.profile_image.attached?
url_for(user.github_identity.profile_image)
else
cloudinary_image_url(user.twitter_handle)
'https://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png'
end
end

def cloudinary_image_url(twitter_handle)
"https://res.cloudinary.com/#{ENV['CLOUDINARY_HANDLE']}/image/twitter_name/#{twitter_handle}.jpg"
end
end
2 changes: 2 additions & 0 deletions app/models/external_identity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ class ExternalIdentity < ActiveRecord::Base
validates :provider, uniqueness: { scope: :user_id, message: 'has already been taken for this user' }
validates :provider, presence: true
validates :uid, uniqueness: { scope: :provider, message: 'has already been taken for this provider' }

has_one_attached :profile_image
end
86 changes: 70 additions & 16 deletions app/models/user.rb
Original file line number Diff line number Diff line change
@@ -1,41 +1,95 @@
require 'open-uri'


class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :token_authenticatable, :encryptable, :confirmable, :lockable, :timeoutable and :omniauthable
devise :registerable, :rememberable, :trackable, :database_authenticatable, :omniauthable,
omniauth_providers: [:twitter]
omniauth_providers: [:github, :twitter]


# Setup accessible (or protected) attributes for your model
# attr_accessible :remember_me, :name, :twitter_handle, :twitter_description, :twitter_description, :twitter_oauth, :website, :image


has_many :letters
has_many :likes
has_many :external_identities
has_one :twitter_identity, -> { where provider: 'twitter' }, class_name: "ExternalIdentity"
has_one :github_identity, -> { where provider: 'github' }, class_name: "ExternalIdentity"

class << self
def find_for_twitter_oauth(auth)
user = User.where(uid: auth.uid, provider: auth.provider).first

user = User.joins(:external_identities)
.where(external_identities: { uid: auth.uid, provider: auth.provider })
.first
if user
profile_image_url = auth["info"]["image"]
user.update(image: profile_image_url) if profile_image_url.present?

# TODO: handle exceptions later. For now, continue to sign in the user, regardless of whether image url is saved
user.update(name: auth.info.name)
user.twitter_identity.update(
handle: auth.info.nickname,
description: auth.info.description,
website: auth.info.urls.Website,
oauth: auth.credentials.token,
name: auth.info.name
)
user.update_profile_image(auth.info.image)
user
else
User.create(
uid: auth["uid"],
provider: auth["provider"],
name: auth["info"]["name"],
twitter_handle: auth["info"]["nickname"],
twitter_description: auth["info"]["description"],
website: (auth["info"]["urls"]["Website"] rescue nil),
twitter_oauth: auth["credentials"]["token"],
image: auth["info"]["image"]
user = User.create(name: auth.info.name)
user.external_identities.create(
uid: auth.uid,
provider: auth.provider,
handle: auth.info.nickname,
name: auth.info.name,
description: auth.info.description,
website: auth.info.urls.Website,
oauth: auth.credentials.token
)
user.update_profile_image(auth.info.image)
user
end
end

def find_for_github_oauth(auth)
data = auth.info
user = User.joins(:external_identities)
.where(external_identities: { uid: auth.uid, provider: auth.provider })
.first
if user
user.update(name: auth.info.name, email: auth.info.email)
user.github_identity.update(
handle: auth.extra.raw_info.login,
name: data['name'],
email: data['email']
)
user.update_profile_image(auth.extra.raw_info.avatar_url)
user
else
user = User.create(name: auth.info.name, email: auth.info.email)
user.external_identities.create(
uid: auth.uid,
provider: auth.provider,
handle: auth.extra.raw_info.login,
name: data.name,
email: data.email
)
user.update_profile_image(auth.extra.raw_info.avatar_url)
user
end
end
end

def update_profile_image(image_url)
if twitter_identity
twitter_identity.profile_image.attach(io: open_uri(image_url), filename: "#{twitter_identity.handle}.jpg")
elsif github_identity
github_identity.profile_image.attach(io: open_uri(image_url), filename: "#{github_identity.handle}.jpg")
end
end

private

def open_uri(url)
URI.parse(url).open
end
end
Loading