Files
nixos-workstations/workstation-setup/src/Main.qml
T

1329 lines
59 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 var steps: [
{ "title": "Bienvenue", "subtitle": "Présentation" },
{ "title": "Mot de passe", "subtitle": "Accès de secours" },
{ "title": "YubiKey", "subtitle": "PIN personnel" },
{ "title": "Terminé", "subtitle": "Poste prêt" }
]
property bool passwordStepValid:
currentPassword.text.trim().length > 0 &&
newPassword.text.length > 0 &&
confirmPassword.text.length > 0 &&
newPassword.text === confirmPassword.text
property bool pinStepValid:
currentPin.text.length >= 4 &&
newPin.text.length >= 4 &&
confirmPin.text.length >= 4 &&
newPin.text === confirmPin.text
// En production, l'état réel vient du backend et des marqueurs locaux.
// La simple saisie des champs ne signifie jamais que l'opération a réussi.
property bool workflowComplete: provisioning.complete
property bool closeAttempted: false
property string operationError: ""
function goNext() {
if (currentStep === 0)
currentStep = 1
}
function goBack() {
// Une étape déjà appliquée au système n'est jamais rejouée.
if (currentStep === 1 && !provisioning.passwordDone)
currentStep = 0
}
Component.onCompleted: {
if (provisioning.complete)
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
}
}
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
}
// Indicateur volontairement informatif : il ne bloque jamais
// la validation du mot de passe. La longueur est privilégiée,
// la diversité des caractères apporte un bonus visuel.
function passwordStrengthLevel(password) {
if (password.length === 0)
return 0
var classes = 0
if (/[a-z]/.test(password)) classes += 1
if (/[A-Z]/.test(password)) classes += 1
if (/[0-9]/.test(password)) classes += 1
if (/[^A-Za-z0-9]/.test(password)) classes += 1
var level = 1
if (password.length >= 16)
level = classes >= 2 ? 4 : 3
else if (password.length >= 12)
level = classes >= 2 ? 3 : 2
else if (password.length >= 8)
level = classes >= 3 ? 2 : 1
// Quelques motifs manifestement faibles : on réduit
// l'indication sans empêcher l'utilisateur de continuer.
var lower = password.toLowerCase()
if (lower === "password" || lower === "motdepasse" ||
lower === "azerty" || lower === "qwerty" ||
lower === "12345678" || /^(.)\1+$/.test(password)) {
level = 1
}
return level
}
function passwordStrengthLabel(level) {
if (level === 1) return "Faible"
if (level === 2) return "Moyen"
if (level === 3) return "Bon"
if (level === 4) return "Très bon"
return "Non évalué"
}
function passwordStrengthColor(level) {
if (level === 1) return "#dc2626"
if (level === 2) return "#d97706"
if (level === 3) return "#2563eb"
if (level === 4) return "#16a34a"
return "#cbd5e1"
}
function passwordStrengthHint(level) {
if (level === 1) return "Privilégiez surtout la longueur : une phrase de passe de 12 caractères ou plus est un bon point de départ."
if (level === 2) return "Correct pour un usage courant, mais quelques caractères supplémentaires amélioreraient sensiblement la robustesse."
if (level === 3) return "Bonne robustesse apparente : la longueur et la diversité sont satisfaisantes."
if (level === 4) return "Très bonne robustesse apparente : mot de passe long et suffisamment diversifié."
return "Saisissez votre nouveau mot de passe pour obtenir une indication de robustesse."
}
component PrimaryButton: Controls.Button {
id: control
implicitHeight: 48
implicitWidth: 170
leftPadding: 24
rightPadding: 24
contentItem: Controls.Label {
text: control.text
font.pixelSize: 15
font.weight: Font.DemiBold
color: "white"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
radius: 12
color: control.down ? "#1e40af" : control.hovered ? "#2563eb" : "#1d4ed8"
Behavior on color { ColorAnimation { duration: 120 } }
}
}
component SecondaryButton: Controls.Button {
id: control
implicitHeight: 48
implicitWidth: 140
leftPadding: 22
rightPadding: 22
contentItem: Controls.Label {
text: control.text
font.pixelSize: 15
font.weight: Font.Medium
color: "#334155"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
radius: 12
color: control.down ? "#dbe3ee" : control.hovered ? "#edf2f7" : "transparent"
border.width: 1
border.color: "#cbd5e1"
}
}
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: 56
selectByMouse: true
background: Rectangle {
radius: 10
color: field.activeFocus ? "#ffffff" : "#f8fafc"
border.width: field.activeFocus ? 2 : 1
border.color: field.activeFocus ? "#2563eb" : "#cbd5e1"
}
Controls.ToolButton {
id: visibilityButton
anchors.right: parent.right
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
width: 38
height: 38
hoverEnabled: true
focusPolicy: Qt.NoFocus
Accessible.name: field.secretVisible ? "Masquer la valeur" : "Afficher la valeur"
onClicked: field.secretVisible = !field.secretVisible
contentItem: Kirigami.Icon {
source: field.secretVisible ? "view-hidden" : "view-visible"
implicitWidth: 20
implicitHeight: 20
color: visibilityButton.hovered ? "#2563eb" : "#64748b"
}
background: Rectangle {
radius: 8
color: visibilityButton.pressed ? "#dbeafe"
: visibilityButton.hovered ? "#eff6ff"
: "transparent"
Behavior on color {
ColorAnimation { duration: 100 }
}
}
}
}
component FieldLabel: RowLayout {
id: fieldLabel
property string iconName: "dialog-password"
property string labelText: ""
spacing: 8
Kirigami.Icon {
source: fieldLabel.iconName
implicitWidth: 17
implicitHeight: 17
color: "#64748b"
}
Controls.Label {
text: fieldLabel.labelText
color: "#334155"
font.pixelSize: 13
font.weight: Font.DemiBold
}
}
component StepDot: Rectangle {
required property int stepIndex
width: 34
height: 34
radius: 17
color: stepIndex < root.currentStep ? "#22c55e"
: stepIndex === root.currentStep ? "#ffffff"
: "#17345f"
border.width: stepIndex === root.currentStep ? 2 : 0
border.color: "#93c5fd"
Controls.Label {
anchors.centerIn: parent
text: parent.stepIndex < root.currentStep ? "✓" : (parent.stepIndex + 1)
color: parent.stepIndex === root.currentStep ? "#0f2b50" : "white"
font.pixelSize: 14
font.weight: Font.Bold
}
}
RowLayout {
anchors.fill: parent
spacing: 0
// -----------------------------------------------------
// Colonne gauche : identité + progression
// -----------------------------------------------------
Rectangle {
Layout.preferredWidth: Math.max(330, root.width * 0.29)
Layout.fillHeight: true
gradient: Gradient {
GradientStop { position: 0.0; color: "#081a33" }
GradientStop { position: 0.55; color: "#0b2a50" }
GradientStop { position: 1.0; color: "#123d70" }
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 44
spacing: 0
RowLayout {
spacing: 14
Rectangle {
width: 54
height: 54
radius: 16
color: "#1d4ed8"
Controls.Label {
anchors.centerIn: parent
text: "N"
color: "white"
font.pixelSize: 25
font.weight: Font.Bold
}
}
ColumnLayout {
spacing: 2
Controls.Label {
text: "NixOS Workstations"
color: "white"
font.pixelSize: 19
font.weight: Font.DemiBold
}
Controls.Label {
text: "Configuration du poste"
color: "#a8c1e1"
font.pixelSize: 13
}
}
}
Item { Layout.preferredHeight: 70 }
Controls.Label {
text: "VOTRE CONFIGURATION"
color: "#7fa4d1"
font.pixelSize: 11
font.weight: Font.Bold
font.letterSpacing: 1.5
}
Item { Layout.preferredHeight: 26 }
Repeater {
model: root.steps
delegate: Item {
required property int index
required property var modelData
Layout.fillWidth: true
Layout.preferredHeight: 78
RowLayout {
anchors.fill: parent
spacing: 16
// Colonne de largeur fixe : tous les marqueurs et tous
// les titres commencent exactement au même endroit.
Item {
Layout.preferredWidth: 42
Layout.fillHeight: true
StepDot {
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
stepIndex: index
}
Rectangle {
visible: index < root.steps.length - 1
anchors.top: parent.top
anchors.topMargin: 34
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
width: 2
color: index < root.currentStep ? "#22c55e" : "#28517e"
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
spacing: 3
Controls.Label {
Layout.fillWidth: true
text: modelData.title
color: index === root.currentStep ? "white" : "#c1d3e8"
font.pixelSize: 15
font.weight: index === root.currentStep ? Font.DemiBold : Font.Medium
horizontalAlignment: Text.AlignLeft
}
Controls.Label {
Layout.fillWidth: true
text: modelData.subtitle
color: "#7699c2"
font.pixelSize: 12
horizontalAlignment: Text.AlignLeft
}
}
}
}
}
Item { Layout.fillHeight: true }
Rectangle {
Layout.fillWidth: true
implicitHeight: 86
radius: 14
color: "#102f55"
border.width: 1
border.color: "#214b78"
RowLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
Controls.Label {
text: "🛡"
font.pixelSize: 23
}
ColumnLayout {
Layout.fillWidth: true
spacing: 3
Controls.Label {
text: "Configuration locale"
color: "white"
font.pixelSize: 13
font.weight: Font.DemiBold
}
Controls.Label {
Layout.fillWidth: true
text: "Les secrets définitifs ne seront pas enregistrés dans Git."
wrapMode: Text.WordWrap
color: "#91add0"
font.pixelSize: 11
}
}
}
}
Item { Layout.preferredHeight: 20 }
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 6
Kirigami.Icon {
source: "lock"
implicitWidth: 14
implicitHeight: 14
color: "#6289b6"
}
Controls.Label {
text: "Assistant obligatoire jusqu'à la fin"
color: "#6289b6"
font.pixelSize: 11
}
}
}
}
// -----------------------------------------------------
// Partie droite
// -----------------------------------------------------
Rectangle {
id: contentArea
Layout.fillWidth: true
Layout.fillHeight: true
color: "#eef2f7"
ColumnLayout {
anchors.fill: parent
anchors.leftMargin: Math.max(48, contentArea.width * 0.07)
anchors.rightMargin: Math.max(48, contentArea.width * 0.07)
anchors.topMargin: 34
anchors.bottomMargin: 34
spacing: 20
RowLayout {
Layout.fillWidth: true
Rectangle {
implicitWidth: previewLabel.implicitWidth + 28
implicitHeight: 32
radius: 16
color: provisioning.busy ? "#eff6ff" : "#f0fdf4"
border.width: 1
border.color: provisioning.busy ? "#bfdbfe" : "#bbf7d0"
Controls.Label {
id: previewLabel
anchors.centerIn: parent
text: provisioning.busy ? "OPÉRATION SÉCURISÉE EN COURS" : "FINALISATION SÉCURISÉE DU POSTE"
color: provisioning.busy ? "#1d4ed8" : "#166534"
font.pixelSize: 11
font.weight: Font.Bold
}
}
Item { Layout.fillWidth: true }
Rectangle {
implicitWidth: lockStatusRow.implicitWidth + 24
implicitHeight: 34
radius: 17
color: root.closeAttempted ? "#fef2f2" : "#f8fafc"
border.width: 1
border.color: root.closeAttempted ? "#fecaca" : "#cbd5e1"
RowLayout {
id: lockStatusRow
anchors.centerIn: parent
spacing: 7
Kirigami.Icon {
source: root.workflowComplete ? "lock-open" : "lock"
implicitWidth: 16
implicitHeight: 16
color: root.closeAttempted ? "#dc2626" : "#64748b"
}
Controls.Label {
text: root.closeAttempted
? "Terminez la configuration avant de quitter"
: root.workflowComplete
? "Configuration complète"
: "Configuration en cours"
color: root.closeAttempted ? "#b91c1c" : "#475569"
font.pixelSize: 11
font.weight: Font.DemiBold
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: 24
color: "white"
border.width: 1
border.color: "#dce3ec"
ColumnLayout {
anchors.fill: parent
anchors.margins: Math.max(34, Math.min(64, parent.width * 0.055))
spacing: 24
// Petit indicateur de progression supérieur
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"
Behavior on color { ColorAnimation { duration: 180 } }
}
}
}
StackLayout {
id: wizard
Layout.fillWidth: true
Layout.fillHeight: true
currentIndex: root.currentStep
// ---------------------------------
// 0 - Bienvenue
// ---------------------------------
Item {
ColumnLayout {
anchors.fill: parent
spacing: 20
Item { Layout.fillHeight: true }
Rectangle {
Layout.preferredWidth: 86
Layout.preferredHeight: 86
Layout.alignment: Qt.AlignHCenter
radius: 24
color: "#eff6ff"
border.width: 1
border.color: "#bfdbfe"
Controls.Label {
anchors.centerIn: parent
text: "✦"
color: "#2563eb"
font.pixelSize: 42
font.weight: Font.Bold
}
}
Controls.Label {
Layout.fillWidth: true
text: "Bienvenue sur votre nouveau poste"
color: "#0f172a"
font.pixelSize: 32
font.weight: Font.Bold
horizontalAlignment: Text.AlignHCenter
}
Controls.Label {
Layout.maximumWidth: 720
Layout.alignment: Qt.AlignHCenter
text: "Quelques étapes rapides vont personnaliser vos moyens d'authentification et finaliser la préparation de votre environnement de travail."
wrapMode: Text.WordWrap
color: "#64748b"
font.pixelSize: 16
lineHeight: 1.25
horizontalAlignment: Text.AlignHCenter
}
Item { Layout.preferredHeight: 12 }
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 14
Repeater {
model: [
{ "icon": "🔑", "title": "Mot de passe", "text": "Définir votre accès local de secours" },
{ "icon": "🔐", "title": "YubiKey", "text": "Personnaliser votre PIN FIDO2" }
]
Rectangle {
required property var modelData
Layout.preferredWidth: 290
Layout.preferredHeight: 128
radius: 16
color: "#f8fafc"
border.width: 1
border.color: "#e2e8f0"
RowLayout {
anchors.fill: parent
anchors.margins: 18
spacing: 14
Controls.Label {
text: modelData.icon
font.pixelSize: 28
}
ColumnLayout {
Layout.fillWidth: true
spacing: 5
Controls.Label {
text: modelData.title
color: "#0f172a"
font.pixelSize: 15
font.weight: Font.DemiBold
}
Controls.Label {
Layout.fillWidth: true
text: modelData.text
wrapMode: Text.WordWrap
color: "#64748b"
font.pixelSize: 12
}
}
}
}
}
}
Item { Layout.fillHeight: true }
PrimaryButton {
Layout.alignment: Qt.AlignHCenter
text: "Commencer"
onClicked: root.goNext()
}
}
}
// ---------------------------------
// 1 - Mot de passe
// ---------------------------------
Item {
ColumnLayout {
anchors.fill: parent
spacing: 18
RowLayout {
Layout.fillWidth: true
spacing: 20
Rectangle {
Layout.preferredWidth: 80
Layout.preferredHeight: 80
radius: 22
color: "#eff6ff"
border.width: 1
border.color: "#bfdbfe"
Kirigami.Icon {
anchors.centerIn: parent
source: "dialog-password"
implicitWidth: 38
implicitHeight: 38
color: "#2563eb"
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 5
Controls.Label {
text: "Mot de passe local"
color: "#0f172a"
font.pixelSize: 29
font.weight: Font.Bold
}
Controls.Label {
Layout.fillWidth: true
text: "Ce mot de passe reste disponible comme solution de secours si votre YubiKey est momentanément indisponible."
wrapMode: Text.WordWrap
color: "#64748b"
font.pixelSize: 15
lineHeight: 1.2
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: 70
radius: 12
color: "#eff6ff"
border.width: 1
border.color: "#bfdbfe"
RowLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
Kirigami.Icon {
source: "emblem-information"
implicitWidth: 22
implicitHeight: 22
color: "#2563eb"
}
Controls.Label {
Layout.fillWidth: true
text: "Le changement est effectué localement par le mécanisme NixOS prévu à cet effet. Votre nouveau mot de passe nest ni enregistré dans Git, ni transmis au service informatique."
wrapMode: Text.WordWrap
color: "#1e40af"
font.pixelSize: 13
}
}
}
Item { Layout.preferredHeight: 4 }
FieldLabel {
iconName: "dialog-password"
labelText: "Mot de passe temporaire actuel"
}
SecretField {
id: currentPassword
placeholderText: "Saisissez le mot de passe temporaire"
}
FieldLabel {
iconName: "document-edit"
labelText: "Nouveau mot de passe"
}
SecretField {
id: newPassword
placeholderText: "Choisissez votre mot de passe personnel"
}
// Indicateur de robustesse purement informatif.
// Il ne participe pas à la condition d'activation
// du bouton Continuer.
Rectangle {
id: strengthCard
Layout.fillWidth: true
implicitHeight: 112
radius: 12
color: "#f8fafc"
border.width: 1
border.color: "#e2e8f0"
property int strengthLevel: root.passwordStrengthLevel(newPassword.text)
ColumnLayout {
anchors.fill: parent
anchors.margins: 14
spacing: 8
RowLayout {
Layout.fillWidth: true
spacing: 8
Kirigami.Icon {
source: "security-high"
implicitWidth: 17
implicitHeight: 17
color: root.passwordStrengthColor(strengthCard.strengthLevel)
}
Controls.Label {
text: "Robustesse du mot de passe"
color: "#334155"
font.pixelSize: 12
font.weight: Font.DemiBold
}
Item { Layout.fillWidth: true }
Controls.Label {
text: root.passwordStrengthLabel(strengthCard.strengthLevel)
color: root.passwordStrengthColor(strengthCard.strengthLevel)
font.pixelSize: 12
font.weight: Font.Bold
}
}
RowLayout {
Layout.fillWidth: true
spacing: 6
Repeater {
model: 4
Rectangle {
required property int index
Layout.fillWidth: true
implicitHeight: 7
radius: 4
color: index < parent.parent.parent.strengthLevel
? root.passwordStrengthColor(strengthCard.strengthLevel)
: "#e2e8f0"
Behavior on color {
ColorAnimation { duration: 140 }
}
}
}
}
Controls.Label {
Layout.fillWidth: true
text: root.passwordStrengthHint(strengthCard.strengthLevel)
wrapMode: Text.WordWrap
color: "#64748b"
font.pixelSize: 11
}
}
}
Controls.Label {
text: "Indication uniquement : un mot de passe faible n'est pas bloqué."
color: "#94a3b8"
font.pixelSize: 11
}
FieldLabel {
iconName: "dialog-ok-apply"
labelText: "Confirmation"
}
SecretField {
id: confirmPassword
placeholderText: "Confirmez votre nouveau mot de passe"
}
Controls.Label {
visible: confirmPassword.text.length > 0 && newPassword.text !== confirmPassword.text
text: "Les deux nouveaux mots de passe ne correspondent pas."
color: "#dc2626"
font.pixelSize: 12
}
Rectangle {
visible: root.operationError.length > 0 && root.currentStep === 1
Layout.fillWidth: true
implicitHeight: errorPasswordRow.implicitHeight + 24
radius: 12
color: "#fef2f2"
border.width: 1
border.color: "#fecaca"
RowLayout {
id: errorPasswordRow
anchors.fill: parent
anchors.margins: 12
spacing: 10
Kirigami.Icon {
source: "dialog-error"
implicitWidth: 20
implicitHeight: 20
color: "#dc2626"
}
Controls.Label {
Layout.fillWidth: true
text: root.operationError
wrapMode: Text.WordWrap
color: "#991b1b"
font.pixelSize: 12
}
}
}
Item { Layout.fillHeight: true }
RowLayout {
Layout.fillWidth: true
SecondaryButton {
text: "Retour"
onClicked: root.goBack()
}
Item { Layout.fillWidth: true }
PrimaryButton {
text: provisioning.busy ? "Modification…" : "Enregistrer et continuer"
enabled: root.passwordStepValid && !provisioning.busy
opacity: enabled ? 1.0 : 0.45
onClicked: {
root.operationError = ""
var oldValue = currentPassword.text
var newValue = newPassword.text
var confirmationValue = confirmPassword.text
provisioning.changePassword(oldValue, newValue, confirmationValue)
// Les champs graphiques sont vidés immédiatement après transmission
// au backend afin de réduire leur durée de présence en mémoire UI.
currentPassword.text = ""
newPassword.text = ""
confirmPassword.text = ""
}
}
}
}
}
// ---------------------------------
// 2 - YubiKey
// ---------------------------------
Item {
ColumnLayout {
anchors.fill: parent
spacing: 18
RowLayout {
Layout.fillWidth: true
spacing: 20
Rectangle {
Layout.preferredWidth: 80
Layout.preferredHeight: 80
radius: 22
color: "#f0fdf4"
border.width: 1
border.color: "#bbf7d0"
Controls.Label {
anchors.centerIn: parent
text: "🔐"
font.pixelSize: 34
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 5
Controls.Label {
text: "PIN de votre YubiKey"
color: "#0f172a"
font.pixelSize: 29
font.weight: Font.Bold
}
Controls.Label {
Layout.fillWidth: true
text: "Remplacez le PIN temporaire de préparation par un PIN personnel que vous serez seul à connaître."
wrapMode: Text.WordWrap
color: "#64748b"
font.pixelSize: 15
}
}
}
Rectangle {
Layout.fillWidth: true
implicitHeight: 78
radius: 12
color: "#fffbeb"
border.width: 1
border.color: "#fde68a"
RowLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
Controls.Label {
text: "⌨"
color: "#b45309"
font.pixelSize: 23
}
Controls.Label {
Layout.fillWidth: true
text: "Vérifiez Verr Num avant d'utiliser le pavé numérique. En cas de doute, utilisez les chiffres situés au-dessus des lettres."
wrapMode: Text.WordWrap
color: "#92400e"
font.pixelSize: 13
}
}
}
FieldLabel {
iconName: "dialog-password"
labelText: "PIN temporaire actuel"
}
SecretField {
id: currentPin
placeholderText: "PIN temporaire"
}
FieldLabel {
iconName: "security-high"
labelText: "Nouveau PIN"
}
SecretField {
id: newPin
placeholderText: "Nouveau PIN personnel"
}
FieldLabel {
iconName: "dialog-ok-apply"
labelText: "Confirmation du nouveau PIN"
}
SecretField {
id: confirmPin
placeholderText: "Confirmez le nouveau PIN"
}
Controls.Label {
visible: confirmPin.text.length > 0 && newPin.text !== confirmPin.text
text: "Les deux nouveaux PIN ne correspondent pas."
color: "#dc2626"
font.pixelSize: 12
}
Rectangle {
visible: root.operationError.length > 0 && root.currentStep === 2
Layout.fillWidth: true
implicitHeight: errorPinRow.implicitHeight + 24
radius: 12
color: "#fef2f2"
border.width: 1
border.color: "#fecaca"
RowLayout {
id: errorPinRow
anchors.fill: parent
anchors.margins: 12
spacing: 10
Kirigami.Icon {
source: "dialog-error"
implicitWidth: 20
implicitHeight: 20
color: "#dc2626"
}
Controls.Label {
Layout.fillWidth: true
text: root.operationError
wrapMode: Text.WordWrap
color: "#991b1b"
font.pixelSize: 12
}
}
}
Item { Layout.fillHeight: true }
RowLayout {
Layout.fillWidth: true
SecondaryButton {
visible: !provisioning.passwordDone
text: "Retour"
enabled: !provisioning.busy
onClicked: root.goBack()
}
Item { Layout.fillWidth: true }
PrimaryButton {
text: provisioning.busy ? "Configuration…" : "Configurer la YubiKey"
enabled: root.pinStepValid && !provisioning.busy
opacity: enabled ? 1.0 : 0.45
onClicked: {
root.operationError = ""
var oldValue = currentPin.text
var newValue = newPin.text
var confirmationValue = confirmPin.text
provisioning.changePin(oldValue, newValue, confirmationValue)
currentPin.text = ""
newPin.text = ""
confirmPin.text = ""
}
}
}
}
}
// ---------------------------------
// 3 - Terminé
// ---------------------------------
Item {
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: 680
Layout.alignment: Qt.AlignHCenter
text: "Votre mot de passe local de secours et le PIN personnel de votre YubiKey ont été configurés avec succès. Vos nouveaux secrets restent personnels et ne sont pas enregistrés dans Git."
wrapMode: Text.WordWrap
color: "#64748b"
font.pixelSize: 15
lineHeight: 1.25
horizontalAlignment: Text.AlignHCenter
}
Item { Layout.preferredHeight: 12 }
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 12
Repeater {
model: [
{ "icon": "dialog-password", "title": "Mot de passe", "text": "Accès local configuré" },
{ "icon": "security-high", "title": "YubiKey", "text": "PIN personnel configuré" },
{ "icon": "computer-laptop", "title": "Poste", "text": "Configuration finalisée" }
]
Rectangle {
required property var modelData
Layout.preferredWidth: 205
Layout.preferredHeight: 126
radius: 16
color: "#f8fafc"
border.width: 1
border.color: "#e2e8f0"
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 7
RowLayout {
Layout.fillWidth: true
Rectangle {
Layout.preferredWidth: 38
Layout.preferredHeight: 38
radius: 11
color: "#eff6ff"
Kirigami.Icon {
anchors.centerIn: parent
source: modelData.icon
implicitWidth: 21
implicitHeight: 21
color: "#2563eb"
}
}
Item { Layout.fillWidth: true }
Rectangle {
Layout.preferredWidth: 25
Layout.preferredHeight: 25
radius: 13
color: "#dcfce7"
Kirigami.Icon {
anchors.centerIn: parent
source: "dialog-ok-apply"
implicitWidth: 15
implicitHeight: 15
color: "#16a34a"
}
}
}
Controls.Label {
text: modelData.title
color: "#0f172a"
font.pixelSize: 14
font.weight: Font.DemiBold
}
Controls.Label {
Layout.fillWidth: true
text: modelData.text
color: "#64748b"
font.pixelSize: 11
wrapMode: Text.WordWrap
}
}
}
}
}
Item { Layout.fillHeight: true }
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 12
PrimaryButton {
text: "Commencer à travailler"
enabled: root.workflowComplete && !provisioning.busy
opacity: enabled ? 1.0 : 0.45
onClicked: root.close()
}
}
}
}
}
}
}
}
}
}
}