- Add 022_workstations.sql migration (UUID PK, user_id FK, UNIQUE(user_id, name)) - Add Sessions module (depends on Auth, default enabled) with config toggle - Add WorkstationRepository with Upsert, GetByID, ListByUser, Touch, Delete - Add workstation handlers: register (POST upsert), list (GET), delete (DELETE) - Add /api/workstations routes gated by sessions module - Wire WorkstationRepository into Server struct - Update module tests for new Sessions module Closes #161
139 lines
4.0 KiB
Go
139 lines
4.0 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/kindredsystems/silo/internal/auth"
|
|
"github.com/kindredsystems/silo/internal/db"
|
|
)
|
|
|
|
// HandleRegisterWorkstation registers or re-registers a workstation for the current user.
|
|
func (s *Server) HandleRegisterWorkstation(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
user := auth.UserFromContext(ctx)
|
|
if user == nil {
|
|
writeError(w, http.StatusUnauthorized, "unauthorized", "Authentication required")
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Name string `json:"name"`
|
|
Hostname string `json:"hostname"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid_json", err.Error())
|
|
return
|
|
}
|
|
if req.Name == "" {
|
|
writeError(w, http.StatusBadRequest, "validation_error", "name is required")
|
|
return
|
|
}
|
|
|
|
ws := &db.Workstation{
|
|
Name: req.Name,
|
|
UserID: user.ID,
|
|
Hostname: req.Hostname,
|
|
}
|
|
if err := s.workstations.Upsert(ctx, ws); err != nil {
|
|
s.logger.Error().Err(err).Str("name", req.Name).Msg("failed to register workstation")
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to register workstation")
|
|
return
|
|
}
|
|
|
|
s.broker.Publish("workstation.registered", mustMarshal(map[string]any{
|
|
"id": ws.ID,
|
|
"name": ws.Name,
|
|
"user_id": ws.UserID,
|
|
"hostname": ws.Hostname,
|
|
}))
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"id": ws.ID,
|
|
"name": ws.Name,
|
|
"hostname": ws.Hostname,
|
|
"last_seen": ws.LastSeen.UTC().Format("2006-01-02T15:04:05Z"),
|
|
"created_at": ws.CreatedAt.UTC().Format("2006-01-02T15:04:05Z"),
|
|
})
|
|
}
|
|
|
|
// HandleListWorkstations returns all workstations for the current user.
|
|
func (s *Server) HandleListWorkstations(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
user := auth.UserFromContext(ctx)
|
|
if user == nil {
|
|
writeError(w, http.StatusUnauthorized, "unauthorized", "Authentication required")
|
|
return
|
|
}
|
|
|
|
workstations, err := s.workstations.ListByUser(ctx, user.ID)
|
|
if err != nil {
|
|
s.logger.Error().Err(err).Msg("failed to list workstations")
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list workstations")
|
|
return
|
|
}
|
|
|
|
type wsResponse struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Hostname string `json:"hostname"`
|
|
LastSeen string `json:"last_seen"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
out := make([]wsResponse, len(workstations))
|
|
for i, ws := range workstations {
|
|
out[i] = wsResponse{
|
|
ID: ws.ID,
|
|
Name: ws.Name,
|
|
Hostname: ws.Hostname,
|
|
LastSeen: ws.LastSeen.UTC().Format("2006-01-02T15:04:05Z"),
|
|
CreatedAt: ws.CreatedAt.UTC().Format("2006-01-02T15:04:05Z"),
|
|
}
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
// HandleDeleteWorkstation removes a workstation owned by the current user (or any, for admins).
|
|
func (s *Server) HandleDeleteWorkstation(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
user := auth.UserFromContext(ctx)
|
|
if user == nil {
|
|
writeError(w, http.StatusUnauthorized, "unauthorized", "Authentication required")
|
|
return
|
|
}
|
|
|
|
id := chi.URLParam(r, "id")
|
|
ws, err := s.workstations.GetByID(ctx, id)
|
|
if err != nil {
|
|
s.logger.Error().Err(err).Str("id", id).Msg("failed to get workstation")
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to get workstation")
|
|
return
|
|
}
|
|
if ws == nil {
|
|
writeError(w, http.StatusNotFound, "not_found", "Workstation not found")
|
|
return
|
|
}
|
|
|
|
if ws.UserID != user.ID && user.Role != auth.RoleAdmin {
|
|
writeError(w, http.StatusForbidden, "forbidden", "You can only delete your own workstations")
|
|
return
|
|
}
|
|
|
|
if err := s.workstations.Delete(ctx, id); err != nil {
|
|
s.logger.Error().Err(err).Str("id", id).Msg("failed to delete workstation")
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to delete workstation")
|
|
return
|
|
}
|
|
|
|
s.broker.Publish("workstation.removed", mustMarshal(map[string]any{
|
|
"id": ws.ID,
|
|
"name": ws.Name,
|
|
"user_id": ws.UserID,
|
|
}))
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|