diff --git a/Gemfile b/Gemfile
index 681eed93cee..6a32409b2a6 100644
--- a/Gemfile
+++ b/Gemfile
@@ -18,7 +18,6 @@ gem "gravtastic", "~> 3.2"
gem "high_voltage", "~> 3.1"
gem "honeybadger", "~> 5.4"
gem "http_accept_language", "~> 2.1"
-gem "jquery-rails", "~> 4.5"
gem "kaminari", "~> 1.2"
gem "launchdarkly-server-sdk", "~> 8.1"
gem "mail", "~> 2.8"
@@ -44,6 +43,7 @@ gem "faraday_middleware-aws-sigv4", "~> 1.0"
gem "xml-simple", "~> 1.1"
gem "compact_index", "~> 0.15.0"
gem "sprockets-rails", "~> 3.4"
+gem "importmap-rails", "~> 2.0"
gem "rack-attack", "~> 6.6"
gem "rqrcode", "~> 2.1"
gem "rotp", "~> 6.2"
@@ -76,6 +76,7 @@ group :assets, :development do
end
group :assets do
+ gem "stimulus-rails", "~> 1.3"
gem "dartsass-sprockets", "~> 3.1"
gem "terser", "~> 1.2"
gem "autoprefixer-rails", "~> 10.4"
diff --git a/Gemfile.lock b/Gemfile.lock
index b43d512a941..f387de08bc4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -285,6 +285,10 @@ GEM
multi_xml (>= 0.5.2)
i18n (1.14.1)
concurrent-ruby (~> 1.0)
+ importmap-rails (2.0.1)
+ actionpack (>= 6.0.0)
+ activesupport (>= 6.0.0)
+ railties (>= 6.0.0)
inline_svg (1.9.0)
activesupport (>= 3.0)
nokogiri (>= 1.6)
@@ -295,10 +299,6 @@ GEM
jmespath (1.6.2)
job-iteration (1.4.1)
activejob (>= 5.2)
- jquery-rails (4.6.0)
- rails-dom-testing (>= 1, < 3)
- railties (>= 4.2.0)
- thor (>= 0.14, < 2.0)
json (2.7.1)
json-jwt (1.16.5)
activesupport (>= 4.2)
@@ -655,6 +655,8 @@ GEM
activesupport (>= 5.2)
sprockets (>= 3.0.0)
statsd-instrument (3.6.1)
+ stimulus-rails (1.3.3)
+ railties (>= 6.0.0)
stringio (3.1.0)
strong_migrations (1.7.0)
activerecord (>= 5.2)
@@ -762,7 +764,7 @@ DEPENDENCIES
high_voltage (~> 3.1)
honeybadger (~> 5.4)
http_accept_language (~> 2.1)
- jquery-rails (~> 4.5)
+ importmap-rails (~> 2.0)
kaminari (~> 1.2)
launchdarkly-server-sdk (~> 8.1)
launchy (~> 2.5)
@@ -819,6 +821,7 @@ DEPENDENCIES
simplecov-cobertura (~> 2.1)
sprockets-rails (~> 3.4)
statsd-instrument (~> 3.5)
+ stimulus-rails (~> 1.3)
strong_migrations (~> 1.7)
tailwindcss-rails (~> 2.3)
terser (~> 1.2)
@@ -937,12 +940,12 @@ CHECKSUMS
http_accept_language (2.1.1) sha256=0043f0d55a148cf45b604dbdd197cb36437133e990016c68c892d49dbea31634
httparty (0.21.0) sha256=00ef7bf9a71f30a3bff88edeb5b16a34bea883ab67c246b3f0db2d6794fe1214
i18n (1.14.1) sha256=9d03698903547c060928e70a9bc8b6b87fda674453cda918fc7ab80235ae4a61
+ importmap-rails (2.0.1) sha256=e739a6e70c09f797688c6983fa79567ec1edc9becc30d55b3f7cc897b1825586
inline_svg (1.9.0) sha256=f44c5e3d2e401fd619ad3047b7c8cee384517d855edb1d1fb1a248d3cae535d6
io-console (0.7.2) sha256=f0dccff252f877a4f60d04a4dc6b442b185ebffb4b320ab69212a92b48a7a221
irb (1.11.1) sha256=0700d626c92f7d47d12e73932ddb0c11b73c1f00608f5eae78dbc44968690842
jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1
job-iteration (1.4.1) sha256=7243c40e4decc3d49529867e9c504afaea332976c967ffdebed9ff863c6424af
- jquery-rails (4.6.0) sha256=3c4e6bf47274340b44d836b8aa1b5472c6d451e2739af5ec094421f39025a7e2
json (2.7.1) sha256=187ea312fb58420ff0c40f40af1862651d4295c8675267c6a1c353f1a0ac3265
json-jwt (1.16.5) sha256=c899d6d9c6892e1ed8e423ac153837d4ca4f7069777342f0e3a3398482f309fb
jwt (2.7.1) sha256=07357cd2f180739b2f8184eda969e252d850ac996ed0a23f616e8ff0a90ae19b
@@ -1077,6 +1080,7 @@ CHECKSUMS
sprockets (4.2.1) sha256=951b13dd2f2fcae840a7184722689a803e0ff9d2702d902bd844b196da773f97
sprockets-rails (3.4.2) sha256=36d6327757ccf7460a00d1d52b2d5ef0019a4670503046a129fa1fb1300931ad
statsd-instrument (3.6.1) sha256=fdaf73665c9a4d99aeddcda2e70fc266935919225dc0bf01257234f59f8f55df
+ stimulus-rails (1.3.3) sha256=4d1f9ab1d64e605f4c9cdd4cc530a9538b510606d32d02249d106256845c562c
stringio (3.1.0) sha256=c1f6263ae03a15025e51194ab19b06b15e06adcaaedb7f5f6c06ab60f5d67718
strong_migrations (1.7.0) sha256=c60e164ef24a80db2b5c40353a8f9225a636e8ab52be42a47cd1923b88c9829c
swd (2.0.3) sha256=4cdbe2a4246c19f093fce22e967ec3ebdd4657d37673672e621bf0c7eb770655
diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js
index 996515ed84c..4c8a20fc3a8 100644
--- a/app/assets/config/manifest.js
+++ b/app/assets/config/manifest.js
@@ -2,3 +2,5 @@
//= link application.js
//= link_tree ../../../vendor/assets/images
//= link_tree ../builds
+//= link_tree ../../javascript .js
+//= link_tree ../../../vendor/javascript .js
diff --git a/app/assets/javascripts/api_key_form.js b/app/assets/javascripts/api_key_form.js
deleted file mode 100644
index 71c8db372f2..00000000000
--- a/app/assets/javascripts/api_key_form.js
+++ /dev/null
@@ -1,36 +0,0 @@
-$(function() {
- var enableGemScopeCheckboxes = $("#push_rubygem, #yank_rubygem, #add_owner, #remove_owner");
- var hiddenRubygemId = "hidden_api_key_rubygem_id";
- toggleGemSelector();
-
- enableGemScopeCheckboxes.click(function() {
- toggleGemSelector();
- });
-
- function toggleGemSelector() {
- var isApplicableGemScopeSelected = enableGemScopeCheckboxes.is(":checked");
- var gemScopeSelector = $("#api_key_rubygem_id");
-
- if (isApplicableGemScopeSelected) {
- gemScopeSelector.removeAttr("disabled");
- removeHiddenRubygemField();
- } else {
- gemScopeSelector.val("");
- gemScopeSelector.prop("disabled", true);
- addHiddenRubygemField();
- }
- }
-
- function addHiddenRubygemField() {
- $("").attr({
- type: "hidden",
- id: hiddenRubygemId,
- name: "api_key[rubygem_id]",
- value: ""
- }).appendTo(".t-body form .api_key_rubygem_id_form");
- }
-
- function removeHiddenRubygemField() {
- $("#" + hiddenRubygemId + ":hidden").remove();
- }
-});
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
deleted file mode 100644
index 9c370293063..00000000000
--- a/app/assets/javascripts/application.js
+++ /dev/null
@@ -1,24 +0,0 @@
-// This is a manifest file that'll be compiled into including all the files listed below.
-// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
-// be included in the compiled file accessible from http://example.com/assets/application.js
-// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
-// the compiled file.
-//
-//= require jquery3
-//= require jquery_ujs
-//= require clipboard
-//= require github_buttons
-//= require webauthn-json
-//= require_tree .
-
-function handleClick(event, nav, removeNavExpandedClass, addNavExpandedClass) {
- var isMobileNavExpanded = nav.popUp.hasClass(nav.expandedClass);
-
- event.preventDefault();
-
- if (isMobileNavExpanded) {
- removeNavExpandedClass();
- } else {
- addNavExpandedClass();
- }
-}
diff --git a/app/assets/javascripts/autocomplete.js b/app/assets/javascripts/autocomplete.js
deleted file mode 100644
index 2ec6f66c149..00000000000
--- a/app/assets/javascripts/autocomplete.js
+++ /dev/null
@@ -1,76 +0,0 @@
-$(function() {
- if ($('#home_query').length){
- autocomplete($('#home_query'));
- var suggest = $('#suggest-home');
- } else {
- autocomplete($('#query'));
- var suggest = $('#suggest');
- }
-
- var indexNumber = -1;
-
- function autocomplete(search) {
- search.bind('input', function(e) {
- var term = $.trim($(search).val());
- if (term.length >= 2) {
- $.ajax({
- url: '/api/v1/search/autocomplete',
- type: 'GET',
- data: ('query=' + term),
- processData: false,
- dataType: 'json'
- }).done(function(data) {
- addToSuggestList(search, data);
- });
- } else {
- suggest.find('li').remove();
- }
- });
-
- search.keydown(function(e) {
- if (e.keyCode == 38) {
- indexNumber--;
- focusItem(search);
- } else if (e.keyCode == 40) {
- indexNumber++;
- focusItem(search);
- };
- });
- };
-
- function addToSuggestList(search, data) {
- suggest.find('li').remove();
-
- for (var i = 0; i < data.length && i < 10; i++) {
- var newItem = $('
').text(data[i]);
- $(newItem).attr('class', 'menu-item');
- suggest.append(newItem);
-
- /* submit the search form if li item was clicked */
- newItem.click(function() {
- search.val($(this).html());
- search.parent().submit()
- });
-
- newItem.hover(function () {
- $('li').removeClass('selected');
- $(this).addClass("selected");
- });
- }
-
- indexNumber = -1;
- };
-
- function focusItem(search){
- var suggestLength = suggest.find('li').length;
- if (indexNumber >= suggestLength) indexNumber = 0;
- if (indexNumber < 0) indexNumber = suggestLength - 1;
-
- $('li').removeClass('selected');
- suggest.find('li').eq(indexNumber).addClass('selected');
- search.val(suggest.find('.selected').text());
- };
-
- /* remove suggest drop down if clicked anywhere on page */
- $('html').click(function(e) { suggest.find('li').remove(); });
-});
diff --git a/app/assets/javascripts/clipboard_buttons.js b/app/assets/javascripts/clipboard_buttons.js
deleted file mode 100644
index 4b3f0036b43..00000000000
--- a/app/assets/javascripts/clipboard_buttons.js
+++ /dev/null
@@ -1,35 +0,0 @@
-$(function() {
- var clipboard = new ClipboardJS('.gem__code__icon');
- var copyTooltip = $('.gem__code__tooltip--copy');
- var copiedTooltip = $('.gem__code__tooltip--copied');
- var copyButtons = $('.gem__code__icon');
-
- function hideCopyShowCopiedTooltips(e) {
- copyTooltip.removeClass("clipboard-is-hover");
- copiedTooltip.insertAfter(e.trigger);
- copiedTooltip.addClass("clipboard-is-active");
- };
-
- clipboard.on('success', function(e) {
- hideCopyShowCopiedTooltips(e);
- e.clearSelection();
- });
-
- clipboard.on('error', function(e) {
- hideCopyShowCopiedTooltips(e);
- copiedTooltip.text("Ctrl-C to Copy");
- });
-
- copyButtons.hover(function() {
- copyTooltip.insertAfter(this);
- copyTooltip.addClass("clipboard-is-hover");
- });
-
- copyButtons.mouseout(function() {
- copyTooltip.removeClass("clipboard-is-hover");
- });
-
- copyButtons.mouseout(function() {
- copiedTooltip.removeClass("clipboard-is-active");
- });
-});
diff --git a/app/assets/javascripts/mobile-nav.js b/app/assets/javascripts/mobile-nav.js
deleted file mode 100644
index 445ab1d8dff..00000000000
--- a/app/assets/javascripts/mobile-nav.js
+++ /dev/null
@@ -1,67 +0,0 @@
-$(function() {
- // cache jQuery lookups into variables
- // so we don't have to traverse the DOM every time
- var sandwichIcon = $('.header__club-sandwich');
- var header = $('.header');
- var main = $('main');
- var footer = $('.footer');
- var signUpLink = $('.header__nav-link.js-sign-up-trigger');
- var navExpandedClass = 'mobile-nav-is-expanded';
- var headerSearch = $('.header__search');
- var headerLogo = $('.header__logo-wrap');
-
- // variable to support mobile nav tab behaviour
- // * skipSandwichIcon is for skipping sandwich icon
- // when you tab from "gem" icon
- // * tabDirection is for hiding and showing navbar
- // when you tab in and out
- var skipSandwichIcon = true;
- var tabDirection = true;
-
- function removeNavExpandedClass() {
- header.removeClass(navExpandedClass);
- main.removeClass(navExpandedClass);
- footer.removeClass(navExpandedClass);
- }
-
- function addNavExpandedClass() {
- header.addClass(navExpandedClass);
- main.addClass(navExpandedClass);
- footer.addClass(navExpandedClass);
- }
-
- function handleFocusIn() {
- if (skipSandwichIcon) {
- addNavExpandedClass();
- headerSearch.focus();
- skipSandwichIcon = false;
- } else {
- removeNavExpandedClass();
- headerLogo.focus();
- skipSandwichIcon = true;
- }
- }
-
- sandwichIcon.click(function(e){
- var nav = {expandedClass: navExpandedClass, popUp: header}
- handleClick(e, nav, removeNavExpandedClass, addNavExpandedClass);
- });
-
- sandwichIcon.on('focusin', handleFocusIn);
-
- signUpLink.on('focusin', function() {
- if (!tabDirection) {
- addNavExpandedClass();
- }
- });
-
- signUpLink.on('focusout', function() {
- if (tabDirection) {
- tabDirection = false;
- removeNavExpandedClass();
- } else {
- tabDirection = true;
- addNavExpandedClass();
- }
- });
-});
diff --git a/app/assets/javascripts/multifactor_auths.js b/app/assets/javascripts/multifactor_auths.js
deleted file mode 100644
index 9bf65609fc8..00000000000
--- a/app/assets/javascripts/multifactor_auths.js
+++ /dev/null
@@ -1,40 +0,0 @@
-function popUp (e) {
- e.preventDefault();
- e.returnValue = "";
-};
-
-function confirmNoRecoveryCopy (e, from) {
- if (from == null){
- e.preventDefault();
- if (confirm("Leave without copying recovery codes?")) {
- window.removeEventListener("beforeunload", popUp);
- $(this).trigger('click', ["non-null"]);
- }
- }
-}
-
-if($("#recovery-code-list").length){
- new ClipboardJS(".recovery__copy__icon");
-
- $(".recovery__copy__icon").on("click", function(e){
- $(this).text("[ copied ]");
-
- if( !$(this).is(".clicked") ) {
- e.preventDefault();
- $(this).addClass("clicked");
- window.removeEventListener("beforeunload", popUp);
- $(".form__submit").unbind("click", confirmNoRecoveryCopy);
- }
- });
-
- window.addEventListener("beforeunload", popUp);
- $(".form__submit").on("click", confirmNoRecoveryCopy);
-
- $(".form__checkbox__input").change(function() {
- if(this.checked) {
- $(".form__submit").prop('disabled', false);
- } else {
- $(".form__submit").prop('disabled', true);
- }
- });
-}
diff --git a/app/assets/javascripts/pages.js b/app/assets/javascripts/pages.js
deleted file mode 100644
index a65eca563d9..00000000000
--- a/app/assets/javascripts/pages.js
+++ /dev/null
@@ -1,75 +0,0 @@
-//data page
-$(document).ready(function() {
- var getDumpData = function(target, type) {
- return $.get('https://s3-us-west-2.amazonaws.com/rubygems-dumps/?prefix=production/public_' + type).done(function(data) {
- var files, xml;
- xml = $(data);
- files = parseS3Listing(xml);
- files = sortByLastModified(files);
- $(target).html(renderDumpList(files));
- }).fail(function(error) {
- console.error(error);
- });
- };
-
- var parseS3Listing = function(xml) {
- var files;
- files = $.map(xml.find('Contents'), function(item) {
- item = $(item);
- return {
- Key: item.find('Key').text(),
- LastModified: item.find('LastModified').text(),
- Size: item.find('Size').text(),
- StorageClass: item.find('StorageClass').text()
- };
- });
- return files;
- };
-
- var sortByLastModified = function(files) {
- return files.sort(function(a, b) {return Date.parse(b.LastModified) - Date.parse(a.LastModified)});
- };
-
- var bytesToSize = function(bytes) {
- var i, k, sizes;
- if (bytes === 0) {
- return '0 Byte';
- }
- k = 1024;
- sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
- i = Math.floor(Math.log(bytes) / Math.log(k));
- return (bytes / Math.pow(k, i)).toPrecision(3) + " " + sizes[i];
- };
-
- var renderDumpList = function(files) {
- var content;
- content = [];
- jQuery.each(files, function(idx, item) {
- if ('STANDARD' === item.StorageClass) {
- return content.push("" + (item.LastModified.replace('.000Z', '')) + " (" + (bytesToSize(item.Size)) + ")");
- }
- });
- return content.join("\n");
- };
-
- if($("#data-dump").length) {
- getDumpData('ul.rubygems-dump-listing-postgresql', 'postgresql');
- getDumpData('ul.rubygems-dump-listing-redis', 'redis');
- }
-});
-
-//stats page
-$('.stats__graph__gem__meter').each(function() {
- bar_width = $(this).data("bar_width");
- $(this).animate({ width: bar_width + '%' }, 700).removeClass('t-item--hidden').css("display", "block");
-});
-
-//gem page
-$(document).ready(function() {
- $('.gem__users__mfa-text.mfa-warn').on('click', function() {
- $('.gem__users__mfa-text.mfa-warn').toggleClass('t-item--hidden');
-
- $owners = $('.gem__users__mfa-disabled');
- $owners.toggleClass('t-item--hidden');
- });
-});
diff --git a/app/assets/javascripts/popup-nav.js b/app/assets/javascripts/popup-nav.js
deleted file mode 100644
index ed86b3d6735..00000000000
--- a/app/assets/javascripts/popup-nav.js
+++ /dev/null
@@ -1,20 +0,0 @@
-$(function() {
- var arrowIcon = $('.header__popup-link');
- var popupNav = $('.header__popup__nav-links');
-
- var navExpandedClass = 'is-expanded';
-
- function removeNavExpandedClass() {
- popupNav.removeClass(navExpandedClass);
- }
-
- function addNavExpandedClass() {
- popupNav.addClass(navExpandedClass);
- }
-
- arrowIcon.click(function(e){
- var nav = {expandedClass: navExpandedClass, popUp: popupNav}
- handleClick(e, nav, removeNavExpandedClass, addNavExpandedClass);
- });
-});
-
diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js
deleted file mode 100644
index 6c4ecae4439..00000000000
--- a/app/assets/javascripts/search.js
+++ /dev/null
@@ -1,26 +0,0 @@
-if($("#advanced-search").length){
- var $main = $('#home_query');
- var $name = $('input#name');
- var $summary = $('input#summary');
- var $description = $('input#description');
- var $downloads = $('input#downloads');
- var $updated = $('input#updated');
-
- $name.add($summary)
- .add($description)
- .add($downloads)
- .add($updated)
- .on('input', function(e) {
- var name = $name.val().length > 0 ? 'name: ' + $name.val() : '';
- var summary = $summary.val().length > 0 ? 'summary: ' + $summary.val() : '';
- var description = $description.val().length > 0 ? 'description: ' + $description.val() : '';
- var downloads = $downloads.val().length > 0 ? 'downloads: ' + $downloads.val() : '';
- var updated = $updated.val().length > 0 ? 'updated: ' + $updated.val() : '';
-
- $main.val($.trim(name + ' ' + summary + ' ' + description + ' ' + downloads + ' ' + updated));
- }).on('keypress', function(e) {
- if (e.key === 'Enter') {
- $("input#advanced_search_submit").click();
- }
- });
-}
diff --git a/app/assets/javascripts/transitive_dependencies.js b/app/assets/javascripts/transitive_dependencies.js
deleted file mode 100755
index 36cccd41a68..00000000000
--- a/app/assets/javascripts/transitive_dependencies.js
+++ /dev/null
@@ -1,47 +0,0 @@
-$(document).on('click', '.deps_expanded-link', function () {
- var current = $(this);
- var gem_id = $(this).attr('data-gem_id');
- var version_id = $(this).attr('data-version');
- $.ajax({
- type: "get",
- url: "/gems/"+gem_id+"/versions/"+version_id+"/dependencies.json",
- success: function(resp) {
- renderDependencies(resp, current);
- },
- error: function() {
- var error_message = "Request failed. please reload the page and try again
";
- current.parent().next().next().html(error_message);
- }
- });
-});
-
-function renderDependencies(resp, current) {
- scope_display(current, resp.run_html, "runtime");
- scope_display(current, resp.dev_html, "development");
- arrow_toggler(current);
-}
-
-function arrow_toggler(current) {
- var toggler = "";
- current.parent().html(toggler);
-}
-
-function scope_display(current, deps, scope) {
- if (deps.length != 0){
- var new_gems = current.parent().next().next();
- if (scope == "development") { new_gems = new_gems.next(); }
- new_gems.find(".deps_scope").append(deps);
- }
-}
-
-$(document).on('click', '.scope', function () {
- $(this).toggleClass("scope--expanded");
- $(this).next().toggleClass("deps_toggle");
-});
-
-$(document).on('click', '.arrow_toggle', function () {
- var runtime_div = $(this).parent().next().next();
- runtime_div.toggleClass('deps_toggle');
- runtime_div.next().toggleClass('deps_toggle');
- $(this).toggleClass('deps_expanded-down');
-});
diff --git a/app/assets/javascripts/webauthn.js b/app/assets/javascripts/webauthn.js
deleted file mode 100644
index 30aff380ac9..00000000000
--- a/app/assets/javascripts/webauthn.js
+++ /dev/null
@@ -1,177 +0,0 @@
-(function() {
- const handleEvent = function(event) {
- event.preventDefault();
- return event.target;
- };
-
- const setError = function(submit, error, message) {
- submit.attr("disabled", false);
- error.attr("hidden", false);
- error.text(message);
- };
-
- const handleHtmlResponse = function(submit, responseError, response) {
- if (response.redirected) {
- window.location.href = response.url;
- } else {
- response.text().then(function (html) {
- document.body.innerHTML = html;
- }).catch(function (error) {
- setError(submit, responseError, error);
- });
- }
- };
-
- const credentialsToBase64 = function(credentials) {
- return {
- type: credentials.type,
- id: credentials.id,
- rawId: bufferToBase64url(credentials.rawId),
- clientExtensionResults: credentials.clientExtensionResults,
- response: {
- authenticatorData: bufferToBase64url(credentials.response.authenticatorData),
- attestationObject: bufferToBase64url(credentials.response.attestationObject),
- clientDataJSON: bufferToBase64url(credentials.response.clientDataJSON),
- signature: bufferToBase64url(credentials.response.signature),
- userHandle: bufferToBase64url(credentials.response.userHandle),
- },
- };
- };
-
- const credentialsToBuffer = function(credentials) {
- return credentials.map(function(credential) {
- return {
- id: base64urlToBuffer(credential.id),
- type: credential.type
- };
- });
- };
-
- const parseCreationOptionsFromJSON = function(json) {
- return {
- ...json,
- challenge: base64urlToBuffer(json.challenge),
- user: { ...json.user, id: base64urlToBuffer(json.user.id) },
- excludeCredentials: credentialsToBuffer(json.excludeCredentials),
- };
- };
-
- const parseRequestOptionsFromJSON = function(json) {
- return {
- ...json,
- challenge: base64urlToBuffer(json.challenge),
- allowCredentials: credentialsToBuffer(json.allowCredentials),
- };
- };
-
- $(function() {
- const credentialForm = $(".js-new-webauthn-credential--form");
- const credentialError = $(".js-new-webauthn-credential--error");
- const credentialSubmit = $(".js-new-webauthn-credential--submit");
- const csrfToken = $("[name='csrf-token']").attr("content");
-
- credentialForm.submit(function(event) {
- const form = handleEvent(event);
- const nickname = $(".js-new-webauthn-credential--nickname").val();
-
- fetch(form.action + ".json", {
- method: "POST",
- credentials: "same-origin",
- headers: { "X-CSRF-Token": csrfToken }
- }).then(function (response) {
- return response.json();
- }).then(function (json) {
- return navigator.credentials.create({
- publicKey: parseCreationOptionsFromJSON(json)
- });
- }).then(function (credentials) {
- return fetch(form.action + "/callback.json", {
- method: "POST",
- credentials: "same-origin",
- headers: {
- "X-CSRF-Token": csrfToken,
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- credentials: credentialsToBase64(credentials),
- webauthn_credential: { nickname: nickname }
- })
- });
- }).then(function (response) {
- response.json().then(function (json) {
- if (json.redirect_url) {
- window.location.href = json.redirect_url;
- } else {
- setError(credentialSubmit, credentialError, json.message);
- }
- }).catch(function (error) {
- setError(credentialSubmit, credentialError, error);
- });
- }).catch(function (error) {
- setError(credentialSubmit, credentialError, error);
- });
- });
- });
-
- const getCredentials = async function(event, csrfToken) {
- const form = handleEvent(event);
- const options = JSON.parse(form.dataset.options);
- const credentials = await navigator.credentials.get({
- publicKey: parseRequestOptionsFromJSON(options)
- });
- return await fetch(form.action, {
- method: "POST",
- credentials: "same-origin",
- redirect: "follow",
- headers: {
- "X-CSRF-Token": csrfToken,
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- credentials: credentialsToBase64(credentials),
- })
- });
- };
-
- $(function() {
- const cliSessionForm = $(".js-webauthn-session-cli--form");
- const cliSessionError = $(".js-webauthn-session-cli--error");
- const csrfToken = $("[name='csrf-token']").attr("content");
-
- function failed_verification_url(message) {
- const url = new URL(`${location.origin}/webauthn_verification/failed_verification`);
- url.searchParams.append("error", message);
- return url.href;
- };
-
- cliSessionForm.submit(function(event) {
- getCredentials(event, csrfToken).then(function (response) {
- response.text().then(function (text) {
- if (text == "success") {
- window.location.href = `${location.origin}/webauthn_verification/successful_verification`;
- } else {
- window.location.href = failed_verification_url(text);
- }
- });
- }).catch(function (error) {
- window.location.href = failed_verification_url(error.message);
- });
- });
- });
-
- $(function() {
- const sessionForm = $(".js-webauthn-session--form");
- const sessionSubmit = $(".js-webauthn-session--submit");
- const sessionError = $(".js-webauthn-session--error");
- const csrfToken = $("[name='csrf-token']").attr("content");
-
- sessionForm.submit(async function(event) {
- try {
- const response = await getCredentials(event, csrfToken);
- handleHtmlResponse(sessionSubmit, sessionError, response);
- } catch (error) {
- setError(sessionSubmit, sessionError, error);
- }
- });
- });
-})();
diff --git a/app/helpers/api_keys_helper.rb b/app/helpers/api_keys_helper.rb
index 6adf9c8c341..252903ad49f 100644
--- a/app/helpers/api_keys_helper.rb
+++ b/app/helpers/api_keys_helper.rb
@@ -5,6 +5,19 @@ def gem_scope(api_key)
api_key.rubygem ? api_key.rubygem.name : t("api_keys.all_gems")
end
+ def api_key_scope_html_data(api_scope)
+ if ApiKey::EXCLUSIVE_SCOPES.include?(api_scope)
+ {
+ api_key_form_target: "exclusiveCheckbox"
+ }
+ else
+ {
+ api_key_form_target: "checkbox",
+ gemscope: ApiKey::APPLICABLE_GEM_API_SCOPES.include?(api_scope) ? api_scope : nil
+ }
+ end
+ end
+
private
def invalid_gem_tooltip(name)
diff --git a/app/javascript/application.js b/app/javascript/application.js
new file mode 100644
index 00000000000..bd3f5f20ce9
--- /dev/null
+++ b/app/javascript/application.js
@@ -0,0 +1,5 @@
+// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
+import * as $ from 'jquery';
+import "controllers"
+import Rails from "@rails/ujs"
+Rails.start()
diff --git a/app/javascript/controllers/advanced_search_controller.js b/app/javascript/controllers/advanced_search_controller.js
new file mode 100644
index 00000000000..2f58b4b034b
--- /dev/null
+++ b/app/javascript/controllers/advanced_search_controller.js
@@ -0,0 +1,30 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ connect() {
+ var $main = $('#home_query');
+ var $name = $('input#name');
+ var $summary = $('input#summary');
+ var $description = $('input#description');
+ var $downloads = $('input#downloads');
+ var $updated = $('input#updated');
+
+ $name.add($summary)
+ .add($description)
+ .add($downloads)
+ .add($updated)
+ .on('input', function(e) {
+ var name = $name.val().length > 0 ? 'name: ' + $name.val() : '';
+ var summary = $summary.val().length > 0 ? 'summary: ' + $summary.val() : '';
+ var description = $description.val().length > 0 ? 'description: ' + $description.val() : '';
+ var downloads = $downloads.val().length > 0 ? 'downloads: ' + $downloads.val() : '';
+ var updated = $updated.val().length > 0 ? 'updated: ' + $updated.val() : '';
+
+ $main.val($.trim(name + ' ' + summary + ' ' + description + ' ' + downloads + ' ' + updated));
+ }).on('keypress', function(e) {
+ if (e.key === 'Enter') {
+ $("input#advanced_search_submit").click();
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/javascript/controllers/api_key_form_controller.js b/app/javascript/controllers/api_key_form_controller.js
new file mode 100644
index 00000000000..3b7cd859d6e
--- /dev/null
+++ b/app/javascript/controllers/api_key_form_controller.js
@@ -0,0 +1,56 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ static targets = ["checkbox", "exclusiveCheckbox", "gemSelector"]
+
+ connect() {
+ this.toggleGemSelector();
+ }
+
+ checkboxTargetConnected(el) {
+ el.addEventListener("change", () => {
+ if (el.checked) { this.exclusiveCheckboxTarget.checked = false }
+ if (el.dataset.gemscope) { this.toggleGemSelector(); }
+ })
+ }
+
+ exclusiveCheckboxTargetConnected(el) {
+ el.addEventListener("change", () => {
+ if (el.checked) { this.checkboxTargets.forEach((checkbox) => checkbox.checked = false) }
+ this.toggleGemSelector();
+ });
+ }
+
+ toggleGemSelector() {
+ // what type is checkboxTargets?
+ var selected = this.checkboxTargets.find(function(target) {
+ return target.dataset.gemscope && target.checked
+ })
+
+ if (selected) {
+ this.gemSelectorTarget.disabled = false;
+ this.removeHiddenRubygemField();
+ } else {
+ this.gemSelectorTarget.value = "";
+ this.gemSelectorTarget.disabled = true;
+ this.addHiddenRubygemField();
+ }
+ }
+
+ addHiddenRubygemField() {
+ if (this.hiddenField) { return }
+ this.hiddenField = document.createElement("input");
+ this.hiddenField.type = "hidden";
+ this.hiddenField.name = "api_key[rubygem_id]";
+ this.hiddenField.value = "";
+ this.element.appendChild(this.hiddenField);
+ }
+
+ removeHiddenRubygemField() {
+ if (this.hiddenField) {
+ this.hiddenField.remove();
+ this.hiddenField = null;
+ }
+ }
+}
+
diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js
new file mode 100644
index 00000000000..1213e85c7ac
--- /dev/null
+++ b/app/javascript/controllers/application.js
@@ -0,0 +1,9 @@
+import { Application } from "@hotwired/stimulus"
+
+const application = Application.start()
+
+// Configure Stimulus development experience
+application.debug = false
+window.Stimulus = application
+
+export { application }
diff --git a/app/javascript/controllers/autocomplete_controller.js b/app/javascript/controllers/autocomplete_controller.js
new file mode 100644
index 00000000000..a3f8983b33e
--- /dev/null
+++ b/app/javascript/controllers/autocomplete_controller.js
@@ -0,0 +1,74 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ static targets = ["query suggest"]
+
+ connect() {
+ this.indexNumber = -1;
+ /* remove suggest drop down if clicked anywhere on page */
+ $('html').click(function(e) { $(this.suggestTarget).find('li').remove(); });
+ }
+
+ queryTargetConnected(search) {
+ search.addEventListener('input', (e) => {
+ var term = $.trim($(search).val());
+ if (term.length >= 2) {
+ console.log(term);
+ $.ajax({
+ url: '/api/v1/search/autocomplete',
+ type: 'GET',
+ data: ('query=' + term),
+ processData: false,
+ dataType: 'json'
+ }).done((data) => {
+ this.addToSuggestList(search, data);
+ });
+ } else {
+ $(this.suggestTarget).find('li').remove();
+ }
+ });
+
+ search.addEventListener('keydown', (e) => {
+ if (e.keyCode == 38) {
+ this.indexNumber--;
+ this.focusItem(search);
+ } else if (e.keyCode == 40) {
+ this.indexNumber++;
+ this.focusItem(search);
+ };
+ });
+ }
+
+ addToSuggestList(search, data) {
+ $(this.suggestTarget).find('li').remove();
+
+ for (var i = 0; i < data.length && i < 10; i++) {
+ var newItem = $('').text(data[i]);
+ $(newItem).attr('class', 'menu-item');
+ $(this.suggestTarget).append(newItem);
+
+ /* submit the search form if li item was clicked */
+ newItem.click(function() {
+ $(search).val($(this).html());
+ $(search).parent().submit()
+ });
+
+ newItem.hover(function () {
+ $('li').removeClass('selected');
+ $(this).addClass("selected");
+ });
+ }
+
+ this.indexNumber = -1;
+ };
+
+ focusItem(search){
+ var suggestLength = $(this.suggestTarget).find('li').length;
+ if (this.indexNumber >= suggestLength) this.indexNumber = 0;
+ if (this.indexNumber < 0) this.indexNumber = suggestLength - 1;
+
+ $('li').removeClass('selected');
+ $(this.suggestTarget).find('li').eq(this.indexNumber).addClass('selected');
+ $(search).val($(this.suggestTarget).find('.selected').text());
+ }
+}
diff --git a/app/javascript/controllers/clipboard_buttons_controller.js b/app/javascript/controllers/clipboard_buttons_controller.js
new file mode 100644
index 00000000000..2997b1817bd
--- /dev/null
+++ b/app/javascript/controllers/clipboard_buttons_controller.js
@@ -0,0 +1,44 @@
+import { Controller } from "@hotwired/stimulus"
+import ClipboardJS from "clipboard"
+
+export default class extends Controller {
+ static targets = [ "copyTooltip copiedTooltip button" ]
+
+ connect() {
+ this.copyTooltip = $(this.copyTooltipTarget);
+ this.copiedTooltip = $(this.copiedTooltipTarget);
+ }
+
+ buttonTargetConnected(el) {
+ console.log("buttonTargetConnected", el);
+ const controller = this;
+
+ el.addEventListener('hover', function() {
+ controller.copyTooltip.insertAfter(this);
+ controller.copyTooltip.addClass("clipboard-is-hover");
+ });
+
+ el.addEventListener('mouseout', function() {
+ controller.copyTooltip.removeClass("clipboard-is-hover");
+ controller.copiedTooltip.removeClass("clipboard-is-active");
+ });
+
+ const clipboard = new ClipboardJS(el);
+
+ clipboard.on('success', (e) => {
+ this.hideCopyShowCopiedTooltips(e);
+ e.clearSelection();
+ });
+
+ clipboard.on('error', (e) => {
+ this.hideCopyShowCopiedTooltips(e);
+ this.copiedTooltip.text("Ctrl-C to Copy");
+ });
+ }
+
+ hideCopyShowCopiedTooltips(e) {
+ this.copyTooltip.removeClass("clipboard-is-hover");
+ this.copiedTooltip.insertAfter(e.trigger);
+ this.copiedTooltip.addClass("clipboard-is-active");
+ }
+}
\ No newline at end of file
diff --git a/app/javascript/controllers/hello_controller.js b/app/javascript/controllers/hello_controller.js
new file mode 100644
index 00000000000..5975c0789d7
--- /dev/null
+++ b/app/javascript/controllers/hello_controller.js
@@ -0,0 +1,7 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ connect() {
+ this.element.textContent = "Hello World!"
+ }
+}
diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js
new file mode 100644
index 00000000000..54ad4cad4d4
--- /dev/null
+++ b/app/javascript/controllers/index.js
@@ -0,0 +1,11 @@
+// Import and register all your controllers from the importmap under controllers/*
+
+import { application } from "controllers/application"
+
+// Eager load all controllers defined in the import map under controllers/**/*_controller
+import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
+eagerLoadControllersFrom("controllers", application)
+
+// Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!)
+// import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
+// lazyLoadControllersFrom("controllers", application)
diff --git a/app/javascript/controllers/multifactor_auths_controller.js b/app/javascript/controllers/multifactor_auths_controller.js
new file mode 100644
index 00000000000..ff63a93f9b1
--- /dev/null
+++ b/app/javascript/controllers/multifactor_auths_controller.js
@@ -0,0 +1,46 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ connect() {
+ function popUp (e) {
+ e.preventDefault();
+ e.returnValue = "";
+ };
+
+ function confirmNoRecoveryCopy (e, from) {
+ if (from == null){
+ e.preventDefault();
+ if (confirm("Leave without copying recovery codes?")) {
+ window.removeEventListener("beforeunload", popUp);
+ $(this).trigger('click', ["non-null"]);
+ }
+ }
+ }
+
+ if($("#recovery-code-list").length){
+ new ClipboardJS(".recovery__copy__icon");
+
+ $(".recovery__copy__icon").on("click", function(e){
+ $(this).text("[ copied ]");
+
+ if( !$(this).is(".clicked") ) {
+ e.preventDefault();
+ $(this).addClass("clicked");
+ window.removeEventListener("beforeunload", popUp);
+ $(".form__submit").unbind("click", confirmNoRecoveryCopy);
+ }
+ });
+
+ window.addEventListener("beforeunload", popUp);
+ $(".form__submit").on("click", confirmNoRecoveryCopy);
+
+ $(".form__checkbox__input").change(function() {
+ if(this.checked) {
+ $(".form__submit").prop('disabled', false);
+ } else {
+ $(".form__submit").prop('disabled', true);
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/javascript/controllers/nav_controller.js b/app/javascript/controllers/nav_controller.js
new file mode 100644
index 00000000000..66435d1d512
--- /dev/null
+++ b/app/javascript/controllers/nav_controller.js
@@ -0,0 +1,77 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ static targets = [
+ "dropdownButton", // carrot icon in full size
+ "dropdown", // full size dropdown
+ "sandwichIcon", // mobile sandwich icon
+ "header",
+ "main",
+ "footer",
+ "headerSearch",
+ "headerLogo"
+ ]
+
+ static mobileNavExpandedClass = 'mobile-nav-is-expanded';
+
+ connect() {
+ // variable to support mobile nav tab behaviour
+ // * skipSandwichIcon is for skipping sandwich icon
+ // when you tab from "gem" icon
+ // * tabDirection is for hiding and showing navbar
+ // when you tab in and out
+ this.skipSandwichIcon = true;
+
+ document.addEventListener("click", (e) => {
+ // Must check both dropdowns otherwise you always close
+ // the dropdown because one or the other isn't being clicked
+ if (!this.dropdownTarget.contains(e.target) && !this.headerTarget.contains(e.target)) {
+ this.dropdownTarget.classList.remove('is-expanded');
+ this.collapseMobileNav();
+ }
+ });
+ }
+
+ dropdownButtonTargetConnected(el) {
+ el.addEventListener("click", (e) => {
+ e.preventDefault();
+ this.dropdownTarget.classList.toggle('is-expanded');
+ });
+ }
+
+ sandwichIconTargetConnected(el) {
+ el.addEventListener("click", (e) => {
+ e.preventDefault();
+
+ if (this.headerTarget.classList.contains(this.constructor.mobileNavExpandedClass)) {
+ this.collapseMobileNav();
+ } else {
+ this.expandMobileNav();
+ }
+ });
+
+ el.addEventListener('focusin', () => {
+ if (this.skipSandwichIcon) {
+ this.expandeMobileNav();
+ this.headerSearchTarget.focus();
+ this.skipSandwichIcon = false;
+ } else {
+ this.collapseMobileNav();
+ this.headerLogoTarget.focus();
+ this.skipSandwichIcon = true;
+ }
+ });
+ }
+
+ collapseMobileNav() {
+ this.headerTarget.classList.remove(this.constructor.mobileNavExpandedClass);
+ this.mainTarget.classList.remove(this.constructor.mobileNavExpandedClass);
+ this.footerTarget.classList.remove(this.constructor.mobileNavExpandedClass);
+ }
+
+ expandMobileNav() {
+ this.headerTarget.classList.add(this.constructor.mobileNavExpandedClass);
+ this.mainTarget.classList.add(this.constructor.mobileNavExpandedClass);
+ this.footerTarget.classList.add(this.constructor.mobileNavExpandedClass);
+ }
+}
\ No newline at end of file
diff --git a/app/assets/javascripts/oidc_api_key_role_form.js b/app/javascript/controllers/oidc_api_key_role_form_controller.js
similarity index 87%
rename from app/assets/javascripts/oidc_api_key_role_form.js
rename to app/javascript/controllers/oidc_api_key_role_form_controller.js
index 76654487870..601c351ac41 100644
--- a/app/assets/javascripts/oidc_api_key_role_form.js
+++ b/app/javascript/controllers/oidc_api_key_role_form_controller.js
@@ -1,5 +1,7 @@
-$(function () {
- function wire() {
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ connect() {
var removeNestedButtons = $("button.form__remove_nested_button");
removeNestedButtons.off("click");
@@ -27,6 +29,4 @@ $(function () {
wire();
});
}
-
- wire();
-});
+}
\ No newline at end of file
diff --git a/app/javascript/controllers/pages_controller.js b/app/javascript/controllers/pages_controller.js
new file mode 100644
index 00000000000..c5baedd013f
--- /dev/null
+++ b/app/javascript/controllers/pages_controller.js
@@ -0,0 +1,82 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ connect() {
+ //data page
+ $(document).ready(function() {
+ var getDumpData = function(target, type) {
+ return $.get('https://s3-us-west-2.amazonaws.com/rubygems-dumps/?prefix=production/public_' + type).done(function(data) {
+ var files, xml;
+ xml = $(data);
+ files = parseS3Listing(xml);
+ files = sortByLastModified(files);
+ $(target).html(renderDumpList(files));
+ }).fail(function(error) {
+ console.error(error);
+ });
+ };
+
+ var parseS3Listing = function(xml) {
+ var files;
+ files = $.map(xml.find('Contents'), function(item) {
+ item = $(item);
+ return {
+ Key: item.find('Key').text(),
+ LastModified: item.find('LastModified').text(),
+ Size: item.find('Size').text(),
+ StorageClass: item.find('StorageClass').text()
+ };
+ });
+ return files;
+ };
+
+ var sortByLastModified = function(files) {
+ return files.sort(function(a, b) {return Date.parse(b.LastModified) - Date.parse(a.LastModified)});
+ };
+
+ var bytesToSize = function(bytes) {
+ var i, k, sizes;
+ if (bytes === 0) {
+ return '0 Byte';
+ }
+ k = 1024;
+ sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+ i = Math.floor(Math.log(bytes) / Math.log(k));
+ return (bytes / Math.pow(k, i)).toPrecision(3) + " " + sizes[i];
+ };
+
+ var renderDumpList = function(files) {
+ var content;
+ content = [];
+ jQuery.each(files, function(idx, item) {
+ if ('STANDARD' === item.StorageClass) {
+ return content.push("" + (item.LastModified.replace('.000Z', '')) + " (" + (bytesToSize(item.Size)) + ")");
+ }
+ });
+ return content.join("\n");
+ };
+
+ if($("#data-dump").length) {
+ getDumpData('ul.rubygems-dump-listing-postgresql', 'postgresql');
+ getDumpData('ul.rubygems-dump-listing-redis', 'redis');
+ }
+ });
+
+ //stats page
+ $('.stats__graph__gem__meter').each(function() {
+ bar_width = $(this).data("bar_width");
+ $(this).animate({ width: bar_width + '%' }, 700).removeClass('t-item--hidden').css("display", "block");
+ });
+
+ //gem page
+ $(document).ready(function() {
+ $('.gem__users__mfa-text.mfa-warn').on('click', function() {
+ $('.gem__users__mfa-text.mfa-warn').toggleClass('t-item--hidden');
+
+ $owners = $('.gem__users__mfa-disabled');
+ $owners.toggleClass('t-item--hidden');
+ });
+ });
+
+ }
+}
\ No newline at end of file
diff --git a/app/javascript/controllers/transitive_dependencies_controller.js b/app/javascript/controllers/transitive_dependencies_controller.js
new file mode 100755
index 00000000000..c883de7fced
--- /dev/null
+++ b/app/javascript/controllers/transitive_dependencies_controller.js
@@ -0,0 +1,53 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ connect() {
+ $(document).on('click', '.deps_expanded-link', function () {
+ var current = $(this);
+ var gem_id = $(this).attr('data-gem_id');
+ var version_id = $(this).attr('data-version');
+ $.ajax({
+ type: "get",
+ url: "/gems/"+gem_id+"/versions/"+version_id+"/dependencies.json",
+ success: function(resp) {
+ renderDependencies(resp, current);
+ },
+ error: function() {
+ var error_message = "Request failed. please reload the page and try again
";
+ current.parent().next().next().html(error_message);
+ }
+ });
+ });
+
+ function renderDependencies(resp, current) {
+ scope_display(current, resp.run_html, "runtime");
+ scope_display(current, resp.dev_html, "development");
+ arrow_toggler(current);
+ }
+
+ function arrow_toggler(current) {
+ var toggler = "";
+ current.parent().html(toggler);
+ }
+
+ function scope_display(current, deps, scope) {
+ if (deps.length != 0){
+ var new_gems = current.parent().next().next();
+ if (scope == "development") { new_gems = new_gems.next(); }
+ new_gems.find(".deps_scope").append(deps);
+ }
+ }
+
+ $(document).on('click', '.scope', function () {
+ $(this).toggleClass("scope--expanded");
+ $(this).next().toggleClass("deps_toggle");
+ });
+
+ $(document).on('click', '.arrow_toggle', function () {
+ var runtime_div = $(this).parent().next().next();
+ runtime_div.toggleClass('deps_toggle');
+ runtime_div.next().toggleClass('deps_toggle');
+ $(this).toggleClass('deps_expanded-down');
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/javascript/controllers/webauthn_controller.js b/app/javascript/controllers/webauthn_controller.js
new file mode 100644
index 00000000000..847c1b11a1a
--- /dev/null
+++ b/app/javascript/controllers/webauthn_controller.js
@@ -0,0 +1,183 @@
+import { Controller } from "@hotwired/stimulus"
+import $ from "jquery"
+import { bufferToBase64url, base64urlToBuffer } from "webauthn-json"
+
+export default class extends Controller {
+ connect() {
+ const handleEvent = function(event) {
+ event.preventDefault();
+ return event.target;
+ };
+
+ const setError = function(submit, error, message) {
+ submit.attr("disabled", false);
+ error.attr("hidden", false);
+ error.text(message);
+ };
+
+ const handleHtmlResponse = function(submit, responseError, response) {
+ if (response.redirected) {
+ window.location.href = response.url;
+ } else {
+ response.text().then(function (html) {
+ document.body.innerHTML = html;
+ }).catch(function (error) {
+ setError(submit, responseError, error);
+ });
+ }
+ };
+
+ const credentialsToBase64 = function(credentials) {
+ return {
+ type: credentials.type,
+ id: credentials.id,
+ rawId: bufferToBase64url(credentials.rawId),
+ clientExtensionResults: credentials.clientExtensionResults,
+ response: {
+ authenticatorData: bufferToBase64url(credentials.response.authenticatorData),
+ attestationObject: bufferToBase64url(credentials.response.attestationObject),
+ clientDataJSON: bufferToBase64url(credentials.response.clientDataJSON),
+ signature: bufferToBase64url(credentials.response.signature),
+ userHandle: bufferToBase64url(credentials.response.userHandle),
+ },
+ };
+ };
+
+ const credentialsToBuffer = function(credentials) {
+ return credentials.map(function(credential) {
+ return {
+ id: base64urlToBuffer(credential.id),
+ type: credential.type
+ };
+ });
+ };
+
+ const parseCreationOptionsFromJSON = function(json) {
+ return {
+ ...json,
+ challenge: base64urlToBuffer(json.challenge),
+ user: { ...json.user, id: base64urlToBuffer(json.user.id) },
+ excludeCredentials: credentialsToBuffer(json.excludeCredentials),
+ };
+ };
+
+ const parseRequestOptionsFromJSON = function(json) {
+ return {
+ ...json,
+ challenge: base64urlToBuffer(json.challenge),
+ allowCredentials: credentialsToBuffer(json.allowCredentials),
+ };
+ };
+
+ $(function() {
+ const credentialForm = $(".js-new-webauthn-credential--form");
+ const credentialError = $(".js-new-webauthn-credential--error");
+ const credentialSubmit = $(".js-new-webauthn-credential--submit");
+ const csrfToken = $("[name='csrf-token']").attr("content");
+
+ credentialForm.submit(function(event) {
+ const form = handleEvent(event);
+ const nickname = $(".js-new-webauthn-credential--nickname").val();
+
+ fetch(form.action + ".json", {
+ method: "POST",
+ credentials: "same-origin",
+ headers: { "X-CSRF-Token": csrfToken }
+ }).then(function (response) {
+ return response.json();
+ }).then(function (json) {
+ return navigator.credentials.create({
+ publicKey: parseCreationOptionsFromJSON(json)
+ });
+ }).then(function (credentials) {
+ return fetch(form.action + "/callback.json", {
+ method: "POST",
+ credentials: "same-origin",
+ headers: {
+ "X-CSRF-Token": csrfToken,
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({
+ credentials: credentialsToBase64(credentials),
+ webauthn_credential: { nickname: nickname }
+ })
+ });
+ }).then(function (response) {
+ response.json().then(function (json) {
+ if (json.redirect_url) {
+ window.location.href = json.redirect_url;
+ } else {
+ setError(credentialSubmit, credentialError, json.message);
+ }
+ }).catch(function (error) {
+ setError(credentialSubmit, credentialError, error);
+ });
+ }).catch(function (error) {
+ setError(credentialSubmit, credentialError, error);
+ });
+ });
+ });
+
+ const getCredentials = async function(event, csrfToken) {
+ const form = handleEvent(event);
+ const options = JSON.parse(form.dataset.options);
+ const credentials = await navigator.credentials.get({
+ publicKey: parseRequestOptionsFromJSON(options)
+ });
+ return await fetch(form.action, {
+ method: "POST",
+ credentials: "same-origin",
+ redirect: "follow",
+ headers: {
+ "X-CSRF-Token": csrfToken,
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({
+ credentials: credentialsToBase64(credentials),
+ })
+ });
+ };
+
+ $(function() {
+ const cliSessionForm = $(".js-webauthn-session-cli--form");
+ const cliSessionError = $(".js-webauthn-session-cli--error");
+ const csrfToken = $("[name='csrf-token']").attr("content");
+
+ function failed_verification_url(message) {
+ const url = new URL(`${location.origin}/webauthn_verification/failed_verification`);
+ url.searchParams.append("error", message);
+ return url.href;
+ };
+
+ cliSessionForm.submit(function(event) {
+ getCredentials(event, csrfToken).then(function (response) {
+ response.text().then(function (text) {
+ if (text == "success") {
+ window.location.href = `${location.origin}/webauthn_verification/successful_verification`;
+ } else {
+ window.location.href = failed_verification_url(text);
+ }
+ });
+ }).catch(function (error) {
+ window.location.href = failed_verification_url(error.message);
+ });
+ });
+ });
+
+ $(function() {
+ const sessionForm = $(".js-webauthn-session--form");
+ const sessionSubmit = $(".js-webauthn-session--submit");
+ const sessionError = $(".js-webauthn-session--error");
+ const csrfToken = $("[name='csrf-token']").attr("content");
+
+ sessionForm.submit(async function(event) {
+ try {
+ const response = await getCredentials(event, csrfToken);
+ handleHtmlResponse(sessionSubmit, sessionError, response);
+ } catch (error) {
+ setError(sessionSubmit, sessionError, error);
+ }
+ });
+ });
+ }
+}
diff --git a/app/models/api_key.rb b/app/models/api_key.rb
index f8d81d6e8f3..6f14e2c67df 100644
--- a/app/models/api_key.rb
+++ b/app/models/api_key.rb
@@ -1,6 +1,7 @@
class ApiKey < ApplicationRecord
- API_SCOPES = %i[index_rubygems push_rubygem yank_rubygem add_owner remove_owner access_webhooks show_dashboard].freeze
+ API_SCOPES = %i[show_dashboard index_rubygems push_rubygem yank_rubygem add_owner remove_owner access_webhooks].freeze
APPLICABLE_GEM_API_SCOPES = %i[push_rubygem yank_rubygem add_owner remove_owner].freeze
+ EXCLUSIVE_SCOPES = %i[show_dashboard].freeze
belongs_to :owner, polymorphic: true
diff --git a/app/views/api_keys/new.html.erb b/app/views/api_keys/new.html.erb
index 38fc61db031..00cdf077303 100644
--- a/app/views/api_keys/new.html.erb
+++ b/app/views/api_keys/new.html.erb
@@ -1,15 +1,24 @@
<% @title = t(".new_api_key") %>
- <%= form_for @api_key, url: profile_api_keys_path do |f| %>
+ <%= form_for @api_key, url: profile_api_keys_path, data: { controller: "api-key-form" } do |f| %>
<%= label_tag :name, t("api_keys.index.name"), class: "form__label" %>
<%= f.text_field :name, class: "form__input", autocomplete: :off %>
+
<%= label_tag :scope, t("api_keys.index.scopes"), class: "form__label" %>
- <% ApiKey::API_SCOPES.each do |api_scope| %>
+ <% (ApiKey::API_SCOPES - ApiKey::EXCLUSIVE_SCOPES).each do |api_scope| %>
- <%= f.check_box "#{api_scope}", { class: "form__checkbox__input", id: "#{api_scope}" } , "true", "false" %>
+ <%= f.check_box "#{api_scope}", { class: "form__checkbox__input", id: "#{api_scope}", data: api_key_scope_html_data(api_scope) }, "true", "false" %>
<%= label_tag api_scope, t("api_keys.index.#{api_scope}"), class: "form__checkbox__label" %>
<% end %>
@@ -18,7 +27,7 @@
<% unless current_user.mfa_disabled? || current_user.mfa_ui_and_api? %>
diff --git a/app/views/dependencies/_dependencies.html.erb b/app/views/dependencies/_dependencies.html.erb
index f5568e662af..7abb53d9eea 100644
--- a/app/views/dependencies/_dependencies.html.erb
+++ b/app/views/dependencies/_dependencies.html.erb
@@ -1,22 +1,23 @@
<% if dependencies.key?(scope) %>
-
"><%= scope.titlecase %> :
-
">
- <% dependencies[scope].each do |name,version,req| %>
-
+ <% end %>
+
+
<% end %>
diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb
index 22134450d73..5da819e49f1 100644
--- a/app/views/home/index.html.erb
+++ b/app/views/home/index.html.erb
@@ -3,9 +3,9 @@
<%= t '.find_blurb' %>
- <%= form_tag search_path, :method => :get do %>
- <%= search_field_tag :query, params[:query], :placeholder => t('layouts.application.header.search_gem_html'), autofocus: current_page?(root_url), :id => 'home_query', :class => "home__search", :autocomplete => "off" %>
-
+ <%= form_tag search_path, :method => :get, data: { controller: "autocomplete" } do %>
+ <%= search_field_tag :query, params[:query], :placeholder => t('layouts.application.header.search_gem_html'), autofocus: current_page?(root_url), :id => 'home_query', :class => "home__search", :autocomplete => "off", data: { autocomplete_target: "query" } %>
+
<%= label_tag :home_query do %>
<%= t('layouts.application.header.search_gem_html') %>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 0753cbb8f9e..24d9bd21d84 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -21,29 +21,30 @@
<%= render "layouts/feeds" %>
<%= csrf_meta_tag %>
<%= yield :head %>
+ <%= javascript_importmap_tags %>
-
+
<% if content_for?(:banner) %>
<%= yield :banner %>
<% end %>
-