less scary consent screen for HQ stuff! (#208)

This commit is contained in:
nora 2026-03-24 16:22:52 -04:00 committed by GitHub
parent e007096005
commit aba5b912e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 193 additions and 22 deletions

View file

@ -153,6 +153,10 @@ class DeveloperAppsController < ApplicationController
permitted << :trust_level permitted << :trust_level
end end
if policy(@app || Program.new).update_byline?
permitted << :byline
end
if policy(@app || Program.new).update_onboarding_scenario? if policy(@app || Program.new).update_onboarding_scenario?
permitted << :onboarding_scenario permitted << :onboarding_scenario
end end

View file

@ -3,12 +3,18 @@
.oauth-consent { .oauth-consent {
max-width: 580px; max-width: 580px;
.oauth-official-byline {
font-size: 0.875rem;
color: var(--text-muted-strong);
text-align: center;
}
.oauth-permissions { .oauth-permissions {
margin: $space-6 0; margin: $space-6 0;
fieldset { fieldset {
margin: 0; margin: 0;
legend { legend {
font-size: 0.875rem; font-size: 0.875rem;
font-weight: 600; font-weight: 600;
@ -18,6 +24,43 @@
margin-bottom: 1rem; margin-bottom: 1rem;
} }
} }
// collapsed variant for hq official apps
&[open] summary {
margin-bottom: 1rem;
}
> summary {
font-size: 0.8125rem;
font-weight: 500;
color: var(--text-muted-strong);
cursor: pointer;
display: flex;
align-items: center;
gap: 0.375rem;
list-style-type: none;
&::-webkit-details-marker,
&::marker {
display: none;
content: none;
}
&::after {
display: none !important;
}
&::before {
content: "";
font-size: 1rem;
line-height: 1;
transition: transform 0.15s ease;
}
}
&[open] summary::before {
transform: rotate(90deg);
}
} }
.permissions-list { .permissions-list {

View file

@ -33,6 +33,7 @@ class Program < ApplicationRecord
audit_field :scopes_array, type: :array, label: "scopes" audit_field :scopes_array, type: :array, label: "scopes"
audit_field :redirect_uris, type: :array, label: "redirect URIs" audit_field :redirect_uris, type: :array, label: "redirect URIs"
audit_field :active, type: :boolean audit_field :active, type: :boolean
audit_field :byline
audit_field :onboarding_scenario, transform: ->(v) { v&.titleize } audit_field :onboarding_scenario, transform: ->(v) { v&.titleize }
COLLABORATOR_ACTIVITY_KEYS = %w[ COLLABORATOR_ACTIVITY_KEYS = %w[

View file

@ -56,6 +56,10 @@ class ProgramPolicy < ApplicationPolicy
end end
end end
def update_byline?
user.can_hq_officialize? || admin?
end
def update_onboarding_scenario? def update_onboarding_scenario?
super_admin? super_admin?
end end

View file

@ -114,9 +114,16 @@
</div> </div>
<%# === Card 4: Admin Settings (if applicable) === %> <%# === Card 4: Admin Settings (if applicable) === %>
<% if policy(@app).update_onboarding_scenario? || policy(@app).update_active? %> <% if policy(@app).update_byline? || policy(@app).update_onboarding_scenario? || policy(@app).update_active? %>
<section class="section-card"> <section class="section-card">
<h3><%= t(".admin_settings_heading", default: "Admin Settings") %></h3> <h3><%= t(".admin_settings_heading", default: "Admin Settings") %></h3>
<% if policy(@app).update_byline? %>
<div class="field-group">
<%= f.label :byline, t(".byline") %>
<%= f.text_field :byline, placeholder: t(".byline_placeholder") %>
<small class="usn"><%= t(".byline_hint") %></small>
</div>
<% end %>
<% if policy(@app).update_onboarding_scenario? %> <% if policy(@app).update_onboarding_scenario? %>
<div class="field-group"> <div class="field-group">
<%= f.label :onboarding_scenario, t(".onboarding_scenario") %> <%= f.label :onboarding_scenario, t(".onboarding_scenario") %>

View file

@ -22,6 +22,12 @@
</small> </small>
</header> </header>
<% if application&.hq_official? %>
<div class="oauth-official-byline">
<%= t('.official_byline', byline: application.byline.presence || t('.official_byline_default')) %>
</div>
<% end %>
<% if @pre_auth.scopes.count > 0 %> <% if @pre_auth.scopes.count > 0 %>
<% <%
scopes = @pre_auth.scopes.map(&:to_s) scopes = @pre_auth.scopes.map(&:to_s)
@ -31,7 +37,7 @@
%> %>
<% if scope_data.any? || unknown_scopes.any? %> <% if scope_data.any? || unknown_scopes.any? %>
<div class="oauth-permissions"> <% permissions_content = capture do %>
<fieldset> <fieldset>
<legend><%= t('.able_to') %></legend> <legend><%= t('.able_to') %></legend>
<ul class="permissions-list"> <ul class="permissions-list">
@ -60,11 +66,22 @@
<% end %> <% end %>
</ul> </ul>
</fieldset> </fieldset>
</div> <% end %>
<% if application&.hq_official? %>
<details class="oauth-permissions">
<summary><%= t('.permissions_details') %></summary>
<%= permissions_content %>
</details>
<% else %>
<div class="oauth-permissions">
<%= permissions_content %>
</div>
<% end %>
<% end %> <% end %>
<% end %> <% end %>
<% auth_label = t('.authorize') %> <% auth_label = application&.hq_official? ? t('.authorize_official') : t('.authorize') %>
<div class="oauth-actions" x-data="{ countdown: <%= application&.hq_official? ? 0 : 3 %>, ready: <%= application&.hq_official? ? true : false %>, authLabel: '<%= auth_label.gsub("'", "\\'") %>' }" x-init="if (countdown > 0) { let interval = setInterval(() => { countdown--; if (countdown <= 0) { ready = true; clearInterval(interval); } }, 1000); }"> <div class="oauth-actions" x-data="{ countdown: <%= application&.hq_official? ? 0 : 3 %>, ready: <%= application&.hq_official? ? true : false %>, authLabel: '<%= auth_label.gsub("'", "\\'") %>' }" x-init="if (countdown > 0) { let interval = setInterval(() => { countdown--; if (countdown <= 0) { ready = true; clearInterval(interval); } }, 1000); }">
<%= form_tag oauth_authorization_path, method: :delete do %> <%= form_tag oauth_authorization_path, method: :delete do %>
<%= hidden_field_tag :client_id, @pre_auth.client.uid, id: nil %> <%= hidden_field_tag :client_id, @pre_auth.client.uid, id: nil %>

View file

@ -260,6 +260,9 @@ en:
scopes_not_valid_for_trust_level: not valid for this trust level. scopes_not_valid_for_trust_level: not valid for this trust level.
scopes_locked_by_higher_permission: "Managed by a higher-permission user:" scopes_locked_by_higher_permission: "Managed by a higher-permission user:"
admin_settings_heading: Admin Settings admin_settings_heading: Admin Settings
byline: Consent Screen Byline
byline_hint: Shown on the consent screen for HQ official apps. Leave blank to default to "Hack Club HQ".
byline_placeholder: "@nora, @msw, @zrl"
onboarding_scenario: Onboarding Scenario onboarding_scenario: Onboarding Scenario
onboarding_default: "(default)" onboarding_default: "(default)"
onboarding_scenario_hint: Users signing up through this OAuth app will use this onboarding flow onboarding_scenario_hint: Users signing up through this OAuth app will use this onboarding flow
@ -329,8 +332,12 @@ en:
new: new:
prompt: Authorize %{client_name} to use your account? prompt: Authorize %{client_name} to use your account?
able_to: "This application will be able to:" able_to: "This application will be able to:"
official_byline: "This is an official Hack Club program run by %{byline}."
official_byline_default: Hack Club HQ
permissions_details: What data will be shared?
deny: Deny deny: Deny
authorize: Authorize → authorize: Authorize →
authorize_official: Okay! →
banners: banners:
community_untrusted: community_untrusted:
title: Community Application title: Community Application

52
db/analytics_schema.rb Normal file
View file

@ -0,0 +1,52 @@
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2026_01_12_000002) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
create_table "ahoy_events", force: :cascade do |t|
t.bigint "visit_id"
t.string "name"
t.jsonb "properties"
t.datetime "time"
t.index ["name", "time"], name: "index_ahoy_events_on_name_and_time"
t.index ["name"], name: "index_ahoy_events_on_name"
t.index ["properties"], name: "index_ahoy_events_on_properties", using: :gin
t.index ["time"], name: "index_ahoy_events_on_time"
t.index ["visit_id"], name: "index_ahoy_events_on_visit_id"
end
create_table "ahoy_visits", force: :cascade do |t|
t.string "visit_token"
t.string "visitor_token"
t.string "ip"
t.text "user_agent"
t.text "referrer"
t.string "referring_domain"
t.text "landing_page"
t.string "browser"
t.string "os"
t.string "device_type"
t.string "utm_source"
t.string "utm_medium"
t.string "utm_campaign"
t.string "utm_term"
t.string "utm_content"
t.datetime "started_at"
t.index ["started_at"], name: "index_ahoy_visits_on_started_at"
t.index ["visit_token"], name: "index_ahoy_visits_on_visit_token", unique: true
t.index ["visitor_token"], name: "index_ahoy_visits_on_visitor_token"
end
add_foreign_key "ahoy_events", "ahoy_visits", column: "visit_id"
end

View file

@ -0,0 +1,5 @@
class AddBylineToOAuthApplications < ActiveRecord::Migration[8.0]
def change
add_column :oauth_applications, :byline, :string
end
end

19
db/schema.rb generated
View file

@ -10,9 +10,10 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2026_03_02_000002) do ActiveRecord::Schema[8.0].define(version: 2026_03_24_000001) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql" enable_extension "pg_catalog.plpgsql"
enable_extension "pg_trgm"
enable_extension "pgcrypto" enable_extension "pgcrypto"
create_table "active_storage_attachments", force: :cascade do |t| create_table "active_storage_attachments", force: :cascade do |t|
@ -301,7 +302,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_03_02_000002) do
t.boolean "saml_debug" t.boolean "saml_debug"
t.boolean "is_in_workspace", default: false, null: false t.boolean "is_in_workspace", default: false, null: false
t.string "slack_dm_channel_id" t.string "slack_dm_channel_id"
t.string "webauthn_id"
t.boolean "is_alum", default: false t.boolean "is_alum", default: false
t.boolean "can_hq_officialize", default: false, null: false t.boolean "can_hq_officialize", default: false, null: false
t.index "lower((primary_email)::text)", name: "idx_identities_unique_primary_email", unique: true, where: "(deleted_at IS NULL)" t.index "lower((primary_email)::text)", name: "idx_identities_unique_primary_email", unique: true, where: "(deleted_at IS NULL)"
@ -447,6 +447,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_03_02_000002) do
t.integer "sign_count" t.integer "sign_count"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.datetime "compromised_at"
t.index ["external_id"], name: "index_identity_webauthn_credentials_on_external_id", unique: true t.index ["external_id"], name: "index_identity_webauthn_credentials_on_external_id", unique: true
t.index ["identity_id"], name: "index_identity_webauthn_credentials_on_identity_id" t.index ["identity_id"], name: "index_identity_webauthn_credentials_on_identity_id"
end end
@ -516,6 +517,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_03_02_000002) do
t.integer "trust_level", default: 0, null: false t.integer "trust_level", default: 0, null: false
t.bigint "owner_identity_id" t.bigint "owner_identity_id"
t.string "onboarding_scenario" t.string "onboarding_scenario"
t.string "byline"
t.index ["owner_identity_id"], name: "index_oauth_applications_on_owner_identity_id" t.index ["owner_identity_id"], name: "index_oauth_applications_on_owner_identity_id"
t.index ["program_key_bidx"], name: "index_oauth_applications_on_program_key_bidx", unique: true t.index ["program_key_bidx"], name: "index_oauth_applications_on_program_key_bidx", unique: true
t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true
@ -602,18 +604,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_03_02_000002) do
t.index ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id" t.index ["item_type", "item_id"], name: "index_versions_on_item_type_and_item_id"
end end
create_table "webauthn_credentials", force: :cascade do |t|
t.bigint "identity_id", null: false
t.string "external_id", null: false
t.string "public_key", null: false
t.string "nickname", null: false
t.integer "sign_count", default: 0, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["external_id"], name: "index_webauthn_credentials_on_external_id", unique: true
t.index ["identity_id"], name: "index_webauthn_credentials_on_identity_id"
end
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "addresses", "identities" add_foreign_key "addresses", "identities"
@ -648,5 +638,4 @@ ActiveRecord::Schema[8.0].define(version: 2026_03_02_000002) do
add_foreign_key "verifications", "identities" add_foreign_key "verifications", "identities"
add_foreign_key "verifications", "identity_aadhaar_records", column: "aadhaar_record_id" add_foreign_key "verifications", "identity_aadhaar_records", column: "aadhaar_record_id"
add_foreign_key "verifications", "identity_documents" add_foreign_key "verifications", "identity_documents"
add_foreign_key "webauthn_credentials", "identities"
end end

View file

@ -47,7 +47,7 @@ RSpec.describe "doorkeeper/authorizations/new", type: :view do
it "shows verification status for verification_status scope" do it "shows verification status for verification_status scope" do
allow(pre_auth).to receive(:scopes).and_return(Doorkeeper::OAuth::Scopes.from_string("verification_status")) allow(pre_auth).to receive(:scopes).and_return(Doorkeeper::OAuth::Scopes.from_string("verification_status"))
render render
expect(rendered).to include(identity.verification_status) expect(rendered).to include(identity.verification_status.humanize)
expect(rendered).to include("YSWS eligible") expect(rendered).to include("YSWS eligible")
end end
@ -86,6 +86,48 @@ RSpec.describe "doorkeeper/authorizations/new", type: :view do
end end
end end
describe "hq official byline" do
it "shows default byline for hq_official apps without a custom byline" do
allow(program).to receive(:hq_official?).and_return(true)
allow(program).to receive(:byline).and_return(nil)
render
expect(rendered).to include("official Hack Club program")
expect(rendered).to include("Hack Club HQ")
end
it "shows custom byline when set" do
allow(program).to receive(:hq_official?).and_return(true)
allow(program).to receive(:byline).and_return("@nora, @msw, @zrl")
render
expect(rendered).to include("@nora, @msw, @zrl")
end
it "does not show byline for community apps" do
allow(program).to receive(:hq_official?).and_return(false)
allow(program).to receive(:trust_level).and_return("community_untrusted")
render
expect(rendered).not_to include("official Hack Club program")
end
end
describe "permissions display" do
it "collapses permissions behind details for hq_official apps" do
allow(program).to receive(:hq_official?).and_return(true)
allow(program).to receive(:byline).and_return(nil)
render
expect(rendered).to include("<details")
expect(rendered).to include("What data will be shared?")
end
it "shows permissions directly for non-official apps" do
allow(program).to receive(:hq_official?).and_return(false)
allow(program).to receive(:trust_level).and_return("community_untrusted")
render
expect(rendered).not_to include("<details")
expect(rendered).to include("oauth-permissions")
end
end
describe "trust level banners" do describe "trust level banners" do
it "shows warning banner for community_untrusted apps" do it "shows warning banner for community_untrusted apps" do
allow(program).to receive(:trust_level).and_return("community_untrusted") allow(program).to receive(:trust_level).and_return("community_untrusted")