• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gameap / gameap / 25960901307

16 May 2026 11:29AM UTC coverage: 76.887% (+0.2%) from 76.64%
25960901307

push

github

et-nik
audit logs tests

45395 of 59041 relevant lines covered (76.89%)

33895.16 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.24
/internal/api/filemanager/delete/handler.go
1
package deletefiles
2

3
import (
4
        "context"
5
        "encoding/json"
6
        "log/slog"
7
        "net/http"
8
        "path/filepath"
9
        "strconv"
10
        "strings"
11

12
        "github.com/gameap/gameap/internal/api/base"
13
        serversbase "github.com/gameap/gameap/internal/api/servers/base"
14
        "github.com/gameap/gameap/internal/audit"
15
        "github.com/gameap/gameap/internal/domain"
16
        "github.com/gameap/gameap/internal/filters"
17
        "github.com/gameap/gameap/internal/repositories"
18
        "github.com/gameap/gameap/pkg/api"
19
        "github.com/gameap/gameap/pkg/auth"
20
        "github.com/pkg/errors"
21
)
22

23
type fileService interface {
24
        Remove(ctx context.Context, node *domain.Node, path string, recursive bool) error
25
}
26

27
type Handler struct {
28
        serverFinder   *serversbase.ServerFinder
29
        abilityChecker *serversbase.AbilityChecker
30
        nodeRepo       repositories.NodeRepository
31
        daemonFiles    fileService
32
        responder      base.Responder
33
        audit          audit.Logger
34
}
35

36
func NewHandler(
37
        serverRepo repositories.ServerRepository,
38
        nodeRepo repositories.NodeRepository,
39
        rbac base.RBAC,
40
        daemonFiles fileService,
41
        responder base.Responder,
42
        auditLogger audit.Logger,
43
) *Handler {
17✔
44
        if auditLogger == nil {
32✔
45
                auditLogger = audit.NopLogger{}
15✔
46
        }
15✔
47

48
        return &Handler{
17✔
49
                serverFinder:   serversbase.NewServerFinder(serverRepo, rbac),
17✔
50
                abilityChecker: serversbase.NewAbilityChecker(rbac),
17✔
51
                nodeRepo:       nodeRepo,
17✔
52
                daemonFiles:    daemonFiles,
17✔
53
                responder:      responder,
17✔
54
                audit:          auditLogger,
17✔
55
        }
17✔
56
}
57

58
func (h *Handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
17✔
59
        ctx := r.Context()
17✔
60

17✔
61
        session := auth.SessionFromContext(ctx)
17✔
62
        if !session.IsAuthenticated() {
18✔
63
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
1✔
64
                        errors.New("user not authenticated"),
1✔
65
                        http.StatusUnauthorized,
1✔
66
                ))
1✔
67

1✔
68
                return
1✔
69
        }
1✔
70

71
        input := api.NewInputReader(r)
16✔
72

16✔
73
        serverID, err := input.ReadUint("server")
16✔
74
        if err != nil {
17✔
75
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
1✔
76
                        errors.WithMessage(err, "invalid server id"),
1✔
77
                        http.StatusBadRequest,
1✔
78
                ))
1✔
79

1✔
80
                return
1✔
81
        }
1✔
82

83
        server, err := h.serverFinder.FindUserServer(ctx, session.User, serverID)
15✔
84
        if err != nil {
18✔
85
                h.responder.WriteError(ctx, rw, err)
3✔
86

3✔
87
                return
3✔
88
        }
3✔
89

90
        err = h.abilityChecker.CheckOrError(
12✔
91
                ctx,
12✔
92
                session.User.ID,
12✔
93
                server.ID,
12✔
94
                []domain.AbilityName{domain.AbilityNameGameServerFiles},
12✔
95
        )
12✔
96
        if err != nil {
13✔
97
                h.responder.WriteError(ctx, rw, err)
1✔
98

1✔
99
                return
1✔
100
        }
1✔
101

102
        // Parse and validate request
103
        var req deleteRequest
11✔
104
        err = json.NewDecoder(r.Body).Decode(&req)
11✔
105
        if err != nil {
12✔
106
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(
1✔
107
                        errors.WithMessage(err, "invalid request body"),
1✔
108
                        http.StatusBadRequest,
1✔
109
                ))
1✔
110

1✔
111
                return
1✔
112
        }
1✔
113

114
        if err = h.validateRequest(&req); err != nil {
12✔
115
                h.responder.WriteError(ctx, rw, api.WrapHTTPError(err, http.StatusBadRequest))
2✔
116

2✔
117
                return
2✔
118
        }
2✔
119

120
        // Get node
121
        node, err := h.getNode(ctx, server.DSID)
8✔
122
        if err != nil {
9✔
123
                h.responder.WriteError(ctx, rw, err)
1✔
124

1✔
125
                return
1✔
126
        }
1✔
127

128
        // Process each item
129
        if err = h.processItems(ctx, node, server.Dir, req.Items); err != nil {
8✔
130
                h.responder.WriteError(ctx, rw, err)
1✔
131

1✔
132
                return
1✔
133
        }
1✔
134

135
        audit.SensitiveOp(ctx, h.audit, audit.EventFileDelete, audit.CategoryFileOp,
6✔
136
                "server", strconv.FormatUint(uint64(serverID), 10), "delete",
6✔
137
                slog.Int("items", len(req.Items)))
6✔
138

6✔
139
        h.responder.Write(ctx, rw, newDeleteResponse())
6✔
140
}
141

142
func (h *Handler) validateRequest(req *deleteRequest) error {
10✔
143
        if req.Disk != "server" {
11✔
144
                return errors.Errorf("unsupported disk: %s, only 'server' disk is supported", req.Disk)
1✔
145
        }
1✔
146

147
        if len(req.Items) == 0 {
10✔
148
                return errors.New("items array is empty")
1✔
149
        }
1✔
150

151
        return nil
8✔
152
}
153

154
func (h *Handler) getNode(ctx context.Context, nodeID uint) (*domain.Node, error) {
8✔
155
        nodes, err := h.nodeRepo.Find(ctx, &filters.FindNode{
8✔
156
                IDs: []uint{nodeID},
8✔
157
        }, nil, &filters.Pagination{
8✔
158
                Limit: 1,
8✔
159
        })
8✔
160
        if err != nil {
8✔
161
                return nil, errors.WithMessage(err, "failed to find node")
×
162
        }
×
163

164
        if len(nodes) == 0 {
9✔
165
                return nil, api.NewNotFoundError("node not found")
1✔
166
        }
1✔
167

168
        return &nodes[0], nil
7✔
169
}
170

171
func (h *Handler) processItems(
172
        ctx context.Context,
173
        node *domain.Node,
174
        serverDir string,
175
        items []deleteItem,
176
) error {
7✔
177
        for _, item := range items {
16✔
178
                if err := validatePath(item.Path); err != nil {
10✔
179
                        return api.WrapHTTPError(err, http.StatusBadRequest)
1✔
180
                }
1✔
181

182
                fullPath := filepath.Join(node.WorkPath, serverDir, item.Path)
8✔
183
                recursive := item.Type == "dir"
8✔
184

8✔
185
                err := h.daemonFiles.Remove(ctx, node, fullPath, recursive)
8✔
186
                if err != nil {
8✔
187
                        return errors.WithMessage(err, "failed to delete file or directory")
×
188
                }
×
189
        }
190

191
        return nil
6✔
192
}
193

194
func validatePath(path string) error {
21✔
195
        if strings.Contains(path, "..") {
28✔
196
                return errors.New("path contains invalid directory traversal")
7✔
197
        }
7✔
198

199
        cleanPath := filepath.Clean(path)
14✔
200
        if strings.HasPrefix(cleanPath, "..") {
14✔
201
                return errors.New("path attempts to escape base directory")
×
202
        }
×
203

204
        return nil
14✔
205
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc