feat(backend): implement HTTP API, auth, chat, and admin surfaces
Expose learning, registration controls, mentor chat, realtime WebSocket, and staff administration.
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"pargar/backend/internal/observability"
|
||||
)
|
||||
|
||||
// Stable machine-readable error codes for clients.
|
||||
const (
|
||||
CodeBadRequest = "bad_request"
|
||||
CodeUnauthorized = "unauthorized"
|
||||
CodeForbidden = "forbidden"
|
||||
CodeNotFound = "not_found"
|
||||
CodeConflict = "conflict"
|
||||
CodeLocked = "account_locked"
|
||||
CodePayloadTooLarge = "payload_too_large"
|
||||
CodeRateLimited = "rate_limited"
|
||||
CodeValidation = "validation_error"
|
||||
CodeInternal = "internal_error"
|
||||
)
|
||||
|
||||
type apiError struct {
|
||||
Error string `json:"error"`
|
||||
Code string `json:"code,omitempty"`
|
||||
RequestID string `json:"requestId,omitempty"`
|
||||
}
|
||||
|
||||
func requestIDFromWriter(w http.ResponseWriter) string {
|
||||
if w == nil {
|
||||
return ""
|
||||
}
|
||||
return w.Header().Get("X-Request-ID")
|
||||
}
|
||||
|
||||
func defaultCodeForStatus(status int) string {
|
||||
switch status {
|
||||
case http.StatusBadRequest:
|
||||
return CodeBadRequest
|
||||
case http.StatusUnauthorized:
|
||||
return CodeUnauthorized
|
||||
case http.StatusForbidden:
|
||||
return CodeForbidden
|
||||
case http.StatusNotFound:
|
||||
return CodeNotFound
|
||||
case http.StatusConflict:
|
||||
return CodeConflict
|
||||
case http.StatusLocked:
|
||||
return CodeLocked
|
||||
case http.StatusRequestEntityTooLarge:
|
||||
return CodePayloadTooLarge
|
||||
case http.StatusTooManyRequests:
|
||||
return CodeRateLimited
|
||||
case http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable:
|
||||
return CodeInternal
|
||||
default:
|
||||
if status >= 500 {
|
||||
return CodeInternal
|
||||
}
|
||||
if status >= 400 {
|
||||
return CodeBadRequest
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// writeErr responds with a Persian user message and a stable code derived from status.
|
||||
func writeErr(w http.ResponseWriter, status int, msg string) {
|
||||
writeErrCode(w, status, defaultCodeForStatus(status), msg)
|
||||
}
|
||||
|
||||
// writeErrCode responds with an explicit machine-readable code.
|
||||
func writeErrCode(w http.ResponseWriter, status int, code, msg string) {
|
||||
if code == "" {
|
||||
code = defaultCodeForStatus(status)
|
||||
}
|
||||
if msg == "" {
|
||||
msg = "خطایی رخ داد؛ دوباره تلاش کنید"
|
||||
}
|
||||
writeJSON(w, status, apiError{
|
||||
Error: msg,
|
||||
Code: code,
|
||||
RequestID: requestIDFromWriter(w),
|
||||
})
|
||||
}
|
||||
|
||||
// writeInternalErr logs the real error server-side and returns a safe 500 to the client.
|
||||
func writeInternalErr(w http.ResponseWriter, op string, err error, userMsg string) {
|
||||
if userMsg == "" {
|
||||
userMsg = "مشکل موقتی سرور؛ کمی بعد دوباره تلاش کنید."
|
||||
}
|
||||
attrs := []any{"op", op, "request_id", requestIDFromWriter(w)}
|
||||
if err != nil {
|
||||
attrs = append(attrs, "error", err)
|
||||
}
|
||||
observability.L().Error("api.error", attrs...)
|
||||
writeErrCode(w, http.StatusInternalServerError, CodeInternal, userMsg)
|
||||
}
|
||||
|
||||
// writeValidationErr is a 400 with validation_error code.
|
||||
func writeValidationErr(w http.ResponseWriter, msg string) {
|
||||
writeErrCode(w, http.StatusBadRequest, CodeValidation, msg)
|
||||
}
|
||||
Reference in New Issue
Block a user