Skip to content

Commit 8572313

Browse files
committed
overlord: use a better reponse format
Modify the http server to respond in the following format: { "status": "success" | "error" "data": payload | "erorr_str" }
1 parent 61b88f7 commit 8572313

File tree

17 files changed

+253
-258
lines changed

17 files changed

+253
-258
lines changed

app-ios/app-ios/Extensions/URLSession+Auth.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,18 @@ extension URLSession {
3131

3232
// Extension to add a convenience method for decoding JSON responses
3333
extension Publisher where Output == (data: Data, response: URLResponse), Failure == Error {
34-
func decodeAuth<T: Decodable>(type: T.Type, decoder: JSONDecoder = JSONDecoder()) -> AnyPublisher<T, Error> {
34+
func decodeStandardResponse<T: Decodable>(
35+
type: T.Type,
36+
decoder: JSONDecoder = JSONDecoder()
37+
) -> AnyPublisher<T, Error> {
3538
return map { $0.data }
36-
.decode(type: type, decoder: decoder)
39+
.decode(type: StandardResponse<T>.self, decoder: decoder)
40+
.map { $0.data } // Extract the data from the standard response
3741
.eraseToAnyPublisher()
3842
}
43+
44+
// For backward compatibility
45+
func decodeAuth<T: Decodable>(type: T.Type, decoder: JSONDecoder = JSONDecoder()) -> AnyPublisher<T, Error> {
46+
return decodeStandardResponse(type: type, decoder: decoder)
47+
}
3948
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import Foundation
2+
3+
/// Standard response format from the server
4+
struct StandardResponse<T: Decodable>: Decodable {
5+
let status: String
6+
let data: T
7+
}

app-ios/app-ios/Services/APIService.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class APIService {
3535
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
3636

3737
return session.authHandledDataTaskPublisher(for: request)
38-
.decodeAuth(type: [Client].self)
38+
.decodeStandardResponse(type: [Client].self)
3939
.eraseToAnyPublisher()
4040
}
4141

@@ -47,7 +47,7 @@ class APIService {
4747
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
4848

4949
return session.authHandledDataTaskPublisher(for: request)
50-
.decodeAuth(type: [String: String].self)
50+
.decodeStandardResponse(type: [String: String].self)
5151
.eraseToAnyPublisher()
5252
}
5353

app-ios/app-ios/ViewModels/AuthViewModel.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ class AuthViewModel: ObservableObject {
6565
// since we're not sending an auth token yet
6666
URLSession.shared.dataTaskPublisher(for: request)
6767
.map { $0.data }
68-
.decode(type: AuthResponse.self, decoder: JSONDecoder())
68+
.decode(type: StandardResponse<AuthResponse>.self, decoder: JSONDecoder())
6969
.receive(on: DispatchQueue.main)
7070
.sink(receiveCompletion: { [weak self] completion in
7171
self?.isLoading = false
@@ -74,11 +74,11 @@ class AuthViewModel: ObservableObject {
7474
self?.handleLoginError(error)
7575
}
7676
}, receiveValue: { [weak self] response in
77-
self?.token = response.token
77+
self?.token = response.data.token
7878
self?.isAuthenticated = true
7979

8080
// Save token
81-
UserDefaults.standard.set(response.token, forKey: "authToken")
81+
UserDefaults.standard.set(response.data.token, forKey: "authToken")
8282
})
8383
.store(in: &cancellables)
8484
}

apps/dashboard/src/services/api.js

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,28 @@ import axios from "axios";
22

33
const API_BASE_URL = "/api";
44

5+
// Helper function to extract data from standardized response format
6+
const extractData = (response) => {
7+
if (
8+
!response.data ||
9+
response.data.status !== "success" ||
10+
response.data.data === undefined
11+
) {
12+
throw new Error("Invalid response format from server");
13+
}
14+
return response.data.data;
15+
};
16+
517
export const apiService = {
618
async getClients() {
719
try {
820
const response = await axios.get(`${API_BASE_URL}/agents`);
9-
return response.data;
21+
return extractData(response);
1022
} catch (error) {
23+
if (error.response?.data?.status === "error") {
24+
throw new Error(error.response.data.data);
25+
}
1126
console.error("Error fetching clients:", error);
12-
// Propagate the error so it can be handled by the caller
1327
throw error;
1428
}
1529
},
@@ -19,21 +33,25 @@ export const apiService = {
1933
const response = await axios.get(
2034
`${API_BASE_URL}/agents/${mid}/properties`,
2135
);
22-
return response.data;
36+
return extractData(response);
2337
} catch (error) {
38+
if (error.response?.data?.status === "error") {
39+
throw new Error(error.response.data.data);
40+
}
2441
console.error(`Error fetching properties for client ${mid}:`, error);
25-
// Propagate the error so it can be handled by the caller
2642
throw error;
2743
}
2844
},
2945

3046
async upgradeClients() {
3147
try {
3248
const response = await axios.post(`${API_BASE_URL}/agents/upgrade`);
33-
return response.data;
49+
return extractData(response);
3450
} catch (error) {
51+
if (error.response?.data?.status === "error") {
52+
throw new Error(error.response.data.data);
53+
}
3554
console.error("Error triggering client upgrade:", error);
36-
// Propagate the error so it can be handled by the caller
3755
throw error;
3856
}
3957
},

apps/dashboard/src/services/axios.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ axios.interceptors.response.use(
1818
(error) => {
1919
// Handle 401 Unauthorized errors
2020
if (error.response && error.response.status === 401) {
21-
console.error("Authentication failed:", error.response.data);
21+
// Extract error message from standardized response format
22+
let errorMessage = "Authentication failed";
23+
if (error.response.data?.status === "error") {
24+
errorMessage = error.response.data.data;
25+
}
26+
console.error(errorMessage, error.response.data);
2227

2328
try {
2429
// Get the auth store and log out the user

apps/dashboard/src/stores/authStore.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@ export const useAuthStore = defineStore("auth", {
4343
password,
4444
});
4545

46-
const { token } = response.data;
46+
// Handle the standardized response format
47+
if (response.data?.status !== "success" || !response.data?.data) {
48+
throw new Error("Invalid response format from server");
49+
}
50+
51+
const { token } = response.data.data;
4752

4853
// Store token in localStorage and state
4954
localStorage.setItem("token", token);
@@ -55,9 +60,14 @@ export const useAuthStore = defineStore("auth", {
5560
return true;
5661
} catch (error) {
5762
console.error("Login failed:", error);
58-
this.error =
59-
error.response?.data?.error ||
60-
"Login failed. Please check your credentials.";
63+
64+
// Extract error message from standardized response format
65+
if (error.response?.data?.status === "error") {
66+
this.error = error.response.data.data;
67+
} else {
68+
this.error = "Login failed. Please check your credentials.";
69+
}
70+
6171
return false;
6272
} finally {
6373
this.loading = false;

apps/dashboard/src/stores/uploadProgressStore.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,9 @@ export const useUploadProgressStore = defineStore("uploadProgress", () => {
9090
.catch((error) => {
9191
let errorMessage = "Upload failed";
9292

93-
// Extract error message from response if available
94-
if (error.response && error.response.data) {
95-
errorMessage = error.response.data.error || errorMessage;
93+
// Extract error message from standardized response format
94+
if (error.response?.data?.status === "error") {
95+
errorMessage = error.response.data.data;
9696
}
9797

9898
addRecord({

overlord/auth.go

Lines changed: 11 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"errors"
1212
"fmt"
1313
"io"
14-
"io/ioutil"
1514
"log"
1615
"net/http"
1716
"os"
@@ -80,7 +79,7 @@ func (config *JWTConfig) LoadJWTSecret() error {
8079
return errors.New("JWT secret file path not provided")
8180
}
8281

83-
data, err := ioutil.ReadFile(config.SecretPath)
82+
data, err := os.ReadFile(config.SecretPath)
8483
if err != nil {
8584
return fmt.Errorf("failed to read JWT secret file: %v", err)
8685
}
@@ -184,55 +183,41 @@ func (auth *JWTAuth) loadHtpasswd(htpasswdPath string) error {
184183
}
185184

186185
// Authenticate authenticates a user with the provided username and password
187-
func (auth *JWTAuth) Authenticate(user, passwd string) (bool, error) {
188-
deniedError := errors.New("permission denied")
189-
186+
func (auth *JWTAuth) Authenticate(user, passwd string) bool {
190187
passwdHash, ok := auth.secrets[user]
191188
if !ok {
192189
log.Printf("JWTAuth: user %s not found in secrets", user)
193-
return false, deniedError
190+
return false
194191
}
195192

196193
err := bcrypt.CompareHashAndPassword([]byte(passwdHash), []byte(passwd))
197194
if err != nil {
198195
log.Printf("JWTAuth: password comparison failed for user %s: %v", user, err)
199-
return false, deniedError
196+
return false
200197
}
201-
return true, nil
198+
return true
202199
}
203200

204201
// Login handles login requests and returns a JWT token
205202
func (auth *JWTAuth) Login(w http.ResponseWriter, r *http.Request) {
206-
// Check if IP is blocked
207203
if auth.IsBlocked(r) {
208-
log.Printf("JWTAuth: login attempt from blocked IP: %s", getRequestIP(r))
209-
ResponseError(w, "too many retries", http.StatusUnauthorized)
204+
auth.Unauthorized(w, r, "Too many failed attempts", true)
210205
return
211206
}
212207

213-
// Parse the login request
214208
var loginReq LoginRequest
215209
if err := json.NewDecoder(r.Body).Decode(&loginReq); err != nil {
216-
log.Printf("JWTAuth: failed to parse login request: %v", err)
217-
ResponseError(w, "Invalid request body", http.StatusBadRequest)
210+
auth.Unauthorized(w, r, "Invalid request", true)
218211
return
219212
}
220213

221-
log.Printf("JWTAuth: login attempt for user: %s", loginReq.Username)
222-
223-
// Authenticate the user
224-
pass, err := auth.Authenticate(loginReq.Username, loginReq.Password)
225-
if !pass {
226-
log.Printf("JWTAuth: authentication failed for user %s: %v",
227-
loginReq.Username, err)
228-
auth.Unauthorized(w, r, "Invalid username or password", true)
214+
if !auth.Authenticate(loginReq.Username, loginReq.Password) {
215+
auth.Unauthorized(w, r, "Authentication error", true)
229216
return
230217
}
231218

232-
// Reset fail count
233219
auth.ResetFailCount(r)
234220

235-
// Generate JWT token
236221
expirationTime := time.Now().Add(tokenExpirationTime)
237222
claims := &JWTClaims{
238223
Username: loginReq.Username,
@@ -247,16 +232,15 @@ func (auth *JWTAuth) Login(w http.ResponseWriter, r *http.Request) {
247232
tokenString, err := token.SignedString([]byte(auth.config.GetSecret()))
248233
if err != nil {
249234
log.Printf("JWTAuth: error generating token: %v", err)
250-
http.Error(w, "Error generating token", http.StatusInternalServerError)
235+
ResponseError(w, "Error generating token", http.StatusInternalServerError)
251236
return
252237
}
253238

254239
log.Printf("JWTAuth: token generated successfully for user: %s",
255240
loginReq.Username)
256241

257242
// Return the token
258-
w.Header().Set("Content-Type", "application/json")
259-
json.NewEncoder(w).Encode(LoginResponse{
243+
ResponseSuccess(w, LoginResponse{
260244
Token: tokenString,
261245
Expire: expirationTime.Unix(),
262246
})
@@ -288,10 +272,8 @@ func (auth *JWTAuth) VerifyToken(tokenString string) (*JWTClaims, error) {
288272
// Middleware creates a JWT middleware for HTTP handlers
289273
func (auth *JWTAuth) Middleware(next http.Handler) http.Handler {
290274
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
291-
// Get token from Authorization header or query parameter
292275
var tokenString string
293276

294-
// First try to get token from Authorization header
295277
authHeader := r.Header.Get("Authorization")
296278
if authHeader != "" {
297279
// Check if it's a Bearer token
@@ -303,30 +285,24 @@ func (auth *JWTAuth) Middleware(next http.Handler) http.Handler {
303285
}
304286
}
305287

306-
// If no token in header, try query parameter
307288
if tokenString == "" {
308289
tokenString = r.URL.Query().Get("token")
309290
}
310291

311-
// If still no token, return unauthorized
312292
if tokenString == "" {
313293
log.Printf("JWTAuth: no token found in header or query parameter")
314294
auth.Unauthorized(w, r, "JWT token required", false)
315295
return
316296
}
317297

318-
// Verify token
319298
claims, err := auth.VerifyToken(tokenString)
320299
if err != nil {
321300
log.Printf("JWTAuth: token verification failed: %v", err)
322301
auth.Unauthorized(w, r, "invalid token", false)
323302
return
324303
}
325304

326-
// Add claims to request context
327305
r = r.WithContext(WithAuthClaims(r.Context(), claims))
328-
329-
// Continue to the next handler
330306
next.ServeHTTP(w, r)
331307
})
332308
}
@@ -372,7 +348,6 @@ func (auth *JWTAuth) IsBlocked(r *http.Request) bool {
372348

373349
delete(auth.failedCount, ip)
374350
delete(auth.blockedIps, ip)
375-
376351
return false
377352
}
378353

overlord/ghost.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -573,9 +573,7 @@ func (ghost *Ghost) handleListTreeRequest(req *Request) error {
573573
return ghost.SendResponse(NewErrorResponse(req.Rid, err.Error()))
574574
}
575575
return ghost.SendResponse(
576-
NewResponse(req.Rid, Success, map[string]interface{}{
577-
"entries": entries,
578-
}))
576+
NewResponse(req.Rid, Success, entries))
579577
}
580578

581579
func (ghost *Ghost) handleFstatRequest(req *Request) error {

0 commit comments

Comments
 (0)