diff --git a/README.md b/README.md index ed0ead8..e30b918 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,7 @@ The password database is created and is active now. You can start adding entries Password (enter to generate new): Generating password ...done Notes: Website uses Nginx auth + Do you want to add custom fields [y/N]: Created new entry with id: 1 You can now list the entry with one of the list options. @@ -163,6 +164,36 @@ You can now list the entry with one of the list options. Modified: 2021-21-09 23:12:35 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +## Add an entry with custom fields + +From version 0.3 onwards, custom fields are supported. + + $ varuh -A + Title: Github token + URL: https://github.com/mydev/myproject + Username: mydev + Password (enter to generate new): ghp_ipQrStuVwxYz1a2b3cdEF10ghI689kLaMnOp + Notes: Never Expires + Do you want to add custom fields [y/N]: y + Field Name: Domain + Value for Domain: github.com + Field Name: Type + Value for Type: Auth Token + Field Name: + Created new entry with id: 6 + + $ varuh -l 6 + ID: 6 + Title: Github token + User: mydev + URL: https://github.com/mydev/myproject + Password: ghp_ipQrStuVwxYz1a2b3cdEF10ghI689kLaMnOp + Notes: Never Expires + Domain: github.com + Type: Auth Token + Modified: 2021-21-13 00:07:18 + + For more on listing see the [Listing and Searching](#listing-and-searching) section below. ## Edit an entry @@ -178,6 +209,7 @@ For more on listing see the [Listing and Searching](#listing-and-searching) sect New Password ([y/Y] to generate new, enter will keep old one): Current Notes: Website uses Nginx auth New Notes: Website uses Apache + Do you want to add custom fields [y/N]: Updated entry. $ varuh -l 1 -s @@ -191,6 +223,42 @@ For more on listing see the [Listing and Searching](#listing-and-searching) sect Modified: 2021-21-09 23:15:29 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +## Edit an entry with custom fields + +When you edit an entry with custom fields, you get the option to change the name of the fields or delete the fields entirely. + + $ varuh -E 6 + Current Title: Github token + New Title: + Current URL: https://github.com/mydev/myproject + New URL: + Current Username: mydev + New Username: + Current Password: ghp_ipQrStuVwxYz1a2b3cdEF10ghI689kLaMnOp + New Password ([y/Y] to generate new, enter will keep old one): + Current Notes: Never Expires + New Notes: + Editing/deleting custom fields + Field Name: Domain + New Field Name (Enter to keep, "x" to delete): x + Field Name: Type + New Field Name (Enter to keep, "x" to delete): Token Type + Field Value: Auth Token + New Field Value (Enter to keep): + Do you want to add custom fields [y/N]: + Created 1 custom entries for entry: 21. + Updated entry. + + $ varuh -l 6 -s + ID: 6 + Title: Github token + User: mydev + URL: https://github.com/mydev/myproject + Password: ghp_ipQrStuVwxYz1a2b3cdEF10ghI689kLaMnOp + Notes: Never Expires + Token Type: Auth Token + Modified: 2021-21-13 00:16:41 + (*-s* turns on visible passwords) ## Clone an entry @@ -198,7 +266,7 @@ For more on listing see the [Listing and Searching](#listing-and-searching) sect To clone (copy) an entry, $ $ varuh -C 1 - Cloned to new entry, id: 2 + Cloned to new entry, id: 3 ## Remove an entry @@ -207,8 +275,8 @@ To clone (copy) an entry, It is an error if the id does not exist. - $ varuh -R 3 - No entry with id 3 was found + $ varuh -R 4 + No entry with id 4 was found ## Switch to a new database @@ -260,7 +328,7 @@ Manually decrypt the database using `-d` option. Now the database is active again and you can see the listings. - $ varuh -l 2 + $ varuh -l 3 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ID: 2 Title: My Blog Login diff --git a/actions.go b/actions.go index e6db91c..e413c78 100644 --- a/actions.go +++ b/actions.go @@ -15,6 +15,11 @@ import ( "syscall" ) +type CustomEntry struct { + fieldName string + fieldValue string +} + // Wrappers (closures) for functions accepting strings as input for in/out encryption func WrapperMaxKryptStringFunc(fn actionFunc) actionFunc { @@ -219,6 +224,7 @@ func addNewEntry() error { var notes string var passwd string var err error + var customEntries []CustomEntry if err = checkActiveDatabase(); err != nil { return err @@ -260,8 +266,10 @@ func addNewEntry() error { return errors.New("invalid input") } + customEntries = addCustomFields(reader) + // Trim spaces - err = addNewDatabaseEntry(title, userName, url, passwd, notes) + err = addNewDatabaseEntry(title, userName, url, passwd, notes, customEntries) if err != nil { fmt.Printf("Error adding entry - \"%s\"\n", err.Error()) @@ -270,6 +278,86 @@ func addNewEntry() error { return err } +// Function to update existing custom entries and add new ones +// The bool part of the return value indicates whether to take action +func addOrUpdateCustomFields(reader *bufio.Reader, entry *Entry) ([]CustomEntry, bool) { + + var customEntries []ExtendedEntry + var editedCustomEntries []CustomEntry + var newCustomEntries []CustomEntry + var flag bool + + customEntries = getExtendedEntries(entry) + + if len(customEntries) > 0 { + + fmt.Println("Editing/deleting custom fields") + for _, customEntry := range customEntries { + var fieldName string + var fieldValue string + + fmt.Println("Field Name: " + customEntry.FieldName) + fieldName = readInput(reader, "\tNew Field Name (Enter to keep, \"x\" to delete)") + if strings.ToLower(strings.TrimSpace(fieldName)) == "x" { + fmt.Println("Deleting field " + fieldName) + } else { + if strings.TrimSpace(fieldName) == "" { + fieldName = customEntry.FieldName + } + + fmt.Println("Field Value: " + customEntry.FieldValue) + fieldValue = readInput(reader, "\tNew Field Value (Enter to keep)") + if strings.TrimSpace(fieldValue) == "" { + fieldValue = customEntry.FieldValue + } + + editedCustomEntries = append(editedCustomEntries, CustomEntry{fieldName, fieldValue}) + } + } + } + + newCustomEntries = addCustomFields(reader) + + editedCustomEntries = append(editedCustomEntries, newCustomEntries...) + + // Cases where length == 0 + // 1. Existing entries - all deleted + flag = len(customEntries) > 0 || len(editedCustomEntries) > 0 + + return editedCustomEntries, flag +} + +// Function to add custom fields to an entry +func addCustomFields(reader *bufio.Reader) []CustomEntry { + + // Custom fields + var custom string + var customEntries []CustomEntry + + custom = readInput(reader, "Do you want to add custom fields [y/N]") + if strings.ToLower(custom) == "y" { + + fmt.Println("Keep entering custom field name followed by the value. Press return with no input once done.") + for true { + var customFieldName string + var customFieldValue string + + customFieldName = strings.TrimSpace(readInput(reader, "Field Name")) + if customFieldName != "" { + customFieldValue = strings.TrimSpace(readInput(reader, "Value for "+customFieldName)) + } + + if customFieldName == "" && customFieldValue == "" { + break + } + + customEntries = append(customEntries, CustomEntry{customFieldName, customFieldValue}) + } + } + + return customEntries +} + // Edit a current entry by id func editCurrentEntry(idString string) error { @@ -322,8 +410,10 @@ func editCurrentEntry(idString string) error { fmt.Printf("\nCurrent Notes: %s\n", entry.Notes) notes = readInput(reader, "New Notes") + customEntries, flag := addOrUpdateCustomFields(reader, entry) + // Update - err = updateDatabaseEntry(entry, title, userName, url, passwd, notes) + err = updateDatabaseEntry(entry, title, userName, url, passwd, notes, customEntries, flag) if err != nil { fmt.Printf("Error updating entry - \"%s\"\n", err.Error()) } diff --git a/db.go b/db.go index 9992591..423085f 100644 --- a/db.go +++ b/db.go @@ -29,6 +29,21 @@ func (e *Entry) TableName() string { return "entries" } +// Structure representing an extended entry in the db - for custom fields +type ExtendedEntry struct { + ID int `gorm:"column:id;autoIncrement;primaryKey"` + FieldName string `gorm:"column:field_name"` + FieldValue string `gorm:"column:field_value"` + Timestamp time.Time `gorm:"type:timestamp;default:(datetime('now','localtime'))"` // sqlite3 + + Entry Entry `gorm:"foreignKey:EntryID"` + EntryID int +} + +func (ex *ExtendedEntry) TableName() string { + return "exentries" +} + // Clone an entry func (e1 *Entry) Copy(e2 *Entry) { @@ -55,6 +70,11 @@ func createNewEntry(db *gorm.DB) error { return db.AutoMigrate(&Entry{}) } +// Create a new table for Extended Entries in the database +func createNewExEntry(db *gorm.DB) error { + return db.AutoMigrate(&ExtendedEntry{}) +} + // Init new database including tables func initNewDatabase(dbPath string) error { @@ -94,6 +114,12 @@ func initNewDatabase(dbPath string) error { return err } + err = createNewExEntry(db) + if err != nil { + fmt.Printf("Error creating schema - \"%s\"\n", err.Error()) + return err + } + fmt.Printf("Created new database - %s\n", dbPath) // Update config @@ -133,8 +159,67 @@ func openActiveDatabase() (error, *gorm.DB) { return nil, db } +// Add custom entries to a database entry +func addCustomEntries(db *gorm.DB, entry *Entry, customEntries []CustomEntry) error { + + var count int + var err error + + err = createNewExEntry(db) + if err != nil { + fmt.Printf("Error creating schema - \"%s\"\n", err.Error()) + return err + } + + for _, customEntry := range customEntries { + var exEntry ExtendedEntry + + exEntry = ExtendedEntry{FieldName: customEntry.fieldName, FieldValue: customEntry.fieldValue, + EntryID: entry.ID} + + resultEx := db.Create(&exEntry) + if resultEx.Error == nil && resultEx.RowsAffected == 1 { + count += 1 + } + } + + fmt.Printf("Created %d custom entries for entry: %d.\n", count, entry.ID) + return nil +} + +// Replace custom entries to a database entry (Drop existing and add fresh) +func replaceCustomEntries(db *gorm.DB, entry *Entry, updatedEntries []CustomEntry) error { + + var count int + var err error + var customEntries []ExtendedEntry + + err = createNewExEntry(db) + if err != nil { + fmt.Printf("Error creating schema - \"%s\"\n", err.Error()) + return err + } + + db.Where("entry_id = ?", entry.ID).Delete(&customEntries) + + for _, customEntry := range updatedEntries { + var exEntry ExtendedEntry + + exEntry = ExtendedEntry{FieldName: customEntry.fieldName, FieldValue: customEntry.fieldValue, + EntryID: entry.ID} + + resultEx := db.Create(&exEntry) + if resultEx.Error == nil && resultEx.RowsAffected == 1 { + count += 1 + } + } + + fmt.Printf("Created %d custom entries for entry: %d.\n", count, entry.ID) + return nil +} + // Add a new entry to current database -func addNewDatabaseEntry(title, userName, url, passwd, notes string) error { +func addNewDatabaseEntry(title, userName, url, passwd, notes string, customEntries []CustomEntry) error { var entry Entry var err error @@ -147,7 +232,11 @@ func addNewDatabaseEntry(title, userName, url, passwd, notes string) error { // result := db.Debug().Create(&entry) result := db.Create(&entry) if result.Error == nil && result.RowsAffected == 1 { + // Add custom fields if given fmt.Printf("Created new entry with id: %d.\n", entry.ID) + if len(customEntries) > 0 { + return addCustomEntries(db, &entry, customEntries) + } return nil } else if result.Error != nil { return result.Error @@ -158,7 +247,7 @@ func addNewDatabaseEntry(title, userName, url, passwd, notes string) error { } // Update current database entry with new values -func updateDatabaseEntry(entry *Entry, title, userName, url, passwd, notes string) error { +func updateDatabaseEntry(entry *Entry, title, userName, url, passwd, notes string, customEntries []CustomEntry, flag bool) error { var updateMap map[string]interface{} @@ -172,7 +261,7 @@ func updateDatabaseEntry(entry *Entry, title, userName, url, passwd, notes strin } } - if len(updateMap) == 0 { + if len(updateMap) == 0 && !flag { fmt.Printf("Nothing to update\n") return nil } @@ -188,6 +277,9 @@ func updateDatabaseEntry(entry *Entry, title, userName, url, passwd, notes strin return result.Error } + if flag { + replaceCustomEntries(db, entry, customEntries) + } fmt.Println("Updated entry.") return nil } @@ -354,3 +446,19 @@ func entriesToStringArray(skipLongFields bool) (error, [][]string) { return err, dataArray } + +// Get extended entries associated to an entry +func getExtendedEntries(entry *Entry) []ExtendedEntry { + + var err error + var db *gorm.DB + var customEntries []ExtendedEntry + + err, db = openActiveDatabase() + + if err == nil && db != nil { + db.Where("entry_id = ?", entry.ID).Find(&customEntries) + } + + return customEntries +} diff --git a/main.go b/main.go index 6f90f87..0df1d8f 100644 --- a/main.go +++ b/main.go @@ -185,7 +185,7 @@ func initializeCmdLine(parser *argparse.Parser) map[string]interface{} { {"A", "add", "Add a new entry", "", ""}, {"p", "path", "Show current database path", "", ""}, {"a", "list-all", "List all entries in current database", "", ""}, - {"g", "genpass", "Generate a strong password of length from 12 - 16", "", ""}, + {"g", "genpass", "Generate a strong password (length: 12 - 16)", "", ""}, {"s", "show", "Show passwords when listing entries", "", ""}, {"c", "copy", "Copy password to clipboard", "", ""}, {"v", "version", "Show version information and exit", "", ""}, diff --git a/utils.go b/utils.go index dab41f7..440cad3 100644 --- a/utils.go +++ b/utils.go @@ -283,6 +283,7 @@ func printEntry(entry *Entry, delim bool) error { var err error var settings *Settings + var customEntries []ExtendedEntry err, settings = getOrCreateLocalConfig(APP) @@ -316,7 +317,17 @@ func printEntry(entry *Entry, delim bool) error { fmt.Printf("Password: %s\n", strings.Join(asterisks, "")) } fmt.Printf("Notes: %s\n", entry.Notes) + // Query extended entries + customEntries = getExtendedEntries(entry) + + if len(customEntries) > 0 { + for _, customEntry := range customEntries { + fmt.Printf("%s: %s\n", customEntry.FieldName, customEntry.FieldValue) + } + } + fmt.Printf("Modified: %s\n", entry.Timestamp.Format("2006-06-02 15:04:05")) + printDelim(settings.Delim, settings.Color) // Reset