All checks were successful
Check / check (push) Successful in 4s
Bumps golangci-lint from v2.10.1 to v2.12.2 everywhere it is pinned and installs the canonical `.golangci.yml`, then fixes every finding the new linter surfaces so `make check` is green. ## Version pins - `Dockerfile` lint stage: `golangci/golangci-lint:v2.12.2` (Debian-based), tag plus digest pin - `script/bootstrap`: `GOLANGCI_LINT_VERSION=2.12.2` with updated `linux-amd64`/`linux-arm64` release-archive sha256 pins ## Config `.golangci.yml` replaced with the canonical config. Material change: the old file declared `version: "2"` but kept settings under the legacy top-level `linters-settings` key, which golangci-lint v2 ignores — so the intended thresholds (`lll` 88, `funlen` 80/50, `cyclop` 15, `dupl` 100) were not being applied. The canonical file moves them under `linters.settings` and drops `issues.exclude-use-default`. ## Lint fixes (216 findings) - `lll` (96): wrapped lines to the 88-column limit - `noctx` (46): `httptest.NewRequestWithContext` with `t.Context()` throughout the tests - `goconst` (24): shared constants for template/JSON keys in `internal/handlers` and repeated test literals - `gosec` (23): app-page redirects now go through a `redirectToApp` helper that path-escapes the app ID (G710 open redirect); `http.ServeFile` of the internally derived deployment log path annotated like the adjacent `os.Stat` (G703) - `dupl` (22): extracted a generic `findAllByAppID` in `internal/models`, a `deleteAppResource` helper in `internal/handlers`, a shared `parsePush` in `internal/service/webhook`, and table-driven/helper-based dedup in tests - `nolintlint` (5): removed `//nolint:funlen` directives made obsolete by the new limits (plus one more that became obsolete after refactoring) - `nilerr` (3, surfaced during fixing): resource-delete lookups now propagate the find error to the caller No behavior changes intended; all tests pass and `make check` is green. Note: golangci-lint v2.12 warns that `gomodguard` is deprecated in favor of `gomodguard_v2` — a future canonical-config update should address this centrally. Co-authored-by: sneak <sneak@sneak.berlin> Reviewed-on: #187 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org>
246 lines
6.5 KiB
Go
246 lines
6.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"sneak.berlin/go/upaas/internal/models"
|
|
)
|
|
|
|
// apiAppResponse is the JSON representation of an app.
|
|
type apiAppResponse struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
RepoURL string `json:"repoUrl"`
|
|
Branch string `json:"branch"`
|
|
DockerfilePath string `json:"dockerfilePath"`
|
|
Status string `json:"status"`
|
|
WebhookSecret string `json:"webhookSecret"`
|
|
SSHPublicKey string `json:"sshPublicKey"`
|
|
CreatedAt string `json:"createdAt"`
|
|
UpdatedAt string `json:"updatedAt"`
|
|
}
|
|
|
|
// apiDeploymentResponse is the JSON representation of a deployment.
|
|
type apiDeploymentResponse struct {
|
|
ID int64 `json:"id"`
|
|
AppID string `json:"appId"`
|
|
CommitSHA string `json:"commitSha,omitempty"`
|
|
Status string `json:"status"`
|
|
Duration string `json:"duration,omitempty"`
|
|
StartedAt string `json:"startedAt"`
|
|
FinishedAt string `json:"finishedAt,omitempty"`
|
|
}
|
|
|
|
func appToAPI(a *models.App) apiAppResponse {
|
|
return apiAppResponse{
|
|
ID: a.ID,
|
|
Name: a.Name,
|
|
RepoURL: a.RepoURL,
|
|
Branch: a.Branch,
|
|
DockerfilePath: a.DockerfilePath,
|
|
Status: string(a.Status),
|
|
WebhookSecret: a.WebhookSecret,
|
|
SSHPublicKey: a.SSHPublicKey,
|
|
CreatedAt: a.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
|
UpdatedAt: a.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
|
}
|
|
}
|
|
|
|
func deploymentToAPI(d *models.Deployment) apiDeploymentResponse {
|
|
resp := apiDeploymentResponse{
|
|
ID: d.ID,
|
|
AppID: d.AppID,
|
|
Status: string(d.Status),
|
|
Duration: d.Duration(),
|
|
StartedAt: d.StartedAt.Format("2006-01-02T15:04:05Z"),
|
|
}
|
|
|
|
if d.CommitSHA.Valid {
|
|
resp.CommitSHA = d.CommitSHA.String
|
|
}
|
|
|
|
if d.FinishedAt.Valid {
|
|
resp.FinishedAt = d.FinishedAt.Time.Format("2006-01-02T15:04:05Z")
|
|
}
|
|
|
|
return resp
|
|
}
|
|
|
|
// HandleAPILoginPOST returns a handler that authenticates via JSON credentials
|
|
// and sets a session cookie.
|
|
func (h *Handlers) HandleAPILoginPOST() http.HandlerFunc {
|
|
type loginResponse struct {
|
|
UserID int64 `json:"userId"`
|
|
Username string `json:"username"`
|
|
}
|
|
|
|
return func(writer http.ResponseWriter, request *http.Request) {
|
|
var req map[string]string
|
|
|
|
decodeErr := json.NewDecoder(request.Body).Decode(&req)
|
|
if decodeErr != nil {
|
|
h.respondJSON(writer, request,
|
|
map[string]string{jsonKeyError: "invalid JSON body"},
|
|
http.StatusBadRequest)
|
|
|
|
return
|
|
}
|
|
|
|
username := req["username"]
|
|
credential := req["password"]
|
|
|
|
if username == "" || credential == "" {
|
|
h.respondJSON(writer, request,
|
|
map[string]string{jsonKeyError: "username and password are required"},
|
|
http.StatusBadRequest)
|
|
|
|
return
|
|
}
|
|
|
|
user, authErr := h.auth.Authenticate(request.Context(), username, credential)
|
|
if authErr != nil {
|
|
h.respondJSON(writer, request,
|
|
map[string]string{jsonKeyError: "invalid credentials"},
|
|
http.StatusUnauthorized)
|
|
|
|
return
|
|
}
|
|
|
|
sessionErr := h.auth.CreateSession(writer, request, user)
|
|
if sessionErr != nil {
|
|
h.log.Error("api: failed to create session", "error", sessionErr)
|
|
h.respondJSON(writer, request,
|
|
map[string]string{jsonKeyError: "failed to create session"},
|
|
http.StatusInternalServerError)
|
|
|
|
return
|
|
}
|
|
|
|
h.respondJSON(writer, request, loginResponse{
|
|
UserID: user.ID,
|
|
Username: user.Username,
|
|
}, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
// HandleAPIListApps returns a handler that lists all apps as JSON.
|
|
func (h *Handlers) HandleAPIListApps() http.HandlerFunc {
|
|
return func(writer http.ResponseWriter, request *http.Request) {
|
|
apps, err := h.appService.ListApps(request.Context())
|
|
if err != nil {
|
|
h.respondJSON(writer, request,
|
|
map[string]string{jsonKeyError: "failed to list apps"},
|
|
http.StatusInternalServerError)
|
|
|
|
return
|
|
}
|
|
|
|
result := make([]apiAppResponse, 0, len(apps))
|
|
for _, a := range apps {
|
|
result = append(result, appToAPI(a))
|
|
}
|
|
|
|
h.respondJSON(writer, request, result, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
// HandleAPIGetApp returns a handler that gets a single app by ID.
|
|
func (h *Handlers) HandleAPIGetApp() http.HandlerFunc {
|
|
return func(writer http.ResponseWriter, request *http.Request) {
|
|
appID := chi.URLParam(request, "id")
|
|
|
|
application, err := h.appService.GetApp(request.Context(), appID)
|
|
if err != nil {
|
|
h.respondJSON(writer, request,
|
|
map[string]string{jsonKeyError: "internal server error"},
|
|
http.StatusInternalServerError)
|
|
|
|
return
|
|
}
|
|
|
|
if application == nil {
|
|
h.respondJSON(writer, request,
|
|
map[string]string{jsonKeyError: "app not found"},
|
|
http.StatusNotFound)
|
|
|
|
return
|
|
}
|
|
|
|
h.respondJSON(writer, request, appToAPI(application), http.StatusOK)
|
|
}
|
|
}
|
|
|
|
// deploymentsPageLimit is the default number of deployments per page.
|
|
const deploymentsPageLimit = 20
|
|
|
|
// HandleAPIListDeployments returns a handler that lists deployments for an app.
|
|
func (h *Handlers) HandleAPIListDeployments() http.HandlerFunc {
|
|
return func(writer http.ResponseWriter, request *http.Request) {
|
|
appID := chi.URLParam(request, "id")
|
|
|
|
application, err := h.appService.GetApp(request.Context(), appID)
|
|
if err != nil || application == nil {
|
|
h.respondJSON(writer, request,
|
|
map[string]string{jsonKeyError: "app not found"},
|
|
http.StatusNotFound)
|
|
|
|
return
|
|
}
|
|
|
|
limit := deploymentsPageLimit
|
|
|
|
if l := request.URL.Query().Get("limit"); l != "" {
|
|
parsed, parseErr := strconv.Atoi(l)
|
|
if parseErr == nil && parsed > 0 {
|
|
limit = parsed
|
|
}
|
|
}
|
|
|
|
deployments, deployErr := application.GetDeployments(
|
|
request.Context(), limit,
|
|
)
|
|
if deployErr != nil {
|
|
h.respondJSON(writer, request,
|
|
map[string]string{jsonKeyError: "failed to list deployments"},
|
|
http.StatusInternalServerError)
|
|
|
|
return
|
|
}
|
|
|
|
result := make([]apiDeploymentResponse, 0, len(deployments))
|
|
for _, d := range deployments {
|
|
result = append(result, deploymentToAPI(d))
|
|
}
|
|
|
|
h.respondJSON(writer, request, result, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
// HandleAPIWhoAmI returns a handler that shows the current authenticated user.
|
|
func (h *Handlers) HandleAPIWhoAmI() http.HandlerFunc {
|
|
type whoAmIResponse struct {
|
|
UserID int64 `json:"userId"`
|
|
Username string `json:"username"`
|
|
}
|
|
|
|
return func(writer http.ResponseWriter, request *http.Request) {
|
|
user, err := h.auth.GetCurrentUser(request.Context(), request)
|
|
if err != nil || user == nil {
|
|
h.respondJSON(writer, request,
|
|
map[string]string{jsonKeyError: "unauthorized"},
|
|
http.StatusUnauthorized)
|
|
|
|
return
|
|
}
|
|
|
|
h.respondJSON(writer, request, whoAmIResponse{
|
|
UserID: user.ID,
|
|
Username: user.Username,
|
|
}, http.StatusOK)
|
|
}
|
|
}
|