1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-06 05:25:41 -05:00
Files
DankMaterialShell/backend/internal/tui/views_password.go
2025-11-12 17:18:45 -05:00

339 lines
8.7 KiB
Go

package tui
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
)
func (m Model) viewAuthMethodChoice() string {
var b strings.Builder
b.WriteString(m.renderBanner())
b.WriteString("\n")
title := m.styles.Title.Render("Authentication Method")
b.WriteString(title)
b.WriteString("\n\n")
message := "Fingerprint authentication is available.\nHow would you like to authenticate?"
b.WriteString(m.styles.Normal.Render(message))
b.WriteString("\n\n")
// Option 0: Fingerprint
if m.selectedConfig == 0 {
option := m.styles.SelectedOption.Render("▶ Use Fingerprint")
b.WriteString(option)
} else {
option := m.styles.Normal.Render(" Use Fingerprint")
b.WriteString(option)
}
b.WriteString("\n")
// Option 1: Password
if m.selectedConfig == 1 {
option := m.styles.SelectedOption.Render("▶ Use Password")
b.WriteString(option)
} else {
option := m.styles.Normal.Render(" Use Password")
b.WriteString(option)
}
b.WriteString("\n\n")
help := m.styles.Subtle.Render("↑/↓: Navigate, Enter: Select, Esc: Back")
b.WriteString(help)
return b.String()
}
func (m Model) viewFingerprintAuth() string {
var b strings.Builder
b.WriteString(m.renderBanner())
b.WriteString("\n")
title := m.styles.Title.Render("Fingerprint Authentication")
b.WriteString(title)
b.WriteString("\n\n")
if m.fingerprintFailed {
errorMsg := m.styles.Error.Render("✗ Fingerprint authentication failed")
b.WriteString(errorMsg)
b.WriteString("\n")
retryMsg := m.styles.Subtle.Render("Returning to authentication menu...")
b.WriteString(retryMsg)
} else {
message := "Please place your finger on the fingerprint reader."
b.WriteString(m.styles.Normal.Render(message))
b.WriteString("\n\n")
spinner := m.spinner.View()
status := m.styles.Normal.Render("Waiting for fingerprint...")
b.WriteString(fmt.Sprintf("%s %s", spinner, status))
}
return b.String()
}
func (m Model) viewPasswordPrompt() string {
var b strings.Builder
b.WriteString(m.renderBanner())
b.WriteString("\n")
title := m.styles.Title.Render("Password Authentication")
b.WriteString(title)
b.WriteString("\n\n")
message := "Installation requires sudo privileges.\nPlease enter your password to continue:"
b.WriteString(m.styles.Normal.Render(message))
b.WriteString("\n\n")
// Password input
b.WriteString(m.passwordInput.View())
b.WriteString("\n")
// Show validation status
if m.packageProgress.step == "Validating sudo password..." {
spinner := m.spinner.View()
status := m.styles.Normal.Render(m.packageProgress.step)
b.WriteString(spinner + " " + status)
b.WriteString("\n")
} else if m.packageProgress.error != nil {
errorMsg := m.styles.Error.Render("✗ " + m.packageProgress.error.Error() + ". Please try again.")
b.WriteString(errorMsg)
b.WriteString("\n")
} else if m.packageProgress.step == "Password validation failed" {
errorMsg := m.styles.Error.Render("✗ Incorrect password. Please try again.")
b.WriteString(errorMsg)
b.WriteString("\n")
}
b.WriteString("\n")
help := m.styles.Subtle.Render("Enter: Continue, Esc: Back, Ctrl+C: Cancel")
b.WriteString(help)
return b.String()
}
func (m Model) updateAuthMethodChoiceState(msg tea.Msg) (tea.Model, tea.Cmd) {
m.fingerprintFailed = false
if keyMsg, ok := msg.(tea.KeyMsg); ok {
switch keyMsg.String() {
case "up":
if m.selectedConfig > 0 {
m.selectedConfig--
}
case "down":
if m.selectedConfig < 1 {
m.selectedConfig++
}
case "enter":
if m.selectedConfig == 0 {
m.state = StateFingerprintAuth
m.isLoading = true
return m, tea.Batch(m.spinner.Tick, m.tryFingerprint())
} else {
m.state = StatePasswordPrompt
m.passwordInput.Focus()
return m, nil
}
case "esc":
m.state = StateDependencyReview
return m, nil
}
}
return m, nil
}
func (m Model) updateFingerprintAuthState(msg tea.Msg) (tea.Model, tea.Cmd) {
if validMsg, ok := msg.(passwordValidMsg); ok {
if validMsg.valid {
m.sudoPassword = ""
m.packageProgress = packageInstallProgressMsg{}
m.state = StateInstallingPackages
m.isLoading = true
return m, tea.Batch(m.spinner.Tick, m.installPackages())
} else {
m.fingerprintFailed = true
return m, m.delayThenReturn()
}
}
if _, ok := msg.(delayCompleteMsg); ok {
m.fingerprintFailed = false
m.selectedConfig = 0
m.state = StateAuthMethodChoice
return m, nil
}
return m, m.listenForLogs()
}
func (m Model) updatePasswordPromptState(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
if validMsg, ok := msg.(passwordValidMsg); ok {
if validMsg.valid {
// Password is valid, proceed with installation
m.sudoPassword = validMsg.password
m.passwordInput.SetValue("") // Clear password input
// Clear any error state
m.packageProgress = packageInstallProgressMsg{}
m.state = StateInstallingPackages
m.isLoading = true
return m, tea.Batch(m.spinner.Tick, m.installPackages())
} else {
// Password is invalid, show error and stay on password prompt
m.packageProgress = packageInstallProgressMsg{
progress: 0.0,
step: "Password validation failed",
error: fmt.Errorf("incorrect password"),
logOutput: "Authentication failed",
}
m.passwordInput.SetValue("")
m.passwordInput.Focus()
return m, nil
}
}
if keyMsg, ok := msg.(tea.KeyMsg); ok {
switch keyMsg.String() {
case "enter":
// Don't allow multiple validation attempts while one is in progress
if m.packageProgress.step == "Validating sudo password..." {
return m, nil
}
// Validate password first
password := m.passwordInput.Value()
if password == "" {
return m, nil // Don't proceed with empty password
}
// Clear any previous error and show validation in progress
m.packageProgress = packageInstallProgressMsg{
progress: 0.01,
step: "Validating sudo password...",
isComplete: false,
logOutput: "Testing password with sudo -v",
}
return m, m.validatePassword(password)
case "esc":
// Go back to dependency review
m.passwordInput.SetValue("")
m.packageProgress = packageInstallProgressMsg{} // Clear any validation state
m.state = StateDependencyReview
return m, nil
}
}
m.passwordInput, cmd = m.passwordInput.Update(msg)
return m, cmd
}
func checkFingerprintEnabled() bool {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
// Check if pam_fprintd.so is in PAM config
cmd := exec.CommandContext(ctx, "grep", "-q", "pam_fprintd.so", "/etc/pam.d/system-auth")
if err := cmd.Run(); err != nil {
return false
}
// Check if fprintd-list exists and user has enrolled fingerprints
user := os.Getenv("USER")
if user == "" {
return false
}
listCmd := exec.CommandContext(ctx, "fprintd-list", user)
output, err := listCmd.CombinedOutput()
if err != nil {
return false
}
// If output contains "finger:" or similar, fingerprints are enrolled
return strings.Contains(string(output), "finger")
}
func (m Model) delayThenReturn() tea.Cmd {
return func() tea.Msg {
time.Sleep(2 * time.Second)
return delayCompleteMsg{}
}
}
func (m Model) tryFingerprint() tea.Cmd {
return func() tea.Msg {
clearCmd := exec.Command("sudo", "-k")
clearCmd.Run()
tmpDir := os.TempDir()
askpassScript := filepath.Join(tmpDir, fmt.Sprintf("danklinux-fp-%d.sh", time.Now().UnixNano()))
scriptContent := "#!/bin/sh\nexit 1\n"
if err := os.WriteFile(askpassScript, []byte(scriptContent), 0700); err != nil {
return passwordValidMsg{password: "", valid: false}
}
defer os.Remove(askpassScript)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "sudo", "-A", "-v")
cmd.Env = append(os.Environ(), fmt.Sprintf("SUDO_ASKPASS=%s", askpassScript))
err := cmd.Run()
if err != nil {
return passwordValidMsg{password: "", valid: false}
}
return passwordValidMsg{password: "", valid: true}
}
}
func (m Model) validatePassword(password string) tea.Cmd {
return func() tea.Msg {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
stdin, err := cmd.StdinPipe()
if err != nil {
return passwordValidMsg{password: "", valid: false}
}
if err := cmd.Start(); err != nil {
return passwordValidMsg{password: "", valid: false}
}
_, err = fmt.Fprintf(stdin, "%s\n", password)
stdin.Close()
if err != nil {
return passwordValidMsg{password: "", valid: false}
}
err = cmd.Wait()
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
return passwordValidMsg{password: "", valid: false}
}
return passwordValidMsg{password: "", valid: false}
}
return passwordValidMsg{password: password, valid: true}
}
}