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 = ""; - 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 = ""; + 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.exclusive_scopes"), class: "form__label" %> + <% ApiKey::EXCLUSIVE_SCOPES.each do |api_scope| %> +
    + <%= 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 %> +
    <%= 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 @@
    <%= label_tag :rubygem_id, t(".rubygem_scope"), class: "form__label" %>

    <%= t(".rubygem_scope_info") %>

    - <%= f.collection_select :rubygem_id, current_user.rubygems.by_name, :id, :name, { include_blank: t("api_keys.all_gems") }, selected: :rubygem_id, class: "form__input form__select" %> + <%= f.collection_select :rubygem_id, current_user.rubygems.by_name, :id, :name, { include_blank: t("api_keys.all_gems") }, selected: :rubygem_id, class: "form__input form__select", data: { api_key_form_target: "gemSelector" } %>
    <% 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) %> <% end %> -
    +
    - <%= link_to(root_path, title: 'RubyGems', class: 'header__logo-wrap') do %> + <%= link_to(root_path, title: 'RubyGems', class: 'header__logo-wrap', data: { nav_target: "headerLogo" }) do %> RubyGems <% end %> - + Navigation menu