diff --git a/app.rb b/app.rb index 556894a0..72dd448a 100644 --- a/app.rb +++ b/app.rb @@ -125,6 +125,23 @@ post '/site/:username/set_editor_theme' do 'ok' end +post '/settings/create_child' do + require_login + site = Site.new + + site.parent_site_id = parent_site.id + site.username = params[:username] + + if site.valid? + site.save + flash[:success] = 'Your new site has been created!' + redirect '/settings#sites' + else + flash[:error] = site.errors.first.last.first + redirect '/settings#sites' + end +end + post '/site/:username/comment' do |username| require_login @@ -254,68 +271,74 @@ post '/plan/create' do DB.transaction do customer = Stripe::Customer.create( card: params[:stripe_token], - description: current_site.username, + description: "#{parent_site.username} - #{parent_site.id}", email: current_site.email, plan: params[:selected_plan] ) - current_site.update stripe_customer_id: customer.id, plan_ended: false + parent_site.update stripe_customer_id: customer.id, plan_ended: false plan_name = customer.subscriptions.first['plan']['name'] - EmailWorker.perform_async({ - from: 'web@neocities.org', - reply_to: 'contact@neocities.org', - to: current_site.email, - subject: "[Neocities] You've become a supporter!", - body: Tilt.new('./views/templates/email_subscription.erb', pretty: true).render(self, plan_name: plan_name) - }) + if current_site.email || parent_site.email + EmailWorker.perform_async({ + from: 'web@neocities.org', + reply_to: 'contact@neocities.org', + to: current_site.email || parent_site.email, + subject: "[Neocities] You've become a supporter!", + body: Tilt.new('./views/templates/email_subscription.erb', pretty: true).render(self, plan_name: plan_name) + }) + end end redirect '/plan' end def get_plan_name(customer_id) - subscriptions = Stripe::Customer.retrieve(current_site.stripe_customer_id).subscriptions.all + subscriptions = Stripe::Customer.retrieve(parent_site.stripe_customer_id).subscriptions.all @plan_name = subscriptions.first.plan.name end +def require_active_subscription + redirect '/plan' unless parent_site.supporter? && !parent_site.plan_ended +end + get '/plan/manage' do require_login - redirect '/plan' unless current_site.supporter? && !current_site.plan_ended + require_active_subscription @title = 'Manage Plan' - @plan_name = get_plan_name current_site.stripe_customer_id + @plan_name = get_plan_name parent_site.stripe_customer_id erb :'plan/manage' end get '/plan/end' do require_login - redirect '/plan' unless current_site.supporter? && !current_site.plan_ended + require_active_subscription @title = 'End Plan' - @plan_name = get_plan_name current_site.stripe_customer_id + @plan_name = get_plan_name parent_site.stripe_customer_id erb :'plan/end' end post '/plan/end' do require_login - redirect '/plan' unless current_site.supporter? && !current_site.plan_ended + require_active_subscription recaptcha_is_valid = ENV['RACK_ENV'] == 'test' || recaptcha_valid? if !recaptcha_is_valid @error = 'Recaptcha was filled out incorrectly, please try re-entering.' - @plan_name = get_plan_name current_site.stripe_customer_id + @plan_name = get_plan_name parent_site.stripe_customer_id halt erb :'plan/end' end - customer = Stripe::Customer.retrieve current_site.stripe_customer_id + customer = Stripe::Customer.retrieve parent_site.stripe_customer_id subscriptions = customer.subscriptions.all DB.transaction do subscriptions.each do |subscription| customer.subscriptions.retrieve(subscription.id).delete end - current_site.update plan_ended: true + parent_site.update plan_ended: true end redirect '/plan' @@ -531,43 +554,59 @@ get '/dashboard' do erb :'dashboard' end -get '/signin' do - dashboard_if_signed_in - erb :'signin' +get '/settings/?' do + require_login + @site = parent_site + erb :'settings/account' end -get '/settings' do - require_login - erb :'settings' +def require_ownership_for_settings + @site = Site[username: params[:username]] + + not_found if @site.nil? + + unless @site.owned_by? parent_site + flash[:error] = 'Cannot edit this site, you do not have permission.' + redirect request.referrer + end end -post '/settings/profile' do +get '/settings/:username/?' do require_login - current_site.update( + require_ownership_for_settings + erb :'settings/site' +end + +post '/settings/:username/profile' do + require_login + require_ownership_for_settings + + @site.update( profile_comments_enabled: params[:site][:profile_comments_enabled] ) flash[:success] = 'Profile settings changed.' - redirect '/settings' + redirect "/settings/#{@site.username}#profile" end -post '/settings/ssl' do +post '/settings/:username/ssl' do require_login + require_ownership_for_settings unless params[:key] && params[:cert] flash[:error] = 'SSL key and certificate are required.' - redirect '/custom_domain' + redirect "/settings/#{@site.username}#custom_domain" end begin key = OpenSSL::PKey::RSA.new params[:key][:tempfile].read, '' rescue => e flash[:error] = 'Could not process SSL key, file may be incorrect, damaged, or passworded (you need to remove the password).' - redirect '/custom_domain' + redirect "/settings/#{@site.username}#custom_domain" end if !key.private? flash[:error] = 'SSL Key file does not have private key data.' - redirect '/custom_domain' + redirect "/settings/#{@site.username}#custom_domain" end certs_string = params[:cert][:tempfile].read @@ -576,7 +615,7 @@ post '/settings/ssl' do if cert_array.empty? flash[:error] = 'Cert file does not contain any certificates.' - redirect '/custom_domain' + redirect "/settings/#{@site.username}#custom_domain" end cert_valid_for_domain = false @@ -586,21 +625,21 @@ post '/settings/ssl' do cert = OpenSSL::X509::Certificate.new cert_string rescue => e flash[:error] = 'Could not process SSL certificate, file may be incorrect or damaged.' - redirect '/custom_domain' + redirect "/settings/#{@site.username}#custom_domain" end if cert.not_after < Time.now flash[:error] = 'SSL Certificate has expired, please create a new one.' - redirect '/custom_domain' + redirect "/settings/#{@site.username}#custom_domain" end cert_cn = cert.subject.to_a.select {|a| a.first == 'CN'}.flatten[1] - cert_valid_for_domain = true if cert_cn && cert_cn.match(current_site.domain) + cert_valid_for_domain = true if cert_cn && cert_cn.match(@site.domain) end unless cert_valid_for_domain - flash[:error] = "Your certificate CN (common name) does not match your domain: #{current_site.domain}" - redirect '/custom_domain' + flash[:error] = "Your certificate CN (common name) does not match your domain: #{@site.domain}" + redirect "/settings/#{@site.username}#custom_domain" end # Everything else was worse. @@ -623,7 +662,7 @@ post '/settings/ssl' do access_log off; server { listen 60000 ssl; - server_name #{current_site.domain} *.#{current_site.domain}; + server_name #{@site.domain} *.#{@site.domain}; ssl_certificate #{crtfile.path}; ssl_certificate_key #{keyfile.path}; } @@ -641,21 +680,209 @@ post '/settings/ssl' do output = line.run path: nginx_testfile.path rescue Cocaine::ExitStatusError => e flash[:error] = "There is something wrong with your certificate, please check with your issuing CA." - redirect '/custom_domain' + redirect "/settings/#{@site.username}#custom_domain" end end - current_site.update ssl_key: key.to_pem, ssl_cert: cert_array.join + @site.update ssl_key: key.to_pem, ssl_cert: cert_array.join flash[:success] = 'Updated SSL key/certificate.' - redirect '/custom_domain' + redirect "/settings/#{@site.username}#custom_domain" +end + +post '/settings/:username/change_name' do + require_login + require_ownership_for_settings + + old_username = @site.username + + if params[:name] == nil || params[:name] == '' + flash[:error] = 'Name cannot be blank.' + redirect "/settings/#{@site.username}#username" + end + + if old_username == params[:name] + flash[:error] = 'You already have this name.' + redirect "/settings/#{@site.username}#username" + end + + old_host = @site.host + old_file_paths = @site.file_list.collect {|f| f[:path]} + + @site.username = params[:name] + + if @site.valid? + DB.transaction { + @site.save_changes + @site.move_files_from old_username + } + + old_file_paths.each do |file_path| + @site.purge_cache file_path + end + + flash[:success] = "Site/user name has been changed. You will need to use this name to login, don't forget it." + redirect "/settings/#{@site.username}#username" + else + flash[:error] = @site.errors.first.last.first + redirect "/settings/#{old_username}#username" + end +end + +post '/settings/:username/change_nsfw' do + require_login + require_ownership_for_settings + + @site.update is_nsfw: params[:is_nsfw] + flash[:success] = @site.is_nsfw ? 'Marked 18+' : 'Unmarked 18+' + redirect "/settings/#{@site.username}#nsfw" +end + +post '/settings/:username/custom_domain' do + require_login + require_ownership_for_settings + + @site.domain = params[:domain] + + if @site.valid? + @site.save_changes + flash[:success] = 'The domain has been successfully updated.' + redirect "/settings/#{@site.username}#custom_domain" + else + flash[:error] = @site.errors.first.last.first + redirect "/settings/#{@site.username}#custom_domain" + end +end + +post '/settings/change_password' do + require_login + + if !Site.valid_login?(parent_site.username, params[:current_password]) + flash[:error] = 'Your provided password does not match the current one.' + redirect "/settings#password" + end + + parent_site.password = params[:new_password] + parent_site.valid? + + if params[:new_password] != params[:new_password_confirm] + parent_site.errors.add :password, 'New passwords do not match.' + end + + if parent_site.errors.empty? + parent_site.save_changes + flash[:success] = 'Successfully changed password.' + redirect "/settings#password" + else + flash[:error] = current_site.errors.first.last.first + redirect '/settings#password' + end +end + +post '/settings/change_email' do + require_login + + if params[:email] == parent_site.email + flash[:error] = 'You are already using this email address for this account.' + redirect '/settings#email' + end + + parent_site.email = params[:email] + parent_site.email_confirmation_token = SecureRandom.hex 3 + parent_site.email_confirmed = false + + if parent_site.valid? + parent_site.save_changes + send_confirmation_email + flash[:success] = 'Successfully changed email. We have sent a confirmation email, please use it to confirm your email address.' + redirect '/settings#email' + end + + flash[:error] = parent_site.errors.first.last.first + redirect '/settings#email' +end + +get '/password_reset' do + erb :'password_reset' +end + +post '/send_password_reset' do + sites = Site.filter(email: params[:email]).all + + if sites.length > 0 + token = SecureRandom.uuid.gsub('-', '') + sites.each do |site| + site.update password_reset_token: token + end + + body = <<-EOT +Hello! This is the Neocities cat, and I have received a password reset request for your e-mail address. Purrrr. + +Go to this URL to reset your password: http://neocities.org/password_reset_confirm?token=#{token} + +After clicking on this link, your password for all the sites registered to this email address will be changed to this token. + +Token: #{token} + +If you didn't request this reset, you can ignore it. Or hide under a bed. Or take a nap. Your call. + +Meow, +the Neocities Cat + EOT + + body.strip! + + EmailWorker.perform_async({ + from: 'web@neocities.org', + to: params[:email], + subject: '[Neocities] Password Reset', + body: body + }) + end + + flash[:success] = 'If your email was valid (and used by a site), the Neocities Cat will send an e-mail to your account with password reset instructions.' + redirect '/' +end + +get '/password_reset_confirm' do + if params[:token].nil? || params[:token].empty? + flash[:error] = 'Could not find a site with this token.' + redirect '/' + end + + reset_site = Site[password_reset_token: params[:token]] + + if reset_site.nil? + flash[:error] = 'Could not find a site with this token.' + redirect '/' + end + + sites = Site.filter(email: reset_site.email).all + + if sites.length > 0 + sites.each do |site| + site.password = reset_site.password_reset_token + site.save_changes + end + + flash[:success] = 'Your password for all sites with your email address has been changed to the token sent in your e-mail. Please login and change your password as soon as possible.' + else + flash[:error] = 'Could not find a site with this token.' + end + + redirect '/' +end + +get '/signin/?' do + dashboard_if_signed_in + erb :'signin' end post '/signin' do dashboard_if_signed_in if Site.valid_login? params[:username], params[:password] - site = Site[username: params[:username]] + site = Site.get_with_identifier params[:username] if site.is_banned flash[:error] = 'Invalid login.' @@ -678,6 +905,21 @@ get '/signout' do redirect '/' end +get '/signin/:username' do + require_login + @site = Site[username: params[:username]] + + not_found if @site.nil? + + if @site.owned_by? current_site + session[:id] = @site.id + redirect request.referrer + end + + flash[:error] = 'You do not have permission to switch to this site.' + redirect request.referrer +end + get '/about' do erb :'about' end @@ -687,96 +929,6 @@ get '/site_files/new_page' do erb :'site_files/new_page' end -post '/change_password' do - require_login - - if !Site.valid_login?(current_site.username, params[:current_password]) - current_site.errors.add :password, 'Your provided password does not match the current one.' - halt erb(:'settings') - end - - current_site.password = params[:new_password] - current_site.valid? - - if params[:new_password] != params[:new_password_confirm] - current_site.errors.add :password, 'New passwords do not match.' - end - - if current_site.errors.empty? - current_site.save_changes - flash[:success] = 'Successfully changed password.' - redirect '/settings' - else - halt erb(:'settings') - end -end - -post '/change_email' do - require_login - - if params[:email] == current_site.email - current_site.errors.add :email, 'You are already using this email address for this account.' - halt erb(:settings) - end - - current_site.email = params[:email] - current_site.email_confirmation_token = SecureRandom.hex 3 - current_site.email_confirmed = false - - if current_site.valid? - current_site.save_changes - send_confirmation_email - flash[:success] = 'Successfully changed email. We have sent a confirmation email, please use it to confirm your email address.' - redirect '/settings' - end - - current_site.reload - erb :settings -end - -post '/change_name' do - require_login - old_username = current_site.username - - if params[:name] == nil || params[:name] == '' - flash[:error] = 'Name cannot be blank.' - redirect '/settings' - end - - if old_username == params[:name] - flash[:error] = 'You already have this name.' - redirect '/settings' - end - - old_host = current_site.host - old_file_paths = current_site.file_list.collect {|f| f[:path]} - - current_site.username = params[:name] - - if current_site.valid? - DB.transaction { - current_site.save_changes - current_site.move_files_from old_username - } - - old_file_paths.each do |file_path| - current_site.purge_cache file_path - end - - flash[:success] = "Site/user name has been changed. You will need to use this name to login, don't forget it." - redirect '/settings' - else - halt erb(:'settings') - end -end - -post '/change_nsfw' do - require_login - current_site.update is_nsfw: params[:is_nsfw] - flash[:success] = current_site.is_nsfw ? 'Marked 18+' : 'Unmarked 18+' - redirect '/settings' -end - post '/site_files/create_page' do require_login @errors = [] @@ -1008,95 +1160,6 @@ post '/admin/mark_nsfw' do redirect '/admin' end -get '/password_reset' do - erb :'password_reset' -end - -post '/send_password_reset' do - sites = Site.filter(email: params[:email]).all - - if sites.length > 0 - token = SecureRandom.uuid.gsub('-', '') - sites.each do |site| - site.update password_reset_token: token - end - - body = <<-EOT -Hello! This is the Neocities cat, and I have received a password reset request for your e-mail address. Purrrr. - -Go to this URL to reset your password: http://neocities.org/password_reset_confirm?token=#{token} - -After clicking on this link, your password for all the sites registered to this email address will be changed to this token. - -Token: #{token} - -If you didn't request this reset, you can ignore it. Or hide under a bed. Or take a nap. Your call. - -Meow, -the Neocities Cat - EOT - - body.strip! - - EmailWorker.perform_async({ - from: 'web@neocities.org', - to: params[:email], - subject: '[Neocities] Password Reset', - body: body - }) - end - - flash[:success] = 'If your email was valid (and used by a site), the Neocities Cat will send an e-mail to your account with password reset instructions.' - redirect '/' -end - -get '/password_reset_confirm' do - if params[:token].nil? || params[:token].empty? - flash[:error] = 'Could not find a site with this token.' - redirect '/' - end - - reset_site = Site[password_reset_token: params[:token]] - - if reset_site.nil? - flash[:error] = 'Could not find a site with this token.' - redirect '/' - end - - sites = Site.filter(email: reset_site.email).all - - if sites.length > 0 - sites.each do |site| - site.password = reset_site.password_reset_token - site.save_changes - end - - flash[:success] = 'Your password for all sites with your email address has been changed to the token sent in your e-mail. Please login and change your password as soon as possible.' - else - flash[:error] = 'Could not find a site with this token.' - end - - redirect '/' -end - -get '/custom_domain' do - require_login - erb :custom_domain -end - -post '/custom_domain' do - require_login - current_site.domain = params[:domain] - - if current_site.valid? - current_site.save_changes - flash[:success] = 'The domain has been successfully updated.' - redirect '/custom_domain' - else - erb :custom_domain - end -end - get '/contact' do erb :'contact' end @@ -1361,14 +1424,6 @@ post '/site/:username/block' do |username| end end -post '/site/delete' do - require_login - if current_site.username != params[:username] - current_site.errors.add :username, 'Could not delete site, site name did not match.' - halt erb(:settings) - end -end - def require_admin redirect '/' unless signed_in? && current_site.is_admin end @@ -1401,7 +1456,12 @@ end def current_site return nil if session[:id].nil? - @site ||= Site[id: session[:id]] + @_site ||= Site[id: session[:id]] +end + +def parent_site + return nil if current_site.nil? + current_site.parent? ? current_site : current_site.parent end def require_unbanned_ip diff --git a/ext/numeric.rb b/ext/numeric.rb index adc6b5b7..cf4897fa 100644 --- a/ext/numeric.rb +++ b/ext/numeric.rb @@ -1,5 +1,17 @@ class Numeric + ONE_MEGABYTE = 1048576 + def roundup(nearest=10) self % nearest == 0 ? self : self + nearest - (self % nearest) end -end + + def to_mb + self/ONE_MEGABYTE.to_f + end + + def to_space_pretty + space = (self.to_f / ONE_MEGABYTE).round(2) + space = space.to_i if space.denominator == 1 + "#{space} MB" + end +end \ No newline at end of file diff --git a/migrations/046_add_site_parent_id.rb b/migrations/046_add_site_parent_id.rb new file mode 100644 index 00000000..1830dec3 --- /dev/null +++ b/migrations/046_add_site_parent_id.rb @@ -0,0 +1,9 @@ +Sequel.migration do + up { + DB.add_column :sites, :parent_site_id, :integer, index: true + } + + down { + DB.drop_column :sites, :parent_site_id + } +end \ No newline at end of file diff --git a/models/site.rb b/models/site.rb index 96ea450b..e791efca 100644 --- a/models/site.rb +++ b/models/site.rb @@ -34,11 +34,8 @@ class Site < Sequel::Model geojson csv tsv mf ico pdf asc key pgp xml mid midi } - ONE_MEGABYTE_IN_BYTES = 1048576 - FREE_MAXIMUM_IN_MEGABYTES = 20 - SUPPORTER_MAXIMUM_IN_MEGABYTES = 1024 - FREE_MAXIMUM_IN_BYTES = FREE_MAXIMUM_IN_MEGABYTES * ONE_MEGABYTE_IN_BYTES - SUPPORTER_MAXIMUM_IN_BYTES = SUPPORTER_MAXIMUM_IN_MEGABYTES * ONE_MEGABYTE_IN_BYTES + FREE_MAXIMUM = 20 * Numeric::ONE_MEGABYTE + SUPPORTER_MAXIMUM = 1000 * Numeric::ONE_MEGABYTE MINIMUM_PASSWORD_LENGTH = 5 BAD_USERNAME_REGEX = /[^\w-]/i @@ -95,6 +92,7 @@ class Site < Sequel::Model SUGGESTIONS_LIMIT = 32 SUGGESTIONS_VIEWS_MIN = 500 + CHILD_SITES_MAX = 100 PLAN_FEATURES[:catbus] = PLAN_FEATURES[:fatcat].merge( name: 'Cat Bus', @@ -147,9 +145,42 @@ class Site < Sequel::Model one_to_many :site_changes + many_to_one :parent, :key => :parent_site_id, :class => self + one_to_many :children, :key => :parent_site_id, :class => self + + def account_sites_dataset + Site.where(Sequel.|({id: owner.id}, {parent_site_id: owner.id})).order(:parent_site_id.desc, :username) + end + + def account_sites + account_sites_dataset.all + end + + def other_sites_dataset + account_sites_dataset.exclude(id: self.id) + end + + def other_sites + account_sites_dataset.exclude(id: self.id).all + end + + def account_sites_events_dataset + ids = account_sites_dataset.select(:id).all.collect {|s| s.id} + Event.where(id: ids) + end + + def owner + parent? ? self : parent + end + + def owned_by?(site) + !account_sites_dataset.select(:id).where(id: site.id).first.nil? + end + class << self - def valid_login?(username, plaintext) - site = self[username: username] + def valid_login?(username_or_email, plaintext) + site = get_with_identifier username_or_email + return false if site.nil? site.valid_password? plaintext end @@ -161,6 +192,16 @@ class Site < Sequel::Model def bcrypt_cost=(cost) @bcrypt_cost = cost end + + def get_with_identifier(username_or_email) + if username_or_email =~ /@/ + site = self.where(email: username_or_email).where(parent_site_id: nil).first + else + site = self[username: username_or_email] + end + return nil if site.nil? || site.is_banned || site.owner.is_banned + site + end end def self.banned_ip?(ip) @@ -204,7 +245,14 @@ class Site < Sequel::Model end def valid_password?(plaintext) - BCrypt::Password.new(values[:password]) == plaintext + valid = BCrypt::Password.new(owner.values[:password]) == plaintext + + if !valid? + return false if values[:password].nil? + valid = BCrypt::Password.new(values[:password]) == plaintext + end + + valid end def password=(plaintext) @@ -297,6 +345,12 @@ class Site < Sequel::Model end end + def ban_all_sites_on_account! + DB.transaction { + account_sites.all {|site| site.ban! } + } + end + =begin def follows_dataset super.where(Sequel.~(site_id: blocking_site_ids)) @@ -315,15 +369,21 @@ class Site < Sequel::Model =end def commenting_allowed? - return true if commenting_allowed + return true if owner.commenting_allowed - if events_dataset.exclude(site_change_id: nil).count >= COMMENTING_ALLOWED_UPDATED_COUNT && - created_at < Time.now - 604800 + if owner.supporter? set commenting_allowed: true save_changes validate: false return true end + if account_sites_events_dataset.exclude(site_change_id: nil).count >= COMMENTING_ALLOWED_UPDATED_COUNT && + created_at < Time.now - 604800 + owner.set commenting_allowed: true + owner.save_changes validate: false + return true + end + false end @@ -554,6 +614,10 @@ class Site < Sequel::Model super end + def parent? + parent_site_id.nil? + end + # def after_destroy # FileUtils.rm_rf files_path # super @@ -576,7 +640,7 @@ class Site < Sequel::Model end # Check that email has been provided - if values[:email].empty? + if parent? && values[:email].empty? errors.add :email, 'An email address is required.' end @@ -586,12 +650,12 @@ class Site < Sequel::Model email_check.exclude!(id: self.id) unless new? email_check = email_check.first - if email_check && email_check.id != self.id + if parent? && email_check && email_check.id != self.id errors.add :email, 'This email address already exists on Neocities, please use your existing account instead of creating a new one.' end end - unless values[:email] =~ EMAIL_SANITY_REGEX + if parent? && (values[:email] =~ EMAIL_SANITY_REGEX).nil? errors.add :email, 'A valid email address is required.' end @@ -604,7 +668,7 @@ class Site < Sequel::Model end end - if values[:password].nil? || (@password_length && @password_length < MINIMUM_PASSWORD_LENGTH) + if parent? && (values[:password].nil? || (@password_length && @password_length < MINIMUM_PASSWORD_LENGTH)) errors.add :password, "Password must be at least #{MINIMUM_PASSWORD_LENGTH} characters." end @@ -622,6 +686,10 @@ class Site < Sequel::Model if !site.nil? && site.id != self.id errors.add :domain, "Domain provided is already being used by another site, please choose another." end + + if new? && !parent? && account_sites_dataset.count >= CHILD_SITES_MAX + errors.add :child_site_id, "Cannot add child site, exceeds #{CHILD_SITES_MAX} limit." + end end if @new_tags_string @@ -715,49 +783,43 @@ class Site < Sequel::Model list.select {|f| f[:is_directory] == false}.sort_by{|f| f[:name].downcase} end - def file_size_too_large?(size_in_bytes) - return true if size_in_bytes + used_space_in_bytes > maximum_space_in_bytes + def file_size_too_large?(size) + return true if size + used_space > maximum_space false end - def used_space_in_bytes + def used_space space = Dir.glob(File.join(files_path, '*')).collect {|p| File.size(p)}.inject {|sum,x| sum += x} space.nil? ? 0 : space end - def used_space_in_megabytes - (used_space_in_bytes.to_f / self.class::ONE_MEGABYTE_IN_BYTES).round(2) + def total_used_space + total = 0 + account_sites.each {|s| total += s.used_space} + total end - def available_space_in_bytes - remaining = maximum_space_in_bytes - used_space_in_bytes + def remaining_space + remaining = maximum_space - total_used_space remaining < 0 ? 0 : remaining end - def available_space_in_megabytes - (available_space_in_bytes.to_f / self.class::ONE_MEGABYTE_IN_BYTES).round(2) - end - - def maximum_space_in_bytes - supporter? ? self.class::SUPPORTER_MAXIMUM_IN_BYTES : self.class::FREE_MAXIMUM_IN_BYTES - end - - def maximum_space_in_megabytes - supporter? ? self.class::SUPPORTER_MAXIMUM_IN_MEGABYTES : self.class::FREE_MAXIMUM_IN_MEGABYTES + def maximum_space + (parent? ? self : parent).supporter? ? SUPPORTER_MAXIMUM : FREE_MAXIMUM end def space_percentage_used - ((used_space_in_bytes.to_f / maximum_space_in_bytes) * 100).round(1) + ((total_used_space.to_f / maximum_space) * 100).round(1) end # This returns true even if they end their support plan. def supporter? - !values[:stripe_customer_id].nil? + !owner.values[:stripe_customer_id].nil? end # This will return false if they have ended their plan. def ended_supporter? - values[:plan_ended] + owner.values[:plan_ended] end def plan_name diff --git a/sass/_project-sass/_project-Main.scss b/sass/_project-sass/_project-Main.scss index 1edcbd49..34d1e9ad 100644 --- a/sass/_project-sass/_project-Main.scss +++ b/sass/_project-sass/_project-Main.scss @@ -1051,4 +1051,8 @@ a.tag:hover { .interior .header-Outro.with-columns .col.filter { padding-top: 0px; padding-bottom: 4px; +} + +.dropdown-submenu .dropdown-menu { + width: 1px; } \ No newline at end of file diff --git a/tests/acceptance/settings/account_tests.rb b/tests/acceptance/settings/account_tests.rb new file mode 100644 index 00000000..fee6ca80 --- /dev/null +++ b/tests/acceptance/settings/account_tests.rb @@ -0,0 +1,86 @@ +require_relative '../environment.rb' + +describe 'site/settings' do + describe 'email' do + include Capybara::DSL + + before do + EmailWorker.jobs.clear + @email = "#{SecureRandom.uuid.gsub('-', '')}@example.com" + @site = Fabricate :site, email: @email + page.set_rack_session id: @site.id + visit '/settings' + end + + it 'should change email' do + @new_email = "#{SecureRandom.uuid.gsub('-', '')}@example.com" + fill_in 'email', with: @new_email + click_button 'Change Email' + page.must_have_content /successfully changed email/i + @site.reload + @site.email.must_equal @new_email + EmailWorker.jobs.length.must_equal 1 + args = EmailWorker.jobs.first['args'].first + args['to'].must_equal @new_email + args['subject'].must_match /confirm your email address/i + args['body'].must_match /hello #{@site.username}/i + args['body'].must_match /#{@site.email_confirmation_token}/ + end + + it 'should fail for invalid email address' do + @new_email = SecureRandom.uuid.gsub '-', '' + fill_in 'email', with: @new_email + click_button 'Change Email' + page.must_have_content /a valid email address is required/i + @site.reload + @site.email.wont_equal @new_email + EmailWorker.jobs.empty?.must_equal true + end + + it 'should fail for existing email' do + @existing_email = "#{SecureRandom.uuid.gsub('-', '')}@example.com" + @existing_site = Fabricate :site, email: @existing_email + + fill_in 'email', with: @existing_email + click_button 'Change Email' + page.must_have_content /this email address already exists on neocities/i + @site.reload + @site.email.wont_equal @new_email + EmailWorker.jobs.empty?.must_equal true + end + end + + describe 'change password' do + include Capybara::DSL + + before do + @site = Fabricate :site, password: 'derpie' + page.set_rack_session id: @site.id + visit '/settings' + end + + it 'should change correctly' do + fill_in 'current_password', with: 'derpie' + fill_in 'new_password', with: 'derpie2' + fill_in 'new_password_confirm', with: 'derpie2' + click_button 'Change Password' + + page.must_have_content /successfully changed password/i + @site.reload + @site.valid_password?('derpie').must_equal false + @site.valid_password?('derpie2').must_equal true + end + + it 'should not change for invalid current password' do + fill_in 'current_password', with: 'dademurphy' + fill_in 'new_password', with: 'derpie2' + fill_in 'new_password_confirm', with: 'derpie2' + click_button 'Change Password' + + page.must_have_content /provided password does not match the current one/i + @site.reload + @site.valid_password?('derpie').must_equal true + @site.valid_password?('derpie2').must_equal false + end + end +end \ No newline at end of file diff --git a/tests/acceptance/settings_tests.rb b/tests/acceptance/settings/site_tests.rb similarity index 68% rename from tests/acceptance/settings_tests.rb rename to tests/acceptance/settings/site_tests.rb index b7175cd6..d1bd336d 100644 --- a/tests/acceptance/settings_tests.rb +++ b/tests/acceptance/settings/site_tests.rb @@ -1,4 +1,4 @@ -require_relative './environment.rb' +require_relative '../environment.rb' def generate_ssl_certs(opts={}) # https://github.com/kyledrake/ruby-openssl-cheat-sheet/blob/master/certificate_authority.rb @@ -80,6 +80,29 @@ def generate_ssl_certs(opts={}) end describe 'site/settings' do + describe 'permissions' do + include Capybara::DSL + + before do + @parent_site = Fabricate :site + @child_site = Fabricate :site, parent_site_id: @parent_site.id + @other_site = Fabricate :site + end + + it 'fails without permissions' do + page.set_rack_session id: @other_site.id + + visit "/settings/#{@parent_site.username}" + page.current_path.must_equal '/' # This could be better + end + + it 'allows child site editing from parent' do + page.set_rack_session id: @parent_site.id + visit "/settings/#{@child_site.username}" + page.current_path.must_equal "/settings/#{@child_site.username}" + end + end + describe 'ssl' do include Capybara::DSL @@ -92,13 +115,13 @@ describe 'site/settings' do it 'fails without domain set' do @site = Fabricate :site page.set_rack_session id: @site.id - visit '/custom_domain' + visit "/settings/#{@site.username}#custom_domain" page.must_have_content /Cannot upload SSL certificate until domain is added/i end it 'fails with expired key' do @ssl = generate_ssl_certs domain: @domain, expired: true - visit '/custom_domain' + visit "/settings/#{@site.username}#custom_domain" attach_file 'key', @ssl[:key_path] attach_file 'cert', @ssl[:combined_cert_path] click_button 'Upload SSL Key and Certificate' @@ -107,14 +130,14 @@ describe 'site/settings' do it 'works with valid key and unified cert' do @ssl = generate_ssl_certs domain: @domain - visit '/custom_domain' + visit "/settings/#{@site.username}#custom_domain" key = File.read @ssl[:key_path] combined_cert = File.read @ssl[:combined_cert_path] page.must_have_content /status: inactive/i attach_file 'key', @ssl[:key_path] attach_file 'cert', @ssl[:combined_cert_path] click_button 'Upload SSL Key and Certificate' - page.current_path.must_equal '/custom_domain' + page.current_path.must_equal "/settings/#{@site.username}" page.must_have_content /Updated SSL/ page.must_have_content /status: installed/i @site.reload @@ -123,9 +146,9 @@ describe 'site/settings' do end it 'fails with no uploads' do - visit '/custom_domain' + visit "/settings/#{@site.username}#custom_domain" click_button 'Upload SSL Key and Certificate' - page.current_path.must_equal '/custom_domain' + page.current_path.must_equal "/settings/#{@site.username}" page.must_have_content /ssl key.+certificate.+required/i @site.reload @site.ssl_key.must_equal nil @@ -134,42 +157,42 @@ describe 'site/settings' do it 'fails gracefully with encrypted key' do @ssl = generate_ssl_certs domain: @domain - visit '/custom_domain' + visit "/settings/#{@site.username}#custom_domain" attach_file 'key', './tests/files/ssl/derpie.com-encrypted.key' attach_file 'cert', @ssl[:cert_path] click_button 'Upload SSL Key and Certificate' - page.current_path.must_equal '/custom_domain' + page.current_path.must_equal "/settings/#{@site.username}" page.must_have_content /could not process ssl key/i end it 'fails with junk key' do @ssl = generate_ssl_certs domain: @domain - visit '/custom_domain' + visit "/settings/#{@site.username}#custom_domain" attach_file 'key', './tests/files/index.html' attach_file 'cert', @ssl[:cert_path] click_button 'Upload SSL Key and Certificate' - page.current_path.must_equal '/custom_domain' + page.current_path.must_equal "/settings/#{@site.username}" page.must_have_content /could not process ssl key/i end it 'fails with junk cert' do @ssl = generate_ssl_certs domain: @domain - visit '/custom_domain' + visit "/settings/#{@site.username}#custom_domain" attach_file 'key', @ssl[:key_path] attach_file 'cert', './tests/files/index.html' click_button 'Upload SSL Key and Certificate' - page.current_path.must_equal '/custom_domain' + page.current_path.must_equal "/settings/#{@site.username}" page.must_have_content /could not process ssl certificate/i end if ENV['TRAVIS'] != 'true' it 'fails with bad cert chain' do @ssl = generate_ssl_certs domain: @domain - visit '/custom_domain' + visit "/settings/#{@site.username}#custom_domain" attach_file 'key', @ssl[:key_path] attach_file 'cert', @ssl[:bad_combined_cert_path] click_button 'Upload SSL Key and Certificate' - page.current_path.must_equal '/custom_domain' + page.current_path.must_equal "/settings/#{@site.username}" page.must_have_content /there is something wrong with your certificate/i end end @@ -200,7 +223,7 @@ describe 'site/settings' do click_button 'Create My Website' fill_in_valid click_button 'Create Home Page' - visit '/settings' + visit "/settings/#{@site[:username]}#username" fill_in 'name', with: '' click_button 'Change Name' fill_in 'name', with: '../hack' @@ -215,87 +238,4 @@ describe 'site/settings' do Site[username: ''].must_equal nil end end - - describe 'email' do - include Capybara::DSL - - before do - EmailWorker.jobs.clear - @email = "#{SecureRandom.uuid.gsub('-', '')}@example.com" - @site = Fabricate :site, email: @email - page.set_rack_session id: @site.id - visit '/settings' - end - - it 'should change email' do - @new_email = "#{SecureRandom.uuid.gsub('-', '')}@example.com" - fill_in 'email', with: @new_email - click_button 'Change Email' - page.must_have_content /successfully changed email/i - @site.reload - @site.email.must_equal @new_email - EmailWorker.jobs.length.must_equal 1 - args = EmailWorker.jobs.first['args'].first - args['to'].must_equal @new_email - args['subject'].must_match /confirm your email address/i - args['body'].must_match /hello #{@site.username}/i - args['body'].must_match /#{@site.email_confirmation_token}/ - end - - it 'should fail for invalid email address' do - @new_email = SecureRandom.uuid.gsub '-', '' - fill_in 'email', with: @new_email - click_button 'Change Email' - page.must_have_content /a valid email address is required/i - @site.reload - @site.email.wont_equal @new_email - EmailWorker.jobs.empty?.must_equal true - end - - it 'should fail for existing email' do - @existing_email = "#{SecureRandom.uuid.gsub('-', '')}@example.com" - @existing_site = Fabricate :site, email: @existing_email - - fill_in 'email', with: @existing_email - click_button 'Change Email' - page.must_have_content /this email address already exists on neocities/i - @site.reload - @site.email.wont_equal @new_email - EmailWorker.jobs.empty?.must_equal true - end - end - - describe 'change password' do - include Capybara::DSL - - before do - @site = Fabricate :site, password: 'derpie' - page.set_rack_session id: @site.id - visit '/settings' - end - - it 'should change correctly' do - fill_in 'current_password', with: 'derpie' - fill_in 'new_password', with: 'derpie2' - fill_in 'new_password_confirm', with: 'derpie2' - click_button 'Change Password' - - page.must_have_content /successfully changed password/i - @site.reload - @site.valid_password?('derpie').must_equal false - @site.valid_password?('derpie2').must_equal true - end - - it 'should not change for invalid current password' do - fill_in 'current_password', with: 'dademurphy' - fill_in 'new_password', with: 'derpie2' - fill_in 'new_password_confirm', with: 'derpie2' - click_button 'Change Password' - - page.must_have_content /provided password does not match the current one/i - @site.reload - @site.valid_password?('derpie').must_equal true - @site.valid_password?('derpie2').must_equal false - end - end end \ No newline at end of file diff --git a/tests/acceptance/signin_tests.rb b/tests/acceptance/signin_tests.rb index 78e13e46..a990870e 100644 --- a/tests/acceptance/signin_tests.rb +++ b/tests/acceptance/signin_tests.rb @@ -18,7 +18,7 @@ describe 'signin' do Capybara.reset_sessions! end - it 'fails for invalid login' do + it 'fails for invalid signin' do visit '/' click_link 'Sign In' page.must_have_content 'Welcome Back' @@ -27,7 +27,7 @@ describe 'signin' do page.must_have_content 'Invalid login' end - it 'fails for missing login' do + it 'fails for missing signin' do visit '/' click_link 'Sign In' auth = {username: SecureRandom.hex, password: Faker::Internet.password} @@ -37,7 +37,7 @@ describe 'signin' do page.must_have_content 'Invalid login' end - it 'logs in with proper credentials' do + it 'signs in with proper credentials' do visit '/' click_button 'Create My Website' fill_in_valid_signup @@ -50,4 +50,18 @@ describe 'signin' do click_button 'Sign In' page.must_have_content 'Your Feed' end + + it 'signs in with email' do + visit '/' + click_button 'Create My Website' + fill_in_valid_signup + click_button 'Create Home Page' + Capybara.reset_sessions! + visit '/' + click_link 'Sign In' + fill_in 'username', with: @site[:email] + fill_in 'password', with: @site[:password] + click_button 'Sign In' + page.must_have_content 'Your Feed' + end end \ No newline at end of file diff --git a/views/_header.erb b/views/_header.erb index 2a2b4777..8828cfab 100644 --- a/views/_header.erb +++ b/views/_header.erb @@ -36,25 +36,39 @@