Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,22 @@ func (a *App) PerformUpdate(downloadURL string) error {
}

err = backend.PerformUpdate(tmpFile)
if err != nil {
// Si sudo est requis, émettre un événement spécifique
if err == backend.ErrSudoRequired {
runtime.EventsEmit(a.ctx, "update:sudo-required", tmpFile)
return err
}
return err
}

runtime.EventsEmit(a.ctx, "update:complete")
return nil
}

// PerformUpdateWithSudo performs the update with sudo password
func (a *App) PerformUpdateWithSudo(tmpFilePath string, password string) error {
err := backend.PerformUpdateWithPassword(tmpFilePath, password)
if err != nil {
return err
}
Expand Down
64 changes: 57 additions & 7 deletions backend/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package backend

import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
Expand All @@ -25,6 +26,9 @@ type UpdateInfo struct {
DownloadURL string `json:"downloadUrl"`
}

// ErrSudoRequired is returned when sudo privileges are required
var ErrSudoRequired = errors.New("sudo_required")

func GetCurrentVersion() string {
return CURRENT_VERSION
}
Expand Down Expand Up @@ -167,26 +171,72 @@ func PerformUpdate(tmpFilePath string) error {
backupPath := currentExe + ".backup"
err = os.Rename(currentExe, backupPath)
if err != nil {
cmd := exec.Command("sudo", "mv", currentExe, backupPath)
if err := cmd.Run(); err != nil {
return fmt.Errorf("erreur lors de la sauvegarde de l'ancien binaire: %v", err)
// Si on ne peut pas renommer, on a besoin de sudo
if os.IsPermission(err) {
return ErrSudoRequired
}
return fmt.Errorf("erreur lors de la sauvegarde de l'ancien binaire: %v", err)
}

err = os.Rename(tmpFilePath, currentExe)
if err != nil {
cmd := exec.Command("sudo", "mv", tmpFilePath, currentExe)
if err := cmd.Run(); err != nil {
os.Rename(backupPath, currentExe)
return fmt.Errorf("erreur lors du remplacement du binaire: %v", err)
// Restaurer l'ancien binaire
os.Rename(backupPath, currentExe)
// Si on ne peut pas renommer, on a besoin de sudo
if os.IsPermission(err) {
return ErrSudoRequired
}
return fmt.Errorf("erreur lors du remplacement du binaire: %v", err)
}

os.Remove(backupPath)

return nil
}

// PerformUpdateWithPassword performs the update using sudo with the provided password
func PerformUpdateWithPassword(tmpFilePath string, password string) error {
currentExe, err := os.Executable()
if err != nil {
return fmt.Errorf("erreur lors de la récupération du chemin de l'exécutable: %v", err)
}

currentExe, err = filepath.EvalSymlinks(currentExe)
if err != nil {
return fmt.Errorf("erreur lors de la résolution du symlink: %v", err)
}

backupPath := currentExe + ".backup"

// Créer un script pour effectuer la mise à jour
script := fmt.Sprintf(`#!/bin/bash
mv "%s" "%s" || exit 1
mv "%s" "%s" || { mv "%s" "%s"; exit 1; }
rm -f "%s"
`,
currentExe, backupPath,
tmpFilePath, currentExe,
backupPath, currentExe,
backupPath)

// Utiliser sudo avec le mot de passe fourni
cmd := exec.Command("sudo", "-S", "bash", "-c", script)
cmd.Stdin = strings.NewReader(password + "\n")

output, err := cmd.CombinedOutput()
if err != nil {
// Vérifier si c'est une erreur d'authentification
if strings.Contains(string(output), "incorrect password") ||
strings.Contains(string(output), "Désolé, essayez de nouveau") ||
strings.Contains(string(output), "Sorry, try again") {
return fmt.Errorf("mot de passe incorrect")
}
return fmt.Errorf("erreur lors de la mise à jour: %v - %s", err, string(output))
}

return nil
}

func GetLatestReleaseInfo() (map[string]interface{}, error) {
url := fmt.Sprintf("https://api.github.com/repos/aidalinfo/aidalinfo-devcli/releases/latest")

Expand Down
106 changes: 106 additions & 0 deletions frontend/src/components/SudoPasswordDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<template>
<v-dialog v-model="dialog" max-width="500px" persistent>
<v-card>
<v-card-title class="text-h5">
Privilèges administrateur requis
</v-card-title>

<v-card-text>
<v-alert type="info" variant="tonal" class="mb-4">
La mise à jour nécessite des privilèges administrateur pour remplacer le fichier exécutable.
</v-alert>

<v-text-field
v-model="password"
label="Mot de passe sudo"
type="password"
variant="outlined"
density="compact"
:error-messages="errorMessage"
@keyup.enter="confirmPassword"
autofocus
/>
</v-card-text>

<v-card-actions>
<v-spacer />
<v-btn color="error" variant="text" @click="cancel">
Annuler
</v-btn>
<v-btn
color="primary"
variant="flat"
@click="confirmPassword"
:loading="loading"
:disabled="!password"
>
Confirmer
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue'

interface Props {
modelValue: boolean
tmpFilePath: string
}

const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'confirm', 'cancel'])

const dialog = ref(false)
const password = ref('')
const loading = ref(false)
const errorMessage = ref('')

watch(() => props.modelValue, (val) => {
dialog.value = val
if (val) {
// Réinitialiser le formulaire quand le dialogue s'ouvre
password.value = ''
errorMessage.value = ''
loading.value = false
}
})

watch(dialog, (val) => {
emit('update:modelValue', val)
})

const confirmPassword = async () => {
if (!password.value) {
errorMessage.value = 'Veuillez entrer votre mot de passe'
return
}

loading.value = true
errorMessage.value = ''

try {
await emit('confirm', password.value, props.tmpFilePath)
// Le composant parent fermera le dialogue en cas de succès
} catch (error: any) {
// Gérer l'erreur de mot de passe incorrect
if (error.message?.includes('mot de passe incorrect')) {
errorMessage.value = 'Mot de passe incorrect'
} else {
errorMessage.value = 'Erreur lors de la mise à jour'
}
} finally {
loading.value = false
// Effacer le mot de passe de la mémoire
if (errorMessage.value) {
password.value = ''
}
}
}

const cancel = () => {
emit('cancel')
dialog.value = false
}
</script>
78 changes: 64 additions & 14 deletions frontend/src/components/UpdateDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,18 @@
</DialogFooter>
</DialogContent>
</Dialog>

<!-- Dialog pour le mot de passe sudo -->
<SudoPasswordDialog
v-model="showSudoDialog"
:tmp-file-path="tmpFilePath"
@confirm="handleSudoPassword"
@cancel="handleSudoCancel"
/>
</template>

<script setup lang="ts">
import { ref, watch } from 'vue'
import { ref, watch, onMounted, onUnmounted } from 'vue'
import {
Dialog,
DialogContent,
Expand All @@ -63,7 +71,8 @@ import {
DialogTitle,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { CheckForUpdates, PerformUpdate } from '../../wailsjs/go/main/App'
import SudoPasswordDialog from './SudoPasswordDialog.vue'
import { CheckForUpdates, PerformUpdate, PerformUpdateWithSudo } from '../../wailsjs/go/main/App'
import { EventsOn, EventsOff } from '../../wailsjs/runtime/runtime'
import { toast } from 'vue-sonner'

Expand All @@ -83,6 +92,8 @@ const isDownloading = ref(false)
const downloadProgress = ref(0)
const updateError = ref('')
const updateSuccess = ref(false)
const showSudoDialog = ref(false)
const tmpFilePath = ref('')

watch(() => props.modelValue, (newVal) => {
isOpen.value = newVal
Expand All @@ -108,6 +119,33 @@ const checkForUpdates = async () => {
}
}

// Écouter l'événement sudo-required
onMounted(() => {
EventsOn('update:sudo-required', (filePath: string) => {
tmpFilePath.value = filePath
showSudoDialog.value = true
isDownloading.value = false
})

EventsOn('update:complete', () => {
updateSuccess.value = true
showSudoDialog.value = false
toast.success('Mise à jour réussie! L\'application va redémarrer...')

// Attendre un peu avant de fermer
setTimeout(() => {
isOpen.value = false
// L'application devrait redémarrer automatiquement
}, 3000)
})
})

onUnmounted(() => {
EventsOff('update:sudo-required')
EventsOff('update:complete')
EventsOff('update:progress')
})

const performUpdate = async () => {
if (!downloadURL.value) return

Expand All @@ -122,22 +160,34 @@ const performUpdate = async () => {
})

await PerformUpdate(downloadURL.value)
// Si sudo est requis, l'événement sera émis et le dialogue s'ouvrira

updateSuccess.value = true
toast.success('Mise à jour réussie! L\'application va redémarrer...')

// Attendre un peu avant de fermer
setTimeout(() => {
isOpen.value = false
// L'application devrait redémarrer automatiquement
}, 3000)

} catch (error) {
} catch (error: any) {
console.error('Erreur lors de la mise à jour:', error)
updateError.value = `Échec de la mise à jour: ${error}`
// Si c'est une erreur sudo_required, le dialogue s'ouvrira automatiquement
if (!error.message?.includes('sudo_required')) {
updateError.value = `Échec de la mise à jour: ${error}`
isDownloading.value = false
}
} finally {
isDownloading.value = false
EventsOff('update:progress')
}
}

const handleSudoPassword = async (password: string, filePath: string) => {
try {
await PerformUpdateWithSudo(filePath, password)
showSudoDialog.value = false
// L'événement update:complete sera émis par le backend
} catch (error: any) {
// Propager l'erreur au composant SudoPasswordDialog
throw error
}
}

const handleSudoCancel = () => {
showSudoDialog.value = false
isDownloading.value = false
updateError.value = 'Mise à jour annulée par l\'utilisateur'
}
</script>