274 lines
7.1 KiB
Go
274 lines
7.1 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/shorebird-server/internal/api/middleware"
|
|
"github.com/shorebird-server/internal/db"
|
|
"github.com/shorebird-server/internal/models"
|
|
)
|
|
|
|
// strPtr returns a pointer to a string.
|
|
func strPtr(s string) *string {
|
|
return &s
|
|
}
|
|
|
|
// intPtr returns a pointer to an int.
|
|
func intPtr(i int) *int {
|
|
return &i
|
|
}
|
|
|
|
// UserHandler handles user-related API endpoints.
|
|
type UserHandler struct {
|
|
DB *db.DB
|
|
}
|
|
|
|
// GetCurrentUser handles GET /api/v1/users/me
|
|
func (h *UserHandler) GetCurrentUser(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.GetClaims(r)
|
|
if claims == nil {
|
|
respondError(w, http.StatusUnauthorized, "Unauthorized", nil)
|
|
return
|
|
}
|
|
|
|
user, err := h.DB.GetUserByID(r.Context(), claims.UserID)
|
|
if err != nil {
|
|
respondError(w, http.StatusNotFound, "User not found", nil)
|
|
return
|
|
}
|
|
|
|
respondJSON(w, http.StatusOK, map[string]interface{}{
|
|
"id": user.ID,
|
|
"email": user.Email,
|
|
"name": user.Name,
|
|
"created_at": user.CreatedAt,
|
|
})
|
|
}
|
|
|
|
// CreateUser handles POST /api/v1/users
|
|
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.GetClaims(r)
|
|
if claims == nil {
|
|
respondError(w, http.StatusUnauthorized, "Unauthorized", nil)
|
|
return
|
|
}
|
|
|
|
var req models.CreateUserRequest
|
|
if err := decodeJSON(r, &req); err != nil {
|
|
respondError(w, http.StatusBadRequest, "Invalid request body", nil)
|
|
return
|
|
}
|
|
|
|
// In the Shorebird protocol, user creation during login flow
|
|
// is handled by the auth server. For API completeness, this
|
|
// endpoint updates the user's name if they exist.
|
|
user, err := h.DB.GetUserByEmail(r.Context(), claims.Email)
|
|
if err != nil {
|
|
respondError(w, http.StatusNotFound, "User not found", nil)
|
|
return
|
|
}
|
|
|
|
respondJSON(w, http.StatusOK, map[string]interface{}{
|
|
"id": user.ID,
|
|
"email": user.Email,
|
|
"name": user.Name,
|
|
"created_at": user.CreatedAt,
|
|
})
|
|
}
|
|
|
|
// AppHandler handles app-related API endpoints.
|
|
type AppHandler struct {
|
|
DB *db.DB
|
|
}
|
|
|
|
// GetApps handles GET /api/v1/apps
|
|
func (h *AppHandler) GetApps(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.GetClaims(r)
|
|
if claims == nil {
|
|
respondError(w, http.StatusUnauthorized, "Unauthorized", nil)
|
|
return
|
|
}
|
|
|
|
apps, err := h.DB.GetAppsByUserID(r.Context(), claims.UserID)
|
|
if err != nil {
|
|
respondError(w, http.StatusInternalServerError, "Failed to fetch apps", nil)
|
|
return
|
|
}
|
|
|
|
var result []models.AppMetadata
|
|
for _, a := range apps {
|
|
result = append(result, models.AppMetadata{
|
|
AppID: a.ID.String(),
|
|
DisplayName: a.DisplayName,
|
|
CreatedAt: a.CreatedAt,
|
|
UpdatedAt: a.UpdatedAt,
|
|
})
|
|
}
|
|
|
|
respondJSON(w, http.StatusOK, map[string]interface{}{"apps": result})
|
|
}
|
|
|
|
// CreateApp handles POST /api/v1/apps
|
|
func (h *AppHandler) CreateApp(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.GetClaims(r)
|
|
if claims == nil {
|
|
respondError(w, http.StatusUnauthorized, "Unauthorized", nil)
|
|
return
|
|
}
|
|
|
|
var req models.CreateAppRequest
|
|
if err := decodeJSON(r, &req); err != nil {
|
|
respondError(w, http.StatusBadRequest, "Invalid request body", nil)
|
|
return
|
|
}
|
|
|
|
if req.OrganizationID == 0 {
|
|
// Default to user's first organization
|
|
orgs, err := h.DB.GetOrganizationsByUserID(r.Context(), claims.UserID)
|
|
if err != nil || len(orgs) == 0 {
|
|
// Create a default org
|
|
orgID, err := h.DB.CreateOrganization(r.Context(), claims.Email+"'s Org", "team")
|
|
if err != nil {
|
|
respondError(w, http.StatusInternalServerError, "Failed to create organization", nil)
|
|
return
|
|
}
|
|
h.DB.AddUserToOrganization(r.Context(), claims.UserID, orgID, "admin")
|
|
req.OrganizationID = orgID
|
|
} else {
|
|
req.OrganizationID = orgs[0].OrgID
|
|
}
|
|
}
|
|
|
|
app, err := h.DB.CreateApp(r.Context(), req.OrganizationID, req.DisplayName)
|
|
if err != nil {
|
|
respondError(w, http.StatusInternalServerError, "Failed to create app", strPtr(err.Error()))
|
|
return
|
|
}
|
|
|
|
// Auto-create default "stable" channel
|
|
_, err = h.DB.CreateChannel(r.Context(), app.ID, "stable")
|
|
if err != nil {
|
|
// Log but don't fail; channel creation is best-effort for compat
|
|
}
|
|
|
|
respondJSON(w, http.StatusCreated, map[string]interface{}{
|
|
"app_id": app.ID.String(),
|
|
"display_name": app.DisplayName,
|
|
"created_at": app.CreatedAt,
|
|
"updated_at": app.UpdatedAt,
|
|
})
|
|
}
|
|
|
|
// DeleteApp handles DELETE /api/v1/apps/{appId}
|
|
func (h *AppHandler) DeleteApp(w http.ResponseWriter, r *http.Request) {
|
|
appID := chi.URLParam(r, "appId")
|
|
id, err := parseUUID(appID)
|
|
if err != nil {
|
|
respondError(w, http.StatusBadRequest, "Invalid app ID", nil)
|
|
return
|
|
}
|
|
|
|
if err := h.DB.DeleteApp(r.Context(), id); err != nil {
|
|
respondError(w, http.StatusInternalServerError, "Failed to delete app", nil)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// ChannelHandler handles channel-related API endpoints.
|
|
type ChannelHandler struct {
|
|
DB *db.DB
|
|
}
|
|
|
|
// GetChannels handles GET /api/v1/apps/{appId}/channels
|
|
func (h *ChannelHandler) GetChannels(w http.ResponseWriter, r *http.Request) {
|
|
appID := chi.URLParam(r, "appId")
|
|
id, err := parseUUID(appID)
|
|
if err != nil {
|
|
respondError(w, http.StatusBadRequest, "Invalid app ID", nil)
|
|
return
|
|
}
|
|
|
|
channels, err := h.DB.GetChannelsByAppID(r.Context(), id)
|
|
if err != nil {
|
|
respondError(w, http.StatusInternalServerError, "Failed to fetch channels", nil)
|
|
return
|
|
}
|
|
|
|
var result []models.Channel
|
|
for _, c := range channels {
|
|
result = append(result, models.Channel{
|
|
ID: c.ID,
|
|
AppID: c.AppID,
|
|
Name: c.Name,
|
|
})
|
|
}
|
|
|
|
respondJSON(w, http.StatusOK, result)
|
|
}
|
|
|
|
// CreateChannel handles POST /api/v1/apps/{appId}/channels
|
|
func (h *ChannelHandler) CreateChannel(w http.ResponseWriter, r *http.Request) {
|
|
appID := chi.URLParam(r, "appId")
|
|
id, err := parseUUID(appID)
|
|
if err != nil {
|
|
respondError(w, http.StatusBadRequest, "Invalid app ID", nil)
|
|
return
|
|
}
|
|
|
|
var req models.CreateChannelRequest
|
|
if err := decodeJSON(r, &req); err != nil {
|
|
respondError(w, http.StatusBadRequest, "Invalid request body", nil)
|
|
return
|
|
}
|
|
|
|
channel, err := h.DB.CreateChannel(r.Context(), id, req.Channel)
|
|
if err != nil {
|
|
respondError(w, http.StatusConflict, "Channel already exists", strPtr(err.Error()))
|
|
return
|
|
}
|
|
|
|
respondJSON(w, http.StatusCreated, models.Channel{
|
|
ID: channel.ID,
|
|
AppID: channel.AppID,
|
|
Name: channel.Name,
|
|
})
|
|
}
|
|
|
|
// OrganizationHandler handles organization-related API endpoints.
|
|
type OrganizationHandler struct {
|
|
DB *db.DB
|
|
}
|
|
|
|
// GetOrganizations handles GET /api/v1/organizations
|
|
func (h *OrganizationHandler) GetOrganizations(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.GetClaims(r)
|
|
if claims == nil {
|
|
respondError(w, http.StatusUnauthorized, "Unauthorized", nil)
|
|
return
|
|
}
|
|
|
|
orgs, err := h.DB.GetOrganizationsByUserID(r.Context(), claims.UserID)
|
|
if err != nil {
|
|
respondError(w, http.StatusInternalServerError, "Failed to fetch organizations", nil)
|
|
return
|
|
}
|
|
|
|
var result []models.OrganizationMembership
|
|
for _, o := range orgs {
|
|
result = append(result, models.OrganizationMembership{
|
|
Organization: models.Organization{
|
|
ID: o.OrgID,
|
|
Name: o.OrgName,
|
|
Type: o.OrgType,
|
|
},
|
|
Role: o.Role,
|
|
})
|
|
}
|
|
|
|
respondJSON(w, http.StatusOK, map[string]interface{}{"organizations": result})
|
|
}
|