805 lines
32 KiB
QML
805 lines
32 KiB
QML
import QtQuick
|
|
import QtQuick.Window
|
|
import QtQuick.Layouts
|
|
import QtQuick.Controls as Controls
|
|
import org.kde.kirigami as Kirigami
|
|
|
|
Kirigami.ApplicationWindow {
|
|
id: root
|
|
|
|
title: "Configuration initiale du poste"
|
|
visibility: Window.FullScreen
|
|
color: "#eef2f7"
|
|
|
|
property int currentStep: 0
|
|
property string operationError: ""
|
|
property bool closeAttempted: false
|
|
|
|
property var steps: [
|
|
{ "title": "Bienvenue", "subtitle": "Première connexion" },
|
|
{ "title": "Mot de passe", "subtitle": "Protection des données" },
|
|
{ "title": "YubiKey", "subtitle": "Création du code PIN" },
|
|
{ "title": "Association", "subtitle": "Association de la clé" },
|
|
{ "title": "Terminé", "subtitle": "Poste prêt" }
|
|
]
|
|
|
|
property bool passwordStepValid:
|
|
currentPassword.text.length > 0 &&
|
|
newPassword.text.length >= 8 &&
|
|
confirmPassword.text.length > 0 &&
|
|
newPassword.text === confirmPassword.text
|
|
|
|
property bool pinStepValid:
|
|
newPin.text.length >= 4 &&
|
|
confirmPin.text.length >= 4 &&
|
|
newPin.text === confirmPin.text
|
|
|
|
property bool fidoStepValid:
|
|
fidoPassword.text.length > 0 &&
|
|
fidoPin.text.length >= 4
|
|
|
|
property bool workflowComplete: provisioning.complete
|
|
|
|
Component.onCompleted: {
|
|
if (provisioning.complete)
|
|
currentStep = 4
|
|
else if (provisioning.fidoDone)
|
|
currentStep = 4
|
|
else if (provisioning.pinDone)
|
|
currentStep = 3
|
|
else if (provisioning.passwordDone)
|
|
currentStep = 2
|
|
else
|
|
currentStep = 0
|
|
}
|
|
|
|
Connections {
|
|
target: provisioning
|
|
|
|
function onPasswordChangeFinished(success, message) {
|
|
root.operationError = success ? "" : message
|
|
if (success)
|
|
root.currentStep = 2
|
|
}
|
|
|
|
function onPinChangeFinished(success, message) {
|
|
root.operationError = success ? "" : message
|
|
if (success)
|
|
root.currentStep = 3
|
|
}
|
|
|
|
function onFidoEnrollmentFinished(success, message) {
|
|
root.operationError = success ? "" : message
|
|
if (success)
|
|
root.currentStep = 4
|
|
}
|
|
}
|
|
|
|
onClosing: function(close) {
|
|
if (!workflowComplete) {
|
|
close.accepted = false
|
|
closeAttempted = true
|
|
closeWarningTimer.restart()
|
|
}
|
|
}
|
|
|
|
onVisibilityChanged: {
|
|
if (!workflowComplete && visibility !== Window.FullScreen)
|
|
visibility = Window.FullScreen
|
|
}
|
|
|
|
Timer {
|
|
id: closeWarningTimer
|
|
interval: 3500
|
|
repeat: false
|
|
onTriggered: root.closeAttempted = false
|
|
}
|
|
|
|
function passwordStrengthLevel(password) {
|
|
if (password.length === 0) return 0
|
|
var classes = 0
|
|
if (/[a-z]/.test(password)) classes++
|
|
if (/[A-Z]/.test(password)) classes++
|
|
if (/[0-9]/.test(password)) classes++
|
|
if (/[^A-Za-z0-9]/.test(password)) classes++
|
|
if (password.length >= 16 && classes >= 2) return 4
|
|
if (password.length >= 12 && classes >= 2) return 3
|
|
if (password.length >= 8 && classes >= 2) return 2
|
|
return 1
|
|
}
|
|
|
|
function strengthLabel(level) {
|
|
if (level === 1) return "Faible"
|
|
if (level === 2) return "Moyen"
|
|
if (level === 3) return "Bon"
|
|
if (level === 4) return "Très bon"
|
|
return ""
|
|
}
|
|
|
|
function strengthColor(level) {
|
|
if (level === 1) return "#dc2626"
|
|
if (level === 2) return "#d97706"
|
|
if (level === 3) return "#2563eb"
|
|
if (level === 4) return "#16a34a"
|
|
return "#cbd5e1"
|
|
}
|
|
|
|
component PrimaryButton: Controls.Button {
|
|
id: control
|
|
implicitHeight: 52
|
|
leftPadding: 26
|
|
rightPadding: 26
|
|
implicitWidth: Math.max(210, buttonLabel.implicitWidth + leftPadding + rightPadding)
|
|
|
|
contentItem: Controls.Label {
|
|
id: buttonLabel
|
|
text: control.text
|
|
color: "white"
|
|
font.pixelSize: 15
|
|
font.weight: Font.DemiBold
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
wrapMode: Text.NoWrap
|
|
}
|
|
|
|
background: Rectangle {
|
|
radius: 11
|
|
color: control.enabled
|
|
? (control.down ? "#1e40af" : control.hovered ? "#2563eb" : "#1d4ed8")
|
|
: "#94a3b8"
|
|
}
|
|
}
|
|
|
|
component SecretField: Controls.TextField {
|
|
id: field
|
|
property bool secretVisible: false
|
|
|
|
Layout.fillWidth: true
|
|
implicitHeight: 50
|
|
echoMode: secretVisible ? TextInput.Normal : TextInput.Password
|
|
passwordCharacter: "●"
|
|
font.pixelSize: 15
|
|
leftPadding: 16
|
|
rightPadding: 54
|
|
selectByMouse: true
|
|
|
|
background: Rectangle {
|
|
radius: 10
|
|
color: "#ffffff"
|
|
border.width: field.activeFocus ? 2 : 1
|
|
border.color: field.activeFocus ? "#2563eb" : "#cbd5e1"
|
|
}
|
|
|
|
Controls.ToolButton {
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 8
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 36
|
|
height: 36
|
|
focusPolicy: Qt.NoFocus
|
|
onClicked: field.secretVisible = !field.secretVisible
|
|
|
|
contentItem: Kirigami.Icon {
|
|
source: field.secretVisible ? "view-hidden" : "view-visible"
|
|
implicitWidth: 19
|
|
implicitHeight: 19
|
|
color: "#64748b"
|
|
}
|
|
}
|
|
}
|
|
|
|
component FieldLabel: Controls.Label {
|
|
font.pixelSize: 13
|
|
font.weight: Font.DemiBold
|
|
color: "#334155"
|
|
}
|
|
|
|
component InfoBox: Rectangle {
|
|
id: infoBox
|
|
property alias text: infoLabel.text
|
|
property string iconText: "i"
|
|
Layout.fillWidth: true
|
|
implicitHeight: infoRow.implicitHeight + 28
|
|
radius: 12
|
|
color: "#eff6ff"
|
|
border.width: 1
|
|
border.color: "#bfdbfe"
|
|
|
|
RowLayout {
|
|
id: infoRow
|
|
anchors.fill: parent
|
|
anchors.margins: 14
|
|
spacing: 12
|
|
|
|
Rectangle {
|
|
Layout.preferredWidth: 30
|
|
Layout.preferredHeight: 30
|
|
radius: 15
|
|
color: "#dbeafe"
|
|
Controls.Label {
|
|
anchors.centerIn: parent
|
|
text: infoBox.iconText
|
|
color: "#1d4ed8"
|
|
font.weight: Font.Bold
|
|
}
|
|
}
|
|
|
|
Controls.Label {
|
|
id: infoLabel
|
|
Layout.fillWidth: true
|
|
wrapMode: Text.WordWrap
|
|
color: "#334155"
|
|
font.pixelSize: 13
|
|
lineHeight: 1.2
|
|
}
|
|
}
|
|
}
|
|
|
|
component ErrorBox: Rectangle {
|
|
visible: root.operationError.length > 0
|
|
Layout.fillWidth: true
|
|
implicitHeight: errorLabel.implicitHeight + 28
|
|
radius: 12
|
|
color: "#fef2f2"
|
|
border.width: 1
|
|
border.color: "#fecaca"
|
|
|
|
Controls.Label {
|
|
id: errorLabel
|
|
anchors.fill: parent
|
|
anchors.margins: 14
|
|
text: root.operationError
|
|
wrapMode: Text.WordWrap
|
|
color: "#991b1b"
|
|
font.pixelSize: 13
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
spacing: 0
|
|
|
|
Rectangle {
|
|
Layout.preferredWidth: Math.max(310, root.width * 0.27)
|
|
Layout.fillHeight: true
|
|
color: "#0f2b50"
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: 34
|
|
spacing: 26
|
|
|
|
RowLayout {
|
|
spacing: 12
|
|
Rectangle {
|
|
Layout.preferredWidth: 44
|
|
Layout.preferredHeight: 44
|
|
radius: 12
|
|
color: "#1d4ed8"
|
|
Controls.Label {
|
|
anchors.centerIn: parent
|
|
text: "N"
|
|
color: "white"
|
|
font.pixelSize: 21
|
|
font.weight: Font.Bold
|
|
}
|
|
}
|
|
ColumnLayout {
|
|
spacing: 2
|
|
Controls.Label {
|
|
text: "NixOS Workstations"
|
|
color: "white"
|
|
font.pixelSize: 18
|
|
font.weight: Font.Bold
|
|
}
|
|
Controls.Label {
|
|
text: "Configuration sécurisée"
|
|
color: "#b9cee5"
|
|
font.pixelSize: 12
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
implicitHeight: 1
|
|
color: "#28517e"
|
|
}
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 8
|
|
|
|
Repeater {
|
|
model: root.steps
|
|
|
|
Rectangle {
|
|
required property var modelData
|
|
required property int index
|
|
Layout.fillWidth: true
|
|
implicitHeight: 66
|
|
radius: 12
|
|
color: index === root.currentStep ? "#173f6f" : "transparent"
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: 10
|
|
spacing: 13
|
|
|
|
Rectangle {
|
|
Layout.preferredWidth: 34
|
|
Layout.preferredHeight: 34
|
|
radius: 17
|
|
color: index < root.currentStep ? "#22c55e"
|
|
: index === root.currentStep ? "#ffffff"
|
|
: "#28517e"
|
|
Controls.Label {
|
|
anchors.centerIn: parent
|
|
text: index < root.currentStep ? "✓" : (index + 1)
|
|
color: index === root.currentStep ? "#0f2b50" : "white"
|
|
font.weight: Font.Bold
|
|
}
|
|
}
|
|
|
|
Item {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
|
|
Column {
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 2
|
|
|
|
Controls.Label {
|
|
width: parent.width
|
|
text: modelData.title
|
|
color: "white"
|
|
font.pixelSize: 14
|
|
font.weight: index === root.currentStep ? Font.DemiBold : Font.Medium
|
|
horizontalAlignment: Text.AlignLeft
|
|
}
|
|
|
|
Controls.Label {
|
|
width: parent.width
|
|
text: modelData.subtitle
|
|
color: "#b9cee5"
|
|
font.pixelSize: 11
|
|
horizontalAlignment: Text.AlignLeft
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Item { Layout.fillHeight: true }
|
|
|
|
Controls.Label {
|
|
visible: root.closeAttempted
|
|
Layout.fillWidth: true
|
|
text: "La configuration doit être terminée avant de fermer cet assistant."
|
|
wrapMode: Text.WordWrap
|
|
color: "#fde68a"
|
|
font.pixelSize: 12
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
color: "#f8fafc"
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: Math.max(34, Math.min(64, parent.width * 0.055))
|
|
spacing: 24
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 10
|
|
Repeater {
|
|
model: root.steps.length
|
|
Rectangle {
|
|
required property int index
|
|
Layout.fillWidth: true
|
|
implicitHeight: 5
|
|
radius: 3
|
|
color: index <= root.currentStep ? "#2563eb" : "#e2e8f0"
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: pageViewport
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
clip: true
|
|
|
|
Row {
|
|
id: pageStrip
|
|
height: parent.height
|
|
spacing: 0
|
|
x: -root.currentStep * pageViewport.width
|
|
|
|
Behavior on x {
|
|
NumberAnimation {
|
|
duration: 330
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
|
|
// 0 - Bienvenue
|
|
Item {
|
|
width: pageViewport.width
|
|
height: pageViewport.height
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
spacing: 20
|
|
Item { Layout.fillHeight: true }
|
|
Controls.Label {
|
|
Layout.fillWidth: true
|
|
text: "Bienvenue sur votre poste"
|
|
color: "#0f172a"
|
|
font.pixelSize: 32
|
|
font.weight: Font.Bold
|
|
horizontalAlignment: Text.AlignHCenter
|
|
}
|
|
Controls.Label {
|
|
Layout.maximumWidth: 720
|
|
Layout.alignment: Qt.AlignHCenter
|
|
text: "Votre espace personnel est déjà chiffré. Il reste à remplacer le mot de passe temporaire et à préparer votre YubiKey."
|
|
wrapMode: Text.WordWrap
|
|
horizontalAlignment: Text.AlignHCenter
|
|
color: "#64748b"
|
|
font.pixelSize: 16
|
|
lineHeight: 1.25
|
|
}
|
|
InfoBox {
|
|
Layout.maximumWidth: 720
|
|
Layout.alignment: Qt.AlignHCenter
|
|
iconText: "🔐"
|
|
text: "À la fin, vous pourrez ouvrir votre espace personnel soit avec votre mot de passe, soit avec votre YubiKey et son code PIN."
|
|
}
|
|
Item { Layout.fillHeight: true }
|
|
PrimaryButton {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
text: "Commencer"
|
|
onClicked: {
|
|
root.operationError = ""
|
|
root.currentStep = 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1 - Mot de passe
|
|
Item {
|
|
width: pageViewport.width
|
|
height: pageViewport.height
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
spacing: 16
|
|
|
|
Controls.Label {
|
|
text: "Remplacez le mot de passe temporaire"
|
|
color: "#0f172a"
|
|
font.pixelSize: 28
|
|
font.weight: Font.Bold
|
|
}
|
|
Controls.Label {
|
|
Layout.fillWidth: true
|
|
text: "Ce mot de passe reste votre solution de secours si la YubiKey est oubliée ou indisponible."
|
|
wrapMode: Text.WordWrap
|
|
color: "#64748b"
|
|
font.pixelSize: 14
|
|
}
|
|
|
|
Item { Layout.preferredHeight: 8 }
|
|
FieldLabel { text: "Mot de passe temporaire actuel" }
|
|
SecretField { id: currentPassword }
|
|
|
|
FieldLabel { text: "Nouveau mot de passe" }
|
|
SecretField { id: newPassword }
|
|
|
|
FieldLabel { text: "Confirmation du nouveau mot de passe" }
|
|
SecretField { id: confirmPassword }
|
|
|
|
Rectangle {
|
|
id: strengthCard
|
|
Layout.fillWidth: true
|
|
implicitHeight: 52
|
|
radius: 10
|
|
color: "#ffffff"
|
|
border.width: 1
|
|
border.color: "#e2e8f0"
|
|
property int level: root.passwordStrengthLevel(newPassword.text)
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: 13
|
|
Controls.Label {
|
|
text: "Robustesse indicative"
|
|
color: "#64748b"
|
|
}
|
|
Item { Layout.fillWidth: true }
|
|
Rectangle {
|
|
width: 92
|
|
height: 8
|
|
radius: 4
|
|
color: strengthCard.level > 0 ? root.strengthColor(strengthCard.level) : "#e2e8f0"
|
|
}
|
|
Controls.Label {
|
|
text: root.strengthLabel(strengthCard.level)
|
|
color: root.strengthColor(strengthCard.level)
|
|
font.weight: Font.DemiBold
|
|
}
|
|
}
|
|
}
|
|
|
|
ErrorBox { }
|
|
Item { Layout.fillHeight: true }
|
|
|
|
PrimaryButton {
|
|
Layout.alignment: Qt.AlignRight
|
|
text: provisioning.busy ? "Modification…" : "Valider le mot de passe"
|
|
enabled: root.passwordStepValid && !provisioning.busy
|
|
onClicked: {
|
|
root.operationError = ""
|
|
var c = currentPassword.text
|
|
var n = newPassword.text
|
|
var r = confirmPassword.text
|
|
provisioning.changePassword(c, n, r)
|
|
currentPassword.text = ""
|
|
newPassword.text = ""
|
|
confirmPassword.text = ""
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2 - PIN YubiKey
|
|
Item {
|
|
width: pageViewport.width
|
|
height: pageViewport.height
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
spacing: 16
|
|
|
|
Controls.Label {
|
|
text: "Initialisez votre YubiKey vierge"
|
|
color: "#0f172a"
|
|
font.pixelSize: 28
|
|
font.weight: Font.Bold
|
|
}
|
|
Controls.Label {
|
|
Layout.fillWidth: true
|
|
text: "Branchez une seule YubiKey vierge puis choisissez son code PIN. Aucun code PIN temporaire n'est utilisé."
|
|
wrapMode: Text.WordWrap
|
|
color: "#64748b"
|
|
font.pixelSize: 14
|
|
}
|
|
|
|
InfoBox {
|
|
iconText: "☝"
|
|
text: "La YubiKey doit être vierge ou avoir été réinitialisée. Elle ne doit pas déjà posséder de code PIN."
|
|
}
|
|
|
|
Item { Layout.preferredHeight: 8 }
|
|
FieldLabel { text: "Nouveau code PIN" }
|
|
SecretField { id: newPin }
|
|
FieldLabel { text: "Confirmation du code PIN" }
|
|
SecretField { id: confirmPin }
|
|
|
|
ErrorBox { }
|
|
Item { Layout.fillHeight: true }
|
|
|
|
PrimaryButton {
|
|
Layout.alignment: Qt.AlignRight
|
|
text: provisioning.busy ? "Initialisation…" : "Créer le code PIN"
|
|
enabled: root.pinStepValid && !provisioning.busy
|
|
onClicked: {
|
|
root.operationError = ""
|
|
var p = newPin.text
|
|
var r = confirmPin.text
|
|
provisioning.initializePin(p, r)
|
|
newPin.text = ""
|
|
confirmPin.text = ""
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3 - Association FIDO2 au home
|
|
Item {
|
|
width: pageViewport.width
|
|
height: pageViewport.height
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
spacing: 16
|
|
|
|
Controls.Label {
|
|
text: "Associez la YubiKey à votre espace personnel"
|
|
color: "#0f172a"
|
|
font.pixelSize: 28
|
|
font.weight: Font.Bold
|
|
}
|
|
Controls.Label {
|
|
Layout.fillWidth: true
|
|
text: "La YubiKey sera ajoutée comme second moyen d'ouvrir votre espace personnel chiffré. Votre mot de passe reste utilisable en secours."
|
|
wrapMode: Text.WordWrap
|
|
color: "#64748b"
|
|
font.pixelSize: 14
|
|
}
|
|
|
|
InfoBox {
|
|
iconText: "🔐"
|
|
text: "Saisissez le mot de passe que vous venez de choisir et le code PIN de la YubiKey."
|
|
}
|
|
|
|
InfoBox {
|
|
visible: !provisioning.busy
|
|
iconText: "☝"
|
|
text: "Après avoir lancé l'association, suivez les indications affichées à l'écran. La YubiKey peut demander plusieurs touchers."
|
|
}
|
|
|
|
Rectangle {
|
|
id: fidoLiveStatus
|
|
visible: provisioning.busy && root.currentStep === 3
|
|
Layout.fillWidth: true
|
|
implicitHeight: fidoLiveRow.implicitHeight + 32
|
|
radius: 12
|
|
color: provisioning.fidoTouchRequired ? "#fff7ed" : "#eff6ff"
|
|
border.width: provisioning.fidoTouchRequired ? 3 : 1
|
|
border.color: provisioning.fidoTouchRequired ? "#f97316" : "#bfdbfe"
|
|
|
|
SequentialAnimation on opacity {
|
|
running: fidoLiveStatus.visible && provisioning.fidoTouchRequired
|
|
loops: Animation.Infinite
|
|
NumberAnimation { from: 1.0; to: 0.58; duration: 520 }
|
|
NumberAnimation { from: 0.58; to: 1.0; duration: 520 }
|
|
}
|
|
|
|
RowLayout {
|
|
id: fidoLiveRow
|
|
anchors.fill: parent
|
|
anchors.margins: 16
|
|
spacing: 14
|
|
|
|
Controls.BusyIndicator {
|
|
visible: !provisioning.fidoTouchRequired
|
|
running: visible
|
|
Layout.preferredWidth: 30
|
|
Layout.preferredHeight: 30
|
|
}
|
|
|
|
Controls.Label {
|
|
visible: provisioning.fidoTouchRequired
|
|
text: "☝"
|
|
font.pixelSize: 30
|
|
Layout.alignment: Qt.AlignTop
|
|
}
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 4
|
|
|
|
Controls.Label {
|
|
Layout.fillWidth: true
|
|
text: provisioning.fidoTouchRequired
|
|
? "TOUCHEZ LA YUBIKEY MAINTENANT"
|
|
: "Association de la YubiKey en cours"
|
|
wrapMode: Text.WordWrap
|
|
color: provisioning.fidoTouchRequired ? "#9a3412" : "#1d4ed8"
|
|
font.pixelSize: provisioning.fidoTouchRequired ? 18 : 15
|
|
font.weight: Font.Bold
|
|
}
|
|
|
|
Controls.Label {
|
|
Layout.fillWidth: true
|
|
text: provisioning.fidoStatus
|
|
wrapMode: Text.WordWrap
|
|
color: provisioning.fidoTouchRequired ? "#9a3412" : "#334155"
|
|
font.pixelSize: 13
|
|
lineHeight: 1.2
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Item { Layout.preferredHeight: 8 }
|
|
FieldLabel { text: "Nouveau mot de passe choisi à l’étape 1" }
|
|
SecretField { id: fidoPassword }
|
|
FieldLabel { text: "Code PIN de la YubiKey" }
|
|
SecretField { id: fidoPin }
|
|
|
|
ErrorBox { }
|
|
Item { Layout.fillHeight: true }
|
|
|
|
PrimaryButton {
|
|
Layout.alignment: Qt.AlignRight
|
|
text: provisioning.busy ? "Association…" : "Associer la YubiKey"
|
|
enabled: root.fidoStepValid && !provisioning.busy
|
|
onClicked: {
|
|
root.operationError = ""
|
|
var p = fidoPassword.text
|
|
var k = fidoPin.text
|
|
provisioning.enrollFido(p, k)
|
|
fidoPassword.text = ""
|
|
fidoPin.text = ""
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4 - Terminé
|
|
Item {
|
|
width: pageViewport.width
|
|
height: pageViewport.height
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
spacing: 18
|
|
Item { Layout.fillHeight: true }
|
|
|
|
Rectangle {
|
|
Layout.preferredWidth: 96
|
|
Layout.preferredHeight: 96
|
|
Layout.alignment: Qt.AlignHCenter
|
|
radius: 48
|
|
color: "#dcfce7"
|
|
border.width: 1
|
|
border.color: "#86efac"
|
|
Kirigami.Icon {
|
|
anchors.centerIn: parent
|
|
source: "dialog-ok-apply"
|
|
implicitWidth: 52
|
|
implicitHeight: 52
|
|
color: "#16a34a"
|
|
}
|
|
}
|
|
|
|
Controls.Label {
|
|
Layout.fillWidth: true
|
|
text: "Votre poste est prêt"
|
|
color: "#0f172a"
|
|
font.pixelSize: 32
|
|
font.weight: Font.Bold
|
|
horizontalAlignment: Text.AlignHCenter
|
|
}
|
|
|
|
Controls.Label {
|
|
Layout.maximumWidth: 720
|
|
Layout.alignment: Qt.AlignHCenter
|
|
text: "Votre espace personnel est chiffré et votre YubiKey est prête. Vous pourrez ouvrir votre session avec votre mot de passe ou avec votre YubiKey et son code PIN."
|
|
wrapMode: Text.WordWrap
|
|
horizontalAlignment: Text.AlignHCenter
|
|
color: "#64748b"
|
|
font.pixelSize: 15
|
|
lineHeight: 1.25
|
|
}
|
|
|
|
InfoBox {
|
|
Layout.maximumWidth: 720
|
|
Layout.alignment: Qt.AlignHCenter
|
|
iconText: "✓"
|
|
text: "Votre configuration sécurisée est terminée. Conservez votre YubiKey avec vous et mémorisez son code PIN."
|
|
}
|
|
|
|
Item { Layout.fillHeight: true }
|
|
|
|
PrimaryButton {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
text: "Commencer à travailler"
|
|
enabled: root.workflowComplete && !provisioning.busy
|
|
onClicked: root.close()
|
|
}
|
|
}
|
|
}
|
|
} // Row pageStrip
|
|
} // pageViewport
|
|
}
|
|
}
|
|
}
|
|
}
|