#!/bin/bash

# Copyright Jean-Philippe Guillemin <jp.guillemin@free.fr>. This program is free software;
# you can redistribute it and/or modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2 of the License,
# or (at your option) any later version. Please take a look at http://www.gnu.org/copyleft/gpl.htm
# Version 1.7 changes by Teran McKinney (sega01).
# Modernized yad GUI edition (X-only, no gettext) by Claude.

# Userconfig can:
# - list existing users/groups and their properties
# - create/delete users and groups
# - modify any user properties (single combined form)

if ! command -v yad >/dev/null 2>&1; then
  echo "yad is required but was not found in PATH." >&2
  exit 1
fi

if [ ! "$DISPLAY" ]; then
  echo "This program requires a running X session." >&2
  exit 1
fi

export PATH="${PATH}:/usr/sbin:/sbin"

skeletonDir='/etc/skel'
defaultShell='/bin/bash'
defaultGroup='users'
passwdFile='/etc/passwd'
groupFile='/etc/group'
shadowFile='/etc/shadow'
title="User Manager"
icon="system-users"

YAD_COMMON=(--center --window-icon="$icon")

for isGroupInSystem in audio video lp cdrom fuse dialout floppy plugdev netdev scanner power polkitd taratata ; do
  grep -q "^${isGroupInSystem}:" $groupFile && defaultMembership="${isGroupInSystem},${defaultMembership}"
done
defaultMembership="$( echo ${defaultMembership} | sed -e 's/^\(.*\),$/\1/' )"

# Common list of shells for the shell combo box
shellList="/bin/bash!/bin/sh!/bin/zsh!/bin/dash!/usr/bin/fish!/sbin/nologin"

infoBox() {
  yad "${YAD_COMMON[@]}" --title "$title" --text "$1" \
    --button="OK:0" --image="dialog-information" --width=380
}

warnBox() {
  yad "${YAD_COMMON[@]}" --title "Warning" --text "$1" \
    --button="OK:0" --image="dialog-warning" --width=420
}

confirmBox() {
  # returns 0 if confirmed (Proceed), 1 otherwise
  yad "${YAD_COMMON[@]}" --title "Confirm" --text "$1" \
    --image="dialog-question" --width=420 \
    --button="Cancel:1" --button="Proceed:0"
}

###############################################
# Main menu (home screen)

promptAction() {
  mode=$(yad "${YAD_COMMON[@]}" --width=520 --height=240 \
    --title "$title" \
    --text "<big><b>Welcome to $(basename $0)</b></big>\n\nSelect an action:" \
    --list --no-headers --separator="" \
    --column="icon:IMG" --column="Action" --column="Description" --column="key:HD" \
    "system-users" "List / Edit Users" "Browse existing users and edit their properties" "List users" \
    "contact-new" "New User" "Create a new user account" "New user" \
    "system-users" "List / Edit Groups" "Browse groups and their members" "List groups" \
    "folder-new" "New Group" "Create a new group" "New group" \
    --button="Exit:1" --button="Select:0" \
    --print-column=4 2>/dev/null)

  rc=$?
  mode="$(echo "$mode" | tr -d '\n' )"
  if [ $rc -ne 0 ] || [ ! "$mode" ]; then
    mode="none"
  fi
}

###############################################
# list existing users

showUsers() {
  local rowArgs=()
  while IFS=: read -r uName _ uUid _; do
    [ ! "$uName" ] && continue
    rowArgs+=("avatar-default" "$uName" "$uUid")
  done < "$passwdFile"

  user=$(yad "${YAD_COMMON[@]}" --width=520 --height=520 \
    --title "Users" \
    --text "<b>Users of the system</b>\nSelect a user to edit, or close to return." \
    --list --no-headers \
    --column "icon:IMG" --column "User" --column "UID" \
    --button="Back:1" --button="Edit:0" \
    --print-column=2 \
    "${rowArgs[@]}")
  rc=$?
  user="${user%|*}"
  [ $rc -ne 0 ] && user=""
}

###############################################
# list existing groups

showGroups() {
  local rowArgs=()
  while IFS=: read -r gName _ gGid _; do
    [ ! "$gName" ] && continue
    rowArgs+=("folder" "$gName" "$gGid")
  done < "$groupFile"

  group=$(yad "${YAD_COMMON[@]}" --width=520 --height=460 \
    --title "Groups" \
    --text "<b>Groups of the system</b>\nSelect a group to Edit, or close to return." \
    --list --no-headers \
    --column "icon:IMG" --column "Group" --column "GID" \
    --button="Back:1" --button="Edit:0" \
    --print-column=2 \
    "${rowArgs[@]}")
  rc=$?
  group="${group%|*}"

  case $rc in
    0) check="" ;;
    *) group=""; check="" ;;
  esac
}

###############################################
# Get user parameters from system

getSystemInfo() {
  uid="$(grep -e "^$user:.*$" $passwdFile | cut -d: -f 3 )"
  gid="$(grep -e "^$user:.*$" $passwdFile | cut -d: -f 4 )"
  homeDir="$(grep -e "^$user:.*$" $passwdFile | cut -d: -f 6 )"
  shell="$(grep -e "^$user:.*$" $passwdFile | cut -d: -f 7 )"
  group="$(groups "$user" | sed -e 's/^.*: \(.*\)$/\1/' | cut -d \  -f 1 )"
  membership="$(groups "$user" | sed -e 's/^.*: \(.*\)$/\1/' | cut -d \  -f 2- )"
  expiryDays="$(grep -e "^$user:.*$" $shadowFile | cut -d: -f 8 )"
  if [ "$expiryDays" ]; then
    expiry="$(date -u --date "Jan 1, 1970 + $expiryDays days" +%Y-%m-%d)"
  else
    expiry=""
  fi
  fingerInfo="$(grep -e "^$user:.*$" $passwdFile | cut -d: -f 5 )"
  realName="$(echo $fingerInfo | cut -d , -f 1 )"
  office="$(echo $fingerInfo | cut -d , -f 2 )"
  workPhone="$(echo $fingerInfo | cut -d , -f 3 )"
  homePhone="$(echo $fingerInfo | cut -d , -f 4 )"
}

###############################################
# Edit a user: single combined form for everything

editUser() {
  [ ! "$user" ] && return
  getSystemInfo

  # Build shell combo with the current shell pre-selected/first
  combo="$shell"
  IFS='!' read -ra shells <<< "$shellList"
  for s in "${shells[@]}"; do
    [ "$s" != "$shell" ] && combo="${combo}!${s}"
  done

  result=$(yad "${YAD_COMMON[@]}" --width=520 --height=560 \
    --title "Edit user: $user" \
    --text "<big><b>$user</b></big>  (UID $uid)\nEdit any field below, then click Save." \
    --form --columns=1 \
    --field="Real Name" "$realName" \
    --field="User Name" "$user" \
    --field="Main Group" "$group" \
    --field="Home directory" "$homeDir" \
    --field="Shell:CB" "$combo" \
    --field="Additional groups (space separated)" "$membership" \
    --field="Account expiry (YYYY-MM-DD, blank = never)" "$expiry" \
    --field="Office" "$office" \
    --field="Work phone" "$workPhone" \
    --field="Home phone" "$homePhone" \
    --field="Change password:CHK" FALSE \
    --field="Delete this user:CHK" FALSE \
    --button="Back:1" --button="Save:0" )
  rc=$?
  [ $rc -ne 0 ] && return

  IFS='|' read -r newRealName newUsername newGroup newHome newShell newMembership \
    newExpiry newOffice newWorkPhone newHomePhone newChangePw newDelete <<< "$result"

  if [ "$newDelete" == "TRUE" ]; then
    killUser
    return
  fi

  local errors=""

  if [ "$newUsername" != "$user" ] && [ "$newUsername" ]; then
    message="$(usermod -l "$newUsername" "$user" 2>&1)"
    if [ $? -eq 0 ]; then
      user="$newUsername"
    else
      errors="${errors}Rename: $message\n"
    fi
  fi

  if [ "$newRealName" != "$realName" ]; then
    message="$(chfn -f "$newRealName" "$user" 2>&1)"
    [ $? -ne 0 ] && errors="${errors}Real name: $message\n"
  fi

  if [ "$newGroup" != "$group" ] && [ "$newGroup" ]; then
    message="$(usermod -g "$newGroup" "$user" 2>&1)"
    [ $? -ne 0 ] && errors="${errors}Main group: $message\n"
  fi

  if [ "$newHome" != "$homeDir" ] && [ "$newHome" ]; then
    message="$(usermod -d "$newHome" "$user" 2>&1)"
    if [ $? -eq 0 ]; then
      if [ -e "$newHome" ]; then
        chown -R "$user:$newGroup" "$newHome" 2>/dev/null
      else
        cp -rf $skeletonDir "$newHome" 2>/dev/null
        chown -R "$user:$newGroup" "$newHome" 2>/dev/null
      fi
    else
      errors="${errors}Home directory: $message\n"
    fi
  fi

  if [ "$newShell" != "$shell" ] && [ "$newShell" ]; then
    message="$(usermod -s "$newShell" "$user" 2>&1)"
    [ $? -ne 0 ] && errors="${errors}Shell: $message\n"
  fi

  if [ "$newMembership" != "$membership" ]; then
    userModMembership="$(echo $newMembership | tr " " "," )"
    message="$(usermod -G "$userModMembership" "$user" 2>&1)"
    [ $? -ne 0 ] && errors="${errors}Membership: $message\n"
  fi

  if [ "$newExpiry" != "$expiry" ]; then
    usermod -e "" "$user" 2>/dev/null
    if [ "$newExpiry" ]; then
      message="$(usermod -e "$newExpiry" "$user" 2>&1)"
      [ $? -ne 0 ] && errors="${errors}Expiry: $message\n"
    fi
  fi

  if [ "$newOffice" != "$office" ]; then
    message="$(chfn -o "$newOffice" "$user" 2>&1)"
    [ $? -ne 0 ] && errors="${errors}Office: $message\n"
  fi

  if [ "$newWorkPhone" != "$workPhone" ]; then
    message="$(chfn -p "$newWorkPhone" "$user" 2>&1)"
    [ $? -ne 0 ] && errors="${errors}Work phone: $message\n"
  fi

  if [ "$newHomePhone" != "$homePhone" ]; then
    message="$(chfn -h "$newHomePhone" "$user" 2>&1)"
    [ $? -ne 0 ] && errors="${errors}Home phone: $message\n"
  fi

  if [ "$newChangePw" == "TRUE" ]; then
    setPw
  fi

  if [ "$errors" ]; then
    warnBox "Some changes could not be applied:\n\n$errors"
  else
    infoBox "Changes saved for user <b>$user</b>."
  fi
}

###############################################
# Edit group

editGroup() {
  gid="$(grep -e "^$group:.*$" $groupFile | cut -d: -f 3)"

  # Members explicitly listed in /etc/group (secondary members)
  secondaryMembers="$(grep -e "^$group:.*$" $groupFile | cut -d: -f 4 | tr "," " ")"

  # Users whose PRIMARY group (field 4 of /etc/passwd) is this group's GID.
  # Match the GID field precisely instead of grepping the whole line.
  primaryMembers="$(awk -F: -v gid="$gid" '$4 == gid { print $1 }' "$passwdFile" | tr "\n" " ")"

  result=$(yad "${YAD_COMMON[@]}" --width=520 --height=240 \
    --title "Edit group: $group" \
    --text "<big><b>$group</b></big>  (GID $gid)\nEdit any field below, then click Save." \
    --form --columns=1 \
    --field="Group Name" "$group" \
    --field="Primary members:RO" "$primaryMembers" \
    --field="Secondary members" "$secondaryMembers" \
    --field="Delete this group:CHK" FALSE \
    --button="Back:1" --button="Save:0" )
  rc=$?
  [ $rc -ne 0 ] && return

  IFS='|' read -r newGroupName readOnlyField newSecondaryMembers newDelete <<< "$result"

  if [ "$newDelete" == "TRUE" ]; then
    killGroup
    return
  fi

  local errors=""

  if [ "$group" != "$newGroupName" ]; then
    message="$(groupmod -n "$newGroupName" "$group" 2>&1)"
    if [ $? -eq 0 ]; then
      group="$newGroupName"
    else
      errors="${errors}Group Name: $message\n"
    fi
  fi

  if [ "$secondaryMembers" != "$newSecondaryMembers" ]; then
    # Remove users no longer listed as secondary members
    for member in $secondaryMembers ; do
      if [[ " $newSecondaryMembers " != *" $member "* ]]; then
        message="$(usermod -rG "$group" "$member" 2>&1)"
        [ $? -ne 0 ] && errors="${errors}Remove $member: $message\n"
      fi
    done
    # Add newly listed secondary members
    for member in $newSecondaryMembers ; do
      if [[ " $secondaryMembers " != *" $member "* ]]; then
        message="$(usermod -aG "$group" "$member" 2>&1)"
        [ $? -ne 0 ] && errors="${errors}Add $member: $message\n"
      fi
    done
  fi

  if [ "$errors" ]; then
    warnBox "Some changes could not be applied:\n\n$errors"
  else
    infoBox "Changes saved for group <b>$group</b>."
  fi
}

###############################################
# Create new user

newUser() {
  result="nok"

  newUserResult=$(yad "${YAD_COMMON[@]}" --width=520 \
    --title "New user" \
    --text "<big><b>Create a new user</b></big>" \
    --form --columns=1 \
    --field="User name" "" \
    --field="Real name" "" \
    --field="Set password now:CHK" TRUE \
    --field="System / service account (no login, no home dir):CHK" FALSE \
    --button="Cancel:1" --button="Create:0")
  rc=$?
  [ $rc -ne 0 ] && return

  IFS='|' read -r newName newRealName setPwNow sysAccount <<< "$newUserResult"

  if [ "$newName" ]; then
    for membershipGroup in $(echo $defaultMembership | tr "," " ") ; do
      groupadd $membershipGroup 2>/dev/null
    done
    if [ "$sysAccount" != "TRUE" ]; then
      message="$(useradd -s $defaultShell -g $defaultGroup -m -k $skeletonDir -G $defaultMembership "$newName" 2>&1 && result="ok")"
      if [ ! $? = 0 ]; then
        warnBox "$message"
      else
        chfn -f "${newRealName:-$newName}" "$newName" 2>/dev/null
        chfn -r "Zenwalk users" "$newName" 2>/dev/null
        user="$newName"
        if [ "$setPwNow" == "TRUE" ]; then
          setPw
        fi
      fi
    else
      message="$(useradd -s $defaultShell -g nobody -r -M "$newName" 2>&1)"
      if [ ! $? = 0 ]; then
        warnBox "$message"
      else
        result="ok"
        user="$newName"
      fi
    fi
  fi
}

###############################################
# Password change

changePw(){
  maxCount=3
  minLength=5

  count=1 ; pw='_'
  while [ "${pw}" != "${password}" ]; do
    password="" ; pw='_'
    if [ $count -gt $maxCount ]; then
      echo -n ""
      break
    fi

    message="Enter new password for user ${1} (#${count} try):"
    [ $count -ge 2 ] && message="Passwords do not match. Enter password for user ${1} (#${count} try):"
    while [ "$(echo -n "${pw}" | wc --chars)" -lt ${minLength} ]; do
      pw="$(yad "${YAD_COMMON[@]}" --width=420 --title "Password" --text "${message}" --entry --hide-text --button="Cancel:1" --button="OK:0")"
      [ $? != 0 ] && return 1
      message="Password is too short (${minLength} chars minimum), please try another one:"
    done
    password="$pw" ; pw='_'
    message="Confirm new password for user ${1} (#${count} try):"
    pw="$(yad "${YAD_COMMON[@]}" --width=420 --title "Password" --text "${message}" --entry --hide-text --button="Cancel:1" --button="OK:0")"
    [ $? != 0 ] && return 1
    count=$((count+1))
  done

  echo -n $password
}

setPw() {
  [ "$user" ] && pass="$(changePw ${user})"
  [ $? == 1 ] && return 1
  if [ ! "$pass" ] ; then
    warnBox "Failed to set password for user \"${user}\"."
  else
    if [ "$(id -u)" == "0" ]; then
      [ "$user" ] && echo "${user}:${pass}" | /usr/sbin/chpasswd 1>/dev/null 2>&1 && result="ok"
    else
      [ "$user" ] && echo "${user}:${pass}" | /usr/bin/pkexec /usr/sbin/chpasswd 1>/dev/null 2>&1 && result="ok"
    fi
    [ "$result" == "ok" ] && infoBox "Password changed for user <b>$user</b>."
  fi
}

###############################################
# Delete a user

killUser() {
  result="nok"
  if [ "$user" ]; then
    if [ "$user" == "root" ]; then
      warnBox "No way to delete $user."
      return 0
    else
      confirmBox "Delete user <b>$user</b>: are you sure? This cannot be undone."
      [ ! $? = 0 ] && return 0
      message="$(userdel -f $user 2>&1 && result="ok")"
      if [ ! $? = 0 ]; then
        warnBox "$message"
      else
        infoBox "User <b>$user</b> deleted."
      fi
      return 0
    fi
  fi
}

###############################################
# Delete a group

killGroup() {
  result="nok"
  if [ "$group" ]; then
    if [ "$group" == "root" ]; then
      warnBox "No way to delete $group."
      return 0
    else
      confirmBox "Delete group <b>$group</b>: are you sure?"
      [ ! $? = 0 ] && return 0
      message="$(groupdel $group 2>&1 && result="ok")"
      if [ ! $? = 0 ] ; then
        warnBox "$message"
      else
        infoBox "Group <b>$group</b> deleted."
      fi
      return 0
    fi
  fi
}

###############################################
# Create a new group

newGroup() {
  result="nok"
  group=$(yad "${YAD_COMMON[@]}" --width=520 \
    --title "New group" \
    --text "<big><b>Create a new group</b></big>" \
    --entry --entry-text="" --entry-label="Group name:" \
    --button="Cancel:1" --button="Create:0")

  if [ "$group" ]; then
    message="$(groupadd $group 2>&1 && result="ok")"
    [ ! $? = 0 ] && warnBox "$message"
  fi
}

###############################################
# Quick options for unprivileged user

if [ "$1" ]; then
  case "$1" in
    'passwd')
    result="nok"
    user=$2
    [ "$user" == "" ] && user="$(whoami)"
    setPw
    exit
    ;;
  esac
fi

if [ "$(id -u)" != "0" ]; then
  warnBox "You need to be root to run $(basename $0)."
  exit
fi

# Quick options for root
if [ "$1" ]; then
  case "$1" in
    'adduser')
      infoBox "Ready to create a new user."
      user=""
      newUser
      while [ "$result" != "ok" ] ; do
        confirmBox "User creation failed or was cancelled. Retry?"
        [ ! $? = 0 ] && break
        user=""
        newUser
      done
      if [ "$result" == "ok" ]; then
        editUser
      fi
    exit 0
    ;;
  esac
fi

###############################################
# main()

while [ "1" = "1" ] ; do
  promptAction
  case $mode in
    "List users")
      while [ "1" = "1" ] ; do
        user=""
        showUsers
        if [ "$user" ]; then
          editUser
        else
          break
        fi
      done
      ;;
    "New user")
      user=""
      newUser
      if [ "$result" == "ok" ]; then
        editUser
      fi
      ;;
    "List groups")
      while [ "1" = "1" ] ; do
        group=""
        showGroups
        if [ "$group" ]; then
          editGroup
        else
          break
        fi
      done
      ;;
    "New group")
      group=""
      newGroup
      ;;
    none)
      exit 0
  esac
done

# end
