Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/authentication')
-rw-r--r--app/assets/javascripts/authentication/mount_2fa.js10
-rw-r--r--app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue174
-rw-r--r--app/assets/javascripts/authentication/two_factor_auth/constants.js11
-rw-r--r--app/assets/javascripts/authentication/two_factor_auth/index.js46
4 files changed, 239 insertions, 2 deletions
diff --git a/app/assets/javascripts/authentication/mount_2fa.js b/app/assets/javascripts/authentication/mount_2fa.js
index dd5a42fa5fc..6dead2f03db 100644
--- a/app/assets/javascripts/authentication/mount_2fa.js
+++ b/app/assets/javascripts/authentication/mount_2fa.js
@@ -13,11 +13,17 @@ export const mount2faAuthentication = () => {
};
export const mount2faRegistration = () => {
+ const el = $('#js-register-token-2fa');
+
+ if (!el.length) {
+ return;
+ }
+
if (gon.webauthn) {
- const webauthnRegister = new WebAuthnRegister($('#js-register-token-2fa'), gon.webauthn);
+ const webauthnRegister = new WebAuthnRegister(el, gon.webauthn);
webauthnRegister.start();
} else {
- const u2fRegister = new U2FRegister($('#js-register-token-2fa'), gon.u2f);
+ const u2fRegister = new U2FRegister(el, gon.u2f);
u2fRegister.start();
}
};
diff --git a/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue b/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue
new file mode 100644
index 00000000000..87502db8b82
--- /dev/null
+++ b/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue
@@ -0,0 +1,174 @@
+<script>
+import Mousetrap from 'mousetrap';
+import { GlSprintf, GlButton, GlAlert } from '@gitlab/ui';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import Tracking from '~/tracking';
+import { __ } from '~/locale';
+import {
+ COPY_BUTTON_ACTION,
+ DOWNLOAD_BUTTON_ACTION,
+ PRINT_BUTTON_ACTION,
+ TRACKING_LABEL_PREFIX,
+ RECOVERY_CODE_DOWNLOAD_FILENAME,
+ COPY_KEYBOARD_SHORTCUT,
+} from '../constants';
+
+export const i18n = {
+ pageTitle: __('Two-factor Authentication Recovery codes'),
+ alertTitle: __('Please copy, download, or print your recovery codes before proceeding.'),
+ pageDescription: __(
+ 'Should you ever lose your phone or access to your one time password secret, each of these recovery codes can be used one time each to regain access to your account. Please save them in a safe place, or you %{boldStart}will%{boldEnd} lose access to your account.',
+ ),
+ copyButton: __('Copy codes'),
+ downloadButton: __('Download codes'),
+ printButton: __('Print codes'),
+ proceedButton: __('Proceed'),
+};
+
+export default {
+ name: 'RecoveryCodes',
+ copyButtonAction: COPY_BUTTON_ACTION,
+ downloadButtonAction: DOWNLOAD_BUTTON_ACTION,
+ printButtonAction: PRINT_BUTTON_ACTION,
+ trackingLabelPrefix: TRACKING_LABEL_PREFIX,
+ recoveryCodeDownloadFilename: RECOVERY_CODE_DOWNLOAD_FILENAME,
+ i18n,
+ mousetrap: null,
+ components: { GlSprintf, GlButton, GlAlert, ClipboardButton },
+ mixins: [Tracking.mixin()],
+ props: {
+ codes: {
+ type: Array,
+ required: true,
+ },
+ profileAccountPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ proceedButtonDisabled: true,
+ };
+ },
+ computed: {
+ codesAsString() {
+ return this.codes.join('\n');
+ },
+ codeDownloadUrl() {
+ return `data:text/plain;charset=utf-8,${encodeURIComponent(this.codesAsString)}`;
+ },
+ },
+ created() {
+ this.$options.mousetrap = new Mousetrap();
+
+ this.$options.mousetrap.bind(COPY_KEYBOARD_SHORTCUT, this.handleKeyboardCopy);
+ },
+ beforeDestroy() {
+ if (!this.$options.mousetrap) {
+ return;
+ }
+
+ this.$options.mousetrap.unbind(COPY_KEYBOARD_SHORTCUT);
+ },
+ methods: {
+ handleButtonClick(action) {
+ this.proceedButtonDisabled = false;
+
+ if (action === this.$options.printButtonAction) {
+ window.print();
+ }
+
+ this.track('click_button', { label: `${this.$options.trackingLabelPrefix}${action}_button` });
+ },
+ handleKeyboardCopy() {
+ if (!window.getSelection) {
+ return;
+ }
+
+ const copiedText = window.getSelection().toString();
+
+ if (copiedText.includes(this.codesAsString)) {
+ this.proceedButtonDisabled = false;
+ this.track('copy_keyboard_shortcut', {
+ label: `${this.$options.trackingLabelPrefix}manual_copy`,
+ });
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <h3 class="page-title">
+ {{ $options.i18n.pageTitle }}
+ </h3>
+ <hr />
+ <gl-alert variant="info" :dismissible="false">
+ {{ $options.i18n.alertTitle }}
+ </gl-alert>
+ <p class="gl-mt-5">
+ <gl-sprintf :message="$options.i18n.pageDescription">
+ <template #bold="{ content }"
+ ><strong>{{ content }}</strong></template
+ >
+ </gl-sprintf>
+ </p>
+
+ <div
+ class="codes-to-print gl-my-5 gl-p-5 gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base"
+ data-testid="recovery-codes"
+ data-qa-selector="codes_content"
+ >
+ <ul class="gl-m-0 gl-pl-5">
+ <li v-for="(code, index) in codes" :key="index">
+ <span class="gl-font-monospace" data-qa-selector="code_content">{{ code }}</span>
+ </li>
+ </ul>
+ </div>
+ <div class="gl-my-n2 gl-mx-n2 gl-display-flex gl-flex-wrap">
+ <div class="gl-p-2">
+ <clipboard-button
+ :title="$options.i18n.copyButton"
+ :text="codesAsString"
+ data-qa-selector="copy_button"
+ @click="handleButtonClick($options.copyButtonAction)"
+ >
+ {{ $options.i18n.copyButton }}
+ </clipboard-button>
+ </div>
+ <div class="gl-p-2">
+ <gl-button
+ :href="codeDownloadUrl"
+ :title="$options.i18n.downloadButton"
+ icon="download"
+ :download="$options.recoveryCodeDownloadFilename"
+ @click="handleButtonClick($options.downloadButtonAction)"
+ >
+ {{ $options.i18n.downloadButton }}
+ </gl-button>
+ </div>
+ <div class="gl-p-2">
+ <gl-button
+ :title="$options.i18n.printButton"
+ @click="handleButtonClick($options.printButtonAction)"
+ >
+ {{ $options.i18n.printButton }}
+ </gl-button>
+ </div>
+ <div class="gl-p-2">
+ <gl-button
+ :href="profileAccountPath"
+ :disabled="proceedButtonDisabled"
+ :title="$options.i18n.proceedButton"
+ variant="success"
+ data-qa-selector="proceed_button"
+ data-track-event="click_button"
+ :data-track-label="`${$options.trackingLabelPrefix}proceed_button`"
+ >{{ $options.i18n.proceedButton }}</gl-button
+ >
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/authentication/two_factor_auth/constants.js b/app/assets/javascripts/authentication/two_factor_auth/constants.js
new file mode 100644
index 00000000000..35fc49c88b2
--- /dev/null
+++ b/app/assets/javascripts/authentication/two_factor_auth/constants.js
@@ -0,0 +1,11 @@
+export const COPY_BUTTON_ACTION = 'copy';
+export const DOWNLOAD_BUTTON_ACTION = 'download';
+export const PRINT_BUTTON_ACTION = 'print';
+
+export const TRACKING_LABEL_PREFIX = '2fa_recovery_codes_';
+
+export const RECOVERY_CODE_DOWNLOAD_FILENAME = 'gitlab-recovery-codes.txt';
+
+export const SUCCESS_QUERY_PARAM = 'two_factor_auth_enabled_successfully';
+
+export const COPY_KEYBOARD_SHORTCUT = 'mod+c';
diff --git a/app/assets/javascripts/authentication/two_factor_auth/index.js b/app/assets/javascripts/authentication/two_factor_auth/index.js
new file mode 100644
index 00000000000..5e59c44e8cd
--- /dev/null
+++ b/app/assets/javascripts/authentication/two_factor_auth/index.js
@@ -0,0 +1,46 @@
+import Vue from 'vue';
+import { updateHistory, removeParams } from '~/lib/utils/url_utility';
+import RecoveryCodes from './components/recovery_codes.vue';
+import { SUCCESS_QUERY_PARAM } from './constants';
+
+export const initRecoveryCodes = () => {
+ const el = document.querySelector('.js-2fa-recovery-codes');
+
+ if (!el) {
+ return false;
+ }
+
+ const { codes = '[]', profileAccountPath = '' } = el.dataset;
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(RecoveryCodes, {
+ props: {
+ codes: JSON.parse(codes),
+ profileAccountPath,
+ },
+ });
+ },
+ });
+};
+
+export const initClose2faSuccessMessage = () => {
+ const closeButton = document.querySelector('.js-close-2fa-enabled-success-alert');
+
+ if (!closeButton) {
+ return;
+ }
+
+ closeButton.addEventListener(
+ 'click',
+ () => {
+ updateHistory({
+ url: removeParams([SUCCESS_QUERY_PARAM]),
+ title: document.title,
+ replace: true,
+ });
+ },
+ { once: true },
+ );
+};