diff --git a/assets/src/bundles/webapp/webapp-utils.js b/assets/src/bundles/webapp/webapp-utils.js index 34e53093..db762941 100644 --- a/assets/src/bundles/webapp/webapp-utils.js +++ b/assets/src/bundles/webapp/webapp-utils.js @@ -1,397 +1,401 @@ /** * Copyright (C) 2018-2021 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ import objectFitImages from 'object-fit-images'; import {selectText} from 'utils/functions'; import {BREAKPOINT_MD} from 'utils/constants'; let collapseSidebar = false; let previousSidebarState = localStorage.getItem('remember.lte.pushmenu'); if (previousSidebarState !== undefined) { collapseSidebar = previousSidebarState === 'sidebar-collapse'; } $(document).on('DOMContentLoaded', () => { // set state to collapsed on smaller devices if ($(window).width() < BREAKPOINT_MD) { collapseSidebar = true; } // restore previous sidebar state (collapsed/expanded) if (collapseSidebar) { // hack to avoid animated transition for collapsing sidebar // when loading a page let sidebarTransition = $('.main-sidebar, .main-sidebar:before').css('transition'); let sidebarEltsTransition = $('.sidebar .nav-link p, .main-sidebar .brand-text, .sidebar .user-panel .info').css('transition'); $('.main-sidebar, .main-sidebar:before').css('transition', 'none'); $('.sidebar .nav-link p, .main-sidebar .brand-text, .sidebar .user-panel .info').css('transition', 'none'); $('body').addClass('sidebar-collapse'); $('.swh-words-logo-swh').css('visibility', 'visible'); // restore transitions for user navigation setTimeout(() => { $('.main-sidebar, .main-sidebar:before').css('transition', sidebarTransition); $('.sidebar .nav-link p, .main-sidebar .brand-text, .sidebar .user-panel .info').css('transition', sidebarEltsTransition); }); } }); $(document).on('collapsed.lte.pushmenu', event => { if ($('body').width() >= BREAKPOINT_MD) { $('.swh-words-logo-swh').css('visibility', 'visible'); } }); $(document).on('shown.lte.pushmenu', event => { $('.swh-words-logo-swh').css('visibility', 'hidden'); }); function ensureNoFooterOverflow() { $('body').css('padding-bottom', $('footer').outerHeight() + 'px'); } $(document).ready(() => { // redirect to last browse page if any when clicking on the 'Browse' entry // in the sidebar $(`.swh-browse-link`).click(event => { let lastBrowsePage = sessionStorage.getItem('last-browse-page'); if (lastBrowsePage) { event.preventDefault(); window.location = lastBrowsePage; } }); const mainSideBar = $('.main-sidebar'); function updateSidebarState() { const body = $('body'); if (body.hasClass('sidebar-collapse') && !mainSideBar.hasClass('swh-sidebar-collapsed')) { mainSideBar.removeClass('swh-sidebar-expanded'); mainSideBar.addClass('swh-sidebar-collapsed'); $('.swh-words-logo-swh').css('visibility', 'visible'); } else if (!body.hasClass('sidebar-collapse') && !mainSideBar.hasClass('swh-sidebar-expanded')) { mainSideBar.removeClass('swh-sidebar-collapsed'); mainSideBar.addClass('swh-sidebar-expanded'); $('.swh-words-logo-swh').css('visibility', 'hidden'); } // ensure correct sidebar state when loading a page if (body.hasClass('hold-transition')) { setTimeout(() => { updateSidebarState(); }); } } // set sidebar state after collapse / expand animation mainSideBar.on('transitionend', evt => { updateSidebarState(); }); updateSidebarState(); // ensure footer do not overflow main content for mobile devices // or after resizing the browser window ensureNoFooterOverflow(); $(window).resize(function() { ensureNoFooterOverflow(); if ($('body').hasClass('sidebar-collapse') && $('body').width() >= BREAKPOINT_MD) { $('.swh-words-logo-swh').css('visibility', 'visible'); } }); // activate css polyfill 'object-fit: contain' in old browsers objectFitImages(); // reparent the modals to the top navigation div in order to be able // to display them $('.swh-browse-top-navigation').append($('.modal')); let selectedCode = null; function getCodeOrPreEltUnderPointer(e) { let elts = document.elementsFromPoint(e.clientX, e.clientY); for (let elt of elts) { if (elt.nodeName === 'CODE' || elt.nodeName === 'PRE') { return elt; } } return null; } // click handler to set focus on code block for copy $(document).click(e => { selectedCode = getCodeOrPreEltUnderPointer(e); }); function selectCode(event, selectedCode) { if (selectedCode) { let hljsLnCodeElts = $(selectedCode).find('.hljs-ln-code'); if (hljsLnCodeElts.length) { selectText(hljsLnCodeElts[0], hljsLnCodeElts[hljsLnCodeElts.length - 1]); } else { selectText(selectedCode.firstChild, selectedCode.lastChild); } event.preventDefault(); } } // select the whole text of focused code block when user // double clicks or hits Ctrl+A $(document).dblclick(e => { if ((e.ctrlKey || e.metaKey)) { selectCode(e, getCodeOrPreEltUnderPointer(e)); } }); $(document).keydown(e => { if ((e.ctrlKey || e.metaKey) && e.key === 'a') { selectCode(e, selectedCode); } }); // show/hide back-to-top button let scrollThreshold = 0; scrollThreshold += $('.swh-top-bar').height() || 0; scrollThreshold += $('.navbar').height() || 0; $(window).scroll(() => { if ($(window).scrollTop() > scrollThreshold) { $('#back-to-top').css('display', 'block'); } else { $('#back-to-top').css('display', 'none'); } }); // navbar search form submission callback $('#swh-origins-search-top').submit(event => { event.preventDefault(); if (event.target.checkValidity()) { $(event.target).removeClass('was-validated'); let searchQueryText = $('#swh-origins-search-top-input').val().trim(); let queryParameters = new URLSearchParams(); queryParameters.append('q', searchQueryText); queryParameters.append('with_visit', true); queryParameters.append('with_content', true); window.location = `${Urls.browse_search()}?${queryParameters.toString()}`; } else { $(event.target).addClass('was-validated'); } }); }); export function initPage(page) { $(document).ready(() => { // set relevant sidebar link to page active $(`.swh-${page}-item`).addClass('active'); $(`.swh-${page}-link`).addClass('active'); // triggered when unloading the current page $(window).on('unload', () => { // backup current browse page if (page === 'browse') { sessionStorage.setItem('last-browse-page', window.location); } }); }); } export function initHomePage() { $(document).ready(() => { $('.swh-coverage-list').iFrameResize({heightCalculationMethod: 'taggedElement'}); fetch(Urls.stat_counters()) .then(response => response.json()) .then(data => { if (data.stat_counters && !$.isEmptyObject(data.stat_counters)) { for (let objectType of ['content', 'revision', 'origin', 'directory', 'person', 'release']) { const count = data.stat_counters[objectType]; if (count !== undefined) { $(`#swh-${objectType}-count`).html(count.toLocaleString()); } else { $(`#swh-${objectType}-count`).closest('.swh-counter-container').hide(); } } } else { $('.swh-counter').html('0'); } if (data.stat_counters_history && !$.isEmptyObject(data.stat_counters_history)) { for (let objectType of ['content', 'revision', 'origin']) { const history = data.stat_counters_history[objectType]; if (history) { swh.webapp.drawHistoryCounterGraph(`#swh-${objectType}-count-history`, history); } else { $(`#swh-${objectType}-count-history`).hide(); } } } else { $('.swh-counter-history').hide(); } }); }); initPage('home'); } export function showModalMessage(title, message) { $('#swh-web-modal-message .modal-title').text(title); $('#swh-web-modal-message .modal-content p').text(message); $('#swh-web-modal-message').modal('show'); } export function showModalConfirm(title, message, callback) { $('#swh-web-modal-confirm .modal-title').text(title); $('#swh-web-modal-confirm .modal-content p').text(message); $('#swh-web-modal-confirm #swh-web-modal-confirm-ok-btn').bind('click', () => { callback(); $('#swh-web-modal-confirm').modal('hide'); $('#swh-web-modal-confirm #swh-web-modal-confirm-ok-btn').unbind('click'); }); $('#swh-web-modal-confirm').modal('show'); } export function showModalHtml(title, html) { $('#swh-web-modal-html .modal-title').text(title); $('#swh-web-modal-html .modal-body').html(html); $('#swh-web-modal-html').modal('show'); } export function addJumpToPagePopoverToDataTable(dataTableElt) { dataTableElt.on('draw.dt', function() { $('.paginate_button.disabled').css('cursor', 'pointer'); $('.paginate_button.disabled').on('click', event => { const pageInfo = dataTableElt.page.info(); let content = ' / ${pageInfo.pages}`; $(event.target).popover({ 'title': 'Jump to page', 'content': content, 'html': true, 'placement': 'top', 'sanitizeFn': swh.webapp.filterXSS }); $(event.target).popover('show'); $('.jump-to-page').on('change', function() { $('.paginate_button.disabled').popover('hide'); const pageNumber = parseInt($(this).val()) - 1; dataTableElt.page(pageNumber).draw('page'); }); }); }); dataTableElt.on('preXhr.dt', () => { $('.paginate_button.disabled').popover('hide'); }); } let swhObjectIcons; export function setSwhObjectIcons(icons) { swhObjectIcons = icons; } export function getSwhObjectIcon(swhObjectType) { return swhObjectIcons[swhObjectType]; } let browsedSwhObjectMetadata = {}; export function setBrowsedSwhObjectMetadata(metadata) { browsedSwhObjectMetadata = metadata; } export function getBrowsedSwhObjectMetadata() { return browsedSwhObjectMetadata; } // This will contain a mapping between an archived object type // and its related SWHID metadata for each object reachable from // the current browse view. // SWHID metadata contain the following keys: // * object_type: type of archived object // * object_id: sha1 object identifier // * swhid: SWHID without contextual info // * swhid_url: URL to resolve SWHID without contextual info // * context: object describing SWHID context // * swhid_with_context: SWHID with contextual info // * swhid_with_context_url: URL to resolve SWHID with contextual info let swhidsContext_ = {}; export function setSwhIdsContext(swhidsContext) { swhidsContext_ = {}; for (let swhidContext of swhidsContext) { swhidsContext_[swhidContext.object_type] = swhidContext; } } export function getSwhIdsContext() { return swhidsContext_; } function setFullWidth(fullWidth) { if (fullWidth) { $('#swh-web-content').removeClass('container'); $('#swh-web-content').addClass('container-fluid'); } else { $('#swh-web-content').removeClass('container-fluid'); $('#swh-web-content').addClass('container'); } localStorage.setItem('swh-web-full-width', JSON.stringify(fullWidth)); $('#swh-full-width-switch').prop('checked', fullWidth); } export function fullWidthToggled(event) { setFullWidth($(event.target).prop('checked')); } export function setContainerFullWidth() { let previousFullWidthState = JSON.parse(localStorage.getItem('swh-web-full-width')); if (previousFullWidthState !== null) { setFullWidth(previousFullWidthState); } } function coreSWHIDIsLowerCase(swhid) { const qualifiersPos = swhid.indexOf(';'); let coreSWHID = swhid; if (qualifiersPos !== -1) { coreSWHID = swhid.slice(0, qualifiersPos); } return coreSWHID.toLowerCase() === coreSWHID; } export async function validateSWHIDInput(swhidInputElt) { const swhidInput = swhidInputElt.value.trim(); let customValidity = ''; if (swhidInput.toLowerCase().startsWith('swh:')) { if (coreSWHIDIsLowerCase(swhidInput)) { const resolveSWHIDUrl = Urls.api_1_resolve_swhid(swhidInput); const response = await fetch(resolveSWHIDUrl); const responseData = await response.json(); if (responseData.hasOwnProperty('exception')) { customValidity = responseData.reason; } } else { const qualifiersPos = swhidInput.indexOf(';'); if (qualifiersPos === -1) { customValidity = 'Invalid SWHID: all characters must be in lowercase. '; customValidity += `Valid SWHID is ${swhidInput.toLowerCase()}`; } else { customValidity = 'Invalid SWHID: the core part must be in lowercase. '; const coreSWHID = swhidInput.slice(0, qualifiersPos); customValidity += `Valid SWHID is ${swhidInput.replace(coreSWHID, coreSWHID.toLowerCase())}`; } } } swhidInputElt.setCustomValidity(customValidity); $(swhidInputElt).siblings('.invalid-feedback').text(customValidity); } + +export function isUserLoggedIn() { + return JSON.parse($('#swh_user_logged_in').text()); +} diff --git a/cypress/integration/admin.spec.js b/cypress/integration/admin.spec.js index 6e856ee1..bb0eddc1 100644 --- a/cypress/integration/admin.spec.js +++ b/cypress/integration/admin.spec.js @@ -1,205 +1,221 @@ /** * Copyright (C) 2019-2021 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ const $ = Cypress.$; const defaultRedirect = '/admin/origin/save/'; let url; function logout() { cy.contains('a', 'logout') .click(); } describe('Test Admin Login/logout', function() { before(function() { url = this.Urls.admin(); }); it('should redirect to default page', function() { cy.visit(url) .get('input[name="username"]') .type('admin') .get('input[name="password"]') .type('admin') .get('.container form') .submit(); cy.location('pathname') .should('be.equal', defaultRedirect); logout(); }); it('should display admin-origin-save and deposit in sidebar', function() { cy.adminLogin(); cy.visit(url); cy.get(`.sidebar a[href="${this.Urls.admin_origin_save()}"]`) .should('be.visible'); cy.get(`.sidebar a[href="${this.Urls.admin_deposit()}"]`) .should('be.visible'); logout(); }); it('should display username on top-right', function() { cy.adminLogin(); cy.visit(url); cy.get('.swh-position-right') .should('contain', 'admin'); logout(); }); + it('should get info about a user logged in from javascript', function() { + cy.window().then(win => { + expect(win.swh.webapp.isUserLoggedIn()).to.be.false; + }); + cy.adminLogin(); + cy.visit(url); + cy.window().then(win => { + expect(win.swh.webapp.isUserLoggedIn()).to.be.true; + }); + logout(); + cy.visit(url); + cy.window().then(win => { + expect(win.swh.webapp.isUserLoggedIn()).to.be.false; + }); + }); + it('should prevent unauthorized access after logout', function() { cy.visit(this.Urls.admin_origin_save()) .location('pathname') .should('be.equal', '/admin/login/'); cy.visit(this.Urls.admin_deposit()) .location('pathname') .should('be.equal', '/admin/login/'); }); it('should redirect to correct page after login', function() { // mock calls to deposit list api to avoid possible errors // while running the test cy.intercept(`${this.Urls.admin_deposit_list()}**`, { body: { data: [], recordsTotal: 0, recordsFiltered: 0, draw: 1 } }); cy.visit(this.Urls.admin_deposit()) .location('search') .should('contain', `next=${this.Urls.admin_deposit()}`); cy.adminLogin(); cy.visit(this.Urls.admin_deposit()); cy.location('pathname') .should('be.equal', this.Urls.admin_deposit()); logout(); }); }); const existingRowToSelect = 'https://bitbucket.org/'; const originUrlListTestData = [ { listType: 'authorized', originToAdd: 'git://git.archlinux.org/', originToRemove: 'https://github.com/' }, { listType: 'unauthorized', originToAdd: 'https://random.org', originToRemove: 'https://gitlab.com' } ]; const capitalize = s => s.charAt(0).toUpperCase() + s.slice(1); describe('Test Admin Origin Save Urls Filtering', function() { beforeEach(function() { cy.adminLogin(); cy.visit(this.Urls.admin_origin_save()); cy.contains('a', 'Origin urls filtering') .click() .wait(500); }); it(`should select or unselect a table row by clicking on it`, function() { cy.contains(`#swh-authorized-origin-urls tr`, existingRowToSelect) .click() .should('have.class', 'selected') .click() .should('not.have.class', 'selected'); }); originUrlListTestData.forEach(testData => { it(`should add a new origin url prefix in the ${testData.listType} list`, function() { const tabName = capitalize(testData.listType) + ' urls'; cy.contains('a', tabName) .click() .wait(500); cy.get(`#swh-${testData.listType}-origin-urls tr`).each(elt => { if ($(elt).text() === testData.originToAdd) { cy.get(elt).click(); cy.get(`#swh-remove-${testData.listType}-origin-url`).click(); } }); cy.get(`#swh-${testData.listType}-url-prefix`) .type(testData.originToAdd); cy.get(`#swh-add-${testData.listType}-origin-url`) .click(); cy.contains(`#swh-${testData.listType}-origin-urls tr`, testData.originToAdd) .should('be.visible'); cy.contains('.alert-success', `The origin url prefix has been successfully added in the ${testData.listType} list.`) .should('be.visible'); cy.get(`#swh-add-${testData.listType}-origin-url`) .click(); cy.contains('.alert-warning', `The provided origin url prefix is already registered in the ${testData.listType} list.`) .should('be.visible'); }); it(`should remove an origin url prefix from the ${testData.listType} list`, function() { const tabName = capitalize(testData.listType) + ' urls'; cy.contains('a', tabName) .click(); let originUrlMissing = true; cy.get(`#swh-${testData.listType}-origin-urls tr`).each(elt => { if ($(elt).text() === testData.originToRemove) { originUrlMissing = false; } }); if (originUrlMissing) { cy.get(`#swh-${testData.listType}-url-prefix`) .type(testData.originToRemove); cy.get(`#swh-add-${testData.listType}-origin-url`) .click(); cy.get('.alert-dismissible button').click(); } cy.contains(`#swh-${testData.listType}-origin-urls tr`, testData.originToRemove) .click(); cy.get(`#swh-remove-${testData.listType}-origin-url`).click(); cy.contains(`#swh-${testData.listType}-origin-urls tr`, testData.originToRemove) .should('not.exist'); }); }); }); diff --git a/swh/web/templates/layout.html b/swh/web/templates/layout.html index 7775b992..e97007a2 100644 --- a/swh/web/templates/layout.html +++ b/swh/web/templates/layout.html @@ -1,276 +1,278 @@ {% comment %} Copyright (C) 2015-2021 The Software Heritage developers See the AUTHORS file at the top-level directory of this distribution License: GNU Affero General Public License version 3, or any later version See top-level LICENSE file for more information {% endcomment %} {% load js_reverse %} {% load static %} {% load render_bundle from webpack_loader %} {% load swh_templatetags %} {% block title %}{% endblock %} {% render_bundle 'vendors' %} {% render_bundle 'webapp' %} + {{ request.user.is_authenticated|json_script:"swh_user_logged_in" }} + {% block header %}{% endblock %} {% if "production" in DJANGO_SETTINGS_MODULE %} {% endif %}
{% if swh_web_staging %}
Staging
{% endif %} {% block content %}{% endblock %}
{% include "includes/global-modals.html" %}
back to top