Skip to content
Merged
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
4 changes: 2 additions & 2 deletions app/assets/stylesheets/components/form.css
Original file line number Diff line number Diff line change
Expand Up @@ -742,7 +742,7 @@
outline-offset: 2px;
}

@media (width <= theme(--breakpoint-tablet)) {
@media (width <=theme(--breakpoint-tablet)) {
.gh-form-shell {
grid-template-columns: minmax(0, 1fr);
}
Expand Down Expand Up @@ -771,7 +771,7 @@
}
}

@media (width <= theme(--breakpoint-mobile)) {
@media (width <=theme(--breakpoint-mobile)) {
.gh-search-input--wide {
width: 100%;
}
Expand Down
11 changes: 8 additions & 3 deletions app/assets/stylesheets/components/join.css
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,13 @@
.gh-join-hint {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
justify-content: flex-start;
gap: 6px;
}

.gh-join-icon {
flex: none;
width: 16px;
text-align: center;
}

Expand All @@ -76,7 +81,7 @@
letter-spacing: 0.04em;
}

@media (width <= theme(--breakpoint-mobile)) {
@media (width <=theme(--breakpoint-mobile)) {
.gh-join-code-inputs {
gap: 7px;
}
Expand Down
33 changes: 3 additions & 30 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,14 @@

class ApplicationController < ActionController::Base
include Pundit::Authorization
include Authentication
include Sidebar
include StatusRedirects

attr_reader :current_user

# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
allow_browser versions: :modern

# The whole app renders through one layout. `@in_modal` is set by each
# controller's own before_action (the turbo-frame id it dialogs answer into
# differs per screen), so this only decides page-or-frame.
layout -> { @in_modal ? false : 'application' }

before_action :set_current_user
before_action :authenticate!
before_action :set_sidebar_state

rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
rescue_from Pundit::NotAuthorizedError, with: :not_authorized

Expand All @@ -28,24 +21,4 @@ def redirect_outside_turbo_frame(path, notice: nil, alert: nil)

render turbo_stream: turbo_stream.action(:redirect, path)
end

def set_sidebar_state
@sidebar_collapsed = cookies[:sidebar_collapsed] == 'true'
end

def set_current_user
@current_user = User.find_by(id: session[:user_id])
end

def authenticate!
redirect_to login_path, alert: 'Zaloguj się, aby kontynuować.' unless @current_user
end

def record_not_found
redirect_to root_path, alert: 'Nie znaleziono.'
end

def not_authorized
redirect_to root_path, alert: 'Nie znaleziono.'
end
end
33 changes: 9 additions & 24 deletions app/controllers/auth/passwordless_controller.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
# frozen_string_literal: true

class Auth::PasswordlessController < ApplicationController
include Auth::SignIn
include LoggedUserRedirector

# How long the "Wyślij ponownie" button stays disabled on the inbox screen.
# Presentational only — the real limit is the Rack::Attack throttle on
# POST /auth/passwordless (config/initializers/rack_attack.rb).
RESEND_COOLDOWN = 60

skip_before_action :authenticate!
Expand All @@ -18,12 +16,14 @@ def new; end
def inbox
@email = session[:pending_login_email]
sent_at = session[:pending_login_sent_at].to_i
current_time = Time.current.to_i

# No pending send, or one old enough that its link has already expired.
return redirect_to new_auth_passwordless_path if @email.blank? || sent_at.zero?
return redirect_to new_auth_passwordless_path if Time.current.to_i - sent_at > LoginToken::EXPIRES_IN
return redirect_to new_auth_passwordless_path if current_time - sent_at > LoginToken::EXPIRES_IN

@cooldown = [RESEND_COOLDOWN - (Time.current.to_i - sent_at), 0].max
delta = current_time - sent_at

@cooldown = [RESEND_COOLDOWN - delta, 0].max
end

def verify
Expand All @@ -35,23 +35,18 @@ def verify
user = login_token.user
user.consume_login_token!

reset_session
session[:user_id] = user.id

redirect_to home_path, notice: 'Zalogowano pomyślnie.'
sign_in_and_redirect(user)
end

def create
email = params.expect(:email)
user = User.find_by(email: email)
service = BypassLoginService.new(email: email)

# Whether this is a resend depends only on prior session state, never on
# whether the address exists — so it cannot be used to enumerate accounts.
resend = session[:pending_login_email].present?

if user
return bypass_login(user) if service.can_bypass_login?
return sign_in_and_redirect(user) if service.can_bypass_login?

token = user.create_login_token!
token_link = auth_passwordless_verify_url(token: token.raw_token)
Expand All @@ -63,14 +58,10 @@ def create
).token_email.deliver_later
end

# Everything below MUST stay outside the `if user` block: a known and an
# unknown address have to produce byte-identical responses. Do not move
# these next to the mailer call.
session[:pending_login_email] = email
session[:pending_login_sent_at] = Time.current.to_i

# No notice on a first send — the inbox screen is itself the confirmation.
notice = resend ? 'Wysłaliśmy nowy link. Poprzedni już nie działa.' : nil
notice = 'Wysłaliśmy nowy link. Poprzedni już nie działa.' if resend
redirect_to auth_passwordless_inbox_path, notice: notice
end

Expand All @@ -80,10 +71,4 @@ def clear_pending_login
session.delete(:pending_login_email)
session.delete(:pending_login_sent_at)
end

def bypass_login(user)
reset_session
session[:user_id] = user.id
redirect_to home_path, notice: 'Zalogowano pomyślnie.'
end
end
6 changes: 2 additions & 4 deletions app/controllers/auth/usos_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
class Auth::UsosController < ApplicationController
class InvalidProviderError < StandardError; end

include Auth::SignIn
include LoggedUserRedirector

skip_before_action :authenticate!
Expand All @@ -13,10 +14,7 @@ def create
builder = SessionUserBuilder.new(provider)
user = builder.build

reset_session
session[:user_id] = user.id

redirect_to home_path
sign_in_and_redirect(user)
rescue InvalidProviderError
redirect_to root_path, alert: 'Nieprawidłowy dostawca logowania.'
rescue Providers::InvalidAuthError
Expand Down
25 changes: 25 additions & 0 deletions app/controllers/concerns/auth/sign_in.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

module Auth::SignIn
extend ActiveSupport::Concern

private

def sign_in_and_redirect(user)
return_to = session[:return_to]
reset_session
session[:user_id] = user.id

redirect_to after_login_path(return_to), notice: 'Zalogowano pomyślnie.'
end

def after_login_path(path)
return path if local_path?(path)

home_path
end

def local_path?(path)
path&.start_with?('/') && !path.start_with?('//')
end
end
33 changes: 33 additions & 0 deletions app/controllers/concerns/authentication.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

module ::Authentication
extend ::ActiveSupport::Concern

included do
before_action :set_current_user
before_action :authenticate!
end

protected

attr_reader :current_user

def set_current_user
@current_user = User.find_by(id: session[:user_id])
end

def authenticate!
return if @current_user

store_return_path
redirect_to login_path, alert: 'Zaloguj się, aby kontynuować.'
end

def store_return_path
session[:return_to] = request.fullpath if full_page_navigation?
end

def full_page_navigation?
request.get? && !request.xhr? && !turbo_frame_request?
end
end
15 changes: 15 additions & 0 deletions app/controllers/concerns/sidebar.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module Sidebar
extend ActiveSupport::Concern

included do
before_action :set_sidebar_state
end

protected

def set_sidebar_state
@sidebar_collapsed = cookies[:sidebar_collapsed] == 'true'
end
end
13 changes: 13 additions & 0 deletions app/controllers/concerns/status_redirects.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

module StatusRedirects
protected

def record_not_found
redirect_to root_path, alert: 'Nie znaleziono.'
end

def not_authorized
redirect_to root_path, alert: 'Nie znaleziono.'
end
end
26 changes: 10 additions & 16 deletions app/controllers/join_controller.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
# frozen_string_literal: true

class JoinController < ApplicationController
# Inside the dialog only the frame is used, and the layout already carries a
# <turbo-frame id="modal"> of its own. Rendering it too would put two frames
# with the same id in one response and let Turbo pick whichever came first.

before_action :set_presentation

# Step 1.
def new; end

# Step 1 submitted. A bad code falls back to step 1 carrying its reason.
def lookup
@code = params[:code]
@lookup = InviteLookup.new(user: current_user, code: @code).call
Expand All @@ -21,8 +15,6 @@ def lookup
render :show
end

# The QR landing page. Same step, same failure handling — only the wrapper
# differs, and `set_presentation` has already chosen it.
def show
@code = params[:code]
@lookup = InviteLookup.new(user: current_user, code: @code).call
Expand All @@ -32,19 +24,25 @@ def show
@story_group = @lookup.story_group
end

# Step 2 submitted.
def create
@code = params[:code]
@lookup = InviteLookup.new(user: current_user, code: @code).call

return render :new, status: :unprocessable_content unless @lookup.ok?

accept
accept!
end

def done
@story_group = current_user.student_story_groups.find(params[:story_group_id])
@membership = current_user.student_memberships.find_by!(story_group: @story_group)

render :create
end

private

def accept
def accept!
service = AcceptInviteService.new(
user: current_user,
invite: @lookup.invite,
Expand All @@ -56,13 +54,9 @@ def accept

return render :show, status: :unprocessable_content unless @result.success?

@membership = @result.membership
redirect_to done_join_index_path(story_group_id: @story_group.id)
end

# A step renders inside the layout's `modal` dialog when it was requested
# from one, and as a focused, navigation-less page otherwise — which is how
# the QR link lands. The branch mirrors NotificationsController's `panel`
# check.
def set_presentation
@in_modal = turbo_frame_request_id == 'modal'
@chrome = false unless @in_modal
Expand Down
1 change: 0 additions & 1 deletion app/controllers/ranking_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
class RankingController < ApplicationController
include StoryGroupAuthorization


before_action :set_story_group
before_action :set_presentation, only: %i[confirm_visibility confirm_mode]
before_action :authorize_story_group_manage!, except: :show
Expand Down
Loading
Loading