diff --git a/api/internal/model/alias.go b/api/internal/model/alias.go index 50c4328f..7636d81d 100644 --- a/api/internal/model/alias.go +++ b/api/internal/model/alias.go @@ -13,7 +13,7 @@ var ( type Alias struct { BaseModel - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` Name string `gorm:"unique" json:"name"` UserID string `json:"-"` Enabled bool `json:"enabled"` diff --git a/api/internal/repository/alias.go b/api/internal/repository/alias.go index bb92a10e..05fc15d6 100644 --- a/api/internal/repository/alias.go +++ b/api/internal/repository/alias.go @@ -33,7 +33,7 @@ func (d *Database) GetAlias(ctx context.Context, ID string, userID string) (mode return alias, nil } -func (d *Database) GetAliases(ctx context.Context, userID string, limit int, offset int, sortBy string, sortOrder string, catchAll string, search string) ([]model.Alias, error) { +func (d *Database) GetAliases(ctx context.Context, userID string, limit int, offset int, sortBy string, sortOrder string, catchAll string, search string, status string) ([]model.Alias, error) { sortBy = "a." + sortBy if catchAll == "true" { @@ -48,6 +48,15 @@ func (d *Database) GetAliases(ctx context.Context, userID string, limit int, off search = "AND (a.name LIKE '%" + search + "%' OR a.description LIKE '%" + search + "%')" } + var statusFilter string + if status == "deleted" { + statusFilter = "AND a.deleted_at IS NOT NULL" + } else if status == "all" { + statusFilter = "" + } else { + statusFilter = "AND a.deleted_at IS NULL" + } + aliases := []model.Alias{} query := ` SELECT a.*, @@ -58,7 +67,7 @@ func (d *Database) GetAliases(ctx context.Context, userID string, limit int, off FROM aliases a LEFT JOIN messages m ON a.id = m.alias_id - WHERE a.user_id = ? AND a.deleted_at IS NULL ` + catchAll + " " + search + ` + WHERE a.user_id = ? ` + statusFilter + " " + catchAll + " " + search + ` GROUP BY a.id ORDER BY ` + sortBy + " " + sortOrder @@ -106,7 +115,7 @@ func (d *Database) GetAllAliases(ctx context.Context, userID string) ([]model.Al return aliases, err } -func (d *Database) GetAliasCount(ctx context.Context, userID string, catchAll string, search string) (int, error) { +func (d *Database) GetAliasCount(ctx context.Context, userID string, catchAll string, search string, status string) (int, error) { if catchAll == "true" { catchAll = " AND catch_all = true" } else if catchAll == "false" { @@ -120,7 +129,15 @@ func (d *Database) GetAliasCount(ctx context.Context, userID string, catchAll st } var count int64 - err := d.Client.Model(&model.Alias{}).Where("user_id = ?"+catchAll+search, userID).Count(&count).Error + q := d.Client.Model(&model.Alias{}) + if status == "deleted" { + q = q.Unscoped().Where("user_id = ? AND deleted_at IS NOT NULL"+catchAll+search, userID) + } else if status == "all" { + q = q.Unscoped().Where("user_id = ?"+catchAll+search, userID) + } else { + q = q.Where("user_id = ?"+catchAll+search, userID) + } + err := q.Count(&count).Error return int(count), err } @@ -160,3 +177,7 @@ func (d *Database) DeleteAliasByUserID(ctx context.Context, userID string) error func (d *Database) DeleteAliasByDomain(ctx context.Context, domain string, userID string) error { return d.Client.Where("name LIKE ? AND user_id = ?", "%@"+domain, userID).Delete(&model.Alias{}).Error } + +func (d *Database) RestoreAlias(ctx context.Context, ID string, userID string) error { + return d.Client.Model(&model.Alias{}).Unscoped().Where("id = ? AND user_id = ?", ID, userID).Update("deleted_at", nil).Error +} diff --git a/api/internal/service/alias.go b/api/internal/service/alias.go index aaba2c01..ef6d575f 100644 --- a/api/internal/service/alias.go +++ b/api/internal/service/alias.go @@ -27,10 +27,10 @@ var ( type AliasStore interface { GetAlias(context.Context, string, string) (model.Alias, error) - GetAliases(context.Context, string, int, int, string, string, string, string) ([]model.Alias, error) + GetAliases(context.Context, string, int, int, string, string, string, string, string) ([]model.Alias, error) GetAliasesByDomain(context.Context, string, string) ([]model.Alias, error) GetAllAliases(context.Context, string) ([]model.Alias, error) - GetAliasCount(context.Context, string, string, string) (int, error) + GetAliasCount(context.Context, string, string, string, string) (int, error) GetAliasDailyCount(context.Context, string) (int, error) GetAliasByName(string) (model.Alias, error) PostAlias(context.Context, model.Alias) (model.Alias, error) @@ -38,6 +38,7 @@ type AliasStore interface { DeleteAlias(context.Context, string, string) error DeleteAliasByUserID(context.Context, string) error DeleteAliasByDomain(context.Context, string, string) error + RestoreAlias(context.Context, string, string) error } // aliasDomainPart returns the domain portion of an alias name (e.g. "user@example.com" → "example.com"). @@ -83,19 +84,19 @@ func (s *Service) GetAlias(ctx context.Context, ID string, userID string) (model return alias, nil } -func (s *Service) GetAliases(ctx context.Context, userID string, limit int, page int, sortBy string, sortOrder string, catchAll string, search string) (model.AliasList, error) { +func (s *Service) GetAliases(ctx context.Context, userID string, limit int, page int, sortBy string, sortOrder string, catchAll string, search string, status string) (model.AliasList, error) { offset := (page - 1) * limit if page < 1 { offset = 0 } - aliases, err := s.Store.GetAliases(ctx, userID, limit, offset, sortBy, sortOrder, catchAll, search) + aliases, err := s.Store.GetAliases(ctx, userID, limit, offset, sortBy, sortOrder, catchAll, search, status) if err != nil { log.Printf("error fetching aliases: %s", err.Error()) return model.AliasList{}, ErrGetAliases } - total, err := s.Store.GetAliasCount(ctx, userID, catchAll, search) + total, err := s.Store.GetAliasCount(ctx, userID, catchAll, search, status) if err != nil { log.Printf("error fetching alias count: %s", err.Error()) return model.AliasList{}, ErrGetAliases @@ -180,7 +181,7 @@ func (s *Service) PostAlias(ctx context.Context, alias model.Alias, format strin // Catch-all alias if format == model.AliasFormatCatchAll { - userAliases, err := s.Store.GetAliases(ctx, alias.UserID, 0, 0, "created_at", "DESC", "true", "") + userAliases, err := s.Store.GetAliases(ctx, alias.UserID, 0, 0, "created_at", "DESC", "true", "", "active") if err != nil { log.Printf("error fetching user aliases: %s", err.Error()) return model.Alias{}, ErrPostAlias @@ -276,3 +277,13 @@ func (s *Service) FindAlias(email string) (model.Alias, error) { return alias, nil } + +func (s *Service) RestoreAlias(ctx context.Context, ID string, userID string) error { + err := s.Store.RestoreAlias(ctx, ID, userID) + if err != nil { + log.Printf("error restoring alias: %s", err.Error()) + return ErrGetAlias + } + + return nil +} diff --git a/api/internal/service/recipient.go b/api/internal/service/recipient.go index 9654d3e1..ca2660e3 100644 --- a/api/internal/service/recipient.go +++ b/api/internal/service/recipient.go @@ -203,7 +203,7 @@ func (s *Service) DeleteRecipient(ctx context.Context, ID string, userID string, } // Get aliases - aliases, err := s.Store.GetAliases(ctx, userID, 0, 0, "created_at", "DESC", "", "") + aliases, err := s.Store.GetAliases(ctx, userID, 0, 0, "created_at", "DESC", "", "", "active") if err != nil { log.Printf("error deleting recipient, GetAliases: %s", err.Error()) return ErrDeleteRecipient diff --git a/api/internal/transport/api/alias.go b/api/internal/transport/api/alias.go index a4127257..c7080232 100644 --- a/api/internal/transport/api/alias.go +++ b/api/internal/transport/api/alias.go @@ -12,20 +12,22 @@ import ( ) var ( - PostAliasSuccess = "Alias created successfully." - UpdateAliasSuccess = "Alias updated successfully." - DeleteAliasSuccess = "Alias deleted successfully." - ErrInvalidDomain = "Selected domain is invalid." - ErrUnverifiedRcp = "The recipient address has not been verified." + PostAliasSuccess = "Alias created successfully." + UpdateAliasSuccess = "Alias updated successfully." + DeleteAliasSuccess = "Alias deleted successfully." + ErrInvalidDomain = "Selected domain is invalid." + ErrUnverifiedRcp = "The recipient address has not been verified." + RestoreAliasSuccess = "Alias restored successfully." ) type AliasService interface { GetAlias(context.Context, string, string) (model.Alias, error) - GetAliases(context.Context, string, int, int, string, string, string, string) (model.AliasList, error) + GetAliases(context.Context, string, int, int, string, string, string, string, string) (model.AliasList, error) GetAllAliases(context.Context, string) ([]model.Alias, error) PostAlias(context.Context, model.Alias, string, string, string) (model.Alias, error) UpdateAlias(context.Context, model.Alias) error DeleteAlias(context.Context, string, string) error + RestoreAlias(context.Context, string, string) error } // @Summary Get alias @@ -57,6 +59,7 @@ func (h *Handler) GetAlias(c *fiber.Ctx) error { // @Accept json // @Produce json // @Security ApiKeyAuth +// @Param status query string false "Filter by alias status" Enums(active, deleted, all) // @Success 200 {object} model.AliasList // @Failure 400 {object} ErrorRes // @Router /aliases [get] @@ -78,6 +81,7 @@ func (h *Handler) GetAliases(c *fiber.Ctx) error { sortOrder := strings.ToUpper(c.Query("sort_order")) catchAll := c.Query("catch_all") search := c.Query("search") + status := c.Query("status") var allowSortBy = map[string]bool{ "created_at": true, @@ -93,6 +97,12 @@ func (h *Handler) GetAliases(c *fiber.Ctx) error { "false": true, "": true, } + var allowStatus = map[string]bool{ + "active": true, + "deleted": true, + "all": true, + "": true, + } if _, ok := allowSortBy[sortBy]; !ok { sortBy = "created_at" @@ -103,6 +113,12 @@ func (h *Handler) GetAliases(c *fiber.Ctx) error { if _, ok := allowCatchAll[catchAll]; !ok { catchAll = "" } + if _, ok := allowStatus[status]; !ok { + status = "" + } + if status == "" { + status = "active" + } err = h.Validator.Var(search, "omitempty,required,search") if err != nil { @@ -112,7 +128,7 @@ func (h *Handler) GetAliases(c *fiber.Ctx) error { }) } - list, err := h.Service.GetAliases(c.Context(), userID, limit, page, sortBy, sortOrder, catchAll, search) + list, err := h.Service.GetAliases(c.Context(), userID, limit, page, sortBy, sortOrder, catchAll, search, status) if err != nil { return c.Status(400).JSON(fiber.Map{ "error": err.Error(), @@ -331,3 +347,29 @@ func (h *Handler) DeleteAlias(c *fiber.Ctx) error { "message": DeleteAliasSuccess, }) } + +// @Summary Restore alias +// @Description Restore alias +// @Tags alias +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param id path string true "Alias ID" +// @Success 200 {object} SuccessRes +// @Failure 400 {object} ErrorRes +// @Router /alias/restore/{id} [post] +// @Router /api/alias/restore/{id} [post] +func (h *Handler) RestoreAlias(c *fiber.Ctx) error { + userID := auth.GetUserID(c) + id := c.Params("id") + err := h.Service.RestoreAlias(c.Context(), id, userID) + if err != nil { + return c.Status(400).JSON(fiber.Map{ + "error": err.Error(), + }) + } + + return c.Status(200).JSON(fiber.Map{ + "message": RestoreAliasSuccess, + }) +} diff --git a/api/internal/transport/api/routes.go b/api/internal/transport/api/routes.go index 416dc7d5..a7800a8f 100644 --- a/api/internal/transport/api/routes.go +++ b/api/internal/transport/api/routes.go @@ -92,6 +92,7 @@ func (h *Handler) SetupRoutes(cfg config.APIConfig) { v1.Post("/alias", limiter.New(), h.PostAlias) v1.Put("/alias/:id", h.UpdateAlias) v1.Delete("/alias/:id", h.DeleteAlias) + v1.Post("/alias/restore/:id", h.RestoreAlias) v1.Get("/logs", h.GetLogs) v1.Delete("/logs", h.DeleteLogs) diff --git a/app/src/api/alias.ts b/app/src/api/alias.ts index 564414b0..c237c770 100644 --- a/app/src/api/alias.ts +++ b/app/src/api/alias.ts @@ -7,4 +7,5 @@ export const aliasApi = { create: (data: any) => api.post('/alias', data), update: (id: string, data: any) => api.put('/alias/' + id, data), delete: (id: string) => api.delete('/alias/' + id), + restore: (id: string) => api.post('/alias/restore/' + id), } \ No newline at end of file diff --git a/app/src/components/AliasRow.vue b/app/src/components/AliasRow.vue index 968562d4..9cb3291a 100644 --- a/app/src/components/AliasRow.vue +++ b/app/src/components/AliasRow.vue @@ -4,11 +4,14 @@