WIP Store avatars as ActiveStorage attachments
Also push to LDAP as jpegPhoto
This commit is contained in:
parent
9e2210c45b
commit
17ffbde03a
@ -22,7 +22,7 @@ class Admin::UsersController < Admin::BaseController
|
|||||||
|
|
||||||
@services_enabled = @user.services_enabled
|
@services_enabled = @user.services_enabled
|
||||||
|
|
||||||
@avatar = LdapManager::FetchAvatar.call(cn: @user.cn)
|
@ldap_avatar = LdapManager::FetchAvatar.call(cn: @user.cn)
|
||||||
end
|
end
|
||||||
|
|
||||||
# POST /admin/users/:username/invitations
|
# POST /admin/users/:username/invitations
|
||||||
|
@ -25,7 +25,7 @@ class SettingsController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
@user.preferences.merge!(user_params[:preferences] || {})
|
@user.preferences.merge!(user_params[:preferences] || {})
|
||||||
@user.display_name = user_params[:display_name]
|
@user.display_name = user_params[:display_name]
|
||||||
@user.avatar_new = user_params[:avatar]
|
@user.avatar_new = user_params[:avatar_new]
|
||||||
@user.pgp_pubkey = user_params[:pgp_pubkey]
|
@user.pgp_pubkey = user_params[:pgp_pubkey]
|
||||||
|
|
||||||
if @user.save
|
if @user.save
|
||||||
@ -34,7 +34,10 @@ class SettingsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
if @user.avatar_new.present?
|
if @user.avatar_new.present?
|
||||||
LdapManager::UpdateAvatar.call(dn: @user.dn, file: @user.avatar_new)
|
@user.avatar.attach(@user.avatar_new)
|
||||||
|
@user.avatar.blob.update(filename: @user.avatar_filename)
|
||||||
|
@user.save!
|
||||||
|
LdapManager::UpdateAvatar.call(user: @user)
|
||||||
end
|
end
|
||||||
|
|
||||||
if @user.pgp_pubkey && (@user.pgp_pubkey != @user.ldap_entry[:pgp_key])
|
if @user.pgp_pubkey && (@user.pgp_pubkey != @user.ldap_entry[:pgp_key])
|
||||||
@ -162,7 +165,7 @@ class SettingsController < ApplicationController
|
|||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
params.require(:user).permit(
|
params.require(:user).permit(
|
||||||
:display_name, :avatar, :pgp_pubkey,
|
:display_name, :avatar_new, :pgp_pubkey,
|
||||||
preferences: UserPreferences.pref_keys
|
preferences: UserPreferences.pref_keys
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -4,8 +4,8 @@ class User < ApplicationRecord
|
|||||||
include EmailValidatable
|
include EmailValidatable
|
||||||
|
|
||||||
attr_accessor :current_password
|
attr_accessor :current_password
|
||||||
attr_accessor :avatar_new
|
|
||||||
attr_accessor :display_name
|
attr_accessor :display_name
|
||||||
|
attr_accessor :avatar_new
|
||||||
attr_accessor :pgp_pubkey
|
attr_accessor :pgp_pubkey
|
||||||
|
|
||||||
serialize :preferences, coder: UserPreferences
|
serialize :preferences, coder: UserPreferences
|
||||||
@ -27,6 +27,12 @@ class User < ApplicationRecord
|
|||||||
|
|
||||||
has_many :accounts, through: :lndhub_user
|
has_many :accounts, through: :lndhub_user
|
||||||
|
|
||||||
|
#
|
||||||
|
# Attachments
|
||||||
|
#
|
||||||
|
|
||||||
|
has_one_attached :avatar
|
||||||
|
|
||||||
#
|
#
|
||||||
# Validations
|
# Validations
|
||||||
#
|
#
|
||||||
@ -159,13 +165,30 @@ class User < ApplicationRecord
|
|||||||
@display_name ||= ldap_entry[:display_name]
|
@display_name ||= ldap_entry[:display_name]
|
||||||
end
|
end
|
||||||
|
|
||||||
def avatar
|
def avatar_base64(size: :medium)
|
||||||
@avatar ||= LdapManager::FetchAvatar.call(cn: cn)
|
return nil unless avatar.attached?
|
||||||
|
variant = avatar_variant(size: size)
|
||||||
|
data = ActiveStorage::Blob.service.download(variant.key)
|
||||||
|
Base64.strict_encode64(data)
|
||||||
end
|
end
|
||||||
|
|
||||||
def avatar_base64
|
def avatar_filename
|
||||||
return nil if avatar.nil?
|
return nil unless avatar.attached?
|
||||||
@avatar_base64 ||= Base64.strict_encode64(avatar)
|
data = ActiveStorage::Blob.service.download(avatar.key)
|
||||||
|
hash = Digest::SHA256.hexdigest(data)
|
||||||
|
ext = avatar.content_type == "image/png" ? "png" : "jpg"
|
||||||
|
"#{hash}.#{ext}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def avatar_variant(size: :medium)
|
||||||
|
dimensions = case size
|
||||||
|
when :large then [400, 400]
|
||||||
|
when :medium then [256, 256]
|
||||||
|
when :small then [64, 64]
|
||||||
|
else [256, 256]
|
||||||
|
end
|
||||||
|
format = avatar.content_type == "image/png" ? :png : :jpeg
|
||||||
|
avatar.variant(resize_to_fill: dimensions, format: format)
|
||||||
end
|
end
|
||||||
|
|
||||||
def nostr_pubkey
|
def nostr_pubkey
|
||||||
@ -232,7 +255,7 @@ class User < ApplicationRecord
|
|||||||
return unless avatar_new.present?
|
return unless avatar_new.present?
|
||||||
|
|
||||||
if avatar_new.size > 1.megabyte
|
if avatar_new.size > 1.megabyte
|
||||||
errors.add(:avatar, "file size is too large")
|
errors.add(:avatar, "must be less than 1MB file size")
|
||||||
end
|
end
|
||||||
|
|
||||||
acceptable_types = ["image/jpeg", "image/png"]
|
acceptable_types = ["image/jpeg", "image/png"]
|
||||||
|
@ -10,7 +10,7 @@ module LdapManager
|
|||||||
filter = Net::LDAP::Filter.eq("cn", @cn)
|
filter = Net::LDAP::Filter.eq("cn", @cn)
|
||||||
|
|
||||||
entry = client.search(base: treebase, filter: filter, attributes: attributes).first
|
entry = client.search(base: treebase, filter: filter, attributes: attributes).first
|
||||||
entry&.jpegPhoto ? entry.jpegPhoto.first : nil
|
entry[:jpegPhoto].present? ? entry.jpegPhoto.first : nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -2,26 +2,40 @@ require "image_processing/vips"
|
|||||||
|
|
||||||
module LdapManager
|
module LdapManager
|
||||||
class UpdateAvatar < LdapManagerService
|
class UpdateAvatar < LdapManagerService
|
||||||
def initialize(dn:, file:)
|
def initialize(user:)
|
||||||
@dn = dn
|
@user = user
|
||||||
@img_data = process(file)
|
@dn = user.dn
|
||||||
end
|
end
|
||||||
|
|
||||||
def call
|
def call
|
||||||
result = replace_attribute @dn, :jpegPhoto, @img_data
|
unless @user.avatar.attached?
|
||||||
result
|
Rails.logger.error { "Cannot store empty jpegPhoto for user #{@user.cn}" }
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
img_data = @user.avatar.blob.download
|
||||||
|
jpg_data = process(img_data)
|
||||||
|
|
||||||
|
result = replace_attribute(@dn, :jpegPhoto, jpg_data)
|
||||||
|
result == 0
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def process(file)
|
def process(data)
|
||||||
processed = ImageProcessing::Vips
|
@user.avatar.blob.open do |file|
|
||||||
.resize_to_fill(256, 256)
|
processed = ImageProcessing::Vips
|
||||||
.source(file)
|
.source(file)
|
||||||
.convert("jpeg")
|
.resize_to_fill(256, 256)
|
||||||
.saver(strip: true)
|
.convert("jpeg")
|
||||||
.call
|
.saver(strip: true)
|
||||||
processed.read
|
.call
|
||||||
|
processed.read
|
||||||
|
end
|
||||||
|
rescue Vips::Error => e
|
||||||
|
Sentry.capture_exception(e) if Setting.sentry_enabled?
|
||||||
|
Rails.logger.error { "Image processing failed for LDAP avatar: #{e.message}" }
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -95,8 +95,8 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Avatar</th>
|
<th>Avatar</th>
|
||||||
<td>
|
<td>
|
||||||
<% if @avatar.present? %>
|
<% if @ldap_avatar.present? %>
|
||||||
<img src="data:image/jpeg;base64,<%= @avatar %>" class="h-48 w-48" />
|
JPEG size: <%= @ldap_avatar.size %>
|
||||||
<% else %>
|
<% else %>
|
||||||
—
|
—
|
||||||
<% end %>
|
<% end %>
|
||||||
|
@ -33,22 +33,19 @@
|
|||||||
|
|
||||||
<% if Flipper.enabled?(:avatar_upload, current_user) %>
|
<% if Flipper.enabled?(:avatar_upload, current_user) %>
|
||||||
<label class="block">
|
<label class="block">
|
||||||
<p class="font-bold mb-1">
|
<p class="font-bold mb-1">Avatar</p>
|
||||||
Avatar
|
<p class="text-gray-500">Default profile picture</p>
|
||||||
</p>
|
|
||||||
<p class="text-gray-500">
|
|
||||||
Default profile picture
|
|
||||||
</p>
|
|
||||||
<div class="flex items-center gap-6">
|
<div class="flex items-center gap-6">
|
||||||
<% unless current_user.avatar.nil? %>
|
<% if @user.avatar.attached? %>
|
||||||
<p class="flex-none">
|
<p class="flex-none">
|
||||||
<%= image_tag "data:image/jpeg;base64,#{current_user.avatar_base64}",
|
<%= image_tag @user.avatar_variant(size: :medium),
|
||||||
class: "h-24 w-24 rounded-lg" %>
|
class: "h-24 w-24 rounded-lg" %>
|
||||||
</p>
|
</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="grow">
|
<div class="grow">
|
||||||
<p class="mb-2">
|
<p class="mb-2">
|
||||||
<%= f.file_field :avatar, class: "" %>
|
<%= f.file_field :avatar_new, accept: "image/jpeg,image/png" %>
|
||||||
|
</p>
|
||||||
<p class="text-sm text-gray-500">
|
<p class="text-sm text-gray-500">
|
||||||
JPEG or PNG image, not larger than 1 megabyte
|
JPEG or PNG image, not larger than 1 megabyte
|
||||||
</p>
|
</p>
|
||||||
|
@ -16,8 +16,6 @@ RSpec.describe 'Profile settings', type: :feature do
|
|||||||
.and_return({
|
.and_return({
|
||||||
uid: user.cn, ou: user.ou, display_name: "Mark", pgp_key: nil
|
uid: user.cn, ou: user.ou, display_name: "Mark", pgp_key: nil
|
||||||
})
|
})
|
||||||
allow_any_instance_of(User).to receive(:avatar)
|
|
||||||
.and_return(avatar_jpeg)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
feature "Update display name" do
|
feature "Update display name" do
|
||||||
@ -61,13 +59,16 @@ RSpec.describe 'Profile settings', type: :feature do
|
|||||||
end
|
end
|
||||||
|
|
||||||
scenario "fails with validation error for file size too large" do
|
scenario "fails with validation error for file size too large" do
|
||||||
|
expect_any_instance_of(LdapManager::UpdateAvatar)
|
||||||
|
.not_to receive(:replace_attribute).and_return(true)
|
||||||
|
|
||||||
visit setting_path(:profile)
|
visit setting_path(:profile)
|
||||||
attach_file "Avatar", "#{Rails.root}/spec/fixtures/files/fsociety-irc.png"
|
attach_file "Avatar", "#{Rails.root}/spec/fixtures/files/fsociety-irc.png"
|
||||||
click_button "Save"
|
click_button "Save"
|
||||||
|
|
||||||
expect(current_url).to eq(setting_url(:profile))
|
expect(current_url).to eq(setting_url(:profile))
|
||||||
within ".error-msg" do
|
within ".error-msg" do
|
||||||
expect(page).to have_content("file size is too large")
|
expect(page).to have_content("must be less than 1MB")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user