mirror of
https://github.com/joaovitoriasilva/endurain.git
synced 2026-01-09 15:57:59 -05:00
Move MFA backup codes endpoints to profile router
Removed the dedicated MFA backup codes router and integrated its endpoints into the profile router for better organization. Updated frontend to support viewing, generating, and managing MFA backup codes, including a new modal component, status display, and i18n translations. Adjusted backend validation and error handling for MFA codes to support both TOTP and backup codes.
This commit is contained in:
@@ -1,127 +0,0 @@
|
||||
import os
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Annotated, Callable
|
||||
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
Depends,
|
||||
HTTPException,
|
||||
status,
|
||||
Response,
|
||||
Request,
|
||||
)
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
import session.utils as session_utils
|
||||
import auth.security as auth_security
|
||||
import auth.utils as auth_utils
|
||||
import auth.constants as auth_constants
|
||||
import session.crud as session_crud
|
||||
import auth.password_hasher as auth_password_hasher
|
||||
import auth.token_manager as auth_token_manager
|
||||
import auth.schema as auth_schema
|
||||
|
||||
import auth.mfa_backup_codes.schema as mfa_backup_codes_schema
|
||||
import auth.mfa_backup_codes.crud as mfa_backup_codes_crud
|
||||
|
||||
import auth.identity_providers.utils as idp_utils
|
||||
|
||||
import users.user.crud as users_crud
|
||||
import users.user.utils as users_utils
|
||||
import profile.utils as profile_utils
|
||||
|
||||
import core.database as core_database
|
||||
import core.rate_limit as core_rate_limit
|
||||
import core.logger as core_logger
|
||||
|
||||
import session.rotated_refresh_tokens.utils as rotated_tokens_utils
|
||||
|
||||
# Define the API router
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/status",
|
||||
response_model=mfa_backup_codes_schema.MFABackupCodeStatus,
|
||||
)
|
||||
async def get_backup_code_status(
|
||||
token_user_id: Annotated[
|
||||
int,
|
||||
Depends(auth_security.get_sub_from_refresh_token),
|
||||
],
|
||||
db: Annotated[
|
||||
Session,
|
||||
Depends(core_database.get_db),
|
||||
],
|
||||
):
|
||||
codes = mfa_backup_codes_crud.get_user_backup_codes(token_user_id, db)
|
||||
|
||||
if not codes:
|
||||
return mfa_backup_codes_schema.MFABackupCodeStatus(
|
||||
has_codes=False,
|
||||
total=0,
|
||||
unused=0,
|
||||
used=0,
|
||||
created_at=None,
|
||||
)
|
||||
|
||||
unused = sum(1 for code in codes if not code.used)
|
||||
used = sum(1 for code in codes if code.used)
|
||||
created_at = codes[0].created_at if codes else None
|
||||
|
||||
return mfa_backup_codes_schema.MFABackupCodeStatus(
|
||||
has_codes=True,
|
||||
total=len(codes),
|
||||
unused=unused,
|
||||
used=used,
|
||||
created_at=created_at,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"",
|
||||
response_model=mfa_backup_codes_schema.MFABackupCodesResponse,
|
||||
)
|
||||
@core_rate_limit.limiter.limit(core_rate_limit.MFA_VERIFY_LIMIT)
|
||||
async def generate_mfa_backup_codes(
|
||||
response: Response,
|
||||
request: Request,
|
||||
token_user_id: Annotated[
|
||||
int,
|
||||
Depends(auth_security.get_sub_from_refresh_token),
|
||||
],
|
||||
password_hasher: Annotated[
|
||||
auth_password_hasher.PasswordHasher,
|
||||
Depends(auth_password_hasher.get_password_hasher),
|
||||
],
|
||||
db: Annotated[
|
||||
Session,
|
||||
Depends(core_database.get_db),
|
||||
],
|
||||
):
|
||||
user = users_crud.get_user_by_id(token_user_id, db)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
|
||||
)
|
||||
|
||||
if not user.mfa_enabled:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="MFA must be enabled to generate backup codes",
|
||||
)
|
||||
|
||||
# Generate codes (invalidates old codes)
|
||||
codes = mfa_backup_codes_crud.create_backup_codes(
|
||||
token_user_id, password_hasher, db
|
||||
)
|
||||
|
||||
# Log event
|
||||
core_logger.print_to_log(f"User {user.id} generated MFA backup codes", "info")
|
||||
|
||||
return mfa_backup_codes_schema.MFABackupCodesResponse(
|
||||
codes=codes,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
@@ -40,6 +40,18 @@ def verify_and_consume_backup_code(
|
||||
password_hasher: auth_password_hasher.PasswordHasher,
|
||||
db: Session,
|
||||
) -> bool:
|
||||
"""
|
||||
Verify and consume a backup code for MFA authentication.
|
||||
|
||||
Args:
|
||||
user_id: User ID to verify backup code for.
|
||||
code: Backup code to verify (format: XXXX-XXXX).
|
||||
password_hasher: Password hasher for verification.
|
||||
db: Database session.
|
||||
|
||||
Returns:
|
||||
True if code is valid and consumed, False otherwise.
|
||||
"""
|
||||
# Get all unused codes for this user
|
||||
unused_codes = mfa_backup_codes_crud.get_user_unused_backup_codes(user_id, db)
|
||||
|
||||
|
||||
@@ -234,8 +234,8 @@ async def verify_mfa_and_login(
|
||||
# Record failed attempt and apply lockout if threshold exceeded
|
||||
failed_count = pending_mfa_store.record_failed_attempt(mfa_request.username)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=f"Invalid MFA code. Failed attempts: {failed_count}",
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid MFA code, backup code or backup code already used. Failed attempts: {failed_count}.",
|
||||
)
|
||||
|
||||
# Get the user and complete login
|
||||
|
||||
@@ -21,12 +21,16 @@ class MFALoginRequest(BaseModel):
|
||||
Schema for Multi-Factor Authentication (MFA) login request.
|
||||
|
||||
Attributes:
|
||||
username (str): The username of the user attempting to log in. Must be between 1 and 250 characters.
|
||||
mfa_code (str): The 6-digit MFA code provided by the user. Must match the pattern: six consecutive digits.
|
||||
username: Username of the user attempting to log in.
|
||||
mfa_code: Either a 6-digit TOTP code or a backup code
|
||||
(XXXX-XXXX format).
|
||||
"""
|
||||
|
||||
username: str = Field(..., min_length=1, max_length=250)
|
||||
mfa_code: str = Field(..., pattern=r"^\d{6}$")
|
||||
mfa_code: str = Field(
|
||||
...,
|
||||
pattern=r"^(\d{6}|[A-Z0-9]{4}-[A-Z0-9]{4})$",
|
||||
)
|
||||
|
||||
|
||||
class MFARequiredResponse(BaseModel):
|
||||
|
||||
@@ -16,7 +16,6 @@ import activities.activity_summaries.router as activity_summaries_router
|
||||
import activities.activity_workout_steps.router as activity_workout_steps_router
|
||||
import activities.activity_workout_steps.public_router as activity_workout_steps_public_router
|
||||
import auth.router as auth_router
|
||||
import auth.mfa_backup_codes.router as mfa_backup_codes_router
|
||||
import auth.identity_providers.router as identity_providers_router
|
||||
import auth.identity_providers.public_router as identity_providers_public_router
|
||||
import auth.security as auth_security
|
||||
@@ -103,12 +102,6 @@ router.include_router(
|
||||
prefix=core_config.ROOT_PATH + "/auth",
|
||||
tags=["auth"],
|
||||
)
|
||||
router.include_router(
|
||||
mfa_backup_codes_router.router,
|
||||
prefix=core_config.ROOT_PATH + "/auth/mfa/backup-codes",
|
||||
tags=["auth"],
|
||||
dependencies=[Depends(auth_security.validate_access_token)],
|
||||
)
|
||||
router.include_router(
|
||||
followers_router.router,
|
||||
prefix=core_config.ROOT_PATH + "/followers",
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile
|
||||
from datetime import datetime, timezone
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
Depends,
|
||||
HTTPException,
|
||||
status,
|
||||
UploadFile,
|
||||
Response,
|
||||
Request,
|
||||
)
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -28,11 +37,16 @@ import profile.import_service as profile_import_service
|
||||
import profile.exceptions as profile_exceptions
|
||||
|
||||
import auth.security as auth_security
|
||||
|
||||
import auth.mfa_backup_codes.schema as mfa_backup_codes_schema
|
||||
import auth.mfa_backup_codes.crud as mfa_backup_codes_crud
|
||||
|
||||
import session.crud as session_crud
|
||||
import auth.password_hasher as auth_password_hasher
|
||||
|
||||
import core.database as core_database
|
||||
import core.logger as core_logger
|
||||
import core.rate_limit as core_rate_limit
|
||||
import core.config as core_config
|
||||
|
||||
import websocket.schema as websocket_schema
|
||||
@@ -594,6 +608,44 @@ async def get_mfa_status(
|
||||
return profile_schema.MFAStatusResponse(mfa_enabled=is_enabled)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/mfa/backup-codes/status",
|
||||
response_model=mfa_backup_codes_schema.MFABackupCodeStatus,
|
||||
)
|
||||
async def get_backup_code_status(
|
||||
token_user_id: Annotated[
|
||||
int,
|
||||
Depends(auth_security.get_sub_from_access_token),
|
||||
],
|
||||
db: Annotated[
|
||||
Session,
|
||||
Depends(core_database.get_db),
|
||||
],
|
||||
):
|
||||
codes = mfa_backup_codes_crud.get_user_backup_codes(token_user_id, db)
|
||||
|
||||
if not codes:
|
||||
return mfa_backup_codes_schema.MFABackupCodeStatus(
|
||||
has_codes=False,
|
||||
total=0,
|
||||
unused=0,
|
||||
used=0,
|
||||
created_at=None,
|
||||
)
|
||||
|
||||
unused = sum(1 for code in codes if not code.used)
|
||||
used = sum(1 for code in codes if code.used)
|
||||
created_at = codes[0].created_at if codes else None
|
||||
|
||||
return mfa_backup_codes_schema.MFABackupCodeStatus(
|
||||
has_codes=True,
|
||||
total=len(codes),
|
||||
unused=unused,
|
||||
used=used,
|
||||
created_at=created_at,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/mfa/setup", response_model=profile_schema.MFASetupResponse)
|
||||
async def setup_mfa(
|
||||
token_user_id: Annotated[
|
||||
@@ -742,6 +794,54 @@ async def verify_mfa(
|
||||
return {"message": "MFA code verified successfully"}
|
||||
|
||||
|
||||
@router.post(
|
||||
"/mfa/backup-codes",
|
||||
response_model=mfa_backup_codes_schema.MFABackupCodesResponse,
|
||||
)
|
||||
@core_rate_limit.limiter.limit(core_rate_limit.MFA_VERIFY_LIMIT)
|
||||
async def generate_mfa_backup_codes(
|
||||
response: Response,
|
||||
request: Request,
|
||||
token_user_id: Annotated[
|
||||
int,
|
||||
Depends(auth_security.get_sub_from_access_token),
|
||||
],
|
||||
password_hasher: Annotated[
|
||||
auth_password_hasher.PasswordHasher,
|
||||
Depends(auth_password_hasher.get_password_hasher),
|
||||
],
|
||||
db: Annotated[
|
||||
Session,
|
||||
Depends(core_database.get_db),
|
||||
],
|
||||
):
|
||||
user = users_crud.get_user_by_id(token_user_id, db)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
|
||||
)
|
||||
|
||||
if not user.mfa_enabled:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="MFA must be enabled to generate backup codes",
|
||||
)
|
||||
|
||||
# Generate codes (invalidates old codes)
|
||||
codes = mfa_backup_codes_crud.create_backup_codes(
|
||||
token_user_id, password_hasher, db
|
||||
)
|
||||
|
||||
# Log event
|
||||
core_logger.print_to_log(f"User {user.id} generated MFA backup codes", "info")
|
||||
|
||||
return mfa_backup_codes_schema.MFABackupCodesResponse(
|
||||
codes=codes,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
|
||||
# Identity Provider Management Endpoints
|
||||
@router.get(
|
||||
"/idp",
|
||||
|
||||
@@ -335,8 +335,8 @@ def verify_user_mfa(
|
||||
if not user.mfa_enabled or not user.mfa_secret:
|
||||
return False
|
||||
|
||||
# Normalize code (remove dashes, uppercase)
|
||||
normalized_code = mfa_code.strip().replace("-", "").upper()
|
||||
# Normalize code (remove whitespaces in the beginning and end, uppercase)
|
||||
normalized_code = mfa_code.strip().upper()
|
||||
|
||||
# Try TOTP first (6 digits)
|
||||
if len(normalized_code) == 6 and normalized_code.isdigit():
|
||||
@@ -357,15 +357,12 @@ def verify_user_mfa(
|
||||
)
|
||||
return False
|
||||
|
||||
# Try backup code (8 alphanumeric characters)
|
||||
elif len(normalized_code) == 8:
|
||||
# Try backup code (9 alphanumeric characters with dash XXXX-XXXX)
|
||||
elif len(normalized_code) == 9 and normalized_code[4] == "-":
|
||||
try:
|
||||
if mfa_backup_codes_utils.verify_and_consume_backup_code(
|
||||
user_id, normalized_code, password_hasher, db
|
||||
):
|
||||
core_logger.print_to_log(
|
||||
f"User {user_id} verified MFA with backup code", "warning"
|
||||
)
|
||||
return True
|
||||
except Exception as err:
|
||||
core_logger.print_to_log(
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
<template>
|
||||
<div
|
||||
ref="modalRef"
|
||||
class="modal fade"
|
||||
:id="modalId"
|
||||
tabindex="-1"
|
||||
:aria-labelledby="`${modalId}Title`"
|
||||
aria-hidden="true"
|
||||
data-bs-backdrop="static"
|
||||
data-bs-keyboard="false"
|
||||
>
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h1 class="modal-title fs-5" :id="`${modalId}Title`">{{ title }}</h1>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Warning Alert -->
|
||||
<div class="alert alert-warning d-flex align-items-start" role="alert">
|
||||
<font-awesome-icon :icon="['fas', 'triangle-exclamation']" class="me-2 mt-1" />
|
||||
<div>
|
||||
<strong>{{ warningTitle }}</strong>
|
||||
<p class="mb-0 mt-1">{{ warningMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<p>{{ description }}</p>
|
||||
|
||||
<!-- Backup Codes Grid -->
|
||||
<div class="row g-2 mb-3">
|
||||
<div v-for="(code, index) in codes" :key="index" class="col-6 col-md-4">
|
||||
<div class="bg-body-tertiary rounded p-2 text-center font-monospace">
|
||||
{{ code }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="d-flex flex-wrap gap-2 mb-3">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
@click="copyAllCodes"
|
||||
:aria-label="copyButtonText"
|
||||
>
|
||||
<font-awesome-icon :icon="['fas', 'copy']" class="me-1" />
|
||||
{{ copyButtonText }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
@click="downloadCodes"
|
||||
:aria-label="downloadButtonText"
|
||||
>
|
||||
<font-awesome-icon :icon="['fas', 'download']" class="me-1" />
|
||||
{{ downloadButtonText }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Copy Success Message -->
|
||||
<div v-if="copySuccess" class="alert alert-success py-2" role="alert">
|
||||
<font-awesome-icon :icon="['fas', 'check']" class="me-1" />
|
||||
{{ copySuccessMessage }}
|
||||
</div>
|
||||
|
||||
<!-- Confirmation Checkbox -->
|
||||
<div class="form-check mb-3">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
:id="`${modalId}Confirmation`"
|
||||
v-model="confirmed"
|
||||
/>
|
||||
<label class="form-check-label" :for="`${modalId}Confirmation`">
|
||||
{{ confirmationText }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Close Button -->
|
||||
<div class="d-flex justify-content-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
:disabled="!confirmed"
|
||||
@click="handleClose"
|
||||
:aria-label="closeButtonText"
|
||||
>
|
||||
{{ closeButtonText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Vue composition API
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
// Composables
|
||||
import { useBootstrapModal } from '@/composables/useBootstrapModal'
|
||||
|
||||
const props = defineProps({
|
||||
modalId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
warningTitle: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
warningMessage: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
codes: {
|
||||
type: Array as () => string[],
|
||||
required: true
|
||||
},
|
||||
copyButtonText: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
downloadButtonText: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
copySuccessMessage: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
confirmationText: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
closeButtonText: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
downloadFilename: {
|
||||
type: String,
|
||||
default: 'endurain-backup-codes.txt'
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
closed: []
|
||||
}>()
|
||||
|
||||
const { initializeModal, showModal, hideModal, disposeModal } = useBootstrapModal()
|
||||
|
||||
const modalRef = ref<HTMLDivElement | null>(null)
|
||||
const confirmed = ref(false)
|
||||
const copySuccess = ref(false)
|
||||
|
||||
/**
|
||||
* Copies all backup codes to clipboard.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
const copyAllCodes = async (): Promise<void> => {
|
||||
try {
|
||||
const codesText = props.codes.join('\n')
|
||||
await navigator.clipboard.writeText(codesText)
|
||||
copySuccess.value = true
|
||||
setTimeout(() => {
|
||||
copySuccess.value = false
|
||||
}, 3000)
|
||||
} catch (error) {
|
||||
console.error('Failed to copy codes:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads backup codes as a text file.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
const downloadCodes = (): void => {
|
||||
const codesText = `Endurain MFA Backup Codes\n${'='.repeat(30)}\n\n${props.codes.join('\n')}\n\n${'='.repeat(30)}\nStore these codes in a secure location.\nEach code can only be used once.`
|
||||
|
||||
const blob = new Blob([codesText], { type: 'text/plain' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = props.downloadFilename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles modal close and resets state.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
const handleClose = (): void => {
|
||||
hideModal()
|
||||
emit('closed')
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets modal state when hidden.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
const handleModalHidden = (): void => {
|
||||
confirmed.value = false
|
||||
copySuccess.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the modal.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
const show = (): void => {
|
||||
showModal()
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the modal.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
const hide = (): void => {
|
||||
hideModal()
|
||||
handleModalHidden()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await initializeModal(modalRef)
|
||||
|
||||
if (modalRef.value) {
|
||||
modalRef.value.addEventListener('hidden.bs.modal', handleModalHidden)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (modalRef.value) {
|
||||
modalRef.value.removeEventListener('hidden.bs.modal', handleModalHidden)
|
||||
}
|
||||
disposeModal()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
hide
|
||||
})
|
||||
</script>
|
||||
@@ -125,15 +125,47 @@
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>{{ $t('settingsSecurityZone.mfaEnabledDescription') }}</p>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-danger"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#mfaDisableModal"
|
||||
:disabled="mfaDisableLoading"
|
||||
>
|
||||
{{ $t('settingsSecurityZone.disableMFAButton') }}
|
||||
</button>
|
||||
|
||||
<!-- Backup Codes Status -->
|
||||
<div v-if="backupCodeStatusLoading" class="mb-3">
|
||||
<LoadingComponent />
|
||||
</div>
|
||||
<div v-else-if="backupCodeStatus" class="alert alert-info mb-3" role="alert">
|
||||
<font-awesome-icon :icon="['fas', 'key']" class="me-2" />
|
||||
{{
|
||||
$t('settingsSecurityZone.backupCodesRemaining', {
|
||||
remaining: backupCodeStatus.unused,
|
||||
total: backupCodeStatus.total
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
@click="regenerateBackupCodes"
|
||||
:disabled="regenerateBackupCodesLoading"
|
||||
>
|
||||
<span
|
||||
v-if="regenerateBackupCodesLoading"
|
||||
class="spinner-border spinner-border-sm me-2"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<font-awesome-icon v-else :icon="['fas', 'rotate']" class="me-1" />
|
||||
{{ $t('settingsSecurityZone.regenerateBackupCodesButton') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-danger"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#mfaDisableModal"
|
||||
:disabled="mfaDisableLoading"
|
||||
>
|
||||
{{ $t('settingsSecurityZone.disableMFAButton') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -167,6 +199,23 @@
|
||||
@numberToEmitAction="disableMFA"
|
||||
/>
|
||||
|
||||
<!-- MFA Backup Codes Modal -->
|
||||
<ModalComponentMFABackupCodes
|
||||
ref="mfaBackupCodesModalRef"
|
||||
modalId="mfaBackupCodesModal"
|
||||
:title="t('settingsSecurityZone.backupCodesModalTitle')"
|
||||
:description="t('settingsSecurityZone.backupCodesModalDescription')"
|
||||
:warningTitle="t('settingsSecurityZone.backupCodesWarningTitle')"
|
||||
:warningMessage="t('settingsSecurityZone.backupCodesWarningMessage')"
|
||||
:codes="backupCodes"
|
||||
:copyButtonText="t('settingsSecurityZone.backupCodesCopyAll')"
|
||||
:downloadButtonText="t('settingsSecurityZone.backupCodesDownload')"
|
||||
:copySuccessMessage="t('settingsSecurityZone.backupCodesCopySuccess')"
|
||||
:confirmationText="t('settingsSecurityZone.backupCodesSaveConfirmation')"
|
||||
:closeButtonText="t('settingsSecurityZone.backupCodesClose')"
|
||||
@closed="onBackupCodesModalClosed"
|
||||
/>
|
||||
|
||||
<hr />
|
||||
<!-- Linked Accounts (Identity Providers) -->
|
||||
<h4>{{ $t('settingsSecurityZone.subtitleLinkedAccounts') }}</h4>
|
||||
@@ -246,6 +295,7 @@ import { push } from 'notivue'
|
||||
import UsersPasswordRequirementsComponent from '@/components/Settings/SettingsUsersZone/UsersPasswordRequirementsComponent.vue'
|
||||
import ModalComponentNumberInput from '@/components/Modals/ModalComponentNumberInput.vue'
|
||||
import ModalComponentMFASetup from '@/components/Modals/ModalComponentMFASetup.vue'
|
||||
import ModalComponentMFABackupCodes from '@/components/Modals/ModalComponentMFABackupCodes.vue'
|
||||
import UserIdentityProviderListComponent from '@/components/Settings/SettingsUsersZone/UserIdentityProviderListComponent.vue'
|
||||
// Importing validation utilities
|
||||
import { isValidPassword, passwordsMatch } from '@/utils/validationUtils'
|
||||
@@ -282,6 +332,11 @@ const mfaDisableLoading = ref(false)
|
||||
const qrCodeData = ref('')
|
||||
const mfaSecret = ref('')
|
||||
const mfaSetupModalRef = ref(null)
|
||||
const mfaBackupCodesModalRef = ref(null)
|
||||
const backupCodes = ref([])
|
||||
const backupCodeStatus = ref(null)
|
||||
const backupCodeStatusLoading = ref(false)
|
||||
const regenerateBackupCodesLoading = ref(false)
|
||||
|
||||
const showNewPassword = ref(false)
|
||||
const showNewPasswordRepeat = ref(false)
|
||||
@@ -371,12 +426,18 @@ async function setupMFA() {
|
||||
async function enableMFA(verificationCode) {
|
||||
try {
|
||||
mfaEnableLoading.value = true
|
||||
await profile.enableMFA({ mfa_code: verificationCode })
|
||||
const response = await profile.enableMFA({ mfa_code: verificationCode })
|
||||
mfaEnabled.value = true
|
||||
mfaSetupModalRef.value?.hide()
|
||||
qrCodeData.value = ''
|
||||
mfaSecret.value = ''
|
||||
push.success(t('settingsSecurityZone.mfaEnabledSuccess'))
|
||||
|
||||
// Show backup codes modal if codes are returned
|
||||
if (response.backup_codes && response.backup_codes.length > 0) {
|
||||
backupCodes.value = response.backup_codes
|
||||
mfaBackupCodesModalRef.value?.show()
|
||||
}
|
||||
} catch (error) {
|
||||
push.error(`${t('settingsSecurityZone.errorEnableMFA')} - ${error}`)
|
||||
} finally {
|
||||
@@ -391,6 +452,7 @@ async function disableMFA(mfaCode) {
|
||||
mfaDisableLoading.value = true
|
||||
await profile.disableMFA({ mfa_code: mfaCode.toString() })
|
||||
mfaEnabled.value = false
|
||||
backupCodeStatus.value = null
|
||||
push.success(t('settingsSecurityZone.mfaDisabledSuccess'))
|
||||
} catch (error) {
|
||||
push.error(`${t('settingsSecurityZone.errorDisableMFA')} - ${error}`)
|
||||
@@ -399,6 +461,42 @@ async function disableMFA(mfaCode) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBackupCodeStatus() {
|
||||
if (!mfaEnabled.value) return
|
||||
|
||||
try {
|
||||
backupCodeStatusLoading.value = true
|
||||
backupCodeStatus.value = await profile.getBackupCodeStatus()
|
||||
} catch (error) {
|
||||
push.error(`${t('settingsSecurityZone.errorLoadBackupCodeStatus')} - ${error}`)
|
||||
} finally {
|
||||
backupCodeStatusLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function regenerateBackupCodes() {
|
||||
try {
|
||||
regenerateBackupCodesLoading.value = true
|
||||
const response = await profile.regenerateBackupCodes()
|
||||
|
||||
if (response.codes && response.codes.length > 0) {
|
||||
backupCodes.value = response.codes
|
||||
mfaBackupCodesModalRef.value?.show()
|
||||
push.success(t('settingsSecurityZone.successRegenerateBackupCodes'))
|
||||
}
|
||||
} catch (error) {
|
||||
push.error(`${t('settingsSecurityZone.errorRegenerateBackupCodes')} - ${error}`)
|
||||
} finally {
|
||||
regenerateBackupCodesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onBackupCodesModalClosed() {
|
||||
backupCodes.value = []
|
||||
// Refresh backup code status after modal is closed
|
||||
loadBackupCodeStatus()
|
||||
}
|
||||
|
||||
const getProviderCustomLogo = (iconName) => {
|
||||
if (!iconName) return null
|
||||
const logoPath = PROVIDER_CUSTOM_LOGO_MAP[iconName.toLowerCase()]
|
||||
@@ -486,6 +584,9 @@ onMounted(async () => {
|
||||
// Load MFA status
|
||||
await loadMFAStatus()
|
||||
|
||||
// Load backup code status if MFA is enabled
|
||||
await loadBackupCodeStatus()
|
||||
|
||||
// Load linked accounts
|
||||
await loadLinkedAccounts()
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ export const extractStatusCode = (error: unknown): number | null => {
|
||||
|
||||
// Fallback: try to extract from error string
|
||||
const errorString = error?.toString() || ''
|
||||
if (errorString.includes('400')) return HTTP_STATUS.BAD_REQUEST
|
||||
if (errorString.includes('401')) return HTTP_STATUS.UNAUTHORIZED
|
||||
if (errorString.includes('403')) return HTTP_STATUS.FORBIDDEN
|
||||
if (errorString.includes('404')) return HTTP_STATUS.NOT_FOUND
|
||||
|
||||
@@ -23,6 +23,20 @@
|
||||
"errorSetupMFA": "Error configurant MFA",
|
||||
"errorEnableMFA": "Error habilitant MFA",
|
||||
"errorDisableMFA": "Error deshabilitant MFA",
|
||||
"backupCodesModalTitle": "Codis de Reserva",
|
||||
"backupCodesModalDescription": "Desa aquests codis de reserva en un lloc segur. Cada codi només es pot utilitzar una vegada per iniciar sessió si perds l'accés a la teva aplicació d'autenticació.",
|
||||
"backupCodesWarningTitle": "Important",
|
||||
"backupCodesWarningMessage": "Aquests codis només es mostraran una vegada. Desa'ls de manera segura - no els podràs recuperar després.",
|
||||
"backupCodesCopyAll": "Copiar Tot",
|
||||
"backupCodesDownload": "Descarregar",
|
||||
"backupCodesCopySuccess": "Codis de reserva copiats al porta-retalls",
|
||||
"backupCodesSaveConfirmation": "He desat aquests codis en un lloc segur",
|
||||
"backupCodesClose": "Tancar",
|
||||
"backupCodesRemaining": "{remaining} de {total} codis de reserva restants",
|
||||
"regenerateBackupCodesButton": "Regenerar Codis de Reserva",
|
||||
"successRegenerateBackupCodes": "Codis de reserva regenerats correctament",
|
||||
"errorRegenerateBackupCodes": "Error regenerant codis de reserva",
|
||||
"errorLoadBackupCodeStatus": "Error carregant l'estat dels codis de reserva",
|
||||
"subtitleMySessions": "Sessions",
|
||||
"userChangePasswordSuccessMessage": "Contrasenya canviada correctament",
|
||||
"userChangePasswordErrorMessage": "Error canviant contrasenya",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"hidePassword": "Amaga contrasenya",
|
||||
"showPassword": "Mostra contrasenya",
|
||||
"mfaCode": "Codi MFA",
|
||||
"mfaCodeHint": "Introdueix el codi de 6 dígits de la teva aplicació d'autenticació, o fes servir un codi de reserva",
|
||||
"mfaRequired": "Autenticació multifactor requerida. Si us plau entra el codi de 6 dígits.",
|
||||
"verifyMFAButton": "Verificar MFA",
|
||||
"invalidMFACode": "Codi MFA invàlid. Torna a provar.",
|
||||
|
||||
@@ -23,6 +23,20 @@
|
||||
"errorSetupMFA": "设置多重身份验证时出错",
|
||||
"errorEnableMFA": "启用多重身份验证时出错",
|
||||
"errorDisableMFA": "停用多重身份验证时出错",
|
||||
"backupCodesModalTitle": "备用码",
|
||||
"backupCodesModalDescription": "请将这些备用码保存在安全的地方。如果您无法访问验证器应用,每个码只能使用一次来登录。",
|
||||
"backupCodesWarningTitle": "重要提示",
|
||||
"backupCodesWarningMessage": "这些码只会显示一次。请安全保存 - 之后无法再次获取。",
|
||||
"backupCodesCopyAll": "全部复制",
|
||||
"backupCodesDownload": "下载",
|
||||
"backupCodesCopySuccess": "备用码已复制到剪贴板",
|
||||
"backupCodesSaveConfirmation": "我已将这些码保存在安全的地方",
|
||||
"backupCodesClose": "关闭",
|
||||
"backupCodesRemaining": "剩余 {remaining}/{total} 个备用码",
|
||||
"regenerateBackupCodesButton": "重新生成备用码",
|
||||
"successRegenerateBackupCodes": "备用码重新生成成功",
|
||||
"errorRegenerateBackupCodes": "重新生成备用码时出错",
|
||||
"errorLoadBackupCodeStatus": "加载备用码状态时出错",
|
||||
"subtitleMySessions": "我的会话",
|
||||
"userChangePasswordSuccessMessage": "密码修改成功",
|
||||
"userChangePasswordErrorMessage": "修改密码时出错",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"hidePassword": "Hide password",
|
||||
"showPassword": "Show password",
|
||||
"mfaCode": "两步验证代码",
|
||||
"mfaCodeHint": "输入您验证器应用中的 6 位数字代码,或使用备用码",
|
||||
"mfaRequired": "需要两步验证。请输入您的 6 位数字代码。",
|
||||
"verifyMFAButton": "验证 MFA",
|
||||
"invalidMFACode": "无效的两步验证代码,请重试。",
|
||||
|
||||
@@ -23,6 +23,20 @@
|
||||
"errorSetupMFA": "Einrichten der MFA fehlgeschlagen",
|
||||
"errorEnableMFA": "Aktivieren der MFA fehlgeschlagen",
|
||||
"errorDisableMFA": "Deaktivieren der MFA fehlgeschlagen",
|
||||
"backupCodesModalTitle": "Backup-Codes",
|
||||
"backupCodesModalDescription": "Speichern Sie diese Backup-Codes an einem sicheren Ort. Jeder Code kann nur einmal verwendet werden, um sich anzumelden, wenn Sie den Zugang zu Ihrer Authentifizierungs-App verlieren.",
|
||||
"backupCodesWarningTitle": "Wichtig",
|
||||
"backupCodesWarningMessage": "Diese Codes werden nur einmal angezeigt. Bewahren Sie sie sicher auf - Sie können sie später nicht mehr abrufen.",
|
||||
"backupCodesCopyAll": "Alle Kopieren",
|
||||
"backupCodesDownload": "Herunterladen",
|
||||
"backupCodesCopySuccess": "Backup-Codes in die Zwischenablage kopiert",
|
||||
"backupCodesSaveConfirmation": "Ich habe diese Codes an einem sicheren Ort gespeichert",
|
||||
"backupCodesClose": "Schließen",
|
||||
"backupCodesRemaining": "{remaining} von {total} Backup-Codes übrig",
|
||||
"regenerateBackupCodesButton": "Backup-Codes Neu Generieren",
|
||||
"successRegenerateBackupCodes": "Backup-Codes erfolgreich neu generiert",
|
||||
"errorRegenerateBackupCodes": "Fehler beim Neugenerieren der Backup-Codes",
|
||||
"errorLoadBackupCodeStatus": "Fehler beim Laden des Backup-Code-Status",
|
||||
"subtitleMySessions": "Meine Sitzungen",
|
||||
"userChangePasswordSuccessMessage": "Passwort erfolgreich geändert",
|
||||
"userChangePasswordErrorMessage": "Fehler beim Ändern des Passworts",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"hidePassword": "Hide password",
|
||||
"showPassword": "Show password",
|
||||
"mfaCode": "MFA-Code",
|
||||
"mfaCodeHint": "Geben Sie den 6-stelligen Code aus Ihrer Authentifizierungs-App ein, oder verwenden Sie einen Backup-Code",
|
||||
"mfaRequired": "Mehrstufige Authentifizierung erforderlich. Bitte geben Sie Ihren 6-stelligen Code ein.",
|
||||
"verifyMFAButton": "MFA überprüfen",
|
||||
"invalidMFACode": "Ungültiger MFA-Code. Bitte versuchen Sie es erneut.",
|
||||
|
||||
@@ -23,6 +23,20 @@
|
||||
"errorSetupMFA": "Error al configurar MFA",
|
||||
"errorEnableMFA": "Error activando MFA",
|
||||
"errorDisableMFA": "Error desactivando MFA",
|
||||
"backupCodesModalTitle": "Códigos de Respaldo",
|
||||
"backupCodesModalDescription": "Guarda estos códigos de respaldo en un lugar seguro. Cada código solo puede usarse una vez para iniciar sesión si pierdes acceso a tu aplicación de autenticación.",
|
||||
"backupCodesWarningTitle": "Importante",
|
||||
"backupCodesWarningMessage": "Estos códigos solo se mostrarán una vez. Guárdalos de forma segura - no podrás recuperarlos después.",
|
||||
"backupCodesCopyAll": "Copiar Todos",
|
||||
"backupCodesDownload": "Descargar",
|
||||
"backupCodesCopySuccess": "Códigos de respaldo copiados al portapapeles",
|
||||
"backupCodesSaveConfirmation": "He guardado estos códigos en un lugar seguro",
|
||||
"backupCodesClose": "Cerrar",
|
||||
"backupCodesRemaining": "{remaining} de {total} códigos de respaldo restantes",
|
||||
"regenerateBackupCodesButton": "Regenerar Códigos de Respaldo",
|
||||
"successRegenerateBackupCodes": "Códigos de respaldo regenerados con éxito",
|
||||
"errorRegenerateBackupCodes": "Error al regenerar códigos de respaldo",
|
||||
"errorLoadBackupCodeStatus": "Error al cargar el estado de los códigos de respaldo",
|
||||
"subtitleMySessions": "Mis sesiones",
|
||||
"userChangePasswordSuccessMessage": "La contraseña ha sido cambiada correctamente",
|
||||
"userChangePasswordErrorMessage": "Error al cambiar la contraseña",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"hidePassword": "Hide password",
|
||||
"showPassword": "Show password",
|
||||
"mfaCode": "Código MFA",
|
||||
"mfaCodeHint": "Introduce el código de 6 dígitos de tu aplicación de autenticación, o usa un código de respaldo",
|
||||
"mfaRequired": "Se requiere autenticación de múltiples factores. Introduce el código de 6 dígitos.",
|
||||
"verifyMFAButton": "Verificar MFA",
|
||||
"invalidMFACode": "Código MFA no válido. Inténtalo de nuevo.",
|
||||
|
||||
@@ -23,6 +23,20 @@
|
||||
"errorSetupMFA": "Error setting up MFA",
|
||||
"errorEnableMFA": "Error enabling MFA",
|
||||
"errorDisableMFA": "Error disabling MFA",
|
||||
"backupCodesModalTitle": "Codes de Secours",
|
||||
"backupCodesModalDescription": "Enregistrez ces codes de secours dans un endroit sûr. Chaque code ne peut être utilisé qu'une seule fois pour vous connecter si vous perdez l'accès à votre application d'authentification.",
|
||||
"backupCodesWarningTitle": "Important",
|
||||
"backupCodesWarningMessage": "Ces codes ne seront affichés qu'une seule fois. Conservez-les en sécurité - vous ne pourrez pas les récupérer plus tard.",
|
||||
"backupCodesCopyAll": "Tout Copier",
|
||||
"backupCodesDownload": "Télécharger",
|
||||
"backupCodesCopySuccess": "Codes de secours copiés dans le presse-papiers",
|
||||
"backupCodesSaveConfirmation": "J'ai sauvegardé ces codes dans un endroit sûr",
|
||||
"backupCodesClose": "Fermer",
|
||||
"backupCodesRemaining": "{remaining} sur {total} codes de secours restants",
|
||||
"regenerateBackupCodesButton": "Régénérer les Codes de Secours",
|
||||
"successRegenerateBackupCodes": "Codes de secours régénérés avec succès",
|
||||
"errorRegenerateBackupCodes": "Erreur lors de la régénération des codes de secours",
|
||||
"errorLoadBackupCodeStatus": "Erreur lors du chargement du statut des codes de secours",
|
||||
"subtitleMySessions": "Mes sessions",
|
||||
"userChangePasswordSuccessMessage": "Le mot de passe a bien été modifié",
|
||||
"userChangePasswordErrorMessage": "Erreur lors de la modification du mot de passe",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"hidePassword": "Hide password",
|
||||
"showPassword": "Show password",
|
||||
"mfaCode": "MFA Code",
|
||||
"mfaCodeHint": "Entrez le code à 6 chiffres de votre application d'authentification, ou utilisez un code de secours",
|
||||
"mfaRequired": "Multi-factor authentication required. Please enter your 6-digit code.",
|
||||
"verifyMFAButton": "Verify MFA",
|
||||
"invalidMFACode": "Invalid MFA code. Please try again.",
|
||||
|
||||
@@ -23,6 +23,20 @@
|
||||
"errorSetupMFA": "Erro ao configurar AMF",
|
||||
"errorEnableMFA": "Erro ao activar AMF",
|
||||
"errorDisableMFA": "Erro ao desactivar AMF",
|
||||
"backupCodesModalTitle": "Códigos de Respaldo",
|
||||
"backupCodesModalDescription": "Garda estes códigos de respaldo nun lugar seguro. Cada código só pode usarse unha vez para iniciar sesión se perdes o acceso á túa aplicación de autenticación.",
|
||||
"backupCodesWarningTitle": "Importante",
|
||||
"backupCodesWarningMessage": "Estes códigos só se mostrarán unha vez. Gárdaos de forma segura - non poderás recuperalos despois.",
|
||||
"backupCodesCopyAll": "Copiar Todo",
|
||||
"backupCodesDownload": "Descargar",
|
||||
"backupCodesCopySuccess": "Códigos de respaldo copiados ao portapapeis",
|
||||
"backupCodesSaveConfirmation": "Gardei estes códigos nun lugar seguro",
|
||||
"backupCodesClose": "Pechar",
|
||||
"backupCodesRemaining": "{remaining} de {total} códigos de respaldo restantes",
|
||||
"regenerateBackupCodesButton": "Rexenerar Códigos de Respaldo",
|
||||
"successRegenerateBackupCodes": "Códigos de respaldo rexenerados correctamente",
|
||||
"errorRegenerateBackupCodes": "Erro ao rexenerar códigos de respaldo",
|
||||
"errorLoadBackupCodeStatus": "Erro ao cargar o estado dos códigos de respaldo",
|
||||
"subtitleMySessions": "As miñas sesións",
|
||||
"userChangePasswordSuccessMessage": "Contrasial cambiado correctamente",
|
||||
"userChangePasswordErrorMessage": "Erro ao cambiar o contrasinal",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"hidePassword": "Ocultar contrasinal",
|
||||
"showPassword": "Mostrar contrasinal",
|
||||
"mfaCode": "Código AMF",
|
||||
"mfaCodeHint": "Introduce o código de 6 díxitos da túa aplicación de autenticación, ou usa un código de respaldo",
|
||||
"mfaRequired": "Requírese a Autenticación Multi-Factor. Escribe o código de 6 díxitos.",
|
||||
"verifyMFAButton": "Verificar AMF",
|
||||
"invalidMFACode": "Código AMF non válido. Inténtao outra vez.",
|
||||
|
||||
@@ -23,6 +23,20 @@
|
||||
"errorSetupMFA": "Errore nella configurazione di MFA",
|
||||
"errorEnableMFA": "Errore nell'abilitazione di MFA",
|
||||
"errorDisableMFA": "Errore nella disabilitazione di MFA",
|
||||
"backupCodesModalTitle": "Codici di Backup",
|
||||
"backupCodesModalDescription": "Salva questi codici di backup in un luogo sicuro. Ogni codice può essere utilizzato solo una volta per accedere se perdi l'accesso alla tua app di autenticazione.",
|
||||
"backupCodesWarningTitle": "Importante",
|
||||
"backupCodesWarningMessage": "Questi codici verranno mostrati solo una volta. Conservali in modo sicuro - non potrai recuperarli in seguito.",
|
||||
"backupCodesCopyAll": "Copia Tutti",
|
||||
"backupCodesDownload": "Scarica",
|
||||
"backupCodesCopySuccess": "Codici di backup copiati negli appunti",
|
||||
"backupCodesSaveConfirmation": "Ho salvato questi codici in un luogo sicuro",
|
||||
"backupCodesClose": "Chiudi",
|
||||
"backupCodesRemaining": "{remaining} di {total} codici di backup rimanenti",
|
||||
"regenerateBackupCodesButton": "Rigenera Codici di Backup",
|
||||
"successRegenerateBackupCodes": "Codici di backup rigenerati con successo",
|
||||
"errorRegenerateBackupCodes": "Errore nella rigenerazione dei codici di backup",
|
||||
"errorLoadBackupCodeStatus": "Errore nel caricamento dello stato dei codici di backup",
|
||||
"subtitleMySessions": "Le mie sessioni",
|
||||
"userChangePasswordSuccessMessage": "Password cambiata con successo",
|
||||
"userChangePasswordErrorMessage": "Errore nel cambio password",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"hidePassword": "Hide password",
|
||||
"showPassword": "Show password",
|
||||
"mfaCode": "Codice MFA",
|
||||
"mfaCodeHint": "Inserisci il codice a 6 cifre dalla tua app di autenticazione, oppure usa un codice di backup",
|
||||
"mfaRequired": "Autenticazione a più fattori richiesta. Inserisci il tuo codice a 6 cifre.",
|
||||
"verifyMFAButton": "Verifica MFA",
|
||||
"invalidMFACode": "Codice MFA non valido. Riprova.",
|
||||
|
||||
@@ -23,6 +23,20 @@
|
||||
"errorSetupMFA": "Error setting up MFA",
|
||||
"errorEnableMFA": "Error enabling MFA",
|
||||
"errorDisableMFA": "Error disabling MFA",
|
||||
"backupCodesModalTitle": "Backup Codes",
|
||||
"backupCodesModalDescription": "Bewaar deze backup codes op een veilige plek. Elke code kan slechts één keer worden gebruikt om in te loggen als je geen toegang meer hebt tot je authenticatie-app.",
|
||||
"backupCodesWarningTitle": "Belangrijk",
|
||||
"backupCodesWarningMessage": "Deze codes worden slechts één keer getoond. Bewaar ze veilig - je kunt ze later niet meer ophalen.",
|
||||
"backupCodesCopyAll": "Alles Kopiëren",
|
||||
"backupCodesDownload": "Downloaden",
|
||||
"backupCodesCopySuccess": "Backup codes gekopieerd naar klembord",
|
||||
"backupCodesSaveConfirmation": "Ik heb deze codes op een veilige plek opgeslagen",
|
||||
"backupCodesClose": "Sluiten",
|
||||
"backupCodesRemaining": "{remaining} van {total} backup codes resterend",
|
||||
"regenerateBackupCodesButton": "Backup Codes Opnieuw Genereren",
|
||||
"successRegenerateBackupCodes": "Backup codes succesvol opnieuw gegenereerd",
|
||||
"errorRegenerateBackupCodes": "Fout bij het opnieuw genereren van backup codes",
|
||||
"errorLoadBackupCodeStatus": "Fout bij het laden van backup code status",
|
||||
"subtitleMySessions": "Mijn sessies",
|
||||
"userChangePasswordSuccessMessage": "Wachtwoord met succes gewijzigd",
|
||||
"userChangePasswordErrorMessage": "Fout opgetreden bij wijzigen van wachtwoord",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"hidePassword": "Hide password",
|
||||
"showPassword": "Show password",
|
||||
"mfaCode": "MFA Code",
|
||||
"mfaCodeHint": "Voer de 6-cijferige code van je authenticatie-app in, of gebruik een backup code",
|
||||
"mfaRequired": "Multi-factor authentication required. Please enter your 6-digit code.",
|
||||
"verifyMFAButton": "Verify MFA",
|
||||
"invalidMFACode": "Invalid MFA code. Please try again.",
|
||||
|
||||
@@ -23,6 +23,20 @@
|
||||
"errorSetupMFA": "Erro ao configurar MFA",
|
||||
"errorEnableMFA": "Erro ao habilitar MFA",
|
||||
"errorDisableMFA": "Erro ao desabilitar MFA",
|
||||
"backupCodesModalTitle": "Códigos de Backup",
|
||||
"backupCodesModalDescription": "Guarde estes códigos de backup num local seguro. Cada código só pode ser usado uma vez para iniciar sessão se perder acesso à sua aplicação de autenticação.",
|
||||
"backupCodesWarningTitle": "Importante",
|
||||
"backupCodesWarningMessage": "Estes códigos só serão mostrados uma vez. Guarde-os em segurança - não poderá recuperá-los depois.",
|
||||
"backupCodesCopyAll": "Copiar Todos",
|
||||
"backupCodesDownload": "Transferir",
|
||||
"backupCodesCopySuccess": "Códigos de backup copiados para a área de transferência",
|
||||
"backupCodesSaveConfirmation": "Guardei estes códigos num local seguro",
|
||||
"backupCodesClose": "Fechar",
|
||||
"backupCodesRemaining": "{remaining} de {total} códigos de backup restantes",
|
||||
"regenerateBackupCodesButton": "Regenerar Códigos de Backup",
|
||||
"successRegenerateBackupCodes": "Códigos de backup regenerados com sucesso",
|
||||
"errorRegenerateBackupCodes": "Erro ao regenerar códigos de backup",
|
||||
"errorLoadBackupCodeStatus": "Erro ao carregar o estado dos códigos de backup",
|
||||
"subtitleMySessions": "As minhas sessões",
|
||||
"userChangePasswordSuccessMessage": "Palavra-passe alterada com sucesso",
|
||||
"userChangePasswordErrorMessage": "Erro ao alterar palavra-passe",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"hidePassword": "Esconder palavra-passe",
|
||||
"showPassword": "Mostrar palavra-passe",
|
||||
"mfaCode": "Código MFA",
|
||||
"mfaCodeHint": "Insira o código de 6 dígitos da sua aplicação de autenticação, ou use um código de backup",
|
||||
"mfaRequired": "Autenticação multi-fator necessária. Insira o seu código de 6 dígitos.",
|
||||
"verifyMFAButton": "Verificar MFA",
|
||||
"invalidMFACode": "Código MFA inválido. Por favor, tente novamente.",
|
||||
|
||||
@@ -23,6 +23,20 @@
|
||||
"errorSetupMFA": "Napaka pri nastavitvi MFA",
|
||||
"errorEnableMFA": "Napaka pri omogočanju MFA",
|
||||
"errorDisableMFA": "Napaka pri onemogočanju MFA",
|
||||
"backupCodesModalTitle": "Rezervne Kode",
|
||||
"backupCodesModalDescription": "Shranite te rezervne kode na varno mesto. Vsako kodo lahko uporabite samo enkrat za prijavo, če izgubite dostop do aplikacije za preverjanje pristnosti.",
|
||||
"backupCodesWarningTitle": "Pomembno",
|
||||
"backupCodesWarningMessage": "Te kode bodo prikazane samo enkrat. Shranite jih varno - pozneje jih ne boste mogli pridobiti.",
|
||||
"backupCodesCopyAll": "Kopiraj Vse",
|
||||
"backupCodesDownload": "Prenesi",
|
||||
"backupCodesCopySuccess": "Rezervne kode kopirane v odložišče",
|
||||
"backupCodesSaveConfirmation": "Te kode sem shranil na varno mesto",
|
||||
"backupCodesClose": "Zapri",
|
||||
"backupCodesRemaining": "{remaining} od {total} rezervnih kod preostalo",
|
||||
"regenerateBackupCodesButton": "Ponovno Ustvari Rezervne Kode",
|
||||
"successRegenerateBackupCodes": "Rezervne kode uspešno ponovno ustvarjene",
|
||||
"errorRegenerateBackupCodes": "Napaka pri ponovnem ustvarjanju rezervnih kod",
|
||||
"errorLoadBackupCodeStatus": "Napaka pri nalaganju statusa rezervnih kod",
|
||||
"subtitleMySessions": "Moje seje",
|
||||
"userChangePasswordSuccessMessage": "Geslo uspešno spremenjeno",
|
||||
"userChangePasswordErrorMessage": "Napaka pri spreminjanju gesla",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"hidePassword": "Hide password",
|
||||
"showPassword": "Show password",
|
||||
"mfaCode": "MFA koda",
|
||||
"mfaCodeHint": "Vnesite 6-mestno kodo iz aplikacije za preverjanje pristnosti ali uporabite rezervno kodo",
|
||||
"mfaRequired": "Zahteva se večfaktorska avtentikacija. Vnesite svojo 6-mestno kodo.",
|
||||
"verifyMFAButton": "Preveri MFA",
|
||||
"invalidMFACode": "Neveljavna koda MFA. Poskusite znova.",
|
||||
|
||||
@@ -23,6 +23,20 @@
|
||||
"errorSetupMFA": "Error setting up MFA",
|
||||
"errorEnableMFA": "Error enabling MFA",
|
||||
"errorDisableMFA": "Error disabling MFA",
|
||||
"backupCodesModalTitle": "Backup Codes",
|
||||
"backupCodesModalDescription": "Save these backup codes in a secure location. Each code can only be used once to sign in if you lose access to your authenticator app.",
|
||||
"backupCodesWarningTitle": "Important",
|
||||
"backupCodesWarningMessage": "These codes will only be shown once. Store them securely - you cannot retrieve them later.",
|
||||
"backupCodesCopyAll": "Copy All",
|
||||
"backupCodesDownload": "Download",
|
||||
"backupCodesCopySuccess": "Backup codes copied to clipboard",
|
||||
"backupCodesSaveConfirmation": "I have saved these codes in a secure location",
|
||||
"backupCodesClose": "Close",
|
||||
"backupCodesRemaining": "{remaining} of {total} backup codes remaining",
|
||||
"regenerateBackupCodesButton": "Regenerate Backup Codes",
|
||||
"successRegenerateBackupCodes": "Backup codes regenerated successfully",
|
||||
"errorRegenerateBackupCodes": "Error regenerating backup codes",
|
||||
"errorLoadBackupCodeStatus": "Error loading backup code status",
|
||||
"subtitleMySessions": "My sessions",
|
||||
"userChangePasswordSuccessMessage": "Password changed successfully",
|
||||
"userChangePasswordErrorMessage": "Error changing password",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"hidePassword": "Hide password",
|
||||
"showPassword": "Show password",
|
||||
"mfaCode": "MFA Code",
|
||||
"mfaCodeHint": "Enter your 6-digit code from your authenticator app, or use a backup code",
|
||||
"mfaRequired": "Multi-factor authentication required. Please enter your 6-digit code.",
|
||||
"verifyMFAButton": "Verify MFA",
|
||||
"invalidMFACode": "Invalid MFA code. Please try again.",
|
||||
|
||||
@@ -23,6 +23,20 @@
|
||||
"errorSetupMFA": "Error setting up MFA",
|
||||
"errorEnableMFA": "Error enabling MFA",
|
||||
"errorDisableMFA": "Error disabling MFA",
|
||||
"backupCodesModalTitle": "Backup Codes",
|
||||
"backupCodesModalDescription": "Save these backup codes in a secure location. Each code can only be used once to sign in if you lose access to your authenticator app.",
|
||||
"backupCodesWarningTitle": "Important",
|
||||
"backupCodesWarningMessage": "These codes will only be shown once. Store them securely - you cannot retrieve them later.",
|
||||
"backupCodesCopyAll": "Copy All",
|
||||
"backupCodesDownload": "Download",
|
||||
"backupCodesCopySuccess": "Backup codes copied to clipboard",
|
||||
"backupCodesSaveConfirmation": "I have saved these codes in a secure location",
|
||||
"backupCodesClose": "Close",
|
||||
"backupCodesRemaining": "{remaining} of {total} backup codes remaining",
|
||||
"regenerateBackupCodesButton": "Regenerate Backup Codes",
|
||||
"successRegenerateBackupCodes": "Backup codes regenerated successfully",
|
||||
"errorRegenerateBackupCodes": "Error regenerating backup codes",
|
||||
"errorLoadBackupCodeStatus": "Error loading backup code status",
|
||||
"subtitleMySessions": "My sessions",
|
||||
"userChangePasswordSuccessMessage": "Password changed successfully",
|
||||
"userChangePasswordErrorMessage": "Error changing password",
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
"hidePassword": "Hide password",
|
||||
"showPassword": "Show password",
|
||||
"mfaCode": "MFA Code",
|
||||
"mfaCodeHint": "Enter your 6-digit code from your authenticator app, or use a backup code",
|
||||
"mfaRequired": "Multi-factor authentication required. Please enter your 6-digit code.",
|
||||
"verifyMFAButton": "Verify MFA",
|
||||
"invalidMFACode": "Invalid MFA code. Please try again.",
|
||||
"invalidMFACode": "Invalid MFA code, backup code or backup code already used. Please try again.",
|
||||
"neverExpires": "Remember me (do not tick this box if you are using a shared computer)",
|
||||
"signInButton": "Sign in",
|
||||
"signUpText": "Looking for signing up?",
|
||||
|
||||
@@ -60,6 +60,13 @@ export const profile = {
|
||||
verifyMFA(data) {
|
||||
return fetchPostRequest('profile/mfa/verify', data)
|
||||
},
|
||||
// Backup codes endpoints
|
||||
getBackupCodeStatus() {
|
||||
return fetchGetRequest('profile/mfa/backup-codes/status')
|
||||
},
|
||||
regenerateBackupCodes() {
|
||||
return fetchPostRequest('profile/mfa/backup-codes', {})
|
||||
},
|
||||
getMyIdentityProviders() {
|
||||
return fetchGetRequest('profile/idp')
|
||||
},
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
autocomplete="one-time-code"
|
||||
/>
|
||||
<label for="mfaCode">{{ $t('loginView.mfaCode') }}</label>
|
||||
<small class="form-text text-muted">{{ $t('loginView.mfaCodeHint') }}</small>
|
||||
</div>
|
||||
|
||||
<button class="w-100 btn btn-lg btn-primary" type="submit" :disabled="loading">
|
||||
|
||||
Reference in New Issue
Block a user