feat: add /search?q= endpoint — substring search across all vault fields
This commit is contained in:
1 parent
30ae00d5f4
commit
51a71739c9
3 files changed
+90
No files matched your search
@@ -126,6 +126,7 @@ func main() {
|
||||
api.Put("/secret/:name", h.UpdateSecret)
|
||||
api.Delete("/secret/:name", h.DeleteSecret)
|
||||
api.Get("/folders", h.ListFolders)
|
||||
api.Get("/search", h.SearchSecrets)
|
||||
api.Post("/refresh", h.RefreshCache)
|
||||
|
||||
// Graceful shutdown.
|
||||
|
||||
@@ -271,6 +271,27 @@ func (h *Handler) RefreshCache(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
// SearchSecrets handles GET /search?q=<text>.
|
||||
// Returns every vault item matching the query across ALL fields (name, username,
|
||||
// uri, notes, password, custom fields). Values are masked in the listing.
|
||||
// Requires the q parameter; empty query returns an empty list.
|
||||
func (h *Handler) SearchSecrets(c *fiber.Ctx) error {
|
||||
q := strings.TrimSpace(c.Query("q"))
|
||||
if q == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "query parameter 'q' is required",
|
||||
})
|
||||
}
|
||||
|
||||
results := h.vaultClient.SearchSecrets(q)
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"query": q,
|
||||
"count": len(results),
|
||||
"results": results,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateSecret handles POST /secret. Body (JSON):
|
||||
//
|
||||
// {
|
||||
|
||||
@@ -173,6 +173,74 @@ func (c *Client) getSecretItem(name string, filter SecretFilter) (DecryptedItem,
|
||||
return DecryptedItem{}, fmt.Errorf("secret not found")
|
||||
}
|
||||
|
||||
// SearchSecrets returns every decrypted item whose name, username, uri, notes,
|
||||
// password, or any custom field contains the query string (case-insensitive).
|
||||
// It is a substring search across ALL fields — unlike GetSecret which is limited
|
||||
// to the item name. Values are masked so the list can be returned safely.
|
||||
func (c *Client) SearchSecrets(query string) []map[string]interface{} {
|
||||
q := strings.ToLower(strings.TrimSpace(query))
|
||||
if q == "" {
|
||||
return []map[string]interface{}{}
|
||||
}
|
||||
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
results := make([]map[string]interface{}, 0)
|
||||
for _, item := range c.items {
|
||||
if matchesAllFields(item, q) {
|
||||
m := item.ToMap()
|
||||
// Mask sensitive values in the listing.
|
||||
if v, ok := m["password"].(string); ok && v != "" {
|
||||
m["password"] = maskSecret(v)
|
||||
}
|
||||
if v, ok := m["value"].(string); ok && v != "" {
|
||||
m["value"] = maskSecret(v)
|
||||
}
|
||||
if v, ok := m["username"].(string); ok && v != "" {
|
||||
m["username"] = maskSecret(v)
|
||||
}
|
||||
results = append(results, m)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// matchesAllFields checks the query against every decryptable field of an item.
|
||||
func matchesAllFields(item DecryptedItem, q string) bool {
|
||||
if strings.Contains(strings.ToLower(item.Name), q) {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(strings.ToLower(item.Username), q) {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(strings.ToLower(item.URI), q) {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(strings.ToLower(item.Notes), q) {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(strings.ToLower(item.Password), q) {
|
||||
return true
|
||||
}
|
||||
for k, v := range item.Fields {
|
||||
if strings.Contains(strings.ToLower(k), q) ||
|
||||
strings.Contains(strings.ToLower(v), q) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// maskSecret reveals only the first 2 and last 2 characters of a value.
|
||||
func maskSecret(v string) string {
|
||||
r := []rune(v)
|
||||
if len(r) <= 4 {
|
||||
return "***"
|
||||
}
|
||||
return string(r[:2]) + "***" + string(r[len(r)-2:])
|
||||
}
|
||||
|
||||
// ToMap renders the decrypted item as a flat map for JSON output.
|
||||
// The "value" key holds the same primary value extractSecret() would return,
|
||||
// while the other keys expose every decrypted field.
|
||||
|
||||
Reference in new issue
Block a user