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

DigitalTolk / ex / 24964032432

26 Apr 2026 06:35PM UTC coverage: 90.069% (-0.3%) from 90.372%
24964032432

Pull #13

github

web-flow
Merge 391ad3cab into c7a0902b0
Pull Request #13: Docker fixes

1506 of 1799 branches covered (83.71%)

Branch coverage included in aggregate %.

955 of 1086 new or added lines in 34 files covered. (87.94%)

7935 of 8683 relevant lines covered (91.39%)

18.33 hits per line

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

77.46
/internal/handler/router.go
1
package handler
2

3
import (
4
        "io/fs"
5
        "net/http"
6
        "strings"
7

8
        "github.com/DigitalTolk/ex/internal/auth"
9
        "github.com/DigitalTolk/ex/internal/middleware"
10
)
11

12
// NewRouter builds the application HTTP handler, registering all routes.
13
//
14
// frontendFS should be the frontend/dist subtree (already sub-rooted); pass nil
15
// to disable the embedded SPA.
16
func NewRouter(
17
        authH *AuthHandler,
18
        userH *UserHandler,
19
        channelH *ChannelHandler,
20
        convH *ConversationHandler,
21
        wsH *WSHandler,
22
        uploadH *UploadHandler,
23
        emojiH *EmojiHandler,
24
        presenceH *PresenceHandler,
25
        attachmentH *AttachmentHandler,
26
        adminH *AdminHandler,
27
        threadH *ThreadHandler,
28
        versionH *VersionHandler,
29
        sidebarH *SidebarHandler,
30
        jwtMgr *auth.JWTManager,
31
        frontendFS fs.FS,
32
        allowOrigin string,
33
) http.Handler {
4✔
34
        mux := http.NewServeMux()
4✔
35

4✔
36
        authMW := middleware.Auth(jwtMgr)
4✔
37

4✔
38
        // ------------------------------------------------------------------ Health
4✔
39
        mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
6✔
40
                writeJSON(w, http.StatusOK, JSON{"status": "ok"})
2✔
41
        })
2✔
42

43
        // ------------------------------------------------------------------ Version
44
        // Public — the frontend polls this to detect deploys (the JS bundle
45
        // pins the version it shipped with; mismatch → reload banner).
46
        if versionH != nil {
4✔
NEW
47
                mux.HandleFunc("GET /api/v1/version", versionH.Get)
×
NEW
48
        }
×
49

50
        // ------------------------------------------------------------------ Auth (public)
51
        mux.HandleFunc("GET /auth/oidc/login", authH.OIDCLogin)
4✔
52
        mux.HandleFunc("GET /auth/oidc/callback", authH.OIDCCallback)
4✔
53
        mux.HandleFunc("POST /auth/token/refresh", authH.RefreshToken)
4✔
54
        mux.HandleFunc("POST /auth/logout", authH.Logout)
4✔
55
        mux.HandleFunc("POST /auth/invite/accept", authH.AcceptInvite)
4✔
56
        mux.HandleFunc("POST /auth/login", authH.GuestLogin)
4✔
57

4✔
58
        // ------------------------------------------------------------------ Auth (protected)
4✔
59
        mux.Handle("POST /auth/invite", middleware.WrapFunc(authH.CreateInvite, authMW))
4✔
60

4✔
61
        // ------------------------------------------------------------------ Users
4✔
62
        mux.Handle("GET /api/v1/users/me", middleware.WrapFunc(userH.GetMe, authMW))
4✔
63
        mux.Handle("PATCH /api/v1/users/me", middleware.WrapFunc(userH.UpdateMe, authMW))
4✔
64
        mux.Handle("POST /api/v1/users/me/avatar/upload-url", middleware.WrapFunc(userH.CreateAvatarUploadURL, authMW))
4✔
65
        mux.Handle("POST /api/v1/users/batch", middleware.WrapFunc(userH.BatchGetUsers, authMW))
4✔
66
        mux.Handle("GET /api/v1/users/{id}", middleware.WrapFunc(userH.GetUser, authMW))
4✔
67
        mux.Handle("PATCH /api/v1/users/{id}/role", middleware.WrapFunc(userH.UpdateUserRole, authMW))
4✔
68
        mux.Handle("PATCH /api/v1/users/{id}/status", middleware.WrapFunc(userH.SetUserStatus, authMW))
4✔
69
        mux.Handle("GET /api/v1/users", middleware.WrapFunc(userH.ListUsers, authMW))
4✔
70

4✔
71
        // ------------------------------------------------------------------ Channels
4✔
72
        mux.Handle("POST /api/v1/channels", middleware.WrapFunc(channelH.Create, authMW))
4✔
73
        mux.Handle("GET /api/v1/channels", middleware.WrapFunc(channelH.List, authMW))
4✔
74
        mux.Handle("GET /api/v1/channels/browse", middleware.WrapFunc(channelH.BrowsePublic, authMW))
4✔
75
        mux.Handle("GET /api/v1/channels/{id}", middleware.WrapFunc(channelH.Get, authMW))
4✔
76
        mux.Handle("PATCH /api/v1/channels/{id}", middleware.WrapFunc(channelH.Update, authMW))
4✔
77
        mux.Handle("DELETE /api/v1/channels/{id}", middleware.WrapFunc(channelH.Archive, authMW))
4✔
78

4✔
79
        mux.Handle("POST /api/v1/channels/{id}/join", middleware.WrapFunc(channelH.Join, authMW))
4✔
80
        mux.Handle("POST /api/v1/channels/{id}/leave", middleware.WrapFunc(channelH.Leave, authMW))
4✔
81
        mux.Handle("PUT /api/v1/channels/{id}/mute", middleware.WrapFunc(channelH.SetMute, authMW))
4✔
82

4✔
83
        mux.Handle("GET /api/v1/channels/{id}/members", middleware.WrapFunc(channelH.ListMembers, authMW))
4✔
84
        mux.Handle("POST /api/v1/channels/{id}/members", middleware.WrapFunc(channelH.AddMember, authMW))
4✔
85
        mux.Handle("DELETE /api/v1/channels/{id}/members/{uid}", middleware.WrapFunc(channelH.RemoveMember, authMW))
4✔
86
        mux.Handle("PATCH /api/v1/channels/{id}/members/{uid}", middleware.WrapFunc(channelH.UpdateMemberRole, authMW))
4✔
87

4✔
88
        mux.Handle("GET /api/v1/channels/{id}/messages", middleware.WrapFunc(channelH.ListMessages, authMW))
4✔
89
        mux.Handle("POST /api/v1/channels/{id}/messages", middleware.WrapFunc(channelH.SendMessage, authMW))
4✔
90
        mux.Handle("PATCH /api/v1/channels/{id}/messages/{msgId}", middleware.WrapFunc(channelH.EditMessage, authMW))
4✔
91
        mux.Handle("DELETE /api/v1/channels/{id}/messages/{msgId}", middleware.WrapFunc(channelH.DeleteMessage, authMW))
4✔
92
        mux.Handle("GET /api/v1/channels/{id}/messages/{msgId}/thread", middleware.WrapFunc(channelH.GetThread, authMW))
4✔
93
        mux.Handle("POST /api/v1/channels/{id}/messages/{msgId}/reactions", middleware.WrapFunc(channelH.ToggleReaction, authMW))
4✔
94
        mux.Handle("PUT /api/v1/channels/{id}/messages/{msgId}/pinned", middleware.WrapFunc(channelH.SetPinned, authMW))
4✔
95
        mux.Handle("GET /api/v1/channels/{id}/pinned", middleware.WrapFunc(channelH.ListPinned, authMW))
4✔
96

4✔
97
        // ------------------------------------------------------------------ Conversations
4✔
98
        mux.Handle("POST /api/v1/conversations", middleware.WrapFunc(convH.Create, authMW))
4✔
99
        mux.Handle("GET /api/v1/conversations", middleware.WrapFunc(convH.List, authMW))
4✔
100
        mux.Handle("GET /api/v1/conversations/{id}", middleware.WrapFunc(convH.Get, authMW))
4✔
101

4✔
102
        mux.Handle("GET /api/v1/conversations/{id}/messages", middleware.WrapFunc(convH.ListMessages, authMW))
4✔
103
        mux.Handle("POST /api/v1/conversations/{id}/messages", middleware.WrapFunc(convH.SendMessage, authMW))
4✔
104
        mux.Handle("PATCH /api/v1/conversations/{id}/messages/{msgId}", middleware.WrapFunc(convH.EditMessage, authMW))
4✔
105
        mux.Handle("DELETE /api/v1/conversations/{id}/messages/{msgId}", middleware.WrapFunc(convH.DeleteMessage, authMW))
4✔
106
        mux.Handle("GET /api/v1/conversations/{id}/messages/{msgId}/thread", middleware.WrapFunc(convH.GetThread, authMW))
4✔
107
        mux.Handle("POST /api/v1/conversations/{id}/messages/{msgId}/reactions", middleware.WrapFunc(convH.ToggleReaction, authMW))
4✔
108
        mux.Handle("PUT /api/v1/conversations/{id}/messages/{msgId}/pinned", middleware.WrapFunc(convH.SetPinned, authMW))
4✔
109
        mux.Handle("GET /api/v1/conversations/{id}/pinned", middleware.WrapFunc(convH.ListPinned, authMW))
4✔
110

4✔
111
        // ------------------------------------------------------------------ Threads (cross-parent)
4✔
112
        if threadH != nil {
4✔
113
                mux.Handle("GET /api/v1/threads", middleware.WrapFunc(threadH.List, authMW))
×
114
        }
×
115

116
        // ------------------------------------------------------------------ Sidebar (per-user)
117
        if sidebarH != nil {
4✔
NEW
118
                mux.Handle("PUT /api/v1/channels/{id}/favorite", middleware.WrapFunc(sidebarH.SetFavorite, authMW))
×
NEW
119
                mux.Handle("PUT /api/v1/channels/{id}/category", middleware.WrapFunc(sidebarH.SetCategory, authMW))
×
NEW
120
                mux.Handle("PUT /api/v1/conversations/{id}/favorite", middleware.WrapFunc(sidebarH.SetConversationFavorite, authMW))
×
NEW
121
                mux.Handle("PUT /api/v1/conversations/{id}/category", middleware.WrapFunc(sidebarH.SetConversationCategory, authMW))
×
NEW
122
                mux.Handle("GET /api/v1/sidebar/categories", middleware.WrapFunc(sidebarH.ListCategories, authMW))
×
NEW
123
                mux.Handle("POST /api/v1/sidebar/categories", middleware.WrapFunc(sidebarH.CreateCategory, authMW))
×
NEW
124
                mux.Handle("PATCH /api/v1/sidebar/categories/{id}", middleware.WrapFunc(sidebarH.UpdateCategory, authMW))
×
NEW
125
                mux.Handle("DELETE /api/v1/sidebar/categories/{id}", middleware.WrapFunc(sidebarH.DeleteCategory, authMW))
×
NEW
126
        }
×
127

128
        // ------------------------------------------------------------------ Uploads
129
        if uploadH != nil {
4✔
130
                mux.Handle("POST /api/v1/uploads/url", middleware.WrapFunc(uploadH.CreateUploadURL, authMW))
×
131
        }
×
132

133
        // ------------------------------------------------------------------ Attachments
134
        if attachmentH != nil {
4✔
135
                mux.Handle("POST /api/v1/attachments/url", middleware.WrapFunc(attachmentH.CreateUploadURL, authMW))
×
136
                mux.Handle("GET /api/v1/attachments", middleware.WrapFunc(attachmentH.List, authMW))
×
137
                mux.Handle("GET /api/v1/attachments/{id}", middleware.WrapFunc(attachmentH.Get, authMW))
×
138
                mux.Handle("DELETE /api/v1/attachments/{id}", middleware.WrapFunc(attachmentH.Delete, authMW))
×
139
        }
×
140

141
        // ------------------------------------------------------------------ Custom emojis
142
        if emojiH != nil {
4✔
143
                mux.Handle("GET /api/v1/emojis", middleware.WrapFunc(emojiH.List, authMW))
×
144
                mux.Handle("POST /api/v1/emojis", middleware.WrapFunc(emojiH.Create, authMW))
×
145
                mux.Handle("DELETE /api/v1/emojis/{name}", middleware.WrapFunc(emojiH.Delete, authMW))
×
146
        }
×
147

148
        // ------------------------------------------------------------------ Presence
149
        if presenceH != nil {
4✔
150
                mux.Handle("GET /api/v1/presence", middleware.WrapFunc(presenceH.List, authMW))
×
151
        }
×
152

153
        // ------------------------------------------------------------------ Admin / settings
154
        if adminH != nil {
4✔
155
                // GET is open to any authenticated user — the upload UI shows
×
156
                // the current limits before posting. PUT enforces admin-only
×
157
                // inside the handler.
×
158
                mux.Handle("GET /api/v1/admin/settings", middleware.WrapFunc(adminH.GetSettings, authMW))
×
159
                mux.Handle("PUT /api/v1/admin/settings", middleware.WrapFunc(adminH.UpdateSettings, authMW))
×
160
        }
×
161

162
        // ------------------------------------------------------------------ WebSocket
163
        mux.Handle("GET /api/v1/ws", middleware.WrapFunc(wsH.Connect, authMW))
4✔
164

4✔
165
        // ------------------------------------------------------------------ SPA
4✔
166
        if frontendFS != nil {
5✔
167
                spa := spaHandler{fs: http.FS(frontendFS), fileServer: http.FileServer(http.FS(frontendFS))}
1✔
168
                mux.Handle("/", &spa)
1✔
169
        }
1✔
170

171
        // Apply global middleware: CORS, RequestID, Logging.
172
        handler := middleware.Wrap(mux,
4✔
173
                middleware.CORS(allowOrigin),
4✔
174
                middleware.RequestID,
4✔
175
                middleware.Logging,
4✔
176
        )
4✔
177

4✔
178
        return handler
4✔
179
}
180

181
// spaHandler serves static files from the embedded filesystem and falls back
182
// to index.html for client-side routing.
183
type spaHandler struct {
184
        fs         http.FileSystem
185
        fileServer http.Handler
186
}
187

188
func (h *spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
7✔
189
        // Don't serve SPA for API or auth routes.
7✔
190
        if strings.HasPrefix(r.URL.Path, "/api/") || strings.HasPrefix(r.URL.Path, "/auth/") {
10✔
191
                http.NotFound(w, r)
3✔
192
                return
3✔
193
        }
3✔
194

195
        // Try to serve the file directly.
196
        path := r.URL.Path
4✔
197
        if path == "/" {
5✔
198
                path = "/index.html"
1✔
199
        }
1✔
200

201
        _, err := h.fs.Open(strings.TrimPrefix(path, "/"))
4✔
202
        if err != nil {
6✔
203
                // File not found: serve index.html for SPA client-side routing.
2✔
204
                r.URL.Path = "/"
2✔
205
                h.fileServer.ServeHTTP(w, r)
2✔
206
                return
2✔
207
        }
2✔
208

209
        h.fileServer.ServeHTTP(w, r)
2✔
210
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc