diff --git a/app/authorizers/content_authorizer.rb b/app/authorizers/content_authorizer.rb index eb714272..e3a074be 100644 --- a/app/authorizers/content_authorizer.rb +++ b/app/authorizers/content_authorizer.rb @@ -1,10 +1,16 @@ class ContentAuthorizer < ApplicationAuthorizer def readable_by? user - return true if user && user.site_administrator? - return true if ::PermissionService.user_owns_any_containing_universe?(user: user, content: resource) - return true if ::PermissionService.user_owns_content?(user: user, content: resource) + # Check public content first - these should be accessible to anyone even without a user account return true if ::PermissionService.content_is_public?(content: resource) return true if ::PermissionService.content_is_in_a_public_universe?(content: resource) + + # If no user is provided (logged out), they can only access public content + return false if user.nil? + + # Otherwise check user-specific permissions + return true if user.site_administrator? + return true if ::PermissionService.user_owns_any_containing_universe?(user: user, content: resource) + return true if ::PermissionService.user_owns_content?(user: user, content: resource) return true if ::PermissionService.user_can_contribute_to_containing_universe?(user: user, content: resource) return false diff --git a/app/authorizers/core_content_authorizer.rb b/app/authorizers/core_content_authorizer.rb index c21d9582..75cbc0ec 100644 --- a/app/authorizers/core_content_authorizer.rb +++ b/app/authorizers/core_content_authorizer.rb @@ -1,8 +1,14 @@ class CoreContentAuthorizer < ContentAuthorizer def self.readable_by?(user) - return true if user && resource.user == user # PermissionService.user_owns_content?() + # Check public content first - these should be accessible to anyone return true if resource.privacy == 'public' return true if resource.try(:universe).try(:privacy) == 'public' + + # For private content, require a user + return false if user.nil? + + # Check user-specific permissions + return true if resource.user == user # PermissionService.user_owns_content?() false end diff --git a/app/controllers/browse_controller.rb b/app/controllers/browse_controller.rb index 99bde280..ae5bca64 100644 --- a/app/controllers/browse_controller.rb +++ b/app/controllers/browse_controller.rb @@ -27,8 +27,10 @@ class BrowseController < ApplicationController # Go through each content type and find public items with this tag Rails.application.config.content_types[:all].each do |content_type| - # First find page IDs with this tag - tag_page_ids = PageTag.where(page_type: content_type.name, slug: @tag_slug).pluck(:page_id) + # First find page IDs with this tag - case insensitive matching for slug + tag_page_ids = PageTag.where(page_type: content_type.name) + .where("LOWER(slug) = ?", @tag_slug.downcase) + .pluck(:page_id) if tag_page_ids.any? # Use database-level randomization with the daily seed for caching potential @@ -49,7 +51,9 @@ class BrowseController < ApplicationController end # Add documents separately since they don't use the common content type structure - document_tag_page_ids = PageTag.where(page_type: 'Document', slug: @tag_slug).pluck(:page_id) + document_tag_page_ids = PageTag.where(page_type: 'Document') + .where("LOWER(slug) = ?", @tag_slug.downcase) + .pluck(:page_id) if document_tag_page_ids.any? documents = Document.where(id: document_tag_page_ids) .where(privacy: 'public') @@ -65,7 +69,9 @@ class BrowseController < ApplicationController end # Add timelines separately since they don't use the common content type structure - timeline_tag_page_ids = PageTag.where(page_type: 'Timeline', slug: @tag_slug).pluck(:page_id) + timeline_tag_page_ids = PageTag.where(page_type: 'Timeline') + .where("LOWER(slug) = ?", @tag_slug.downcase) + .pluck(:page_id) if timeline_tag_page_ids.any? timelines = Timeline.where(id: timeline_tag_page_ids) .where(privacy: 'public') diff --git a/app/controllers/content_controller.rb b/app/controllers/content_controller.rb index c048e471..a7919209 100644 --- a/app/controllers/content_controller.rb +++ b/app/controllers/content_controller.rb @@ -89,10 +89,10 @@ class ContentController < ApplicationController def show content_type = content_type_from_controller(self.class) - return redirect_to(root_path, notice: "That page doesn't exist!") unless valid_content_types.include?(content_type.name) + return redirect_to(root_path, notice: "That page doesn't exist!", status: :not_found) unless valid_content_types.include?(content_type.name) @content = content_type.find_by(id: params[:id]) - return redirect_to(root_path, notice: "You don't have permission to view that content.") if @content.nil? + return redirect_to(root_path, notice: "You don't have permission to view that content.", status: :not_found) if @content.nil? return redirect_to(root_path) if @content.user.nil? # deleted user's content return if ENV.key?('CONTENT_BLACKLIST') && ENV['CONTENT_BLACKLIST'].split(',').include?(@content.user.try(:email)) diff --git a/app/controllers/document_analyses_controller.rb b/app/controllers/document_analyses_controller.rb index e36a0ccb..08b974e9 100644 --- a/app/controllers/document_analyses_controller.rb +++ b/app/controllers/document_analyses_controller.rb @@ -47,7 +47,7 @@ class DocumentAnalysesController < ApplicationController def authorize_user_for_document unless @document.present? && (current_user || User.new).can_read?(@document) - redirect_to(root_path, notice: "That document either doesn't exist or you don't have permission to view it.") + redirect_to(root_path, notice: "That document either doesn't exist or you don't have permission to view it.", status: :not_found) return false end end diff --git a/app/controllers/documents_controller.rb b/app/controllers/documents_controller.rb index 71423c0e..822f6970 100644 --- a/app/controllers/documents_controller.rb +++ b/app/controllers/documents_controller.rb @@ -68,11 +68,11 @@ class DocumentsController < ApplicationController def show unless @document.present? && (current_user || User.new).can_read?(@document) - return redirect_to(root_path, notice: "That document either doesn't exist or you don't have permission to view it.") + return redirect_to(root_path, notice: "That document either doesn't exist or you don't have permission to view it.", status: :not_found) end if @document.user.thredded_user_detail.moderation_state == "blocked" - return redirect_to(root_path, notice: "That document either doesn't exist or you don't have permission to view it.") + return redirect_to(root_path, notice: "That document either doesn't exist or you don't have permission to view it.", status: :not_found) end # Put the focus on the document by removing Notebook.ai actions @@ -81,7 +81,7 @@ class DocumentsController < ApplicationController def analysis unless @document.present? && (current_user || User.new).can_read?(@document) - redirect_to(root_path, notice: "That document either doesn't exist or you don't have permission to view it.") + redirect_to(root_path, notice: "That document either doesn't exist or you don't have permission to view it.", status: :not_found) end @analysis = @document.document_analysis.where.not(queued_at: nil).order('updated_at DESC').first @@ -98,7 +98,7 @@ class DocumentsController < ApplicationController end def queue_analysis - return redirect_back(fallback_location: documents_path, notice: "That document doesn't exist!") unless @document.present? + return redirect_back(fallback_location: documents_path, notice: "That document doesn't exist!", status: :not_found) unless @document.present? return redirect_back(fallback_location: documents_path, notice: "Document analysis is a feature for Premium users.") unless @document.user.on_premium_plan? return redirect_back(fallback_location: documents_path, notice: "You don't have permission to do that!") unless @document.user == current_user @@ -214,7 +214,7 @@ class DocumentsController < ApplicationController def plaintext unless @document.present? && (current_user || User.new).can_read?(@document) - return redirect_to(root_path, notice: "That document either doesn't exist or you don't have permission to view it.") + return redirect_to(root_path, notice: "That document either doesn't exist or you don't have permission to view it.", status: :not_found) end render layout: 'plaintext' @@ -335,7 +335,7 @@ class DocumentsController < ApplicationController @document = Document.find_by(id: params[:id]) unless @document - redirect_to root_path, notice: "Either that document doesn't exist or you don't have permission to view it!" + redirect_to root_path, notice: "Either that document doesn't exist or you don't have permission to view it!", status: :not_found return end end diff --git a/app/controllers/universes_controller.rb b/app/controllers/universes_controller.rb index ca1484fe..8eb03810 100644 --- a/app/controllers/universes_controller.rb +++ b/app/controllers/universes_controller.rb @@ -7,7 +7,7 @@ class UniversesController < ContentController @content_type = content_type_name.to_s.singularize.capitalize.constantize @universe = Universe.find_by(id: params[:id]) - return redirect_to(root_path, notice: "That universe doesn't exist!") unless @universe.present? + return redirect_to(root_path, notice: "That universe doesn't exist!", status: :not_found) unless @universe.present? @content_list = @universe.send(content_type_name) # todo just use current_user.can_view?(@universe) and/or individual filtering diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2d994640..817c28e6 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -99,7 +99,10 @@ class UsersController < ApplicationController @tag_slug = params[:tag_slug] @tag = PageTag.find_by(user_id: @user.id, slug: @tag_slug) - return redirect_to(profile_by_username_path(username: @user.username), notice: 'That tag does not exist.') if @tag.nil? + if @tag.nil? + redirect_url = @user.username.present? ? profile_by_username_path(username: @user.username) : user_path(@user) + return redirect_to(redirect_url, notice: 'That tag does not exist.', status: :not_found) + end # Find all public content with this tag @tagged_content = [] @@ -181,7 +184,7 @@ class UsersController < ApplicationController def set_user @user = User.find_by(user_params) - return redirect_to(root_path, notice: 'That user does not exist.') if @user.nil? + return redirect_to(root_path, notice: 'That user does not exist.', status: :not_found) if @user.nil? return redirect_to(root_path, notice: 'That user has chosen to hide their profile.') if @user.private_profile? return redirect_to(root_path, notice: 'That user has had their profile hidden.') if @user.thredded_user_detail.moderation_state == 'blocked' diff --git a/app/services/permission_service.rb b/app/services/permission_service.rb index bfc15d90..c7820b85 100644 --- a/app/services/permission_service.rb +++ b/app/services/permission_service.rb @@ -25,11 +25,18 @@ class PermissionService < Service end def self.user_can_contribute_to_containing_universe?(user:, content:) + # Early return if no user is provided return false if user.nil? + + # Special case for attribute-related content return true if [AttributeCategory, AttributeField, Attribute].include?(content.class) #todo audit this - return true if user.contributable_universe_ids.include?(content.universe_id) - return true if user.universes.pluck(:id).include?(content.universe_id) + # Handle cases where content might not have a universe_id + return false if content.universe_id.nil? + + # Check if user can contribute to this universe + return true if user.respond_to?(:contributable_universe_ids) && user.contributable_universe_ids.include?(content.universe_id) + return true if user.respond_to?(:universes) && user.universes.pluck(:id).include?(content.universe_id) return false end diff --git a/app/views/browse/tag.html.erb b/app/views/browse/tag.html.erb index 575e9857..b990012b 100644 --- a/app/views/browse/tag.html.erb +++ b/app/views/browse/tag.html.erb @@ -80,7 +80,7 @@
category - <%= pluralize(content_types_count, 'category') %> + <%= pluralize(content_types_count, 'page type') %>
@@ -106,20 +106,20 @@
-
+
filter_list Filter by category:
- + layers All (<%= total_items %>) <% @tagged_content.each do |content_group| %> - + <%= content_group[:icon] %> <%= content_group[:type].pluralize %> (<%= content_group[:content].count %>) @@ -453,12 +453,23 @@ .modal-how-to-join { border-radius: 8px; max-width: 500px; + transition: background-color 1s ease; + } + + body.dark .modal-how-to-join { + background-color: #2D2D31 !important; + color: #ddd; } .modal-how-to-join h4 { margin-top: 0; font-size: 20px; font-weight: 500; + transition: color 1s ease; + } + + body.dark .modal-how-to-join h4 { + color: white; } .modal-how-to-join ol { @@ -473,6 +484,17 @@ background-color: #f3e5f5; padding: 2px 5px; border-radius: 3px; + transition: background-color 1s ease, color 1s ease; + } + + body.dark .modal-how-to-join .highlight { + background-color: rgba(156, 39, 176, 0.2); + color: white; + } + + /* Fix modal content border */ + body.dark .modal-content { + border-color: rgba(255, 255, 255, 0.1) !important; } :root { @@ -545,6 +567,39 @@ } /* Category filters styling */ + .category-filters { + background-color: #fff; + transition: background-color 1s ease; + } + + body.dark .category-filters { + background-color: rgba(255, 255, 255, 0.1); + } + + .category-filter { + background-color: #f5f5f5; + color: #333; + transition: background-color 1s ease, color 1s ease; + } + + body.dark .category-filter { + background-color: rgba(255, 255, 255, 0.05); + } + + /* Ensure filter text is always readable regardless of theme */ + .category-filter { + color: #333 !important; + } + + /* Fix the filter title text in dark mode */ + body.dark .category-filter-title span { + color: #ddd; + } + + body.dark .category-filter.active { + color: #333; + } + @media only screen and (max-width: 600px) { .category-filters { overflow-x: auto; @@ -580,10 +635,16 @@ /* Better card styling */ .hoverable.card { - transition: box-shadow 0.25s, transform 0.25s; + transition: box-shadow 0.25s, transform 0.25s, background-color 1s ease; box-shadow: 0 2px 5px rgba(0,0,0,0.1); border-radius: 8px; margin-bottom: 8px; + background-color: #fff; + } + + body.dark .hoverable.card { + background-color: rgba(255, 255, 255, 0.1); + box-shadow: 0 2px 5px rgba(0,0,0,0.3); } .hoverable.card:hover { @@ -591,11 +652,20 @@ transform: translateY(-2px); } + body.dark .hoverable.card:hover { + box-shadow: 0 5px 15px rgba(0,0,0,0.4); + } + /* Position relative for absolute positioning within */ .hoverable.card { position: relative; padding-bottom: 8px; border-bottom: 4px solid #eeeeee; + transition: border-color 1s ease; + } + + body.dark .hoverable.card { + border-bottom: 4px solid rgba(255, 255, 255, 0.1); } .js-content-card-container > a, @@ -623,6 +693,11 @@ overflow: hidden; position: relative; padding-bottom: 20px !important; + transition: background-color 1s ease; + } + + body.dark .card .card-content { + background-color: rgba(255, 255, 255, 0.1); } .tags-container { @@ -653,6 +728,11 @@ height: 15px; background: linear-gradient(to bottom, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%); pointer-events: none; + transition: background 1s ease; + } + + body.dark .card .card-content:after { + background: linear-gradient(to bottom, rgba(40,40,45,0) 0%, rgba(40,40,45,1) 100%); } /* Card footer line - more visible */ @@ -666,5 +746,24 @@ background-color: #f0f0f0; border-radius: 0 0 8px 8px; z-index: 1; + transition: background-color 1s ease; + } + + body.dark .card::after { + background-color: rgba(255, 255, 255, 0.05); + } + + /* Dark mode text styles */ + body.dark .card .card-content span[style*="color: #424242"] { + color: #ddd !important; + } + + body.dark .grey-text { + color: #aaa !important; + } + + body.dark .tag-pill[style*="background-color: #f0f0f0"] { + background-color: rgba(255, 255, 255, 0.1) !important; + color: #aaa !important; } \ No newline at end of file diff --git a/app/views/content/form/images/_edit_list.html.erb b/app/views/content/form/images/_edit_list.html.erb index 2911cbb8..b1c4c8df 100644 --- a/app/views/content/form/images/_edit_list.html.erb +++ b/app/views/content/form/images/_edit_list.html.erb @@ -8,7 +8,7 @@ <% raw_model.image_uploads.each do |image| %>
- <%= link_to image.src(:original), class: 'z-depth-1 hoverable right', target: '_new' do %> + <%= link_to image.src(:original), class: 'z-depth-1 hoverable right card', target: '_new' do %> <%= image_tag image.src(:small) %> <% end %>
@@ -20,9 +20,9 @@
- <%= link_to 'Open', image.src(:original), class: 'btn white black-text', target: '_new' %> + <%= link_to 'Open', image.src(:original), class: 'btn black-text', target: '_new' %> <%= link_to 'Delete', image_deletion_path(image.id), - class: 'btn white black-text js-remove-image', + class: 'btn black-text js-remove-image', method: 'delete', remote: true, data: { confirm: "Are you sure? This can't be undone." } %> @@ -35,7 +35,7 @@ <% @basil_images.each do |commission| %>
- <%= link_to commission.image, class: 'z-depth-1 hoverable right', target: '_new' do %> + <%= link_to commission.image, class: 'z-depth-1 hoverable right card', target: '_new' do %> <%= image_tag commission.image, style: 'max-width: 100%' %> <% end %>
@@ -47,9 +47,9 @@
- <%= link_to 'Open', commission.image, class: 'btn white black-text', target: '_new' %> + <%= link_to 'Open', commission.image, class: 'btn black-text', target: '_new' %> <%= link_to 'Delete', basil_delete_path(commission), - class: 'btn white black-text js-remove-image', + class: 'btn black-text js-remove-image', method: 'delete', remote: true, data: { confirm: "Are you sure? This can't be undone." } %> diff --git a/app/views/content/form/images/_gallery.html.erb b/app/views/content/form/images/_gallery.html.erb index 5160c36e..afa35711 100644 --- a/app/views/content/form/images/_gallery.html.erb +++ b/app/views/content/form/images/_gallery.html.erb @@ -25,7 +25,7 @@
<% all_images.each do |image_item| %>
- <% else %> -