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: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@
.codex-tmp
.idea
tmp

# Ignore key files for decrypting credentials and more.
/config/credentials/*.key

177 changes: 177 additions & 0 deletions app/assets/stylesheets/application.css
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,183 @@ body {
outline-offset: 2px;
}

.auth-page {
align-items: start;
display: grid;
justify-items: center;
min-height: calc(100vh - 4.75rem);
padding: clamp(2rem, 7vw, 5rem) clamp(1rem, 4vw, 2rem);
}

.auth-page__panel {
background:
linear-gradient(145deg, rgba(245, 241, 232, 0.075), rgba(245, 241, 232, 0.025));
border: 1px solid rgba(229, 184, 91, 0.32);
border-radius: 0.75rem;
box-shadow: 0 1.5rem 3.5rem rgba(0, 0, 0, 0.32);
max-width: 34rem;
overflow-wrap: anywhere;
padding: clamp(1.5rem, 5vw, 3rem);
width: 100%;
}

.auth-page__heading {
color: #f5f1e8;
font-size: clamp(2.5rem, 8vw, 4.5rem);
letter-spacing: -0.035em;
line-height: 1;
margin: 0 0 2rem;
}

.auth-form {
display: grid;
gap: 1.25rem;
}

.auth-form__field {
display: grid;
gap: 0.45rem;
}

.auth-form__field br {
display: none;
}

.auth-form__field label,
.auth-form__remember label {
color: #e5b85b;
font-size: 0.9rem;
font-weight: 700;
}

.auth-form__field label {
letter-spacing: 0.04em;
text-transform: uppercase;
}

.auth-form__input {
appearance: none;
background: #f5f1e8;
border: 1px solid rgba(25, 23, 20, 0.28);
border-radius: 0.375rem;
color: #191714;
font: inherit;
min-height: 3rem;
padding: 0.75rem;
width: 100%;
}

.auth-form__input::placeholder {
color: rgba(25, 23, 20, 0.62);
opacity: 1;
}

.auth-form__input:-webkit-autofill,
.auth-form__input:-webkit-autofill:hover,
.auth-form__input:-webkit-autofill:focus {
-webkit-text-fill-color: #191714;
box-shadow: 0 0 0 1000px #f5f1e8 inset;
caret-color: #191714;
}

.auth-form__hint {
color: rgba(245, 241, 232, 0.72);
line-height: 1.4;
}

.auth-form__remember {
align-items: center;
display: flex;
gap: 0.65rem;
}

.auth-form__checkbox {
accent-color: #e5b85b;
cursor: pointer;
height: 1.2rem;
margin: 0;
width: 1.2rem;
}

.auth-form__submit {
appearance: none;
background: #e5b85b;
border: 1px solid rgba(229, 184, 91, 0.74);
border-radius: 0.375rem;
color: #191714;
cursor: pointer;
font: inherit;
font-weight: 800;
min-height: 3rem;
padding: 0.75rem 1rem;
width: 100%;
}

.auth-form__submit:hover {
background: #f5d284;
}

.auth-page__alternate {
color: rgba(245, 241, 232, 0.78);
margin: 1.5rem 0 0;
text-align: center;
}

.auth-page__link {
color: #e5b85b;
font-weight: 700;
text-decoration-thickness: 0.08em;
text-underline-offset: 0.18em;
}

.auth-page__link:hover {
color: #f5d284;
}

.auth-form__input:focus-visible,
.auth-form__checkbox:focus-visible,
.auth-form__submit:focus-visible,
.auth-page__link:focus-visible {
outline: 3px solid #f5d284;
outline-offset: 3px;
}

.auth-errors {
background: rgba(240, 138, 115, 0.08);
border-left: 0.25rem solid #f08a73;
color: #f5f1e8;
margin: 0 0 1.5rem;
overflow-wrap: anywhere;
padding: 0.9rem 1rem;
}

.auth-errors__heading {
font-size: 1rem;
line-height: 1.4;
margin: 0 0 0.5rem;
}

.auth-errors__list {
line-height: 1.5;
margin: 0;
padding-left: 1.2rem;
}

@media (max-width: 40rem) {
.auth-page {
min-height: auto;
padding: 1.25rem 1rem 6rem;
}

.auth-page__panel {
padding: 1.5rem;
}

.auth-page__heading {
margin-bottom: 1.5rem;
}
}

.status-page {
display: grid;
min-height: calc(100vh - 4.75rem);
Expand Down
54 changes: 28 additions & 26 deletions app/views/devise/registrations/new.html.erb
Original file line number Diff line number Diff line change
@@ -1,34 +1,36 @@
<main class="page-shell">
<h1>Sign up</h1>
<main class="page-shell auth-page">
<div class="auth-page__panel">
<h1 class="auth-page__heading">Sign up</h1>

<%= render "devise/shared/error_messages", resource: resource %>
<%= render "devise/shared/error_messages", resource: resource %>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<div>
<%= f.label :email %><br>
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
</div>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { class: "auth-form" }) do |f| %>
<div class="auth-form__field">
<%= f.label :email %><br>
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: "auth-form__input" %>
</div>

<div>
<%= f.label :password %><br>
<%= f.password_field :password, autocomplete: "new-password" %>
<% if @minimum_password_length %>
<small><%= @minimum_password_length %> characters minimum</small>
<% end %>
</div>
<div class="auth-form__field">
<%= f.label :password %><br>
<%= f.password_field :password, autocomplete: "new-password", class: "auth-form__input" %>
<% if @minimum_password_length %>
<small class="auth-form__hint"><%= @minimum_password_length %> characters minimum</small>
<% end %>
</div>

<div>
<%= f.label :password_confirmation %><br>
<%= f.password_field :password_confirmation, autocomplete: "new-password" %>
</div>
<div class="auth-form__field">
<%= f.label :password_confirmation %><br>
<%= f.password_field :password_confirmation, autocomplete: "new-password", class: "auth-form__input" %>
</div>

<div>
<%= f.label :preferred_name, "Preferred name" %><br>
<%= f.text_field :preferred_name, autocomplete: "name" %>
</div>
<div class="auth-form__field">
<%= f.label :preferred_name, "Preferred name" %><br>
<%= f.text_field :preferred_name, autocomplete: "name", class: "auth-form__input" %>
</div>

<%= f.submit "Sign up" %>
<% end %>
<%= f.submit "Sign up", class: "auth-form__submit" %>
<% end %>

<p><%= link_to "Log in", new_session_path(resource_name) %></p>
<p class="auth-page__alternate"><%= link_to "Log in", new_session_path(resource_name), class: "auth-page__link" %></p>
</div>
</main>
40 changes: 21 additions & 19 deletions app/views/devise/sessions/new.html.erb
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
<main class="page-shell">
<h1>Log in</h1>
<main class="page-shell auth-page">
<div class="auth-page__panel">
<h1 class="auth-page__heading">Log in</h1>

<%= render "devise/shared/error_messages", resource: resource %>
<%= render "devise/shared/error_messages", resource: resource %>

<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
<div>
<%= f.label :email %><br>
<%= f.email_field :email, autofocus: true, autocomplete: "email" %>
</div>
<%= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: "auth-form" }) do |f| %>
<div class="auth-form__field">
<%= f.label :email %><br>
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: "auth-form__input" %>
</div>

<div>
<%= f.label :password %><br>
<%= f.password_field :password, autocomplete: "current-password" %>
</div>
<div class="auth-form__field">
<%= f.label :password %><br>
<%= f.password_field :password, autocomplete: "current-password", class: "auth-form__input" %>
</div>

<div>
<%= f.check_box :remember_me %>
<%= f.label :remember_me, "Remember me" %>
</div>
<div class="auth-form__remember">
<%= f.check_box :remember_me, class: "auth-form__checkbox" %>
<%= f.label :remember_me, "Remember me" %>
</div>

<%= f.submit "Log in" %>
<% end %>
<%= f.submit "Log in", class: "auth-form__submit" %>
<% end %>

<p><%= link_to "Sign up", new_registration_path(resource_name) %></p>
<p class="auth-page__alternate"><%= link_to "Sign up", new_registration_path(resource_name), class: "auth-page__link" %></p>
</div>
</main>
6 changes: 3 additions & 3 deletions app/views/devise/shared/_error_messages.html.erb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<% if resource.errors.any? %>
<div id="error_explanation" role="alert">
<h2><%= pluralize(resource.errors.count, "error") %> prevented this account from being saved:</h2>
<ul>
<div id="error_explanation" class="auth-errors" role="alert">
<h2 class="auth-errors__heading"><%= pluralize(resource.errors.count, "error") %> prevented this account from being saved:</h2>
<ul class="auth-errors__list">
<% resource.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
Expand Down
1 change: 1 addition & 0 deletions config/credentials/development.yml.enc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
HofK3YYGZp23CUeV9ZZ3zU2eRYlxyFGxxqnaU94Ckn/47SwwOMdSmkx09TW8xTP1laOHo0l7rVPQQyYHUiOncdRFTacpJXNntvaJV60ok4NTSgI5t9nrttU0PHo8j6R2QQbV+ELDNEIBKoucMuZmiCsaQTmGdePttG4X4TLM+yosn8A=--M9oSEULyeP4dmx8x--EMpX4PFTrS/i0BB+3YjTXA==
57 changes: 50 additions & 7 deletions spec/requests/local_authentication_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,55 @@

let(:password) { "password123" }

it "shows standard sign-in and sign-up pages" do
it "renders the sign-in form with scoped structure and preserved semantics" do
get "/users/sign_in"

expect(response).to have_http_status(:ok)
expect(response.body).to include("Log in")
expect(response.body).to include("Remember me")
page = Nokogiri::HTML(response.body)
panel = page.at_css("main.page-shell.auth-page > div.auth-page__panel")

expect(panel).to be_present

form = panel.at_css('form.auth-form[action="/users/sign_in"][method="post"]')

expect(form).to be_present

expect(panel.at_css("h1.auth-page__heading")&.text).to eq("Log in")
expect(form.at_css('div.auth-form__field label[for="user_email"]')&.text).to eq("Email")
expect(form.at_css('input.auth-form__input[name="user[email]"][type="email"][autofocus][autocomplete="email"]')).to be_present
expect(form.at_css('div.auth-form__field label[for="user_password"]')&.text).to eq("Password")
expect(form.at_css('input.auth-form__input[name="user[password]"][type="password"][autocomplete="current-password"]')).to be_present
expect(form.at_css('div.auth-form__remember input.auth-form__checkbox[name="user[remember_me]"][type="checkbox"]')).to be_present
expect(form.at_css('div.auth-form__remember label[for="user_remember_me"]')&.text).to eq("Remember me")
expect(form.at_css('input.auth-form__submit[type="submit"][value="Log in"]')).to be_present
expect(panel.at_css('p.auth-page__alternate a.auth-page__link[href="/users/sign_up"]')&.text).to eq("Sign up")
end

it "renders the sign-up form with scoped structure and preserved semantics" do
get "/users/sign_up"

expect(response).to have_http_status(:ok)
expect(response.body).to include("Sign up")
expect(response.body).to include("Preferred name")
page = Nokogiri::HTML(response.body)
panel = page.at_css("main.page-shell.auth-page > div.auth-page__panel")

expect(panel).to be_present

form = panel.at_css('form.auth-form[action="/users"][method="post"]')

expect(form).to be_present

expect(panel.at_css("h1.auth-page__heading")&.text).to eq("Sign up")
expect(form.at_css('div.auth-form__field label[for="user_email"]')&.text).to eq("Email")
expect(form.at_css('input.auth-form__input[name="user[email]"][type="email"][autofocus][autocomplete="email"]')).to be_present
expect(form.at_css('div.auth-form__field label[for="user_password"]')&.text).to eq("Password")
expect(form.at_css('input.auth-form__input[name="user[password]"][type="password"][autocomplete="new-password"]')).to be_present
expect(form.at_css("small.auth-form__hint")&.text).to include("characters minimum")
expect(form.at_css('div.auth-form__field label[for="user_password_confirmation"]')&.text).to eq("Password confirmation")
expect(form.at_css('input.auth-form__input[name="user[password_confirmation]"][type="password"][autocomplete="new-password"]')).to be_present
expect(form.at_css('div.auth-form__field label[for="user_preferred_name"]')&.text).to eq("Preferred name")
expect(form.at_css('input.auth-form__input[name="user[preferred_name]"][type="text"][autocomplete="name"]')).to be_present
expect(form.at_css('input.auth-form__submit[type="submit"][value="Sign up"]')).to be_present
expect(panel.at_css('p.auth-page__alternate a.auth-page__link[href="/users/sign_in"]')&.text).to eq("Log in")
end

it "creates one user and profile with a normalized email and secure password" do
Expand Down Expand Up @@ -58,8 +95,14 @@
expect(Profile.count).to eq(0)
expect(response).to have_http_status(:unprocessable_entity)
page = Nokogiri::HTML(response.body)
expect(page.text).to include("Password confirmation doesn't match Password")
expect(page.text).to include("Preferred name can't be blank")
errors = page.at_css('div#error_explanation.auth-errors[role="alert"]')

expect(errors).to be_present

expect(errors.at_css("h2.auth-errors__heading")&.text).to include("prevented this account from being saved")
messages = errors.css("ul.auth-errors__list > li").map { |item| item.text.strip }
expect(messages).to include("Password confirmation doesn't match Password")
expect(messages).to include("Preferred name can't be blank")
end

it "rejects malformed email and short passwords without persisting anything" do
Expand Down
Loading