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 @@ + <% end %> -

- - - Neocities.org - -

- - +

+ + + Neocities.org + +

+ \ No newline at end of file diff --git a/views/admin.erb b/views/admin.erb index dea9e759..da3a2013 100644 --- a/views/admin.erb +++ b/views/admin.erb @@ -17,7 +17,7 @@
-

Ban User

+

Ban Site

<%== csrf_token_input_html %>

Site Name:

diff --git a/views/custom_domain.erb b/views/custom_domain.erb deleted file mode 100644 index 3ee9d4f9..00000000 --- a/views/custom_domain.erb +++ /dev/null @@ -1,80 +0,0 @@ -
-
-

Custom Domain

-

Add your own domain name to your Neocities site!

-
-
- -
-

-
- - <% if flash.keys.length > 0 %> -
- <% flash.keys.each do |key| %> - <%== flash[key] %> - <% end %> -
- <% end %> - -

- Adding a custom domain allows you to have a domain name attached to your web site. So if you had a domain like catsknitting.com, you could have it point to your Neocities site! -

- -

- You will have to purchase a domain name from a registrar like Namecheap, and then add an A record to point your domain (catsknitting.com) to the following IP address: -

- -

198.27.81.179

- -

- If you want to add a www subdomain, or use a wildcard that will answer to everything (*), you will have to make a CNAME pointing to catsknitting.com for www and/or *. -

- -

- After that, you can add the domain to the box below (just the catsknitting.com, don't add any subdomains), and your domain should come online within 5 minutes: -

- - - <%== csrf_token_input_html %> - -
- - -
- -
-

Add SSL Certificate

-

- This allows you to add an SSL key and certificate for your domain, enabling encryption for your site (https). It can take up to 5-30 minutes for the changes to go live, so please be patient. All files must be in PEM format. If your certificate is not bundled with the root and intermediate certificates, ask your certificate provider for help on how to do that. -

- - <% if current_site.domain.nil? || current_site.domain.empty? %> -

Cannot upload SSL certificate until domain is added.

- <% else %> - -
- <%== csrf_token_input_html %> - -

- - Status: <%= current_site.ssl_installed? ? 'Installed' : 'Inactive' %> - -

- -

- SSL Key (yourdomain.com.key): - -

- -

- Bundled Certificates (yourdomain.com-bundle.crt): - -

- - - -
- <% end %> -
-
\ No newline at end of file diff --git a/views/dashboard.erb b/views/dashboard.erb index d2b772d2..7a85ffbb 100644 --- a/views/dashboard.erb +++ b/views/dashboard.erb @@ -34,7 +34,7 @@ <% if current_site.updated_at %>
  • Last updated <%= current_site.updated_at.ago.downcase %>
  • <% end %> -
  • Using <%= current_site.space_percentage_used %>% (<%= current_site.used_space_in_megabytes %>MB) of your <%= current_site.maximum_space_in_megabytes %> MB. +
  • Using <%= current_site.space_percentage_used %>% (<%= current_site.total_used_space.to_space_pretty %>) of your <%= current_site.maximum_space.to_space_pretty %>.
    <% if !current_site.supporter? %>Need more space? Become a Supporter!<% end %>
  • <%= current_site.hits.to_s.reverse.gsub(/...(?=.)/,'\&,').reverse %> hits
  • @@ -222,7 +222,7 @@ Dropzone.options.uploads = { paramName: 'files', - maxFilesize: <%= current_site.available_space_in_megabytes %>, + maxFilesize: <%= current_site.remaining_space.to_mb %>, clickable: false, addRemoveLinks: false, dictDefaultMessage: '', diff --git a/views/index.erb b/views/index.erb index 0f27c476..aea14e8c 100644 --- a/views/index.erb +++ b/views/index.erb @@ -123,7 +123,7 @@

    Create your own free website

    - You get <%= Site::FREE_MAXIMUM_IN_MEGABYTES %> MB of free web space to make whatever you’d like! + You get <%= Site::FREE_MAXIMUM.to_space_pretty %> of free web space to make whatever you’d like!

  • diff --git a/views/new.erb b/views/new.erb index 60839a34..d51fb0ff 100644 --- a/views/new.erb +++ b/views/new.erb @@ -73,17 +73,17 @@

    The site you are creating will be free, forever. We will never charge you for your web site.

    Neocities has to pay the bills though, and we like the idea of being able to work on the site full-time someday. So if you would like to help us reach this goal, we have created the Supporter Plan! -

    Right now, the Supporter Plan is the same as the free plan, except that Supporter Plan members get 200MB of web space. You will also be listed as a supporter on our contributors page, and on your site profile page.

    +

    Right now, the Supporter Plan is the same as the free plan, except that Supporter Plan members get <%= Site::SUPPORTER_MAXIMUM.to_space_pretty %> of web space. You will also be listed as a supporter on our contributors page, and on your site profile page.

    The base plan is $12 ($1/month) billed once per year, which is the cost of a delicious Yafa Combo with a lousy tip. If you ever decide to cancel, you get to keep the extra space. Thanks for helping us run this site!

    > - Free Plan (<%= Site::FREE_MAXIMUM_IN_MEGABYTES %>MB) + Free Plan (<%= Site::FREE_MAXIMUM.to_space_pretty %>)
    > - Supporter Plan (<%= Site::SUPPORTER_MAXIMUM_IN_MEGABYTES %>MB) + Supporter Plan (<%= Site::SUPPORTER_MAXIMUM.to_space_pretty %>)