- Added device token management API for mobile devices, including registration, unregistration, and listing of devices. - Implemented push notification functionality using FCM for Android and APNS for iOS. - Introduced new endpoints for device registration and management in the devices API. - Enhanced the configuration to support mobile push notifications with optional credentials for FCM and APNS. - Updated database schema to include a new table for storing device tokens. - Added integration tests for device management and push notification features.
181 lines
5.1 KiB
Go
181 lines
5.1 KiB
Go
package devices
|
|
|
|
import (
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"github.com/ultisuite/ulti-backend/internal/api/apiresponse"
|
|
"github.com/ultisuite/ulti-backend/internal/api/apivalidate"
|
|
"github.com/ultisuite/ulti-backend/internal/api/middleware"
|
|
)
|
|
|
|
const maxRequestBody = 16 << 10
|
|
|
|
// Handler exposes mobile device token registration endpoints.
|
|
type Handler struct {
|
|
svc *Service
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func NewHandler(db *pgxpool.Pool) *Handler {
|
|
return &Handler{
|
|
svc: NewService(db),
|
|
logger: slog.Default().With("component", "devices-api"),
|
|
}
|
|
}
|
|
|
|
func (h *Handler) Routes() chi.Router {
|
|
r := chi.NewRouter()
|
|
r.Get("/", h.List)
|
|
r.Post("/register", h.Register)
|
|
r.Post("/unregister", h.Unregister)
|
|
r.Delete("/{id}", h.Delete)
|
|
return r
|
|
}
|
|
|
|
type registerRequest struct {
|
|
Platform string `json:"platform"`
|
|
App string `json:"app"`
|
|
PushToken string `json:"push_token"`
|
|
DeviceID string `json:"device_id"`
|
|
}
|
|
|
|
type unregisterRequest struct {
|
|
PushToken string `json:"push_token"`
|
|
}
|
|
|
|
func (h *Handler) Register(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
if claims == nil {
|
|
apiresponse.WriteError(w, r, http.StatusUnauthorized, apiresponse.CodeAuthUnauthorized, "unauthorized", nil)
|
|
return
|
|
}
|
|
|
|
var req registerRequest
|
|
if err := apivalidate.DecodeJSON(w, r, maxRequestBody, &req); err != nil {
|
|
return
|
|
}
|
|
|
|
platform := strings.TrimSpace(strings.ToLower(req.Platform))
|
|
app := strings.TrimSpace(req.App)
|
|
pushToken := strings.TrimSpace(req.PushToken)
|
|
deviceID := strings.TrimSpace(req.DeviceID)
|
|
|
|
if verr := validateRegister(platform, app, pushToken); verr != nil {
|
|
apivalidate.WriteValidationError(w, r, verr)
|
|
return
|
|
}
|
|
|
|
id, err := h.svc.Register(r.Context(), claims.Sub, platform, app, pushToken, deviceID)
|
|
if err != nil {
|
|
if errors.Is(err, ErrUserNotFound) {
|
|
apiresponse.WriteError(w, r, http.StatusNotFound, apiresponse.CodeNotFound, "user not found", nil)
|
|
return
|
|
}
|
|
h.logger.Error("register device", "error", err, "sub", claims.Sub)
|
|
apivalidate.WriteInternal(w, r)
|
|
return
|
|
}
|
|
apiresponse.WriteJSON(w, http.StatusOK, map[string]any{"id": id})
|
|
}
|
|
|
|
func (h *Handler) Unregister(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
if claims == nil {
|
|
apiresponse.WriteError(w, r, http.StatusUnauthorized, apiresponse.CodeAuthUnauthorized, "unauthorized", nil)
|
|
return
|
|
}
|
|
|
|
var req unregisterRequest
|
|
if err := apivalidate.DecodeJSON(w, r, maxRequestBody, &req); err != nil {
|
|
return
|
|
}
|
|
pushToken := strings.TrimSpace(req.PushToken)
|
|
if pushToken == "" {
|
|
apivalidate.WriteValidationError(w, r, apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "push_token", Message: "required",
|
|
}))
|
|
return
|
|
}
|
|
|
|
if err := h.svc.UnregisterByToken(r.Context(), claims.Sub, pushToken); err != nil {
|
|
if errors.Is(err, ErrDeviceNotFound) || errors.Is(err, ErrUserNotFound) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
h.logger.Error("unregister device", "error", err, "sub", claims.Sub)
|
|
apivalidate.WriteInternal(w, r)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
if claims == nil {
|
|
apiresponse.WriteError(w, r, http.StatusUnauthorized, apiresponse.CodeAuthUnauthorized, "unauthorized", nil)
|
|
return
|
|
}
|
|
id := strings.TrimSpace(chi.URLParam(r, "id"))
|
|
if id == "" {
|
|
apivalidate.WriteValidationError(w, r, apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "id", Message: "required",
|
|
}))
|
|
return
|
|
}
|
|
|
|
if err := h.svc.UnregisterByID(r.Context(), claims.Sub, id); err != nil {
|
|
if errors.Is(err, ErrDeviceNotFound) || errors.Is(err, ErrUserNotFound) {
|
|
apivalidate.WriteNotFound(w, r, "device token not found")
|
|
return
|
|
}
|
|
h.logger.Error("delete device", "error", err, "sub", claims.Sub)
|
|
apivalidate.WriteInternal(w, r)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (h *Handler) List(w http.ResponseWriter, r *http.Request) {
|
|
claims := middleware.ClaimsFromContext(r.Context())
|
|
if claims == nil {
|
|
apiresponse.WriteError(w, r, http.StatusUnauthorized, apiresponse.CodeAuthUnauthorized, "unauthorized", nil)
|
|
return
|
|
}
|
|
list, err := h.svc.List(r.Context(), claims.Sub)
|
|
if err != nil {
|
|
if errors.Is(err, ErrUserNotFound) {
|
|
apiresponse.WriteJSON(w, http.StatusOK, map[string]any{"devices": []Device{}})
|
|
return
|
|
}
|
|
h.logger.Error("list devices", "error", err, "sub", claims.Sub)
|
|
apivalidate.WriteInternal(w, r)
|
|
return
|
|
}
|
|
apiresponse.WriteJSON(w, http.StatusOK, map[string]any{"devices": list})
|
|
}
|
|
|
|
func validateRegister(platform, app, pushToken string) *apivalidate.ValidationError {
|
|
if platform != "ios" && platform != "android" {
|
|
return apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "platform", Message: "must be 'ios' or 'android'",
|
|
})
|
|
}
|
|
if app == "" {
|
|
return apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "app", Message: "required",
|
|
})
|
|
}
|
|
if pushToken == "" {
|
|
return apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "push_token", Message: "required",
|
|
})
|
|
}
|
|
return nil
|
|
}
|