package database_clients import ( "encoding/json" "errors" "log/slog" "net/http" "project/internal/api" "project/internal/auth" ) type Handler struct { svc *Service } func NewHandler(svc *Service) *Handler { return &Handler{svc: svc} } // ListDBTypes godoc // // @Summary List tipe database yang didukung beserta definisi field kredensial // @Description Mengembalikan semua tipe database beserta schema field yang dibutuhkan untuk membuat koneksi. Gunakan untuk render form dinamis di frontend. // @Tags database-clients // @Produce json // @Success 200 {object} api.APIResponse{data=[]DBTypeInfo} // @Security BearerAuth // @Router /api/v1/database-clients/dbtypes [get] func (h *Handler) ListDBTypes(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, api.APIResponse{ Status: "success", Message: "supported database types", Data: h.svc.ListDBTypes(), }) } // Create godoc // // @Summary Daftarkan koneksi database baru // @Description Enkripsi credentials dan simpan. Jika sudah ada koneksi dengan identity yang sama, kembalikan yang existing (HTTP 200). // @Tags database-clients // @Accept json // @Produce json // @Param body body object{user_id=string,name=string,db_type=string,credentials=object} true "Database connection info" // @Success 201 {object} api.APIResponse{data=DBClient} // @Success 200 {object} api.APIResponse{data=DBClient} "Duplicate — returning existing" // @Failure 400 {object} api.APIResponse // @Failure 429 {object} map[string]string "Rate limit exceeded" // @Failure 500 {object} api.APIResponse // @Security BearerAuth // @Router /api/v1/database-clients [post] func (h *Handler) Create(w http.ResponseWriter, r *http.Request) { var req struct { UserID string `json:"user_id"` Name string `json:"name"` DBType string `json:"db_type"` Credentials map[string]any `json:"credentials"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, api.APIResponse{Status: "error", Message: "invalid request body"}) return } if req.UserID == "" || req.Name == "" || req.DBType == "" || req.Credentials == nil { writeJSON(w, http.StatusBadRequest, api.APIResponse{Status: "error", Message: "user_id, name, db_type, and credentials are required"}) return } if rejectUserMismatch(w, r, req.UserID) { return } client, isDuplicate, err := h.svc.Create(r.Context(), req.UserID, req.Name, req.DBType, req.Credentials) if err != nil { if errors.Is(err, ErrUnsupportedType) { writeJSON(w, http.StatusBadRequest, api.APIResponse{Status: "error", Message: "unsupported database type"}) return } slog.Error("database client create failed", "err", err) writeJSON(w, http.StatusInternalServerError, api.APIResponse{Status: "error", Message: "internal server error"}) return } status := http.StatusCreated msg := "database client created" if isDuplicate { status = http.StatusOK msg = "database client already exists" } writeJSON(w, status, api.APIResponse{Status: "success", Message: msg, Data: client}) } // List godoc // // @Summary List koneksi database milik user // @Tags database-clients // @Produce json // @Param user_id path string true "User ID" // @Success 200 {object} api.APIResponse{data=[]DBClient} // @Failure 500 {object} api.APIResponse // @Security BearerAuth // @Router /api/v1/database-clients/{user_id} [get] func (h *Handler) List(w http.ResponseWriter, r *http.Request) { userID := r.PathValue("user_id") if rejectUserMismatch(w, r, userID) { return } clients, err := h.svc.List(r.Context(), userID) if err != nil { writeJSON(w, http.StatusInternalServerError, api.APIResponse{Status: "error", Message: "internal server error"}) return } writeJSON(w, http.StatusOK, api.APIResponse{Status: "success", Message: "database clients", Data: clients}) } // GetOne godoc // // @Summary Ambil detail satu koneksi database // @Tags database-clients // @Produce json // @Param user_id path string true "User ID" // @Param client_id path string true "Client ID" // @Success 200 {object} api.APIResponse{data=DBClient} // @Failure 403 {object} api.APIResponse // @Failure 404 {object} api.APIResponse // @Failure 500 {object} api.APIResponse // @Security BearerAuth // @Router /api/v1/database-clients/{user_id}/{client_id} [get] func (h *Handler) GetOne(w http.ResponseWriter, r *http.Request) { userID := r.PathValue("user_id") if rejectUserMismatch(w, r, userID) { return } clientID := r.PathValue("client_id") client, err := h.svc.Get(r.Context(), userID, clientID) if err != nil { switch { case errors.Is(err, ErrNotFound): writeJSON(w, http.StatusNotFound, api.APIResponse{Status: "error", Message: "database client not found"}) case errors.Is(err, ErrForbidden): writeJSON(w, http.StatusForbidden, api.APIResponse{Status: "error", Message: "access denied"}) default: writeJSON(w, http.StatusInternalServerError, api.APIResponse{Status: "error", Message: "internal server error"}) } return } writeJSON(w, http.StatusOK, api.APIResponse{Status: "success", Message: "database client", Data: client}) } // Update godoc // // @Summary Update koneksi database // @Tags database-clients // @Accept json // @Produce json // @Param client_id path string true "Client ID" // @Param user_id query string true "User ID (ownership check)" // @Param body body object{name=string,credentials=object,status=string} false "Fields to update" // @Success 200 {object} api.APIResponse{data=DBClient} // @Failure 400 {object} api.APIResponse // @Failure 403 {object} api.APIResponse // @Failure 404 {object} api.APIResponse // @Failure 500 {object} api.APIResponse // @Security BearerAuth // @Router /api/v1/database-clients/{client_id} [put] func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { clientID := r.PathValue("client_id") userID := r.URL.Query().Get("user_id") if userID == "" { writeJSON(w, http.StatusBadRequest, api.APIResponse{Status: "error", Message: "user_id is required"}) return } if rejectUserMismatch(w, r, userID) { return } var req struct { Name *string `json:"name"` Credentials map[string]any `json:"credentials"` Status *string `json:"status"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, api.APIResponse{Status: "error", Message: "invalid request body"}) return } client, err := h.svc.Update(r.Context(), clientID, userID, req.Name, req.Credentials, req.Status) if err != nil { switch { case errors.Is(err, ErrNotFound): writeJSON(w, http.StatusNotFound, api.APIResponse{Status: "error", Message: "database client not found"}) case errors.Is(err, ErrForbidden): writeJSON(w, http.StatusForbidden, api.APIResponse{Status: "error", Message: "access denied"}) default: writeJSON(w, http.StatusInternalServerError, api.APIResponse{Status: "error", Message: "internal server error"}) } return } writeJSON(w, http.StatusOK, api.APIResponse{Status: "success", Message: "database client updated", Data: client}) } // Delete godoc // // @Summary Hapus koneksi database // @Description Menghapus koneksi dan memicu pembersihan catalog // @Tags database-clients // @Produce json // @Param client_id path string true "Client ID" // @Param user_id query string true "User ID (ownership check)" // @Success 200 {object} api.APIResponse // @Failure 400 {object} api.APIResponse // @Failure 403 {object} api.APIResponse // @Failure 404 {object} api.APIResponse // @Failure 500 {object} api.APIResponse // @Security BearerAuth // @Router /api/v1/database-clients/{client_id} [delete] func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { clientID := r.PathValue("client_id") userID := r.URL.Query().Get("user_id") if userID == "" { writeJSON(w, http.StatusBadRequest, api.APIResponse{Status: "error", Message: "user_id is required"}) return } if rejectUserMismatch(w, r, userID) { return } if err := h.svc.Delete(r.Context(), clientID, userID); err != nil { switch { case errors.Is(err, ErrNotFound): writeJSON(w, http.StatusNotFound, api.APIResponse{Status: "error", Message: "database client not found"}) case errors.Is(err, ErrForbidden): writeJSON(w, http.StatusForbidden, api.APIResponse{Status: "error", Message: "access denied"}) default: writeJSON(w, http.StatusInternalServerError, api.APIResponse{Status: "error", Message: "internal server error"}) } return } writeJSON(w, http.StatusOK, api.APIResponse{Status: "success", Message: "database client deleted"}) } // Ingest godoc // // @Summary Introspect schema database dan simpan ke catalog // @Tags database-clients // @Produce json // @Param client_id path string true "Client ID" // @Param user_id query string true "User ID (ownership check)" // @Success 200 {object} api.APIResponse // @Failure 400 {object} api.APIResponse // @Failure 403 {object} api.APIResponse // @Failure 404 {object} api.APIResponse // @Failure 409 {object} api.APIResponse "Client is inactive" // @Failure 429 {object} map[string]string "Rate limit exceeded" // @Failure 500 {object} api.APIResponse // @Security BearerAuth // @Router /api/v1/database-clients/{client_id}/ingest [post] func (h *Handler) Ingest(w http.ResponseWriter, r *http.Request) { clientID := r.PathValue("client_id") userID := r.URL.Query().Get("user_id") if userID == "" { writeJSON(w, http.StatusBadRequest, api.APIResponse{Status: "error", Message: "user_id is required"}) return } if rejectUserMismatch(w, r, userID) { return } schema, err := h.svc.Ingest(r.Context(), clientID, userID) if err != nil { switch { case errors.Is(err, ErrNotFound): writeJSON(w, http.StatusNotFound, api.APIResponse{Status: "error", Message: "database client not found"}) case errors.Is(err, ErrForbidden): writeJSON(w, http.StatusForbidden, api.APIResponse{Status: "error", Message: "access denied"}) case errors.Is(err, ErrInactive): writeJSON(w, http.StatusConflict, api.APIResponse{Status: "error", Message: "database client is inactive"}) default: writeJSON(w, http.StatusInternalServerError, api.APIResponse{Status: "error", Message: err.Error()}) } return } writeJSON(w, http.StatusOK, api.APIResponse{Status: "success", Message: "schema ingested", Data: schema}) } func rejectUserMismatch(w http.ResponseWriter, r *http.Request, userID string) bool { if err := auth.MatchContextUserID(r.Context(), userID); err != nil { writeJSON(w, http.StatusForbidden, api.APIResponse{Status: "error", Message: "user_id does not match authenticated user"}) return true } return false } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) }