Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions app/assets/javascripts/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@
//= require utility
//= require custom
//= require map
//= require controllers
//= require_tree ./controllers

$(function() {
$(function () {
Utility.disable();
});

$.fn.random = function() {
$.fn.random = function () {
return this.eq(Math.floor(Math.random() * this.length));
};
10 changes: 10 additions & 0 deletions app/assets/javascripts/controllers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Minimal controller registry — mirrors Stimulus conventions without the dependency.
// Files in controllers/ assign to Controllers['name'] = class { connect(el) {} }
const Controllers = {};

document.addEventListener('DOMContentLoaded', function() {
Comment thread
JoschkaSchulz marked this conversation as resolved.
document.querySelectorAll('[data-controller]').forEach(function(el) {
const name = el.dataset.controller;
if (Controllers[name]) new Controllers[name](el).connect();
});
});
36 changes: 36 additions & 0 deletions app/assets/javascripts/controllers/nav_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Reads the _on_ruby_user data cookie (set by UserHandling#sign_in) and swaps the
// anonymous login button for the profile dropdown without a server round-trip.
Controllers['nav'] = class {
constructor(el) { this.el = el; }

connect() {
const user = this.readUserCookie();
if (user) this.renderUserNav(user);
}

readUserCookie() {
const match = document.cookie.match(/(?:^|;\s*)_on_ruby_user=([^;]+)/);
if (!match) return null;
try { return JSON.parse(decodeURIComponent(match[1])); }
catch (e) { return null; }
}

renderUserNav(user) {
const template = this.el.querySelector('template');
if (!template) return;

const frag = template.content.cloneNode(true);
const avatar = frag.querySelector('[data-avatar]');
avatar.src = user.image_path;
avatar.alt = user.name;
frag.querySelector('[data-profile]').href = user.profile_path;
frag.querySelector('[data-edit]').href = user.edit_path;
frag.querySelector('[data-logout]').href = user.logout_path;

const show = (sel) => { const node = frag.querySelector(sel); if (node) node.hidden = false; };
Comment thread
JoschkaSchulz marked this conversation as resolved.
if (user.is_admin) show('[data-admin]');
if (user.is_super_admin) show('[data-super-admin]');

this.el.querySelector('[data-login]').replaceWith(frag);
}
};
4 changes: 4 additions & 0 deletions app/assets/stylesheets/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ body {
display: none;
}
#nav {
.navbar-nav {
align-items: center;
}

img.label {
max-height: 30px;
}
Expand Down
1 change: 1 addition & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class ApplicationController < ActionController::Base
def setup
switch_locale
switch_time_zone
refresh_user_cookie
end

def _404(exception)
Expand Down
45 changes: 42 additions & 3 deletions app/controllers/concerns/user_handling.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,63 @@ def signed_in?
!!current_user
end

def sign_in(user)
def refresh_user_cookie
user_cookie(current_user) if cookies['_on_ruby_user'].blank? && current_user
end

def sign_in(user, permanent: true)
@current_user = user
session[:user_id] = user.id
cookies.permanent.signed[:remember_me] = [user.id, user.salt]
cookies.permanent.signed[:remember_me] = [user.id, user.salt] if permanent
user_cookie(user)
end

def sign_out
session.clear
cookies.permanent.signed[:remember_me] = ['', '']
clear_user_cookie
end

def user_cookie(user)
data = {
slug: user.to_param,
name: user.name.to_s,
image_path: helpers.cache_image_path(user),
profile_path: user_path(user),
edit_path: edit_user_path(user),
logout_path: destroy_session_path,
hide_jobs: user.hide_jobs?,
missing_name: user.missing_name?,
is_admin: (true if user.admin?),
is_super_admin: (true if user.super_admin?),
}.compact

options = { value: data.to_json, domain: request.domain, httponly: false, same_site: :lax }

if remember_me_active?
cookies.permanent['_on_ruby_user'] = options
else
cookies['_on_ruby_user'] = options
end
end

def clear_user_cookie
cookies.delete('_on_ruby_user', domain: request.domain)
end

def find_by_session_or_cookies
User.find_by(id: session[:user_id]) || User.authenticated_with_token(*remember_me)
user = User.find_by(id: session[:user_id]) || User.authenticated_with_token(*remember_me)
clear_user_cookie if user.nil? && cookies['_on_ruby_user'].present?
user
end

def remember_me
cookies.permanent.signed[:remember_me] || ['', '']
rescue StandardError
['', '']
end

def remember_me_active?
remember_me.first.present?
end
end
3 changes: 1 addition & 2 deletions app/controllers/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ class SessionsController < ApplicationController

def offline_login
user = User.find_by(nickname: params[:nickname])
sign_in(user)

sign_in(user, permanent: params[:permanent].present?)
redirect_to root_path, notice: 'Offline Login!'
end

Expand Down
1 change: 1 addition & 0 deletions app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def calendar

def update
if current_user.update user_params
user_cookie(current_user)
redirect_back(notice: t('user.saved_successful'), fallback_location: root_url)
else
redirect_back(alert: current_user.errors.full_messages.join(' '), fallback_location: root_url)
Expand Down
7 changes: 3 additions & 4 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,9 @@ def whitelabel_javascript_include_tag
javascript_include_tag link if Rails.root.join("app/assets/javascripts/#{link}.js").exist?
end

def label_auth_url(provider)
host = Rails.env.development? ? "http://#{Whitelabel[:label_id]}.onruby.localhost:3000" : Whitelabel[:canonical_url]

"#{host}/auth/#{provider}?origin=#{CGI.escape(params[:origin]) if params[:origin]}"
def label_auth_path(provider)
path = "/auth/#{provider}"
params[:origin].present? ? "#{path}?origin=#{CGI.escape(params[:origin])}" : path
end

def label_url(label)
Expand Down
56 changes: 29 additions & 27 deletions app/views/application/_nav.slim
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
nav.navbar.sticky-top.navbar-expand-lg.navbar-light.bg-light#nav
nav.navbar.sticky-top.navbar-expand-lg.navbar-light.bg-light#nav(aria-label="Main navigation")
.label.navbar-brand
= link_to(root_path(anchor: :on_ruby), class: 'navbar-brand ps-4') do
= image_tag(label_logo_asset, title: title, class: "d-inline-block align-bottom label")
Expand All @@ -15,37 +15,39 @@ nav.navbar.sticky-top.navbar-expand-lg.navbar-light.bg-light#nav
= fa_icon(fa_icon_map[section], class: 'fa-fw', text: t("main.#{section}"))

ul.navbar-nav.ms-auto
li.nav-item.dropdown.pe-4
- if signed_in?
li.nav-item.dropdown.pe-4(data-controller="nav")
/ Anonymous default — nav_controller.js replaces with profile dropdown when cookie present
div(data-login="")
a.btn.btn-primary.dropdown-toggle(href="#" id="loginDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" aria-label="Login menu")
= t('login.login')
.dropdown-menu.dropdown-menu-end(aria-labelledby="loginDropdown")
- login_providers.each do |provider|
= button_to(label_auth_path(provider), class: 'dropdown-item') do
= fa_icon(icon_for_provider(provider), class: 'fa-fw', text: t("login.#{provider}_login"))

/ Template for logged-in state — inert until cloned by nav_controller.js
template
.dropdown
a.btn.btn-light.dropdown-toggle(href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false")
= user_image(current_user)
a.btn.btn-light.dropdown-toggle(href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false" aria-label="Account menu")
img.user-image.user-image-small(src="" alt="" data-avatar="")
= t('login.profile')
ul.dropdown-menu.dropdown-menu-end(aria-labelledby="loginDropdown")
li= link_to(user_path(current_user), class: 'dropdown-item') do
= fa_icon('eye', class: 'fa-fw', text: t("login.show_profile"))

li= link_to(edit_user_path(current_user), class: 'dropdown-item') do
= fa_icon('edit', class: 'fa-fw', text: t("login.edit_profile"))

li= link_to(destroy_session_path(current_user), class: 'dropdown-item') do
= fa_icon('times', class: 'fa-fw', text: t("login.logout"))

- if current_user.admin?
li= link_to('/admin', class: 'dropdown-item') do
ul.dropdown-menu.dropdown-menu-end
li
a.dropdown-item(href="" data-profile="")
= fa_icon('eye', class: 'fa-fw', text: t("login.show_profile"))
li
a.dropdown-item(href="" data-edit="")
= fa_icon('edit', class: 'fa-fw', text: t("login.edit_profile"))
li
a.dropdown-item(href="" data-logout="")
= fa_icon('times', class: 'fa-fw', text: t("login.logout"))
li(hidden=true data-admin="")
a.dropdown-item(href="/admin")
= fa_icon('lock', class: 'fa-fw', text: 'Community-Admin')
- if current_user.super_admin?
li= link_to('/super_admin', class: 'dropdown-item') do
li(hidden=true data-super-admin="")
a.dropdown-item(href="/super_admin")
= fa_icon('lock', class: 'fa-fw', text: 'Super-Admin')

- else
a(class="btn btn-primary dropdown-toggle" href="#" id="loginDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false")
= t('login.login')
.dropdown-menu.dropdown-menu-end(aria-labelledby="loginDropdown")
- login_providers.each do |provider|
= button_to(label_auth_url(provider), class: 'dropdown-item') do
= fa_icon(icon_for_provider(provider), class: 'fa-fw', text: t("login.#{provider}_login"))


li.nav-item.dropdown.pe-4
a(class="nav-link btn btn-light dropdown-toggle" href="#" id="localeDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false")
Expand Down
2 changes: 1 addition & 1 deletion app/views/sessions/index.slim
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ section
ul.list-group.list-group-horizontal
- login_providers.each do |provider|
li.list-group-item
= button_to(label_auth_url(provider)) do
= button_to(label_auth_path(provider)) do
= fa_icon(icon_for_provider(provider), class: 'fa-fw dropdown-list-icon', text: t("login.#{provider}_login"))
2 changes: 1 addition & 1 deletion app/views/users/edit.slim
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ section
- existing_providers = user.authorizations.map(&:provider)
- login_providers.each do |provider|
li
= button_to label_auth_url(provider), title: t("login.#{provider}_login"), class: "btn btn-#{existing_providers.include?(provider) ? 'disabled' : 'secondary'}", disabled: existing_providers.include?(provider) do
= button_to label_auth_path(provider), title: t("login.#{provider}_login"), class: "btn btn-#{existing_providers.include?(provider) ? 'disabled' : 'secondary'}", disabled: existing_providers.include?(provider) do
= fa_icon icon_for_provider(provider), class: 'fa-fw dropdown-list-icon'
=> t("login.#{provider}_login")
- if existing_providers.include? provider
Expand Down
2 changes: 1 addition & 1 deletion config/initializers/omniauth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
provider :github, {
setup: proc do |env|
host = env['SERVER_NAME']
tld = host[/(.+\.)?(.+\..+)/, 2]
tld = host[/(.+\.)?(.+\..+)/, 2] || host
token = tld.delete('-').split('.').join('_').upcase
name = "OMNIAUTH_GITHUB_#{token}"

Expand Down
104 changes: 104 additions & 0 deletions spec/controllers/application_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# frozen_string_literal: true

require 'spec_helper'

describe ApplicationController do
controller do
def index
render plain: 'ok'
end

def create
authenticate!
render plain: 'ok' unless performed?
end
end

let(:user) { create(:user, :with_authorization) }

context 'state 1 — fully anonymous' do
it 'does not set _on_ruby_user' do
get :index
expect(cookies['_on_ruby_user']).to be_nil
end
end

context 'state 2 — orphaned _on_ruby_user, no auth' do
before { cookies['_on_ruby_user'] = '{"slug":"ghost","name":"Ghost"}' }

it 'clears _on_ruby_user when authenticate! is triggered' do
post :create
expect(cookies['_on_ruby_user']).to be_blank
end
end

context 'state 3 — valid session, no remember_me' do
before { session[:user_id] = user.id }

it 'authenticates the user via session' do
post :create
expect(response).to have_http_status(:ok)
end

it 'does not set remember_me' do
post :create
expect(cookies[:remember_me]).to be_nil
end

it 're-sets _on_ruby_user when absent' do
get :index
expect(cookies['_on_ruby_user']).to be_present
end
end

context 'state 4 — remember_me valid, _on_ruby_user absent (migration)' do
before { cookies.signed[:remember_me] = [user.id, user.salt] }

it 're-sets _on_ruby_user on the next request' do
get :index
expect(cookies['_on_ruby_user']).to be_present
end
end

context 'state 5 — returning user, both permanent cookies present' do
before do
cookies.signed[:remember_me] = [user.id, user.salt]
cookies['_on_ruby_user'] = '{"slug":"testuser","name":"Test"}'
end

it 'does not overwrite _on_ruby_user' do
original = cookies['_on_ruby_user']
get :index
expect(cookies['_on_ruby_user']).to eq(original)
end
end

context 'state 6 — stale remember_me, _on_ruby_user absent' do
before do
cookies.signed[:remember_me] = [user.id, user.salt]
user.destroy
end

it 'does not crash' do
expect { get :index }.not_to raise_error
end

it 'leaves _on_ruby_user absent' do
get :index
expect(cookies['_on_ruby_user']).to be_blank
end
end

context 'state 7 — stale remember_me and _on_ruby_user, user gone' do
before do
cookies.signed[:remember_me] = [user.id, user.salt]
cookies['_on_ruby_user'] = '{"slug":"testuser","name":"Test"}'
user.destroy
end

it 'clears _on_ruby_user when authenticate! is triggered' do
post :create
expect(cookies['_on_ruby_user']).to be_blank
end
end
end
2 changes: 1 addition & 1 deletion spec/controllers/likes_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require 'spec_helper'

describe LikesController do
let(:user) { build(:user) }
let(:user) { create(:user) }

context 'POST :create' do
let!(:topic) { create(:topic) }
Expand Down
Loading
Loading