Files
shorebird-server/internal/api/handlers/apps.go
T
2026-06-12 04:22:58 +08:00

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})
}