From f003565e83e0eee6ba91b986f5a0ed4d082a2737 Mon Sep 17 00:00:00 2001
From: tss24 <tss24@inf.ufpr.br>
Date: Fri, 28 Feb 2025 21:43:38 -0300
Subject: [PATCH] Holy Refactor: OOP

---
 cmd/user.go             |   5 +-
 cmd/user/bulk.go        | 154 ++-----------
 cmd/user/create.go      | 434 ++++---------------------------------
 cmd/user/create_test.go | 110 ----------
 cmd/user/mod.go         |  83 +++----
 cmd/user/remove.go      | 166 ++------------
 cmd/user/reset.go       |  55 ++---
 cmd/user/show.go        | 317 ++-------------------------
 cmd/user/temp.go        | 197 +++++++++++++++++
 cmd/user/validation.go  |  81 +------
 extras/create_test.go   | 112 ++++++++++
 model/krb.go            |  46 ++++
 model/ldap.go           | 281 ++++++++++++++++++++++++
 model/ltype.go          |  40 ++++
 model/opts.go           |  41 ++--
 model/user.go           | 466 +++++++++++++++++++++++++++++++++++++---
 utils/utils.go          | 197 +++++++++++++++++
 17 files changed, 1486 insertions(+), 1299 deletions(-)
 delete mode 100644 cmd/user/create_test.go
 create mode 100644 cmd/user/temp.go
 create mode 100644 extras/create_test.go
 create mode 100644 model/krb.go
 create mode 100644 model/ldap.go
 create mode 100644 model/ltype.go
 create mode 100644 utils/utils.go

diff --git a/cmd/user.go b/cmd/user.go
index 21cfa72..c2347b0 100644
--- a/cmd/user.go
+++ b/cmd/user.go
@@ -8,14 +8,13 @@ import (
 var userCmd = &cobra.Command{
 	Use:   "user",
 	Short: "User subcommand",
-	Long: `Subcommand for managing unique users in general.
-If what you are trying to do is create a lot of
-new users, please do so with the bulk subcommand.`,
+	Long: `Subcommand for managing users in general.`,
 }
 
 func init() {
 	userCmd.AddCommand(user.ModCmd)
 	userCmd.AddCommand(user.ShowCmd)
+	userCmd.AddCommand(user.TempCmd)
 	userCmd.AddCommand(user.BulkCmd)
 	userCmd.AddCommand(user.ResetCmd)
 	userCmd.AddCommand(user.CreateCmd)
diff --git a/cmd/user/bulk.go b/cmd/user/bulk.go
index 9b45291..ed92d59 100644
--- a/cmd/user/bulk.go
+++ b/cmd/user/bulk.go
@@ -1,178 +1,68 @@
 package user
 
-// TODO: PQ Q N FUNCIONA??????????????
 import (
 	"fmt"
-	"os"
-	"strconv"
 
 	"github.com/spf13/cobra"
 	"gitlab.c3sl.ufpr.br/tss24/useradm/model"
+	"gitlab.c3sl.ufpr.br/tss24/useradm/utils"
 )
 
 var BulkCmd = &cobra.Command{
 	Use:   "bulk",
-	Short: "Create a lot of similar users",
-	RunE:  bulkCreate,
+	Short: "Create a lot of users",
+	Long: `Takes in a file with user opts and creates the accounts on the given
+group. The file must be formatted like the following:
+
+grr:fullname
+grr:fullname
+grr:fullname`,
+	RunE: bulkCreate,
 }
 
 func init() {
-	BulkCmd.Flags().StringP("passwd", "p", "", "Base password, will generate <password>#[1..number]")
-	BulkCmd.Flags().StringP("login", "l", "", "Base login, will generate <login>[1..number]")
-	BulkCmd.Flags().StringP("expiry", "e", "_", "Accounts' expiry date (format dd.mm.yy)")
-	BulkCmd.Flags().StringP("resp", "r", "_", "Person responsible for the accounts")
 	BulkCmd.Flags().StringP("group", "g", "", "Base group of the accounts")
-	BulkCmd.Flags().IntP("number", "n", 0, "Number of accounts to be created")
+	BulkCmd.Flags().StringP("infile", "i", "", "File path with all users")
 
-	BulkCmd.MarkFlagRequired("login")
-	BulkCmd.MarkFlagRequired("passwd")
 	BulkCmd.MarkFlagRequired("group")
-	BulkCmd.MarkFlagRequired("number")
+	BulkCmd.MarkFlagRequired("infile")
 }
 
 func bulkCreate(cmd *cobra.Command, args []string) error {
 	var opts model.Opts
-	var users []model.User
+	var accounts []model.User
 
-	users, err := getUsers()
+	l, err := model.ConnLDAP()
 	if err != nil {
 		return err
 	}
+	defer l.Close()
 
 	err = opts.RetrieveOpts(cmd)
 	if err != nil {
 		return err
 	}
 
-	for i := 1; i <= opts.Number; i++ {
-		if loginExists(users, opts.UID+strconv.Itoa(i)) {
-			return fmt.Errorf("User found with login %v%v, won't overwrite", opts.UID, i)
-		}
-	}
-
-	err = validateGID(opts.GID)
-	if err != nil {
-		return err
-	}
-
-	base := model.User{
-		GRR:      "_",
-		UID:      opts.UID,
-		GID:      opts.GID,
-		Name:     "_",
-		Resp:     opts.Resp,
-		Ltype:    "_",
-		Shell:    "/bin/bash",
-		Status:   "Active",
-		Expiry:   opts.Expiry,
-		Webdir:   "_",
-		Password: opts.Passwd + "#",
+	if !utils.PathExists(opts.Path) {
+		return fmt.Errorf("Path: \"%v\": no such file", opts.Path)
 	}
 
-	base.Homedir, err = genDirPath("/home", base.GID, base.UID, "")
+	accounts, err = extractModelsFile(opts.Path)
 	if err != nil {
 		return err
 	}
 
-	base.Nobackup, err = genDirPath("/nobackup", base.GID, base.UID, "")
-	if err != nil {
-		return err
-	}
-
-	i := 1
-	success := false
-
-	defer func() {
-		if !success {
-			fmt.Println("Error found, cleaning up...")
-			for ; i > 0; i-- {
-				istring := strconv.Itoa(i)
-				_ = delKerberosPrincipal(base.UID + istring)
-				_ = delUserLDAP(base.UID + istring)
-				_ = os.RemoveAll(base.Nobackup + istring)
-				_ = os.RemoveAll(base.Homedir + istring)
-			}
-		}
-	}()
-
-	for ; i <= opts.Number; i++ {
-		err := createTempUser(base, i)
+	for i := range accounts {
+		err = accounts[i].Create(l)
 		if err != nil {
 			return err
 		}
 	}
 
-	success = true
 	return nil
 }
 
-func createTempUser(base model.User, num int) error {
-	var err error
-	var groups map[string]string
-	numstring := strconv.Itoa(num)
-
-	groups, err = getGroups()
-	if err != nil {
-		return err
-	}
-
-	// gen login
-	base.UID = base.UID + numstring
-
-	// gen dn
-	base.DN = "uid=" + base.UID + ",ou=usuarios,dc=c3local"
-
-	// no webdir for temps
-
-	// gen home path
-	base.Homedir = base.Homedir + numstring
-
-	// gen nobkp path
-	base.Nobackup = base.Nobackup + numstring
-
-	// gen password
-	base.Password = base.Password + numstring
-
-	// gen gecos
-	base.Gecos = genGecos(base)
-
-	// get group id
-	base.GIDNumber, err = findGIDNumber(groups, base.GID)
-	if err != nil {
-		return err
-	}
-
-	// gen newuid
-	base.UIDNumber, err = getNewUIDNumber()
-	if err != nil {
-		return err
-	}
-
-	fmt.Printf("Pronto para criar:\n%v\n\n", base.FullToString())
-	confirmationPrompt(false, "creation")
-
-	// create ldap
-	err = addUserLDAP(base)
-	if err != nil {
-		return err
-	}
-	// create kerberos
-	err = addKerberosPrincipal(base.UID)
-	if err != nil {
-		return err
-	}
-
-	// change pass
-	err = modKerberosPassword(base.UID, base.Password)
-	if err != nil {
-		return err
-	}
-
-	// create dirs
-	err = createUserDirs(base)
-	if err != nil {
-		return err
-	}
-
-	return nil
+func extractModelsFile(path string) ([]model.User, error) {
+	// TODO
+	return nil, nil
 }
diff --git a/cmd/user/create.go b/cmd/user/create.go
index 7b4c9b5..6f4dbef 100644
--- a/cmd/user/create.go
+++ b/cmd/user/create.go
@@ -2,15 +2,11 @@ package user
 
 import (
 	"fmt"
-	"os"
-	"os/exec"
-	"path/filepath"
-	"strconv"
-	"strings"
 
 	"github.com/go-ldap/ldap/v3"
 	"github.com/spf13/cobra"
 	"gitlab.c3sl.ufpr.br/tss24/useradm/model"
+	"gitlab.c3sl.ufpr.br/tss24/useradm/utils"
 )
 
 const (
@@ -34,7 +30,7 @@ func init() {
 	// FIXME: maybe leave less flags for user input
 	CreateCmd.Flags().StringP("grr", "r", "_", "User GRR, required for ini type")
 	CreateCmd.Flags().StringP("type", "t", "ini", "Type of auto-generated login: ini, first or last")
-	CreateCmd.Flags().StringP("path", "w", "", "Full path to webdir, /home/html/inf/login if empty")
+	CreateCmd.Flags().StringP("web", "w", "", "Full path to webdir, /home/html/inf/login if empty")
 	CreateCmd.Flags().StringP("name", "n", "_", "User full name, required, use quotes for spaces")
 	CreateCmd.Flags().StringP("login", "l", "", "User login name, auto-generated if empty")
 	CreateCmd.Flags().StringP("group", "g", "", "User base group, required")
@@ -54,45 +50,28 @@ func init() {
 }
 
 func createUserFunc(cmd *cobra.Command, args []string) error {
-	success := false
-	// creates model from users input
-	usr, confirm, err := createNewUserModel(cmd)
+	l, err := model.ConnLDAP()
 	if err != nil {
 		return err
 	}
+	defer l.Close()
 
-	defer func() {
-		if !success {
-			_ = delKerberosPrincipal(usr.UID)
-			_ = delUserLDAP(usr.UID)
-		}
-	}()
+	// creates model from users input
+	usr, confirm, err := createNewUserModel(cmd, l)
+	if err != nil {
+		return err
+	}
 
 	// prints info for confirmation
 	fmt.Printf("%v\n", usr.FullToString()) // for debug
 	//fmt.Printf("%v\n	Passwd:  %v\n\n", usr.ToString(), usr.Password)
-	confirmationPrompt(confirm, "creation")
+	utils.ConfirmationPrompt(confirm, "creation")
 
 	if usr.Password == "[auto-generate]" {
-		usr.Password = genPassword()
-	}
-
-	err = addUserLDAP(usr)
-	if err != nil {
-		return err
-	}
-
-	err = addKerberosPrincipal(usr.UID)
-	if err != nil {
-		return err
+		usr.Password = utils.GenPassword()
 	}
 
-	err = modKerberosPassword(usr.UID, usr.Password)
-	if err != nil {
-		return err
-	}
-
-	err = createUserDirs(usr)
+	err = usr.Create(l)
 	if err != nil {
 		return err
 	}
@@ -103,20 +82,20 @@ func createUserFunc(cmd *cobra.Command, args []string) error {
 	fmt.Printf("%v:%v:%v:%v:%v:%v:%v:%v:%v:\n", usr.UID, usr.Password, usr.GID,
 		usr.Name, usr.Gecos, usr.GRR, usr.Shell, usr.Homedir, usr.Webdir)
 
-	success = true
 	return nil
 }
 
 // creates and validates user inputs into the User model
-func createNewUserModel(cmd *cobra.Command) (model.User, bool, error) {
+func createNewUserModel(cmd *cobra.Command, l *ldap.Conn) (model.User, bool, error) {
 	var u model.User
 	var opts model.Opts
-	users, err := getUsers()
+
+	users, err := model.GetAllUsersLDAP(l)
 	if err != nil {
 		return u, false, err
 	}
 
-	groups, err := getGroups()
+	groups, err := model.GetAllGroupsLDAP(l)
 	if err != nil {
 		return u, false, err
 	}
@@ -138,24 +117,17 @@ func createNewUserModel(cmd *cobra.Command) (model.User, bool, error) {
 		return u, false, fmt.Errorf("GRR is required for \"ini\" login type")
 	}
 
-	if opts.UID == "" {
-		opts.UID, err = genUniqueUID(opts.Name, opts.GRR, opts.Ltype, users)
-		if err != nil {
-			return u, false, err
-		}
-	}
-
-	opts.Homedir, err = genDirPath("/home", opts.GID, opts.UID, opts.Homedir)
+	opts.Homedir, err = utils.GenDirPath("/home", opts.GID, opts.UID, opts.Homedir)
 	if err != nil {
 		return u, false, err
 	}
 
-	opts.Nobkp, err = genDirPath("/nobackup", opts.GID, opts.UID, opts.Nobkp)
+	opts.Nobkp, err = utils.GenDirPath("/nobackup", opts.GID, opts.UID, opts.Nobkp)
 	if err != nil {
 		return u, false, err
 	}
 
-	opts.Webdir, err = genDirPath("/home/html/inf", "", opts.UID, opts.Webdir)
+	opts.Webdir, err = utils.GenDirPath("/home/html/inf", "", opts.UID, opts.Webdir)
 	if err != nil {
 		return u, false, err
 	}
@@ -166,389 +138,66 @@ func createNewUserModel(cmd *cobra.Command) (model.User, bool, error) {
 		GRR:      opts.GRR,
 		Resp:     opts.Resp,
 		Name:     opts.Name,
-		Ltype:    opts.Ltype,
 		Shell:    opts.Shell,
 		Status:   opts.Status,
 		Expiry:   opts.Expiry,
 		Webdir:   opts.Webdir,
 		Homedir:  opts.Homedir,
 		Nobackup: opts.Nobkp,
-		Password: ifThenElse(opts.Passwd != "", opts.Passwd, "[auto-generate]"),
+		Password: utils.IfThenElse(opts.Passwd != "", opts.Passwd, "[auto-generate]"),
 	}
 
-	u.Gecos = genGecos(u)
+	u.Ltype.Parse(opts.Ltype)
+
+	if opts.UID == "" {
+		err = u.GenUniqueUID(users)
+		if err != nil {
+			return u, false, err
+		}
+	}
 
 	// get a new UIDNumber
-	u.UIDNumber, err = getNewUIDNumber()
+	err = u.GetNewUIDNumber(l)
 	if err != nil {
 		return u, false, fmt.Errorf("failed to generate new UIDNumber for user: %v", err)
 	}
 
 	// assign GIDNumber by traversing the groups
-	u.GIDNumber, err = findGIDNumber(groups, u.GID)
+	u.GIDNumber, err = utils.GetGIDNumFromGID(groups, u.GID)
 	if err != nil {
 		return u, false, err
 	}
 
-	u.DN = "uid=" + u.UID + ",ou=usuarios,dc=c3local"
+	u.GenGecos()
+	u.SetDN(u.UID)
 
 	return u, opts.Confirm, nil
 }
 
-func genDirPath(base, group, login, input string) (string, error) {
-	if input != "" {
-		return input, nil
-	}
-	p := filepath.Join(base, group, login)
-	return p, validatePath(p)
-}
-
-func genGecos(u model.User) string {
-	gecos := u.Name + ","
-	gecos += u.GRR + ","
-	gecos += u.Resp + ","
-	gecos += u.Expiry + ","
-	gecos += u.Status + ","
-	gecos += u.Ltype + ","
-	gecos += u.Webdir + ","
-	gecos += u.Nobackup
-	return gecos
-}
-
-type LoginType int
-
-const (
-	Initials LoginType = iota
-	FirstName
-	LastName
-)
-
-func genLogin(name string, grr string, ltype LoginType, variance int) string {
-	parts := formatName(name)
-	if len(parts) == 0 {
-		return "_"
-	}
-
-	partPrefixLen := make([]int, len(parts))
-	if ltype == Initials {
-		// the first letter of each part must appear
-		for i := 0; i < len(parts); i++ {
-			partPrefixLen[i] = 1
-		}
-	} else if ltype == FirstName {
-		// the first name(part) must appear
-		partPrefixLen[0] = len(parts[0])
-	} else {
-		// the first letter of each part must appear
-		for i := 0; i < len(parts); i++ {
-			partPrefixLen[i] = 1
-		}
-		// just as the last part
-		partPrefixLen[len(parts)-1] = len(parts[len(parts)-1])
-	}
-
-	partPrefixIx := 0
-	for i := 0; i < variance; i++ {
-		ok := false
-		for k := 0; k < len(parts) && !ok; k++ {
-			if partPrefixLen[partPrefixIx] < len(parts[partPrefixIx]) {
-				partPrefixLen[partPrefixIx]++
-				ok = true
-			}
-			partPrefixIx = (partPrefixIx + 1) % len(parts)
-		}
-		if !ok {
-			// it's joever, from now on nothing happens, quit :D
-			break
-		}
-	}
-
-	// contruct the login with the given legths
-	login := ""
-	for i := 0; i < len(parts); i++ {
-		login += parts[i][:partPrefixLen[i]]
-	}
-	if login == "" {
-		return "_"
-	}
-
-	if ltype == Initials {
-		login += grr[2:4]
-	}
-	return login
-}
-
-// removes connectives, leave all lowecase and splits the name
-func formatName(name string) []string {
-	connectives := map[string]bool{"da": true, "de": true, "di": true,
-		"do": true, "das": true, "dos": true, "von": true}
-	splitName := strings.Fields(name)
-	var parts []string
-
-	for _, part := range splitName {
-		lowerPart := strings.ToLower(part)
-		if !connectives[lowerPart] {
-			parts = append(parts, string(lowerPart))
-		}
-	}
-	return parts
-}
-
-func genUniqueUID(name, grr string, ltypeString string, users []model.User) (string, error) {
-	var uid string
-	used, variance := true, 0
-	for used {
-		var ltype LoginType
-		if ltypeString == "ini" {
-			ltype = Initials
-		} else if ltypeString == "first" {
-			ltype = FirstName
-		} else {
-			ltype = LastName
-		}
-		uid = genLogin(name, grr, ltype, variance)
-
-		// already taken or alias for it exists :(
-		used = loginExists(users, uid) || mailAliasExists(uid)
-
-		variance++
-		if variance > MAX_VARIANCE {
-			return "", fmt.Errorf("Could't generate login automatically, please inform the desired login\n")
-		}
-	}
-	return uid, nil
-}
-
-func findGIDNumber(groups map[string]string, GID string) (string, error) {
-	for key, val := range groups {
-		if val == GID {
-			return key, nil
-		}
-	}
-	return "", fmt.Errorf("Couldn't find group GIDNumber")
-}
-
-// queries to check if the alias exists
-func mailAliasExists(alias string) bool {
-	cmd := exec.Command("/usr/sbin/postalias", "-q", alias, MAIL_ALIAS_FILE)
-	cmd.Stdout = nil
-	cmd.Stderr = nil
-	return cmd.Run() == nil
-}
-
-// finds next available uidNumber (MEX from uid group)
-func getNewUIDNumber() (string, error) {
-	uids, err := getUIDs()
-	if err != nil {
-		return "", err
-	}
-
-	candidate := MIN_UID
-	for _, uid := range uids {
-		if uid == candidate { // check if taken
-			candidate++
-		} else if uid > candidate { // found a gap
-			break
-		}
-	}
-
-	if candidate > MAX_UID {
-		return "", fmt.Errorf("No more available UID numbers")
-	}
-	return strconv.Itoa(candidate), nil
-}
+func validateInputs(opts model.Opts) error {
 
-// generates a LDAP request and adds the user to LDAP
-func addUserLDAP(u model.User) error {
-	l, err := connLDAP()
+	l, err := model.ConnLDAP()
 	if err != nil {
 		return err
 	}
 	defer l.Close()
 
-	req := ldap.NewAddRequest(u.DN, nil)
-	req.Attribute("uid", []string{u.UID})
-	req.Attribute("cn", []string{u.Name})
-	req.Attribute("objectClass", []string{"account", "posixAccount"})
-	req.Attribute("loginShell", []string{u.Shell})
-	req.Attribute("uidNumber", []string{u.UIDNumber})
-	req.Attribute("gidNumber", []string{u.GIDNumber})
-	req.Attribute("homeDirectory", []string{u.Homedir})
-	req.Attribute("gecos", []string{u.Gecos})
-
-	err = l.Add(req)
-	if err != nil {
-		return fmt.Errorf("Failed to add user %s to LDAP: %v", u.UID, err)
-	}
-
-	err = addUserGroupLDAP(l, u.UID, u.GID)
+	users, err := model.GetAllUsersLDAP(l)
 	if err != nil {
 		return err
 	}
 
-	return nil
-}
-
-// adds a user to a group in LDAP
-func addUserGroupLDAP(l *ldap.Conn, UID, GID string) error {
-	groupDN := fmt.Sprintf("cn=%s,ou=grupos,dc=c3local", GID)
-	modReq := ldap.NewModifyRequest(groupDN, nil)
-	modReq.Add("memberUid", []string{UID})
-
-	err := l.Modify(modReq)
-	if err != nil {
-		return fmt.Errorf("Failed to add user %s to group %s: %v", UID, GID, err)
-	}
-
-	return nil
-}
-
-// creates a KerberosPrincipal for the user
-func addKerberosPrincipal(login string) error {
-	cmd := exec.Command("kadmin.local", "-q",
-		fmt.Sprintf("addprinc -policy padrao -randkey -x dn=uid=%s,ou=usuarios,dc=c3local %s", login, login))
-
-	output, err := cmd.CombinedOutput()
+	groups, err := model.GetAllGroupsLDAP(l)
 	if err != nil {
-		return fmt.Errorf("Failed to add Kerberos principal: %v\nOutput: %s", err, output)
-	}
-
-	return nil
-}
-
-func createUserDirs(u model.User) error {
-	success := false
-
-	defer func() {
-		if !success {
-			fmt.Println("Error found creating dirs, cleaning up...")
-			_ = os.RemoveAll(u.Nobackup)
-			_ = os.RemoveAll(u.Homedir)
-		}
-	}()
-
-	if err := createHome(u, u.Homedir); err != nil {
-		return err
-	}
-
-	if err := createHome(u, u.Nobackup); err != nil {
 		return err
 	}
 
-	if err := createWeb(u); err != nil {
-		return err
-	}
-
-	success = true
-	return nil
-}
-
-func createHome(u model.User, homeDir string) error {
-
-	perm := DEF_PERMISSION
-	if u.Status == "Blocked" {
-		perm = BLK_PERMISSION
-	}
-
-	// create directory
-	cmd := exec.Command("mkdir", "-p", homeDir)
-	cmd.Stdout = nil
-	cmd.Stderr = nil
-	if err := cmd.Run(); err != nil {
-		return fmt.Errorf("Failed to create home directory: %w", err)
-	}
-
-	// copy /etc/skel
-	cmd = exec.Command("cp", "-r", "/etc/skel/.", homeDir)
-	if err := cmd.Run(); err != nil {
-		return fmt.Errorf("Failed to copy /etc/skel contents: %w", err)
-	}
-
-	// change permissions
-	cmd = exec.Command("chmod", perm, homeDir)
-	if err := cmd.Run(); err != nil {
-		return fmt.Errorf("Failed to set permissions: %w", err)
-	}
-
-	// change ownership
-	cmd = exec.Command("chown", "-R", fmt.Sprintf("%s:%s", u.UID, u.GID), homeDir)
-	if err := cmd.Run(); err != nil {
-		return fmt.Errorf("Failed to change ownership: %w", err)
-	}
-
-	return nil
-}
-
-func createWeb(u model.User) error {
-	success := false
-
-	if u.Webdir == "_" {
-		success = true
-		return nil
-	}
-
-	perm := DEF_PERMISSION
-	if u.Status == "Blocked" {
-		perm = BLK_PERMISSION
-	}
-
-	defer func() {
-		if !success {
-			_ = os.RemoveAll(u.Webdir)
-		}
-	}()
-
-	// create directory
-	cmd := exec.Command("mkdir", "-p", u.Webdir)
-	cmd.Stdout = nil
-	cmd.Stderr = nil
-	if err := cmd.Run(); err != nil {
-		return fmt.Errorf("Failed to create web directory: %w", err)
-	}
-
-	// create index
-	cmd = exec.Command("touch", filepath.Join(u.Webdir, "index.html"))
-	if err := cmd.Run(); err != nil {
-		return fmt.Errorf("Failed to create index.html: %w", err)
-	}
-
-	// create link in users home
-	cmd = exec.Command("ln", "-s", u.Webdir, filepath.Join(u.Homedir, "public_html"))
-	if err := cmd.Run(); err != nil {
-		return fmt.Errorf("Failed to create link public_html: %w", err)
-	}
-
-	// change permissions
-	cmd = exec.Command("chmod", perm, u.Webdir)
-	if err := cmd.Run(); err != nil {
-		return fmt.Errorf("Failed to set permissions: %w", err)
-	}
-
-	// change ownership
-	cmd = exec.Command("chown", "-R", fmt.Sprintf("%s:%s", u.UID, u.GID), u.Webdir)
-	if err := cmd.Run(); err != nil {
-		return fmt.Errorf("Failed to change ownership: %w", err)
-	}
-
-	// set permission for public_html
-	cmd = exec.Command("chmod", HTML_BASE_PERM, filepath.Join(u.Homedir, "public_html"))
-	if err := cmd.Run(); err != nil {
-		return fmt.Errorf("Failed to create set permissions for public_html: %w", err)
-	}
-
-	success = true
-	return nil
-}
-
-func validateInputs(opts model.Opts) error {
-	var err error
-
-	err = validateGID(opts.GID)
+	err = validateGID(groups, opts.GID)
 	if err != nil {
 		return err
 	}
 
-	err = validateGRR(opts.GRR)
+	err = validateGRR(users, opts.GRR)
 	if err != nil {
 		return err
 	}
@@ -563,14 +212,9 @@ func validateInputs(opts model.Opts) error {
 		return err
 	}
 
-	err = validateLtype(opts.Ltype)
-	if err != nil {
-		return err
-	}
-
 	// it’s OK if UID is empty here, we generate it later :)
 	if opts.UID != "" {
-		err := validateUID(opts.UID)
+		err := validateUID(users, opts.UID)
 		if err != nil {
 			return err
 		}
diff --git a/cmd/user/create_test.go b/cmd/user/create_test.go
deleted file mode 100644
index ae52d43..0000000
--- a/cmd/user/create_test.go
+++ /dev/null
@@ -1,110 +0,0 @@
-package user
-
-import (
-	"reflect"
-	"testing"
-)
-
-func assertEqual(t *testing.T, a any, b any) {
-	if !reflect.DeepEqual(a, b) {
-		t.Fatalf("%s != %s", a, b)
-	}
-}
-
-func TestGenLogin(t *testing.T) {
-	assertEqual(t, genLogin("", "20241982", Initials, 0), "_")
-	assertEqual(t, genLogin("", "20241982", Initials, 1), "_")
-	assertEqual(t, genLogin("", "20241982", Initials, 2), "_")
-	assertEqual(t, genLogin("", "20241982", Initials, 3), "_")
-
-	assertEqual(t, genLogin("", "20241982", FirstName, 0), "_")
-	assertEqual(t, genLogin("", "20241982", FirstName, 1), "_")
-	assertEqual(t, genLogin("", "20241982", FirstName, 2), "_")
-	assertEqual(t, genLogin("", "20241982", FirstName, 3), "_")
-
-	assertEqual(t, genLogin("", "20241982", LastName, 0), "_")
-	assertEqual(t, genLogin("", "20241982", LastName, 1), "_")
-	assertEqual(t, genLogin("", "20241982", LastName, 2), "_")
-	assertEqual(t, genLogin("", "20241982", LastName, 3), "_")
-
-	assertEqual(t, genLogin("de", "20241982", Initials, 0), "_")
-	assertEqual(t, genLogin("de", "20241982", Initials, 1), "_")
-	assertEqual(t, genLogin("de", "20241982", Initials, 2), "_")
-	assertEqual(t, genLogin("de", "20241982", Initials, 3), "_")
-
-	assertEqual(t, genLogin("de", "20241982", FirstName, 0), "_")
-	assertEqual(t, genLogin("de", "20241982", FirstName, 1), "_")
-	assertEqual(t, genLogin("de", "20241982", FirstName, 2), "_")
-	assertEqual(t, genLogin("de", "20241982", FirstName, 3), "_")
-
-	assertEqual(t, genLogin("de", "20241982", LastName, 0), "_")
-	assertEqual(t, genLogin("de", "20241982", LastName, 1), "_")
-	assertEqual(t, genLogin("de", "20241982", LastName, 2), "_")
-	assertEqual(t, genLogin("de", "20241982", LastName, 3), "_")
-
-	assertEqual(t, genLogin("da de", "20241982", Initials, 0), "_")
-	assertEqual(t, genLogin("da de", "20241982", Initials, 1), "_")
-	assertEqual(t, genLogin("da de", "20241982", Initials, 2), "_")
-	assertEqual(t, genLogin("da de", "20241982", Initials, 3), "_")
-
-	assertEqual(t, genLogin("da de", "20241982", FirstName, 0), "_")
-	assertEqual(t, genLogin("da de", "20241982", FirstName, 1), "_")
-	assertEqual(t, genLogin("da de", "20241982", FirstName, 2), "_")
-	assertEqual(t, genLogin("da de", "20241982", FirstName, 3), "_")
-
-	assertEqual(t, genLogin("da de", "20241982", LastName, 0), "_")
-	assertEqual(t, genLogin("da de", "20241982", LastName, 1), "_")
-	assertEqual(t, genLogin("da de", "20241982", LastName, 2), "_")
-	assertEqual(t, genLogin("da de", "20241982", LastName, 3), "_")
-
-	assertEqual(t, genLogin("Fabiano", "20241982", Initials, 0), "f24")
-	assertEqual(t, genLogin("Fabiano", "20241982", Initials, 1), "fa24")
-	assertEqual(t, genLogin("Fabiano", "20241982", Initials, 2), "fab24")
-	assertEqual(t, genLogin("Fabiano", "20241982", Initials, 3), "fabi24")
-
-	assertEqual(t, genLogin("Fabiano", "20241982", FirstName, 0), "fabiano")
-	assertEqual(t, genLogin("Fabiano", "20241982", FirstName, 1), "fabiano")
-	assertEqual(t, genLogin("Fabiano", "20241982", FirstName, 2), "fabiano")
-	assertEqual(t, genLogin("Fabiano", "20241982", FirstName, 3), "fabiano")
-
-	assertEqual(t, genLogin("Fabiano", "20241982", LastName, 0), "fabiano")
-	assertEqual(t, genLogin("Fabiano", "20241982", LastName, 1), "fabiano")
-	assertEqual(t, genLogin("Fabiano", "20241982", LastName, 2), "fabiano")
-	assertEqual(t, genLogin("Fabiano", "20241982", LastName, 3), "fabiano")
-
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", Initials, 0), "faps24")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", Initials, 1), "faaps24")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", Initials, 2), "faanps24")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", Initials, 3), "faanpes24")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", Initials, 14), "fabiaantunperesouz24")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", Initials, 18), "fabianantunepereisouza24")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", Initials, 19), "fabianantunepereirsouza24")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", Initials, 20), "fabianoantunepereirsouza24")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", Initials, 21), "fabianoantunespereirsouza24")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", Initials, 22), "fabianoantunespereirasouza24")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", Initials, 40), "fabianoantunespereirasouza24")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", Initials, 50), "fabianoantunespereirasouza24")
-
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", FirstName, 0), "fabiano")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", FirstName, 1), "fabianoa")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", FirstName, 2), "fabianoap")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", FirstName, 3), "fabianoaps")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", FirstName, 4), "fabianoanps")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", FirstName, 16), "fabianoantunepereisouza")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", FirstName, 17), "fabianoantunepereirsouza")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", FirstName, 18), "fabianoantunespereirsouza")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", FirstName, 19), "fabianoantunespereirasouza")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", FirstName, 20), "fabianoantunespereirasouza")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", FirstName, 50), "fabianoantunespereirasouza")
-
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", LastName, 0), "fapsouza")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", LastName, 1), "faapsouza")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", LastName, 2), "faanpsouza")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", LastName, 3), "faanpesouza")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", LastName, 15), "fabianantunepereirsouza")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", LastName, 16), "fabianoantunepereirsouza")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", LastName, 17), "fabianoantunespereirsouza")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", LastName, 18), "fabianoantunespereirasouza")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", LastName, 19), "fabianoantunespereirasouza")
-	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", LastName, 100), "fabianoantunespereirasouza")
-}
diff --git a/cmd/user/mod.go b/cmd/user/mod.go
index 2c06d20..fbb397f 100644
--- a/cmd/user/mod.go
+++ b/cmd/user/mod.go
@@ -1,15 +1,14 @@
 package user
 
 import (
-	"bufio"
 	"fmt"
 	"os"
 	"os/exec"
-	"strings"
 
 	"github.com/go-ldap/ldap/v3"
 	"github.com/spf13/cobra"
 	"gitlab.c3sl.ufpr.br/tss24/useradm/model"
+	"gitlab.c3sl.ufpr.br/tss24/useradm/utils"
 	"gopkg.in/yaml.v3"
 )
 
@@ -38,12 +37,28 @@ func init() {
 func modifyUserFunc(cmd *cobra.Command, args []string) error {
 	var opts model.Opts
 
-	err := opts.RetrieveOpts(cmd)
+	l, err := model.ConnLDAP()
+	if err != nil {
+		return err
+	}
+	defer l.Close()
+
+	users, err := model.GetAllUsersLDAP(l)
+	if err != nil {
+		return err
+	}
+
+	groups, err := model.GetAllGroupsLDAP(l)
 	if err != nil {
 		return err
 	}
 
-	curr, err := locateUser(args[0])
+	err = opts.RetrieveOpts(cmd)
+	if err != nil {
+		return err
+	}
+
+	curr, err := model.Locate(l, args[0])
 	if err != nil {
 		return err
 	}
@@ -61,13 +76,13 @@ func modifyUserFunc(cmd *cobra.Command, args []string) error {
 	changes, err := promptUserYaml(state)
 
 	if changes.GRR != curr.GRR {
-		err = validateGRR(changes.GRR)
+		err = validateGRR(users, changes.GRR)
 		if err != nil {
 			return err
 		}
 	}
 
-	err = validateGID(changes.Group)
+	err = validateGID(groups, changes.Group)
 	if err != nil {
 		return err
 	}
@@ -84,35 +99,29 @@ func modifyUserFunc(cmd *cobra.Command, args []string) error {
 
 	oldGroup := curr.GID
 
-	req, err := genRequest(curr, changes)
+	req, err := genRequest(groups, curr, changes)
 	if err != nil {
 		return err
 	}
 
 	fmt.Printf("%v\n\n", curr.ToString())
-	confirmationPrompt(opts.Confirm, "update")
-
-	l, err := connLDAP()
-	if err != nil {
-		return err
-	}
-	defer l.Close()
+	utils.ConfirmationPrompt(opts.Confirm, "update")
 
 	if err := l.Modify(req); err != nil {
 		return fmt.Errorf("Failed to update user attributes: %v", err)
 	}
 
 	if oldGroup != changes.Group {
-		if err := delUserFromGroupLDAP(l, curr.UID, curr.GID); err != nil {
+		if err := model.DelFromGroupLDAP(l, curr.UID, curr.GID); err != nil {
 			return err
 		}
 
-		if err := addUserGroupLDAP(l, curr.UID, changes.Group); err != nil {
+		if err := model.AddToGroupLDAP(l, curr.UID, changes.Group); err != nil {
 			return err
 		}
 	}
 
-	if err := clearCache(); err != nil {
+	if err := utils.ClearCache(); err != nil {
 		fmt.Printf(`Failed to reload cache!
 all is ok but may take a while to apply the changes
 Output: %v`, err)
@@ -123,7 +132,9 @@ Output: %v`, err)
 	return nil
 }
 
-func genRequest(curr model.User, changes cfg) (*ldap.ModifyRequest, error) {
+func genRequest(groups map[string]string,
+	curr model.User, changes cfg) (*ldap.ModifyRequest, error) {
+
 	req := ldap.NewModifyRequest(curr.DN, nil)
 
 	// changed name
@@ -149,11 +160,6 @@ func genRequest(curr model.User, changes cfg) (*ldap.ModifyRequest, error) {
 
 	// changed base group
 	if curr.GID != changes.Group {
-		groups, err := getGroups()
-		if err != nil {
-			return req, err
-		}
-
 		var gidNumber string
 		for id, gid := range groups {
 			if gid == changes.Group {
@@ -167,7 +173,7 @@ func genRequest(curr model.User, changes cfg) (*ldap.ModifyRequest, error) {
 
 	changed := applyChangesToUser(curr, changes)
 
-	changed.Gecos = genGecos(changed)
+	changed.GenGecos()
 
 	// changed gecos
 	if curr.Gecos != changed.Gecos {
@@ -233,32 +239,3 @@ func promptUserYaml(state cfg) (cfg, error) {
 
 	return newState, nil
 }
-
-// prints a confirmation prompt, given the operation being performed
-func confirmationPrompt(confirm bool, operation string) {
-	if !confirm {
-		fmt.Printf("Proceed with user %v? [y/N] ", operation)
-
-		reader := bufio.NewReader(os.Stdin)
-		response, _ := reader.ReadString('\n')
-
-		if strings.TrimSpace(strings.ToLower(response)) != "y" {
-			fmt.Fprintln(os.Stderr, "Aborted.")
-			os.Exit(1)
-		}
-	}
-}
-
-func clearCache() error {
-	cmd := exec.Command("nscd", "-i", "passwd")
-
-	cmd.Stdout = nil
-	cmd.Stderr = nil
-	err := cmd.Run()
-	if err != nil {
-		return err
-	}
-
-	cmd = exec.Command("nscd", "-i", "group")
-	return cmd.Run()
-}
diff --git a/cmd/user/remove.go b/cmd/user/remove.go
index 0c10753..24fbf44 100644
--- a/cmd/user/remove.go
+++ b/cmd/user/remove.go
@@ -3,15 +3,13 @@ package user
 import (
 	"fmt"
 	"log"
-	"os"
-	"os/exec"
 	"path/filepath"
 	"strconv"
 	"time"
 
-	"github.com/go-ldap/ldap/v3"
 	"github.com/spf13/cobra"
 	"gitlab.c3sl.ufpr.br/tss24/useradm/model"
+	"gitlab.c3sl.ufpr.br/tss24/useradm/utils"
 )
 
 var (
@@ -36,13 +34,19 @@ func removeUserFunc(cmd *cobra.Command, args []string) error {
 	var opts model.Opts
 	success := false
 
-	err := opts.RetrieveOpts(cmd)
+	l, err := model.ConnLDAP()
+	if err != nil {
+		return err
+	}
+	defer l.Close()
+
+	err = opts.RetrieveOpts(cmd)
 	if err != nil {
 		return err
 	}
 
 	login := args[0]
-	u, err := locateUser(login)
+	u, err := model.Locate(l, login)
 	if err != nil {
 		return err
 	}
@@ -50,25 +54,25 @@ func removeUserFunc(cmd *cobra.Command, args []string) error {
 	defer func() {
 		if !success {
 			log.Println("Found error, rolling back dirs...")
-			_ = moveAndChown(filepath.Join(NO_BKP_TRASH, filepath.Base(u.Nobackup)),
-				u.Nobackup, u.UID, u.GID)
-			_ = moveAndChown(filepath.Join(HOME_TRASH, filepath.Base(u.Homedir)),
-				u.Homedir, u.UID, u.GID)
-			_ = moveAndChown(filepath.Join(WEB_TRASH, filepath.Base(u.Webdir)),
-				u.Webdir, u.UID, u.GID)
+			_ = utils.MoveAndChown(filepath.Join(NO_BKP_TRASH,
+				filepath.Base(u.Nobackup)), u.Nobackup, u.UID, u.GID)
+			_ = utils.MoveAndChown(filepath.Join(HOME_TRASH,
+				filepath.Base(u.Homedir)), u.Homedir, u.UID, u.GID)
+			_ = utils.MoveAndChown(filepath.Join(WEB_TRASH,
+				filepath.Base(u.Webdir)), u.Webdir, u.UID, u.GID)
 		}
 	}()
 
 	fmt.Printf("Found %v\n\n", u.ToString())
 
-	confirmationPrompt(opts.Confirm, "removal")
+	utils.ConfirmationPrompt(opts.Confirm, "removal")
 
-	err = removeDirs(u)
+	err = u.DirsToTrash()
 	if err != nil {
 		return err
 	}
 
-	err = delUserLDAP(u.UID)
+	err = model.DelFromLDAP(u.UID)
 	if err != nil {
 		return err
 	}
@@ -78,137 +82,3 @@ func removeUserFunc(cmd *cobra.Command, args []string) error {
 	success = true
 	return nil
 }
-
-// searches for the specified login string, there can only be one >:)
-func locateUser(login string) (model.User, error) {
-	var u model.User
-	users, err := getUsers()
-
-	if !loginExists(users, login) {
-		return u, fmt.Errorf("Failed to find login in LDAP database: %v", err)
-	}
-
-	filter := searchUser(users, false, true, login, "", "", "", "", "")
-	if len(filter) != 1 {
-		return u, fmt.Errorf(`More than one user matched the login given.
-search made: "useradm user show -l %v -e"`, login)
-	}
-
-	u = filter[0]
-	return u, nil
-}
-
-// moves dirs to their respective trash dir
-func removeDirs(u model.User) error {
-	err := moveAndChown(u.Homedir, HOME_TRASH, "nobody", "nogroup")
-	if err != nil {
-		return err
-	}
-
-	err = moveAndChown(u.Nobackup, NO_BKP_TRASH, "nobody", "nogroup")
-	if err != nil {
-		return err
-	}
-
-	err = moveAndChown(u.Webdir, WEB_TRASH, "nobody", "nogroup")
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
-func delUserLDAP(UID string) error {
-	l, err := connLDAP()
-	if err != nil {
-		return err
-	}
-	defer l.Close()
-
-	// search for all groups the user is a member of
-	searchReq := ldap.NewSearchRequest(
-		"ou=grupos,dc=c3local,dc=com",
-		ldap.ScopeWholeSubtree,
-		ldap.NeverDerefAliases, 0, 0, false,
-		"(memberUid="+UID+")", // Filter by UID membership
-		[]string{"dn", "cn"},
-		nil,
-	)
-
-	groups, err := l.Search(searchReq)
-	if err != nil && !ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
-		return fmt.Errorf("Group members search failed: %v", err)
-	}
-
-	// iterate and remove user from each group
-	for _, entry := range groups.Entries {
-		groupName := entry.GetAttributeValue("cn")
-		if err := delUserFromGroupLDAP(l, UID, groupName); err != nil {
-			log.Printf("Warning: %v", err)
-		}
-	}
-
-	// removing user entry
-	userDN := "uid=" + UID + "ou=usuarios,dc=c3local"
-	delReq := ldap.NewDelRequest(userDN, nil)
-	if err := l.Del(delReq); err != nil &&
-		!ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
-		return fmt.Errorf("User deletion failed: %v", err)
-	}
-
-	return nil
-}
-
-func delUserFromGroupLDAP(l *ldap.Conn, userUID, GID string) error {
-	groupDN := fmt.Sprintf("cn=%s,ou=grupos,dc=c3local", GID)
-	modReq := ldap.NewModifyRequest(groupDN, nil)
-	modReq.Delete("memberUid", []string{userUID})
-
-	err := l.Modify(modReq)
-	if err != nil && !ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchAttribute) {
-		return fmt.Errorf("Failed to remove user %s from group %s: %v", userUID, groupDN, err)
-	}
-
-	return nil
-}
-
-// usually not necessary but used in a failed create command
-func delKerberosPrincipal(login string) error {
-	cmd := exec.Command("kadmin.local", "-q", fmt.Sprintf("delprinc -force %s", login))
-
-	output, err := cmd.CombinedOutput()
-	if err != nil {
-		return fmt.Errorf("Fail to delete Kerberos principal: %v\nOutput: %s", err, output)
-	}
-
-	return nil
-}
-
-func moveAndChown(orig, dest, owner, group string) error {
-	// check if orig exists
-	if _, err := os.Stat(orig); err != nil {
-		log.Printf("Directory %v not found so not moved\n", orig)
-		return nil
-	}
-
-	// construct destination path
-	destPath := filepath.Join(dest, filepath.Base(orig))
-
-	if _, err := os.Stat(destPath); err == nil {
-		return fmt.Errorf("Directory %v already exists, can't move\n", destPath)
-	}
-
-	// move directory
-	cmd := exec.Command("mv", orig, destPath)
-	if output, err := cmd.CombinedOutput(); err != nil {
-		return fmt.Errorf("Failed to move dirs: %w\nOutput: %v", err, output)
-	}
-
-	// recursive chown
-	cmd = exec.Command("chown", "-R", owner+":"+group, destPath)
-	if output, err := cmd.CombinedOutput(); err != nil {
-		return fmt.Errorf("Failed to set owner/group: %w\nOutput: %v", err, output)
-	}
-
-	return nil
-}
diff --git a/cmd/user/reset.go b/cmd/user/reset.go
index 038e918..aea0b26 100644
--- a/cmd/user/reset.go
+++ b/cmd/user/reset.go
@@ -1,11 +1,11 @@
 package user
 
 import (
-	"crypto/rand"
 	"fmt"
-	"os/exec"
 
 	"github.com/spf13/cobra"
+	"gitlab.c3sl.ufpr.br/tss24/useradm/model"
+	"gitlab.c3sl.ufpr.br/tss24/useradm/utils"
 )
 
 var ResetCmd = &cobra.Command{
@@ -25,57 +25,34 @@ func resetPass(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	users, err := getUsers()
+	l, err := model.ConnLDAP()
 	if err != nil {
 		return err
 	}
-
-	login := args[0]
-	res := searchUser(users, false, true, login, "", "", "", "", "")
-	if len(res) != 1 {
-		return fmt.Errorf("More than one user found")
-	}
+	defer l.Close()
 
 	if pass == "" {
-		pass = genPassword()
+		pass = utils.GenPassword()
 	}
 
-	confirmationPrompt(false, "password reset")
-	err = modKerberosPassword(login, pass)
+	users, err := model.GetAllUsersLDAP(l)
 	if err != nil {
 		return err
 	}
 
-	fmt.Printf("Password for user %v has been reset!\n", login)
-	fmt.Printf("New Password: %v\n", pass)
-	return nil
-}
-
-// do NOT include ':' in the charset, it WILL break the command
-func genPassword() string {
-	const charset = "@*()=+[];,.?123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"
-	b := make([]byte, 20)
-	if _, err := rand.Read(b); err != nil {
-		panic(err)
-	}
-	for i := range b {
-		b[i] = charset[int(b[i])%len(charset)]
+	login := args[0]
+	res := model.Search(users, false, true, login, "", "", "", "", "")
+	if len(res) != 1 {
+		return fmt.Errorf("More than one user found")
 	}
-	return string(b)
-}
 
-// command that changes the password >:D
-// the command kadmin.local returns 0 if the password change
-// fails, bruh. so we have to check differently.
-// FIXME: maybe do the validation with regex?
-func modKerberosPassword(login, password string) error {
-	cmd := exec.Command("kadmin.local", "-q",
-		fmt.Sprintf("cpw -pw %s %s", password, login))
-
-	output, _ := cmd.CombinedOutput()
-	if len(output) > 105 {
-		return fmt.Errorf("Error found changing password, output: \n%v", string(output[:]))
+	utils.ConfirmationPrompt(false, "password reset")
+	err = model.ModKRBPassword(login, pass)
+	if err != nil {
+		return err
 	}
 
+	fmt.Printf("Password for user %v has been reset!\n", login)
+	fmt.Printf("New Password: %v\n", pass)
 	return nil
 }
diff --git a/cmd/user/show.go b/cmd/user/show.go
index 5e9ad4f..6bb389e 100644
--- a/cmd/user/show.go
+++ b/cmd/user/show.go
@@ -2,25 +2,15 @@ package user
 
 import (
 	"fmt"
-	"os"
-	"sort"
-	"strconv"
-	"strings"
 
-	"github.com/go-ldap/ldap/v3"
 	"github.com/spf13/cobra"
 	"gitlab.c3sl.ufpr.br/tss24/useradm/model"
 )
 
-const (
-	NUM_GECOS_FIELDS = 9 // change if going to add new field
-	PASSWD_PATH      = "/etc/ldapscripts/ldapscripts.passwd"
-)
-
 var ShowCmd = &cobra.Command{
 	Use:   "show",
 	Short: "Search for users and show info",
-	RunE:  searchUserFunc,
+	RunE:  searchUserCmd,
 }
 
 func init() {
@@ -39,312 +29,37 @@ func init() {
 	ShowCmd.Flags().BoolP("exact", "e", false, "Make the matching be exact")
 }
 
-func searchUserFunc(cmd *cobra.Command, args []string) error {
+func searchUserCmd(cmd *cobra.Command, args []string) error {
 	var o model.Opts
 
-	users, err := getUsers()
-	if err != nil {
-		return err
-	}
-
-	err = o.RetrieveOpts(cmd)
+	l, err := model.ConnLDAP()
 	if err != nil {
 		return err
 	}
-
-	filtered := searchUser(users, o.Ignore, o.Exact, o.UID, o.GID,
-		o.Name, o.GRR, o.Status, o.Homedir)
-
-	if len(filtered) == 0 {
-		fmt.Printf("No user matched the search!")
-		return nil
-	}
-
-	for i := range filtered {
-		fmt.Printf("%v\n\n", filtered[i].ToString())
-	}
-
-	return nil
-}
-
-func searchUser(users []model.User, ig, ex bool, l, g, n, r, s, h string) []model.User {
-	return Filter(users, func(u model.User) bool {
-		matches := func(src, target string) bool {
-			if ig {
-				src, target = strings.ToLower(src), strings.ToLower(target)
-			}
-			if ex {
-				return src == target // exact match
-			}
-			return strings.Contains(src, target) // partial match
-		}
-
-		// search
-		return (r == "" || matches(u.GRR, r)) &&
-			(n == "" || matches(u.Name, n)) &&
-			(l == "" || matches(u.UID, l)) &&
-			(g == "" || matches(u.GID, g)) &&
-			(s == "" || matches(u.Status, s)) &&
-			(h == "" || matches(u.Homedir, h))
-	})
-}
-
-// generic function for filtering data types, in this case the model User
-func Filter[T any](slice []T, predicate func(T) bool) []T {
-	var result []T
-	for _, v := range slice {
-		if predicate(v) {
-			result = append(result, v)
-		}
-	}
-	return result
-}
-
-func getUsers() ([]model.User, error) {
-	var users []model.User
-
-	l, err := connLDAP()
-	if err != nil {
-		return users, err
-	}
 	defer l.Close()
 
-	// create the LDAP search request
-	searchRequest := ldap.NewSearchRequest(
-		"dc=c3local",                 // search base
-		ldap.ScopeWholeSubtree,       // scope
-		ldap.NeverDerefAliases,       // aliases
-		0,                            // size limit
-		0,                            // time limit
-		false,                        // types only
-		"(objectClass=posixAccount)", // filter
-		[]string{"dn", "uid", "cn", "loginShell", "uidNumber",
-			"gidNumber", "homeDirectory", "gecos"}, // attributes to return
-		nil,
-	)
-
-	// perform the search
-	sr, err := l.Search(searchRequest)
-	if err != nil {
-		err = fmt.Errorf("Failed to fetch users from LDAP: %v", err)
-		return nil, err
-	}
-
-	// get all the groups and ids
-	groups, err := getGroups()
+	users, err := model.GetAllUsersLDAP(l)
 	if err != nil {
-		err = fmt.Errorf("Failed to fetch groups and gids from LDAP: %v", err)
-		return nil, err
-	}
-
-	// iterate over the search results
-	for _, entry := range sr.Entries {
-		shell := entry.GetAttributeValue("loginShell")
-
-		user := model.User{
-			DN:        entry.DN,
-			UID:       entry.GetAttributeValue("uid"),
-			Name:      entry.GetAttributeValue("cn"),
-			Shell:     shell,
-			UIDNumber: entry.GetAttributeValue("uidNumber"),
-			GIDNumber: entry.GetAttributeValue("gidNumber"),
-			Homedir:   entry.GetAttributeValue("homeDirectory"),
-			Gecos:     entry.GetAttributeValue("gecos"),
-			Status:    ifThenElse(shell == "/bin/bash", "Active", "Blocked"),
-		}
-
-		user.GID = groups[user.GIDNumber]
-
-		gidNumber := user.GIDNumber
-
-		// safe assignment :)
-		groupName, exists := groups[gidNumber]
-		if !exists {
-			fmt.Printf("WARNING: no group found for GIDNumber %s, user %s. Continuing...", gidNumber, user.UID)
-		} else {
-			user.GID = groupName
-		}
-
-		user = parseGecos(user)
-		users = append(users, user)
-	}
-
-	return users, nil
-}
-
-func connLDAP() (*ldap.Conn, error) {
-	// connect to the LDAP server
-	l, err := ldap.DialURL("ldapi:///")
-	if err != nil {
-		err = fmt.Errorf("Failed to connect to LDAP: %v", err)
-		return nil, err
-	}
-
-	// get admin credentials
-	password, err := getLDAPPassword(PASSWD_PATH)
-	if err != nil {
-		err = fmt.Errorf("Failed to read LDAP password: %v", err)
-		return nil, err
-	}
-
-	// bind using admin credentials
-	err = l.Bind("cn=admin,dc=c3local", password)
-	if err != nil {
-		l.Close()
-		err = fmt.Errorf("Failed to bind with credentials: %v", err)
-		return nil, err
-	}
-
-	return l, nil
-}
-
-func getLDAPPassword(path string) (string, error) {
-	passwd, err := os.ReadFile(path)
-	if err != nil {
-		err = fmt.Errorf("Error reading password file: %w", err)
-		return "", err
-	}
-
-	password := strings.TrimSpace(string(passwd))
-
-	return password, nil
-}
-
-// Gecos Fields:
-// 0. users full name
-// 1. users grr
-// 2. responsible professor
-// 3. account expiry date (may be used for deletion)
-// 4. account status (Blocked/Active)
-// 5. login type (ini, first or last)
-// 6. users webdir
-// 7. users nobackup dir
-// 8. to be continued...
-func parseGecos(user model.User) model.User {
-	result := [NUM_GECOS_FIELDS]string{0: "_"}
-	for i := range result {
-		result[i] = "_"
-	}
-	parts := strings.Split(user.Gecos, ",")
-
-	for i := 0; i < len(parts) && i < NUM_GECOS_FIELDS; i++ {
-		if strings.TrimSpace(parts[i]) != "" {
-			result[i] = parts[i]
-		}
-	}
-
-	user.GRR = result[1]
-	user.Resp = result[2]
-	user.Expiry = result[3]
-	// only set with gecos if empty
-	if user.Status == "_" {
-		user.Status = result[4]
-	}
-	user.Ltype = result[5]
-	user.Webdir = result[6]
-	user.Nobackup = result[7]
-
-	return user
-}
-
-func getGroups() (map[string]string, error) {
-	groupMap := make(map[string]string)
-
-	l, err := connLDAP()
-	if err != nil {
-		return groupMap, err
-	}
-	defer l.Close()
-
-	// build search
-	searchRequest := ldap.NewSearchRequest(
-		"dc=c3local",
-		ldap.ScopeWholeSubtree,
-		ldap.NeverDerefAliases,
-		0, 0, false,
-		"(&(objectClass=posixGroup))",
-		[]string{"gidNumber", "cn"},
-		nil,
-	)
-
-	// search
-	sr, err := l.Search(searchRequest)
-	if err != nil {
-		return nil, fmt.Errorf("LDAP search failed: %w", err)
-	}
-
-	// arrange result into a map string->string
-	for _, entry := range sr.Entries {
-		gid := entry.GetAttributeValue("gidNumber")
-		cn := entry.GetAttributeValue("cn")
-		if gid != "" && cn != "" {
-			groupMap[gid] = cn
-		}
+		return err
 	}
 
-	return groupMap, nil
-}
-
-func getUIDs() ([]int, error) {
-	var uidNumbers []int
-
-	l, err := connLDAP()
+	err = o.RetrieveOpts(cmd)
 	if err != nil {
-		return uidNumbers, err
+		return err
 	}
-	defer l.Close()
 
-	// build search
-	searchRequest := ldap.NewSearchRequest(
-		"dc=c3local",
-		ldap.ScopeWholeSubtree,
-		ldap.NeverDerefAliases,
-		0, 0, false,
-		"(&(objectClass=posixAccount))",
-		[]string{"uidNumber"},
-		nil,
-	)
+	found := model.Search(users, o.Ignore, o.Exact, o.UID,
+		o.GID, o.Name, o.GRR, o.Status, o.Homedir)
 
-	// search
-	sr, err := l.Search(searchRequest)
-	if err != nil {
-		return nil, fmt.Errorf("LDAP search failed: %w", err)
+	if len(found) == 0 {
+		fmt.Println("No user matched the search!")
+		return nil
 	}
 
-	// arrange result into a array of integers
-	for _, entry := range sr.Entries {
-		uidStr := entry.GetAttributeValue("uidNumber")
-		if uidStr != "" {
-			uid, err := strconv.Atoi(uidStr)
-			if err != nil {
-				return nil, fmt.Errorf("invalid UID number: %s", uidStr)
-			}
-			uidNumbers = append(uidNumbers, uid)
-		}
+	for i := range found {
+		//fmt.Printf("%v\n\n", found[i].ToString())
+		fmt.Printf("%v\n\n", found[i].FullToString()) // for debugging
 	}
 
-	// sort for ease of use :)
-	sort.Ints(uidNumbers)
-
-	return uidNumbers, nil
-}
-
-func loginExists(users []model.User, login string) bool {
-	return exists(users, func(u model.User) bool { return u.UID == login })
-}
-
-// accepts GRRs in the format "GRRXXXXXXXX" aswell as 8 digit number
-func grrExists(users []model.User, grr string) bool {
-	return exists(users, func(u model.User) bool { return u.GRR == grr }) ||
-		exists(users, func(u model.User) bool { return u.GRR == "GRR"+grr })
-}
-
-// generic function for check existence of data types
-func exists[T any](slice []T, predicate func(T) bool) bool {
-	for _, item := range slice {
-		if predicate(item) {
-			return true
-		}
-	}
-	return false
+	return nil
 }
diff --git a/cmd/user/temp.go b/cmd/user/temp.go
new file mode 100644
index 0000000..cd76e7b
--- /dev/null
+++ b/cmd/user/temp.go
@@ -0,0 +1,197 @@
+package user
+
+// TODO: PQ Q N FUNCIONA??????????????
+import (
+	"fmt"
+	"os"
+	"strconv"
+
+	"github.com/spf13/cobra"
+	"gitlab.c3sl.ufpr.br/tss24/useradm/model"
+	"gitlab.c3sl.ufpr.br/tss24/useradm/utils"
+)
+
+var TempCmd = &cobra.Command{
+	Use:   "temp",
+	Short: "Create a lot of similar temporary users",
+	RunE:  tempCreate,
+}
+
+func init() {
+	TempCmd.Flags().StringP("passwd", "p", "", "Base password, will generate <password>#[1..number]")
+	TempCmd.Flags().StringP("login", "l", "", "Base login, will generate <login>[1..number]")
+	TempCmd.Flags().StringP("expiry", "e", "_", "Accounts' expiry date (format dd.mm.yy)")
+	TempCmd.Flags().StringP("resp", "r", "_", "Person responsible for the accounts")
+	TempCmd.Flags().StringP("group", "g", "", "Base group of the accounts")
+	TempCmd.Flags().IntP("number", "n", 0, "Number of accounts to be created")
+
+	TempCmd.MarkFlagRequired("login")
+	TempCmd.MarkFlagRequired("passwd")
+	TempCmd.MarkFlagRequired("group")
+	TempCmd.MarkFlagRequired("number")
+}
+
+func tempCreate(cmd *cobra.Command, args []string) error {
+	var opts model.Opts
+
+	l, err := model.ConnLDAP()
+	if err != nil {
+		return err
+	}
+	defer l.Close()
+
+	users, err := model.GetAllUsersLDAP(l)
+	if err != nil {
+		return err
+	}
+
+	groups, err := model.GetAllGroupsLDAP(l)
+	if err != nil {
+		return err
+	}
+
+	err = opts.RetrieveOpts(cmd)
+	if err != nil {
+		return err
+	}
+
+	for i := 1; i <= opts.Number; i++ {
+		if model.LoginExists(users, opts.UID+strconv.Itoa(i)) {
+			return fmt.Errorf("User found with login %v%v, won't overwrite", opts.UID, i)
+		}
+	}
+
+	err = validateGID(groups, opts.GID)
+	if err != nil {
+		return err
+	}
+
+	base := model.User{
+		GRR:      "_",
+		UID:      opts.UID,
+		GID:      opts.GID,
+		Name:     "_",
+		Resp:     opts.Resp,
+		Ltype:    model.LoginTypeUnknown,
+		Shell:    "/bin/bash",
+		Status:   "Active",
+		Expiry:   opts.Expiry,
+		Webdir:   "_",
+		Password: opts.Passwd + "#",
+	}
+
+	base.Homedir, err = utils.GenDirPath("/home", base.GID, base.UID, "")
+	if err != nil {
+		return err
+	}
+
+	base.Nobackup, err = utils.GenDirPath("/nobackup", base.GID, base.UID, "")
+	if err != nil {
+		return err
+	}
+
+	i := 1
+	success := false
+
+	defer func() {
+		if !success {
+			fmt.Println("Error found, cleaning up...")
+			for ; i > 0; i-- {
+				istring := strconv.Itoa(i)
+				_ = model.DelKRBPrincipal(base.UID + istring)
+				_ = model.DelFromLDAP(base.UID + istring)
+				_ = os.RemoveAll(base.Nobackup + istring)
+				_ = os.RemoveAll(base.Homedir + istring)
+			}
+		}
+	}()
+
+	for ; i <= opts.Number; i++ {
+		err := createTempUser(base, i)
+		if err != nil {
+			return err
+		}
+	}
+
+	success = true
+	return nil
+}
+
+func createTempUser(base model.User, num int) error {
+	var err error
+	var groups map[string]string
+	numstring := strconv.Itoa(num)
+
+	l, err := model.ConnLDAP()
+	if err != nil {
+		return err
+	}
+	defer l.Close()
+
+	groups, err = model.GetAllGroupsLDAP(l)
+	if err != nil {
+		return err
+	}
+
+	// gen login
+	base.UID = base.UID + numstring
+
+	// gen dn
+	base.DN = "uid=" + base.UID + ",ou=usuarios,dc=c3local"
+
+	// no webdir for temps
+
+	// gen home path
+	base.Homedir = base.Homedir + numstring
+
+	// gen nobkp path
+	base.Nobackup = base.Nobackup + numstring
+
+	// gen password
+	base.Password = base.Password + numstring
+
+	// gen gecos
+	base.GenGecos()
+
+	// get group id
+	base.GIDNumber, err = utils.GetGIDNumFromGID(groups, base.GID)
+	if err != nil {
+		return err
+	}
+
+	// gen newuid
+	err = base.GetNewUIDNumber(l)
+	if err != nil {
+		return err
+	}
+
+	fmt.Printf("Pronto para criar:\n%v\n\n", base.FullToString())
+	utils.ConfirmationPrompt(false, "creation")
+
+	// TODO: change for base.Create()
+	// create ldap
+	err = base.AddToLDAP(l)
+	if err != nil {
+		return err
+	}
+
+	// create kerberos
+	err = model.CreateKRBPrincipal(base.UID)
+	if err != nil {
+		return err
+	}
+
+	// change pass
+	err = model.ModKRBPassword(base.UID, base.Password)
+	if err != nil {
+		return err
+	}
+
+	// create dirs
+	err = base.CreateDirs()
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/cmd/user/validation.go b/cmd/user/validation.go
index 26aaeaa..ec2c952 100644
--- a/cmd/user/validation.go
+++ b/cmd/user/validation.go
@@ -2,20 +2,12 @@ package user
 
 import (
 	"fmt"
-	"os"
 	"regexp"
-	"strconv"
 	"strings"
-	"time"
-)
 
-func validatePath(path string) error {
-	_, err := os.Stat(path)
-	if os.IsNotExist(err) {
-		return nil
-	}
-	return fmt.Errorf("Path \"%v\" already exists, please provide a new path", path)
-}
+	"gitlab.c3sl.ufpr.br/tss24/useradm/model"
+	"gitlab.c3sl.ufpr.br/tss24/useradm/utils"
+)
 
 func validateExpiry(expiry string) error {
 	if expiry == "_" {
@@ -23,13 +15,14 @@ func validateExpiry(expiry string) error {
 	}
 
 	parts := strings.Split(expiry, ".")
-	if !isValidDate(parts) {
+	if !utils.IsValidDate(parts) {
 		err := fmt.Errorf("Malformed expiry date string, use \"dd.mm.yy\"")
 		return err
 	}
 	return nil
 }
 
+// TODO: change this check
 func validateStatus(status string) error {
 	if status != "Blocked" && status != "Active" {
 		err := fmt.Errorf("User status can only be \"Active\" or \"Blocked\"")
@@ -38,32 +31,19 @@ func validateStatus(status string) error {
 	return nil
 }
 
-func validateLtype(ltype string) error {
-	if ltype != "ini" && ltype != "first" && ltype != "last" {
-		err := fmt.Errorf("Login type can only be \"ini\", \"first\" or \"last\"")
-		return err
-	}
-	return nil
-}
-
-func validateGRR(grr string) error {
+func validateGRR(users []model.User, grr string) error {
 	// OK if empty, only "ini" login type requires it and we check :)
 	if grr == "_" {
 		return nil
 	}
 
-	users, err := getUsers()
-	if err != nil {
-		return err
-	}
-
 	isValid, _ := regexp.MatchString(`^\d{8}$`, grr) // is 8 digit number
 	if !isValid {
 		err := fmt.Errorf("Malformed GRR string, must be 8 digit number")
 		return err
 	}
 
-	if grrExists(users, grr) {
+	if model.GRRExists(users, grr) {
 		err := fmt.Errorf(`The informed GRR already exists in LDAP database
 Note: To search for the account use "useradm user show -r %s"`, grr)
 		return err
@@ -72,14 +52,9 @@ Note: To search for the account use "useradm user show -r %s"`, grr)
 	return nil
 }
 
-func validateGID(group string) error {
+func validateGID(groups map[string]string, group string) error {
 	var err error
 
-	groups, err := getGroups()
-	if err != nil {
-		return err
-	}
-
 	for _, value := range groups {
 		if value == group {
 			return nil
@@ -89,47 +64,11 @@ func validateGID(group string) error {
 	return err
 }
 
-func validateUID(login string) error {
-	users, err := getUsers()
-	if err != nil {
-		return err
-	}
-	res := searchUser(users, false, true, login, "", "", "", "", "")
+func validateUID(users []model.User, login string) error {
+	res := model.Search(users, false, true, login, "", "", "", "", "")
 	if len(res) != 0 {
 		return fmt.Errorf(`The informed Login already exists in LDAP database
 Note: To search for the account use "useradm user show -l %s"`, login)
 	}
 	return nil
 }
-
-func isValidDate(arr []string) bool {
-	if len(arr) != 3 {
-		return false
-	}
-
-	// convert to int
-	day, err1 := strconv.Atoi(arr[0])
-	mth, err2 := strconv.Atoi(arr[1])
-	year, err3 := strconv.Atoi(arr[2])
-
-	if err1 != nil || err2 != nil || err3 != nil {
-		return false
-	}
-
-	// ensure year is two digits
-	if year < 0 || year > 99 {
-		return false
-	}
-
-	// validate the date
-	fullYear := 2000 + year
-	t := time.Date(fullYear, time.Month(mth), day, 0, 0, 0, 0, time.UTC)
-	return t.Day() == day && t.Month() == time.Month(mth)
-}
-
-func ifThenElse(condition bool, a string, b string) string {
-	if condition {
-		return a
-	}
-	return b
-}
diff --git a/extras/create_test.go b/extras/create_test.go
new file mode 100644
index 0000000..61899cf
--- /dev/null
+++ b/extras/create_test.go
@@ -0,0 +1,112 @@
+package extras
+
+import (
+	"reflect"
+	"testing"
+
+	"gitlab.c3sl.ufpr.br/tss24/useradm/model"
+)
+
+func assertEqual(t *testing.T, a any, b any) {
+	if !reflect.DeepEqual(a, b) {
+		t.Fatalf("%s != %s", a, b)
+	}
+}
+
+func TestGenLogin(t *testing.T) {
+	assertEqual(t, genLogin("", "20241982", model.Initials, 0), "_")
+	assertEqual(t, genLogin("", "20241982", model.Initials, 1), "_")
+	assertEqual(t, genLogin("", "20241982", model.Initials, 2), "_")
+	assertEqual(t, genLogin("", "20241982", model.Initials, 3), "_")
+
+	assertEqual(t, genLogin("", "20241982", model.FirstName, 0), "_")
+	assertEqual(t, genLogin("", "20241982", model.FirstName, 1), "_")
+	assertEqual(t, genLogin("", "20241982", model.FirstName, 2), "_")
+	assertEqual(t, genLogin("", "20241982", model.FirstName, 3), "_")
+
+	assertEqual(t, genLogin("", "20241982", model.LastName, 0), "_")
+	assertEqual(t, genLogin("", "20241982", model.LastName, 1), "_")
+	assertEqual(t, genLogin("", "20241982", model.LastName, 2), "_")
+	assertEqual(t, genLogin("", "20241982", model.LastName, 3), "_")
+
+	assertEqual(t, genLogin("de", "20241982", model.Initials, 0), "_")
+	assertEqual(t, genLogin("de", "20241982", model.Initials, 1), "_")
+	assertEqual(t, genLogin("de", "20241982", model.Initials, 2), "_")
+	assertEqual(t, genLogin("de", "20241982", model.Initials, 3), "_")
+
+	assertEqual(t, genLogin("de", "20241982", model.FirstName, 0), "_")
+	assertEqual(t, genLogin("de", "20241982", model.FirstName, 1), "_")
+	assertEqual(t, genLogin("de", "20241982", model.FirstName, 2), "_")
+	assertEqual(t, genLogin("de", "20241982", model.FirstName, 3), "_")
+
+	assertEqual(t, genLogin("de", "20241982", model.LastName, 0), "_")
+	assertEqual(t, genLogin("de", "20241982", model.LastName, 1), "_")
+	assertEqual(t, genLogin("de", "20241982", model.LastName, 2), "_")
+	assertEqual(t, genLogin("de", "20241982", model.LastName, 3), "_")
+
+	assertEqual(t, genLogin("da de", "20241982", model.Initials, 0), "_")
+	assertEqual(t, genLogin("da de", "20241982", model.Initials, 1), "_")
+	assertEqual(t, genLogin("da de", "20241982", model.Initials, 2), "_")
+	assertEqual(t, genLogin("da de", "20241982", model.Initials, 3), "_")
+
+	assertEqual(t, genLogin("da de", "20241982", model.FirstName, 0), "_")
+	assertEqual(t, genLogin("da de", "20241982", model.FirstName, 1), "_")
+	assertEqual(t, genLogin("da de", "20241982", model.FirstName, 2), "_")
+	assertEqual(t, genLogin("da de", "20241982", model.FirstName, 3), "_")
+
+	assertEqual(t, genLogin("da de", "20241982", model.LastName, 0), "_")
+	assertEqual(t, genLogin("da de", "20241982", model.LastName, 1), "_")
+	assertEqual(t, genLogin("da de", "20241982", model.LastName, 2), "_")
+	assertEqual(t, genLogin("da de", "20241982", model.LastName, 3), "_")
+
+	assertEqual(t, genLogin("Fabiano", "20241982", model.Initials, 0), "f24")
+	assertEqual(t, genLogin("Fabiano", "20241982", model.Initials, 1), "fa24")
+	assertEqual(t, genLogin("Fabiano", "20241982", model.Initials, 2), "fab24")
+	assertEqual(t, genLogin("Fabiano", "20241982", model.Initials, 3), "fabi24")
+
+	assertEqual(t, genLogin("Fabiano", "20241982", model.FirstName, 0), "fabiano")
+	assertEqual(t, genLogin("Fabiano", "20241982", model.FirstName, 1), "fabiano")
+	assertEqual(t, genLogin("Fabiano", "20241982", model.FirstName, 2), "fabiano")
+	assertEqual(t, genLogin("Fabiano", "20241982", model.FirstName, 3), "fabiano")
+
+	assertEqual(t, genLogin("Fabiano", "20241982", model.LastName, 0), "fabiano")
+	assertEqual(t, genLogin("Fabiano", "20241982", model.LastName, 1), "fabiano")
+	assertEqual(t, genLogin("Fabiano", "20241982", model.LastName, 2), "fabiano")
+	assertEqual(t, genLogin("Fabiano", "20241982", model.LastName, 3), "fabiano")
+
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.Initials, 0), "faps24")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.Initials, 1), "faaps24")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.Initials, 2), "faanps24")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.Initials, 3), "faanpes24")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.Initials, 14), "fabiaantunperesouz24")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.Initials, 18), "fabianantunepereisouza24")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.Initials, 19), "fabianantunepereirsouza24")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.Initials, 20), "fabianoantunepereirsouza24")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.Initials, 21), "fabianoantunespereirsouza24")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.Initials, 22), "fabianoantunespereirasouza24")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.Initials, 40), "fabianoantunespereirasouza24")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.Initials, 50), "fabianoantunespereirasouza24")
+
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.FirstName, 0), "fabiano")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.FirstName, 1), "fabianoa")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.FirstName, 2), "fabianoap")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.FirstName, 3), "fabianoaps")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.FirstName, 4), "fabianoanps")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.FirstName, 16), "fabianoantunepereisouza")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.FirstName, 17), "fabianoantunepereirsouza")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.FirstName, 18), "fabianoantunespereirsouza")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.FirstName, 19), "fabianoantunespereirasouza")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.FirstName, 20), "fabianoantunespereirasouza")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.FirstName, 50), "fabianoantunespereirasouza")
+
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.LastName, 0), "fapsouza")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.LastName, 1), "faapsouza")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.LastName, 2), "faanpsouza")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.LastName, 3), "faanpesouza")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.LastName, 15), "fabianantunepereirsouza")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.LastName, 16), "fabianoantunepereirsouza")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.LastName, 17), "fabianoantunespereirsouza")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.LastName, 18), "fabianoantunespereirasouza")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.LastName, 19), "fabianoantunespereirasouza")
+	assertEqual(t, genLogin("Fabiano Antunes Pereira de Souza", "20241982", model.LastName, 100), "fabianoantunespereirasouza")
+}
diff --git a/model/krb.go b/model/krb.go
new file mode 100644
index 0000000..2c9493b
--- /dev/null
+++ b/model/krb.go
@@ -0,0 +1,46 @@
+package model
+
+import (
+	"fmt"
+	"os/exec"
+)
+
+// creates a KerberosPrincipal for the user
+func CreateKRBPrincipal(login string) error {
+	cmd := exec.Command("kadmin.local", "-q",
+		fmt.Sprintf("addprinc -policy padrao -randkey -x dn=uid=%s,ou=usuarios,dc=c3local %s", login, login))
+
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		return fmt.Errorf("Failed to add Kerberos principal: %v\nOutput: %s", err, output)
+	}
+
+	return nil
+}
+
+// command that changes the password >:D
+// the command kadmin.local returns 0 if the password change
+// fails, bruh. so we have to check differently.
+// FIXME: maybe do the validation with regex?
+func ModKRBPassword(login, password string) error {
+	cmd := exec.Command("kadmin.local", "-q",
+		fmt.Sprintf("cpw -pw %s %s", password, login))
+
+	output, _ := cmd.CombinedOutput()
+	if len(output) > 105 {
+		return fmt.Errorf("Error found changing password, output: \n%v", string(output[:]))
+	}
+
+	return nil
+}
+
+func DelKRBPrincipal(login string) error {
+	cmd := exec.Command("kadmin.local", "-q", fmt.Sprintf("delprinc -force %s", login))
+
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		return fmt.Errorf("Fail to delete Kerberos principal: %v\nOutput: %s", err, output)
+	}
+
+	return nil
+}
diff --git a/model/ldap.go b/model/ldap.go
new file mode 100644
index 0000000..4e430b5
--- /dev/null
+++ b/model/ldap.go
@@ -0,0 +1,281 @@
+package model
+
+import (
+	"fmt"
+	"os"
+	"sort"
+	"strconv"
+	"strings"
+
+	"github.com/go-ldap/ldap/v3"
+	"gitlab.c3sl.ufpr.br/tss24/useradm/utils"
+)
+
+const (
+	PASSWD_PATH = "/etc/ldapscripts/ldapscripts.passwd"
+)
+
+// stablishes a connection to LDAP
+func ConnLDAP() (*ldap.Conn, error) {
+	// connect to the LDAP server
+	l, err := ldap.DialURL("ldapi:///")
+	if err != nil {
+		return nil, fmt.Errorf("Failed to connect to LDAP: %v", err)
+	}
+
+	// get admin credentials
+	password, err := getPasswordLDAP(PASSWD_PATH)
+	if err != nil {
+		return nil, fmt.Errorf("Failed to read LDAP password: %v", err)
+	}
+
+	// bind using admin credentials
+	err = l.Bind("cn=admin,dc=c3local", password)
+	if err != nil {
+		l.Close()
+		return nil, fmt.Errorf("Failed to bind with credentials: %v", err)
+	}
+
+	return l, nil
+}
+
+func getPasswordLDAP(path string) (string, error) {
+	passwd, err := os.ReadFile(path)
+	if err != nil {
+		err = fmt.Errorf("Error reading password file: %w", err)
+		return "", err
+	}
+
+	password := strings.TrimSpace(string(passwd))
+
+	return password, nil
+}
+
+// generates a LDAP request and adds the user to LDAP
+func (u *User) AddToLDAP(l *ldap.Conn) error {
+
+	req := ldap.NewAddRequest(u.DN, nil)
+	req.Attribute("uid", []string{u.UID})
+	req.Attribute("cn", []string{u.Name})
+	req.Attribute("objectClass", []string{"account", "posixAccount"})
+	req.Attribute("loginShell", []string{u.Shell})
+	req.Attribute("uidNumber", []string{u.UIDNumber})
+	req.Attribute("gidNumber", []string{u.GIDNumber})
+	req.Attribute("homeDirectory", []string{u.Homedir})
+	req.Attribute("gecos", []string{u.Gecos})
+
+	err := l.Add(req)
+	if err != nil {
+		return fmt.Errorf("Failed to add user %s to LDAP: %v", u.UID, err)
+	}
+
+	err = AddToGroupLDAP(l, u.UID, u.GID)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func DelFromLDAP(UID string) error {
+	l, err := ConnLDAP()
+	if err != nil {
+		return err
+	}
+	defer l.Close()
+
+	// search for all groups the user is a member of
+	searchReq := ldap.NewSearchRequest(
+		"ou=grupos,dc=c3local,dc=com",
+		ldap.ScopeWholeSubtree,
+		ldap.NeverDerefAliases, 0, 0, false,
+		"(memberUid="+UID+")", // Filter by UID membership
+		[]string{"dn", "cn"},
+		nil,
+	)
+
+	groups, err := l.Search(searchReq)
+	if err != nil && !ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
+		return fmt.Errorf("Group members search failed: %v", err)
+	}
+
+	// iterate and remove user from each group
+	for _, entry := range groups.Entries {
+		groupName := entry.GetAttributeValue("cn")
+		if err := DelFromGroupLDAP(l, UID, groupName); err != nil {
+			fmt.Printf("Warning: %v\n", err)
+		}
+	}
+
+	// removing user entry
+	userDN := "uid=" + UID + "ou=usuarios,dc=c3local"
+	delReq := ldap.NewDelRequest(userDN, nil)
+	if err := l.Del(delReq); err != nil &&
+		!ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
+		return fmt.Errorf("User deletion failed: %v", err)
+	}
+
+	return nil
+}
+
+func DelFromGroupLDAP(l *ldap.Conn, userUID, GID string) error {
+	groupDN := fmt.Sprintf("cn=%s,ou=grupos,dc=c3local", GID)
+	modReq := ldap.NewModifyRequest(groupDN, nil)
+	modReq.Delete("memberUid", []string{userUID})
+
+	err := l.Modify(modReq)
+	if err != nil && !ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchAttribute) {
+		return fmt.Errorf("Failed to remove user %s from group %s: %v", userUID, groupDN, err)
+	}
+
+	return nil
+}
+
+// adds a user to a group in LDAP
+func AddToGroupLDAP(l *ldap.Conn, UID, GID string) error {
+	groupDN := fmt.Sprintf("cn=%s,ou=grupos,dc=c3local", GID)
+	modReq := ldap.NewModifyRequest(groupDN, nil)
+	modReq.Add("memberUid", []string{UID})
+
+	err := l.Modify(modReq)
+	if err != nil {
+		return fmt.Errorf("Failed to add user %s to group %s: %v", UID, GID, err)
+	}
+
+	return nil
+}
+
+func GetAllUsersLDAP(l *ldap.Conn) ([]User, error) {
+	var users []User
+
+	// create the LDAP search request
+	req := ldap.NewSearchRequest(
+		"dc=c3local",           // search base
+		ldap.ScopeWholeSubtree, // scope
+		ldap.NeverDerefAliases, // aliases
+		0, 0,                   // size/time limit
+		false,                        // types only
+		"(objectClass=posixAccount)", // filter
+		[]string{"dn", "uid", "cn", "loginShell", "uidNumber",
+			"gidNumber", "homeDirectory", "gecos"}, // attributes to return
+		nil,
+	)
+
+	// perform the search
+	sr, err := l.Search(req)
+	if err != nil {
+		err = fmt.Errorf("Failed to fetch users from LDAP: %v", err)
+		return nil, err
+	}
+
+	// get all the groups and ids
+	groups, err := GetAllGroupsLDAP(l)
+	if err != nil {
+		err = fmt.Errorf("Failed to fetch groups and gids from LDAP: %v", err)
+		return nil, err
+	}
+
+	// iterate over the search results
+	for _, entry := range sr.Entries {
+		shell := entry.GetAttributeValue("loginShell")
+
+		user := User{
+			DN:        entry.DN,
+			UID:       entry.GetAttributeValue("uid"),
+			Name:      entry.GetAttributeValue("cn"),
+			Shell:     shell,
+			UIDNumber: entry.GetAttributeValue("uidNumber"),
+			GIDNumber: entry.GetAttributeValue("gidNumber"),
+			Homedir:   entry.GetAttributeValue("homeDirectory"),
+			Gecos:     entry.GetAttributeValue("gecos"),
+			Status:    utils.IfThenElse(shell == "/bin/bash", "Active", "Blocked"),
+		}
+
+		user.GID = groups[user.GIDNumber]
+
+		gidNumber := user.GIDNumber
+
+		// safe assignment :)
+		groupName, exists := groups[gidNumber]
+		if !exists {
+			fmt.Printf("WARNING: no group found for GIDNumber %s, user %s. Continuing...", gidNumber, user.UID)
+		} else {
+			user.GID = groupName
+		}
+
+		user.ParseGecos()
+		users = append(users, user)
+	}
+
+	return users, nil
+}
+
+func GetAllGroupsLDAP(l *ldap.Conn) (map[string]string, error) {
+	groupMap := make(map[string]string)
+
+	// build search
+	searchRequest := ldap.NewSearchRequest(
+		"dc=c3local",
+		ldap.ScopeWholeSubtree,
+		ldap.NeverDerefAliases,
+		0, 0, false,
+		"(&(objectClass=posixGroup))",
+		[]string{"gidNumber", "cn"},
+		nil,
+	)
+
+	// search
+	sr, err := l.Search(searchRequest)
+	if err != nil {
+		return nil, fmt.Errorf("LDAP search failed: %w", err)
+	}
+
+	// arrange result into a map string->string
+	for _, entry := range sr.Entries {
+		gid := entry.GetAttributeValue("gidNumber")
+		cn := entry.GetAttributeValue("cn")
+		if gid != "" && cn != "" {
+			groupMap[gid] = cn
+		}
+	}
+
+	return groupMap, nil
+}
+
+func GetAllUIDsLDAP(l *ldap.Conn) ([]int, error) {
+	var uidNumbers []int
+
+	// build search
+	searchRequest := ldap.NewSearchRequest(
+		"dc=c3local",
+		ldap.ScopeWholeSubtree,
+		ldap.NeverDerefAliases,
+		0, 0, false,
+		"(&(objectClass=posixAccount))",
+		[]string{"uidNumber"},
+		nil,
+	)
+
+	// search
+	sr, err := l.Search(searchRequest)
+	if err != nil {
+		return nil, fmt.Errorf("LDAP search failed: %w", err)
+	}
+
+	// arrange result into a array of integers
+	for _, entry := range sr.Entries {
+		uidStr := entry.GetAttributeValue("uidNumber")
+		if uidStr != "" {
+			uid, err := strconv.Atoi(uidStr)
+			if err != nil {
+				return nil, fmt.Errorf("invalid UID number: %s", uidStr)
+			}
+			uidNumbers = append(uidNumbers, uid)
+		}
+	}
+
+	// sort for ease of use :)
+	sort.Ints(uidNumbers)
+
+	return uidNumbers, nil
+}
diff --git a/model/ltype.go b/model/ltype.go
new file mode 100644
index 0000000..6c3eb10
--- /dev/null
+++ b/model/ltype.go
@@ -0,0 +1,40 @@
+package model
+
+import "strings"
+
+type LoginType int
+
+const (
+	LoginTypeUnknown LoginType = iota - 1 // error value
+	Initials
+	FirstName
+	LastName
+)
+
+// converts to string
+func (lt LoginType) String() string {
+	switch lt {
+	case Initials:
+		return "Initials"
+	case FirstName:
+		return "FirstName"
+	case LastName:
+		return "LastName"
+	default:
+		return "Unknown"
+	}
+}
+
+// converts to Ltype
+func (lt *LoginType) Parse(s string) {
+	switch strings.ToLower(s) {
+	case "initials":
+		*lt = Initials
+	case "firstname":
+		*lt = FirstName
+	case "lastname":
+		*lt = LastName
+	default:
+		*lt = LoginTypeUnknown
+	}
+}
diff --git a/model/opts.go b/model/opts.go
index a2f2768..a89fdce 100644
--- a/model/opts.go
+++ b/model/opts.go
@@ -12,6 +12,7 @@ type Opts struct {
 	UID     string
 	Resp    string
 	Name    string
+	Path    string
 	Nobkp   string
 	Ltype   string
 	Shell   string
@@ -30,28 +31,29 @@ type Opts struct {
 
 func (o *Opts) ToString() string {
 	return fmt.Sprintf(`Opts:
-	GRR      %s
-	GID      %s
-	UID      %s
-	Resp     %s
-	Name     %s
-	Nobkp    %s
-	Ltype    %s
-	Shell    %s
-	Passwd   %s
-	Webdir   %s
-	Status   %s
-	Expiry   %s
-	Homedir  %s
+	GRR      %v
+	GID      %v
+	UID      %v
+	Resp     %v
+	Name     %v
+	Path     %v
+	Nobkp    %v
+	Ltype    %v
+	Shell    %v
+	Passwd   %v
+	Webdir   %v
+	Status   %v
+	Expiry   %v
+	Homedir  %v
 	Ignore   %v
 	Exact    %v
 	Block    %v
 	Unblock  %v
 	Confirm  %v
 	Number   %v`,
-		o.GRR, o.GID, o.UID, o.Resp, o.Name, o.Nobkp, o.Ltype, o.Shell,
-		o.Passwd, o.Webdir, o.Status, o.Expiry, o.Homedir, o.Ignore,
-		o.Exact, o.Block, o.Unblock, o.Confirm, o.Number)
+		o.GRR, o.GID, o.UID, o.Resp, o.Name, o.Path, o.Nobkp, o.Ltype,
+		o.Shell, o.Passwd, o.Webdir, o.Status, o.Expiry, o.Homedir,
+		o.Ignore, o.Exact, o.Block, o.Unblock, o.Confirm, o.Number)
 }
 
 func (o *Opts) RetrieveOpts(cmd *cobra.Command) error {
@@ -92,7 +94,12 @@ func (o *Opts) RetrieveOpts(cmd *cobra.Command) error {
 		return err
 	}
 
-	o.Webdir, err = getFlagString(cmd, "path")
+	o.Webdir, err = getFlagString(cmd, "web")
+	if err != nil {
+		return err
+	}
+
+	o.Path, err = getFlagString(cmd, "path")
 	if err != nil {
 		return err
 	}
diff --git a/model/user.go b/model/user.go
index 0fc75b1..ade5235 100644
--- a/model/user.go
+++ b/model/user.go
@@ -1,6 +1,34 @@
 package model
 
-import "fmt"
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/go-ldap/ldap/v3"
+	"gitlab.c3sl.ufpr.br/tss24/useradm/utils"
+)
+
+const (
+	MIN_UID          = 1000
+	MAX_UID          = 6000
+	MAX_VARIANCE     = 30
+	DEF_PERMISSION   = "0700" // default user dir permission
+	BLK_PERMISSION   = "0000" // user dir permission if blocked
+	HTML_BASE_PERM   = "0755" // set public_html to this on creation
+	NUM_GECOS_FIELDS = 8
+)
+
+var (
+	ANO          = strconv.Itoa(time.Now().Year())
+	NO_BKP_TRASH = "/nobackup/contas_removidas/" + ANO
+	HOME_TRASH   = "/home/contas_removidas/" + ANO
+	WEB_TRASH    = "/home/contas_removidas/html/" + ANO
+)
 
 type User struct {
 	DN        string
@@ -9,7 +37,7 @@ type User struct {
 	GID       string
 	Name      string
 	Resp      string
-	Ltype     string
+	Ltype     LoginType
 	Gecos     string
 	Shell     string
 	Status    string
@@ -22,19 +50,244 @@ type User struct {
 	UIDNumber string
 }
 
+// Gecos Fields:
+// 0. users full name
+// 1. users grr
+// 2. responsible professor
+// 3. account expiry date (may be used for deletion)
+// 4. account status (Blocked/Active)
+// 5. login type (ini, first or last)
+// 6. users webdir
+// 7. users nobackup dir
+// 8. to be continued...
+func (u *User) ParseGecos() {
+	result := [NUM_GECOS_FIELDS]string{0: "_"}
+	for i := range result {
+		result[i] = "_"
+	}
+	parts := strings.Split(u.Gecos, ",")
+
+	for i := 0; i < len(parts) && i < NUM_GECOS_FIELDS; i++ {
+		if strings.TrimSpace(parts[i]) != "" {
+			result[i] = parts[i]
+		}
+	}
+
+	u.GRR = result[1]
+	u.Resp = result[2]
+	u.Expiry = result[3]
+	// only set with gecos if empty
+	if u.Status == "_" {
+		u.Status = result[4]
+	}
+	u.Ltype.Parse(result[5])
+	u.Webdir = result[6]
+	u.Nobackup = result[7]
+}
+
+func (u *User) Create(l *ldap.Conn) error {
+	success := false
+
+	defer func() {
+		if !success {
+			_ = DelKRBPrincipal(u.UID)
+			_ = DelFromLDAP(u.UID)
+		}
+	}()
+
+	err := u.AddToLDAP(l)
+	if err != nil {
+		return err
+	}
+
+	err = CreateKRBPrincipal(u.UID)
+	if err != nil {
+		return err
+	}
+
+	err = ModKRBPassword(u.UID, u.Password)
+	if err != nil {
+		return err
+	}
+
+	err = u.CreateDirs()
+	if err != nil {
+		return err
+	}
+
+	success = true
+	return nil
+}
+
+func Search(users []User, ig, ex bool, l, g, n, r, s, h string) []User {
+	return utils.Filter(users, func(u User) bool {
+		matches := func(src, target string) bool {
+			if ig {
+				src, target = strings.ToLower(src), strings.ToLower(target)
+			}
+			if ex {
+				return src == target // exact match
+			}
+			return strings.Contains(src, target) // partial match
+		}
+
+		// search
+		return (r == "" || matches(u.GRR, r)) &&
+			(n == "" || matches(u.Name, n)) &&
+			(l == "" || matches(u.UID, l)) &&
+			(g == "" || matches(u.GID, g)) &&
+			(s == "" || matches(u.Status, s)) &&
+			(h == "" || matches(u.Homedir, h))
+	})
+}
+
+// searches for the specified login string, there can only be one >:)
+func Locate(l *ldap.Conn, login string) (User, error) {
+	var u User
+	users, err := GetAllUsersLDAP(l)
+	if err != nil {
+		return u, err
+	}
+
+	if !LoginExists(users, login) {
+		return u, fmt.Errorf("Failed to find login in LDAP database: %v", err)
+	}
+
+	filter := Search(users, false, true, login, "", "", "", "", "")
+	if len(filter) != 1 {
+		return u, fmt.Errorf(`More than one user matched the login given.
+search made: "useradm user show -l %v -e"`, login)
+	}
+
+	u = filter[0]
+	return u, nil
+}
+
+// DN formatting
+func (u *User) SetDN(UID string) {
+	u.DN = "uid=" + UID + "ou=usuarios,dc=c3local"
+}
+
+// finds next available uidNumber (MEX from uid group)
+func (u *User) GetNewUIDNumber(l *ldap.Conn) error {
+	uids, err := GetAllUIDsLDAP(l)
+	if err != nil {
+		return err
+	}
+
+	candidate := MIN_UID
+	for _, uid := range uids {
+		if uid == candidate { // check if taken
+			candidate++
+		} else if uid > candidate { // found a gap
+			break
+		}
+	}
+
+	if candidate > MAX_UID {
+		return fmt.Errorf("No more available UID numbers")
+	}
+
+	u.UIDNumber = strconv.Itoa(candidate)
+	return nil
+}
+
+// creates the login, if it already exists try again with variance
+func (u *User) GenUniqueUID(users []User) error {
+	used, variance := true, 0
+	for used {
+		u.UID = u.genLogin(variance)
+
+		used = LoginExists(users, u.UID) || utils.MailAliasExists(u.UID)
+
+		variance++
+		if variance > MAX_VARIANCE {
+			return fmt.Errorf("Could't generate login automatically, please inform the desired login")
+		}
+	}
+	return nil
+}
+
+// ye
+func (u *User) genLogin(variance int) string {
+	parts := utils.FormatName(u.Name)
+	if len(parts) == 0 || u.Ltype == LoginTypeUnknown {
+		return "_"
+	}
+
+	partPrefixLen := make([]int, len(parts))
+	if u.Ltype == Initials {
+		// the first letter of each part must appear
+		for i := 0; i < len(parts); i++ {
+			partPrefixLen[i] = 1
+		}
+	} else if u.Ltype == FirstName {
+		// the first name (part) must appear
+		partPrefixLen[0] = len(parts[0])
+	} else {
+		// the first letter of each part must appear
+		for i := 0; i < len(parts); i++ {
+			partPrefixLen[i] = 1
+		}
+		// last part aswell
+		partPrefixLen[len(parts)-1] = len(parts[len(parts)-1])
+	}
+
+	partPrefixIx := 0
+	for i := 0; i < variance; i++ {
+		ok := false
+		for k := 0; k < len(parts) && !ok; k++ {
+			if partPrefixLen[partPrefixIx] < len(parts[partPrefixIx]) {
+				partPrefixLen[partPrefixIx]++
+				ok = true
+			}
+			partPrefixIx = (partPrefixIx + 1) % len(parts)
+		}
+		if !ok {
+			// it's joever, from now on nothing happens, quit :D
+			break
+		}
+	}
+
+	// contruct the login with the given legths
+	login := ""
+	for i := 0; i < len(parts); i++ {
+		login += parts[i][:partPrefixLen[i]]
+	}
+	if login == "" {
+		return "_"
+	}
+
+	if u.Ltype == Initials {
+		login += u.GRR[2:4]
+	}
+	return login
+}
+
+func (u *User) GenGecos() {
+	u.Gecos = u.Name + ","
+	u.Gecos += u.GRR + ","
+	u.Gecos += u.Resp + ","
+	u.Gecos += u.Expiry + ","
+	u.Gecos += u.Status + ","
+	u.Gecos += u.Ltype.String() + ","
+	u.Gecos += u.Webdir + ","
+	u.Gecos += u.Nobackup
+}
+
 func (u *User) ToString() string {
 	return fmt.Sprintf(`User:
-	Name:    %s
-	Login:   %s
-	GRR:     %s
-	Group:   %s
-	Shell:   %s
-	Home:    %s
-	Webdir:  %s
-	Nobkp:   %s
-	Status:  %s
-	Resp:    %s
-	Expiry:  %s`,
+	Name:    %v
+	Login:   %v
+	GRR:     %v
+	Group:   %v
+	Shell:   %v
+	Home:    %v
+	Webdir:  %v
+	Nobkp:   %v
+	Status:  %v
+	Resp:    %v
+	Expiry:  %v`,
 		u.Name, u.UID, u.GRR, u.GID, u.Shell, u.Homedir, u.Webdir,
 		u.Nobackup, u.Status, u.Resp, u.Expiry)
 }
@@ -42,24 +295,177 @@ func (u *User) ToString() string {
 // useful for debugging :)
 func (u *User) FullToString() string {
 	return fmt.Sprintf(`User:
-	DN:        %s  
-	GRR:       %s
-	UID:       %s
-	GID:       %s
-	Name:      %s
-	Resp:      %s
-	Ltype:     %s
-	Gecos:     %s
-	Shell:     %s
-	Status:    %s
-	Webdir:    %s
-	Expiry:    %s
-	Homedir:   %s
-	Password:  %s
-	Nobackup:  %s
-	GIDNumber: %s
-	UIDNumber: %s`,
+	DN:        %v
+	GRR:       %v
+	UID:       %v
+	GID:       %v
+	Name:      %v
+	Resp:      %v
+	Ltype:     %v
+	Gecos:     %v
+	Shell:     %v
+	Status:    %v
+	Webdir:    %v
+	Expiry:    %v
+	Homedir:   %v
+	Password:  %v
+	Nobackup:  %v
+	GIDNumber: %v
+	UIDNumber: %v`,
 		u.DN, u.GRR, u.UID, u.GID, u.Name, u.Resp, u.Ltype, u.Gecos,
 		u.Shell, u.Status, u.Webdir, u.Expiry, u.Homedir, u.Password,
 		u.Nobackup, u.GIDNumber, u.UIDNumber)
 }
+
+func LoginExists(users []User, login string) bool {
+	return utils.Exists(users, func(u User) bool { return u.UID == login })
+}
+
+// accepts GRRs in the format "GRRXXXXXXXX" aswell as just 8 digit number
+func GRRExists(users []User, grr string) bool {
+	return utils.Exists(users, func(u User) bool { return u.GRR == grr }) ||
+		utils.Exists(users, func(u User) bool { return u.GRR == "GRR"+grr })
+}
+
+// moves dirs to their respective trash dir
+func (u *User) DirsToTrash() error {
+	err := utils.MoveAndChown(u.Homedir, HOME_TRASH, "nobody", "nogroup")
+	if err != nil {
+		return err
+	}
+
+	err = utils.MoveAndChown(u.Nobackup, NO_BKP_TRASH, "nobody", "nogroup")
+	if err != nil {
+		return err
+	}
+
+	err = utils.MoveAndChown(u.Webdir, WEB_TRASH, "nobody", "nogroup")
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (u *User) CreateDirs() error {
+	success := false
+
+	defer func() {
+		if !success {
+			fmt.Println("Error found creating dirs, cleaning up...")
+			_ = os.RemoveAll(u.Nobackup)
+			_ = os.RemoveAll(u.Homedir)
+		}
+	}()
+
+	if err := u.CreateHome(u.Homedir); err != nil {
+		return err
+	}
+
+	if err := u.CreateHome(u.Nobackup); err != nil {
+		return err
+	}
+
+	if err := u.CreateWeb(); err != nil {
+		return err
+	}
+
+	success = true
+	return nil
+}
+
+func (u *User) CreateHome(homeDir string) error {
+	perm := DEF_PERMISSION
+	if u.Status == "Blocked" {
+		perm = BLK_PERMISSION
+	}
+
+	// create directory
+	cmd := exec.Command("mkdir", "-p", homeDir)
+	cmd.Stdout = nil
+	cmd.Stderr = nil
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("Failed to create home directory: %w", err)
+	}
+
+	// copy /etc/skel
+	cmd = exec.Command("cp", "-r", "/etc/skel/.", homeDir)
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("Failed to copy /etc/skel contents: %w", err)
+	}
+
+	// change permissions
+	cmd = exec.Command("chmod", perm, homeDir)
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("Failed to set permissions: %w", err)
+	}
+
+	// change ownership
+	cmd = exec.Command("chown", "-R", fmt.Sprintf("%s:%s", u.UID, u.GID), homeDir)
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("Failed to change ownership: %w", err)
+	}
+
+	return nil
+}
+
+func (u *User) CreateWeb() error {
+	success := false
+
+	if u.Webdir == "_" {
+		success = true
+		return nil
+	}
+
+	perm := DEF_PERMISSION
+	if u.Status == "Blocked" {
+		perm = BLK_PERMISSION
+	}
+
+	defer func() {
+		if !success {
+			_ = os.RemoveAll(u.Webdir)
+		}
+	}()
+
+	// create directory
+	cmd := exec.Command("mkdir", "-p", u.Webdir)
+	cmd.Stdout = nil
+	cmd.Stderr = nil
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("Failed to create web directory: %w", err)
+	}
+
+	// create index
+	cmd = exec.Command("touch", filepath.Join(u.Webdir, "index.html"))
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("Failed to create index.html: %w", err)
+	}
+
+	// create link in users home
+	cmd = exec.Command("ln", "-s", u.Webdir, filepath.Join(u.Homedir, "public_html"))
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("Failed to create link public_html: %w", err)
+	}
+
+	// change permissions
+	cmd = exec.Command("chmod", perm, u.Webdir)
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("Failed to set permissions: %w", err)
+	}
+
+	// change ownership
+	cmd = exec.Command("chown", "-R", fmt.Sprintf("%s:%s", u.UID, u.GID), u.Webdir)
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("Failed to change ownership: %w", err)
+	}
+
+	// set permission for public_html
+	cmd = exec.Command("chmod", HTML_BASE_PERM, filepath.Join(u.Homedir, "public_html"))
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("Failed to create set permissions for public_html: %w", err)
+	}
+
+	success = true
+	return nil
+}
diff --git a/utils/utils.go b/utils/utils.go
new file mode 100644
index 0000000..464987a
--- /dev/null
+++ b/utils/utils.go
@@ -0,0 +1,197 @@
+package utils
+
+import (
+	"bufio"
+	"crypto/rand"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"time"
+)
+
+const (
+	MAIL_ALIAS_FILE = "/etc/aliases"
+	PASSWD_PATH     = "/etc/ldapscripts/ldapscripts.passwd"
+)
+
+// generic function for filtering data types, in this case the model User
+func Filter[T any](slice []T, predicate func(T) bool) []T {
+	var result []T
+	for _, v := range slice {
+		if predicate(v) {
+			result = append(result, v)
+		}
+	}
+	return result
+}
+
+// generic function for check existence of data types
+func Exists[T any](slice []T, predicate func(T) bool) bool {
+	for _, item := range slice {
+		if predicate(item) {
+			return true
+		}
+	}
+	return false
+}
+
+// removes connectives, leave all lowecase and splits the name
+func FormatName(name string) []string {
+	connectives := map[string]bool{"da": true, "de": true, "di": true,
+		"do": true, "das": true, "dos": true, "von": true}
+	splitName := strings.Fields(name)
+	var parts []string
+
+	for _, part := range splitName {
+		lowerPart := strings.ToLower(part)
+		if !connectives[lowerPart] {
+			parts = append(parts, string(lowerPart))
+		}
+	}
+	return parts
+}
+
+// queries to check if the alias exists
+func MailAliasExists(alias string) bool {
+	cmd := exec.Command("/usr/sbin/postalias", "-q", alias, MAIL_ALIAS_FILE)
+	cmd.Stdout = nil
+	cmd.Stderr = nil
+	return cmd.Run() == nil
+}
+
+func GetGIDNumFromGID(groups map[string]string, GID string) (string, error) {
+	for key, val := range groups {
+		if val == GID {
+			return key, nil
+		}
+	}
+	return "", fmt.Errorf("Couldn't find group GIDNumber")
+}
+
+func GenDirPath(base, group, login, input string) (string, error) {
+	if input != "" {
+		return input, nil
+	}
+	p := filepath.Join(base, group, login)
+
+	if PathExists(p) {
+		return p, fmt.Errorf("Path already exists")
+	}
+
+	return p, nil
+}
+
+func PathExists(path string) bool {
+	_, err := os.Stat(path)
+	if err == nil {
+		return true
+	}
+	return false
+}
+
+// do NOT include ':' in the charset, it WILL break the command
+func GenPassword() string {
+	const charset = "@*()=+[];,.?123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"
+	b := make([]byte, 20)
+	if _, err := rand.Read(b); err != nil {
+		panic(err)
+	}
+	for i := range b {
+		b[i] = charset[int(b[i])%len(charset)]
+	}
+	return string(b)
+}
+
+func IfThenElse(condition bool, a string, b string) string {
+	if condition {
+		return a
+	}
+	return b
+}
+
+func MoveAndChown(orig, dest, owner, group string) error {
+	// check if orig exists
+	if _, err := os.Stat(orig); err != nil {
+		fmt.Printf("Directory %v not found so not moved\n", orig)
+		return nil
+	}
+
+	// construct destination path
+	destPath := filepath.Join(dest, filepath.Base(orig))
+
+	if _, err := os.Stat(destPath); err == nil {
+		return fmt.Errorf("Directory %v already exists, can't move\n", destPath)
+	}
+
+	// move directory
+	cmd := exec.Command("mv", orig, destPath)
+	if output, err := cmd.CombinedOutput(); err != nil {
+		return fmt.Errorf("Failed to move dirs: %w\nOutput: %v", err, output)
+	}
+
+	// recursive chown
+	cmd = exec.Command("chown", "-R", owner+":"+group, destPath)
+	if output, err := cmd.CombinedOutput(); err != nil {
+		return fmt.Errorf("Failed to set owner/group: %w\nOutput: %v", err, output)
+	}
+
+	return nil
+}
+
+// prints a confirmation prompt, given the operation being performed
+func ConfirmationPrompt(confirm bool, operation string) {
+	if !confirm {
+		fmt.Printf("Proceed with user %v? [y/N] ", operation)
+
+		reader := bufio.NewReader(os.Stdin)
+		response, _ := reader.ReadString('\n')
+
+		if strings.TrimSpace(strings.ToLower(response)) != "y" {
+			fmt.Fprintln(os.Stderr, "Aborted.")
+			os.Exit(1)
+		}
+	}
+}
+
+// LDAP has a cache...
+func ClearCache() error {
+	cmd := exec.Command("nscd", "-i", "passwd")
+
+	cmd.Stdout = nil
+	cmd.Stderr = nil
+	err := cmd.Run()
+	if err != nil {
+		return err
+	}
+
+	cmd = exec.Command("nscd", "-i", "group")
+	return cmd.Run()
+}
+
+func IsValidDate(arr []string) bool {
+	if len(arr) != 3 {
+		return false
+	}
+
+	// convert to int
+	day, err1 := strconv.Atoi(arr[0])
+	mth, err2 := strconv.Atoi(arr[1])
+	year, err3 := strconv.Atoi(arr[2])
+
+	if err1 != nil || err2 != nil || err3 != nil {
+		return false
+	}
+
+	// ensure year is two digits
+	if year < 0 || year > 99 {
+		return false
+	}
+
+	// validate the date
+	fullYear := 2000 + year
+	t := time.Date(fullYear, time.Month(mth), day, 0, 0, 0, 0, time.UTC)
+	return t.Day() == day && t.Month() == time.Month(mth)
+}
-- 
GitLab