package handlers import ( "fmt" "github.com/NdoleStudio/httpsms/pkg/requests" "github.com/NdoleStudio/httpsms/pkg/validators" "github.com/davecgh/go-spew/spew" "github.com/NdoleStudio/httpsms/pkg/services" "github.com/NdoleStudio/httpsms/pkg/telemetry" "github.com/gofiber/fiber/v3" "github.com/palantir/stacktrace" ) // UserHandler handles user http requests. type UserHandler struct { handler logger telemetry.Logger tracer telemetry.Tracer validator *validators.UserHandlerValidator service *services.UserService } // NewUserHandler creates a new UserHandler func NewUserHandler( logger telemetry.Logger, tracer telemetry.Tracer, validator *validators.UserHandlerValidator, service *services.UserService, ) (h *UserHandler) { return &UserHandler{ logger: logger.WithService(fmt.Sprintf("%T", h)), tracer: tracer, validator: validator, service: service, } } // RegisterRoutes registers the routes for the MessageHandler func (h *UserHandler) RegisterRoutes(router fiber.Router, middlewares ...fiber.Handler) { h.register(router, fiber.MethodGet, "/v1/users/me", middlewares, h.Show) h.register(router, fiber.MethodPut, "/v1/users/me", middlewares, h.Update) h.register(router, fiber.MethodDelete, "/v1/users/me", middlewares, h.Delete) h.register(router, fiber.MethodDelete, "/v1/users/:userID/api-keys", middlewares, h.DeleteAPIKey) h.register(router, fiber.MethodPut, "/v1/users/:userID/notifications", middlewares, h.UpdateNotifications) h.register(router, fiber.MethodGet, "/v1/users/subscription-update-url", middlewares, h.subscriptionUpdateURL) h.register(router, fiber.MethodDelete, "/v1/users/subscription", middlewares, h.cancelSubscription) h.register(router, fiber.MethodGet, "/v1/users/subscription/payments", middlewares, h.subscriptionPayments) h.register(router, fiber.MethodPost, "/v1/users/subscription/invoices/:subscriptionInvoiceID", middlewares, h.subscriptionInvoice) } // Show returns an entities.User // @Summary Get current user // @Description Get details of the currently authenticated user // @Security ApiKeyAuth // @Tags Users // @Accept json // @Produce json // @Success 200 {object} responses.UserResponse // @Failure 400 {object} responses.BadRequest // @Failure 401 {object} responses.Unauthorized // @Failure 422 {object} responses.UnprocessableEntity // @Failure 500 {object} responses.InternalServerError // @Router /users/me [get] func (h *UserHandler) Show(c fiber.Ctx) error { ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger) defer span.End() authUser := h.userFromContext(c) user, err := h.service.Get(ctx, c.OriginalURL(), authUser) if err != nil { msg := fmt.Sprintf("cannot get user with ID [%s]", authUser.ID) ctxLogger.Error(stacktrace.Propagate(err, msg)) return h.responseInternalServerError(c) } return h.responseOK(c, "user fetched successfully", user) } // Update an entities.User // @Summary Update a user // @Description Updates the details of the currently authenticated user // @Security ApiKeyAuth // @Tags Users // @Accept json // @Produce json // @Param payload body requests.UserUpdate true "Payload of user details to update" // @Success 200 {object} responses.PhoneResponse // @Failure 400 {object} responses.BadRequest // @Failure 401 {object} responses.Unauthorized // @Failure 422 {object} responses.UnprocessableEntity // @Failure 500 {object} responses.InternalServerError // @Router /users/me [put] func (h *UserHandler) Update(c fiber.Ctx) error { ctx, span := h.tracer.StartFromFiberCtx(c) defer span.End() ctxLogger := h.tracer.CtxLogger(h.logger, span) var request requests.UserUpdate if err := c.Bind().Body(&request); err != nil { msg := fmt.Sprintf("cannot marshall params [%s] into %T", c.OriginalURL(), request) ctxLogger.Warn(stacktrace.Propagate(err, msg)) return h.responseBadRequest(c, err) } if errors := h.validator.ValidateUpdate(ctx, request.Sanitize()); len(errors) != 0 { msg := fmt.Sprintf("validation errors [%s], while updating user [%+#v]", spew.Sdump(errors), request) ctxLogger.Warn(stacktrace.NewError(msg)) return h.responseUnprocessableEntity(c, errors, "validation errors while updating user") } user, err := h.service.Update(ctx, c.OriginalURL(), h.userFromContext(c), request.ToUpdateParams()) if err != nil { msg := fmt.Sprintf("cannot update user with params [%+#v]", request) ctxLogger.Error(stacktrace.Propagate(err, msg)) return h.responseInternalServerError(c) } return h.responseOK(c, "user updated successfully", user) } // Delete an entities.User // @Summary Delete a user // @Description Deletes the currently authenticated user together with all their data. // @Security ApiKeyAuth // @Tags Users // @Accept json // @Produce json // @Success 201 {object} responses.NoContent // @Failure 401 {object} responses.Unauthorized // @Failure 500 {object} responses.InternalServerError // @Router /users/me [delete] func (h *UserHandler) Delete(c fiber.Ctx) error { ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger) defer span.End() if err := h.service.Delete(ctx, c.OriginalURL(), h.userIDFomContext(c)); err != nil { msg := fmt.Sprintf("cannot delete user user with ID [%s]", h.userIDFomContext(c)) ctxLogger.Error(stacktrace.Propagate(err, msg)) return h.responseInternalServerError(c) } return h.responseNoContent(c, "user deleted successfully") } // UpdateNotifications an entities.User // @Summary Update notification settings // @Description Update the email notification settings for a user // @Security ApiKeyAuth // @Tags Users // @Accept json // @Produce json // @Param userID path string true "ID of the user to update" default(32343a19-da5e-4b1b-a767-3298a73703ca) // @Param payload body requests.UserNotificationUpdate true "User notification details to update" // @Success 200 {object} responses.UserResponse // @Failure 400 {object} responses.BadRequest // @Failure 401 {object} responses.Unauthorized // @Failure 422 {object} responses.UnprocessableEntity // @Failure 500 {object} responses.InternalServerError // @Router /users/{userID}/notifications [put] func (h *UserHandler) UpdateNotifications(c fiber.Ctx) error { ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger) defer span.End() var request requests.UserNotificationUpdate if err := c.Bind().Body(&request); err != nil { msg := fmt.Sprintf("cannot marshall params [%s] into %T", c.OriginalURL(), request) ctxLogger.Warn(stacktrace.Propagate(err, msg)) return h.responseBadRequest(c, err) } user, err := h.service.UpdateNotificationSettings(ctx, h.userIDFomContext(c), request.ToUserNotificationUpdateParams()) if err != nil { msg := fmt.Sprintf("cannot update notification for [%T] with ID [%s]", user, h.userIDFomContext(c)) ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))) return h.responseInternalServerError(c) } return h.responseOK(c, "user notification settings updated successfully", user) } // subscriptionUpdateURL returns the subscription update URL for the authenticated entities.User // @Summary Currently authenticated user subscription update URL // @Description Fetches the subscription URL of the authenticated user. // @Security ApiKeyAuth // @Tags Users // @Produce json // @Success 200 {object} responses.OkString // @Failure 400 {object} responses.BadRequest // @Failure 401 {object} responses.Unauthorized // @Failure 422 {object} responses.UnprocessableEntity // @Failure 500 {object} responses.InternalServerError // @Router /users/subscription-update-url [get] func (h *UserHandler) subscriptionUpdateURL(c fiber.Ctx) error { ctx, span := h.tracer.StartFromFiberCtx(c) defer span.End() ctxLogger := h.tracer.CtxLogger(h.logger, span) authUser := h.userFromContext(c) url, err := h.service.GetSubscriptionUpdateURL(ctx, authUser.ID) if err != nil { msg := fmt.Sprintf("cannot get user with ID [%s]", authUser.ID) ctxLogger.Error(stacktrace.Propagate(err, msg)) return h.responseInternalServerError(c) } return h.responseOK(c, "Subscription update URL fetched successfully", url) } // cancelSubscription cancels the subscription for the authenticated entities.User // @Summary Cancel the user's subscription // @Description Cancel the subscription of the authenticated user. // @Security ApiKeyAuth // @Tags Users // @Produce json // @Success 200 {object} responses.NoContent // @Failure 400 {object} responses.BadRequest // @Failure 401 {object} responses.Unauthorized // @Failure 422 {object} responses.UnprocessableEntity // @Failure 500 {object} responses.InternalServerError // @Router /users/subscription [delete] func (h *UserHandler) cancelSubscription(c fiber.Ctx) error { ctx, span := h.tracer.StartFromFiberCtx(c) defer span.End() ctxLogger := h.tracer.CtxLogger(h.logger, span) authUser := h.userFromContext(c) err := h.service.InitiateSubscriptionCancel(ctx, authUser.ID) if err != nil { msg := fmt.Sprintf("cannot get user with ID [%s]", authUser.ID) ctxLogger.Error(stacktrace.Propagate(err, msg)) return h.responseInternalServerError(c) } return h.responseNoContent(c, "Subscription cancelled successfully") } // DeleteAPIKey rotates the API Key for a user // @Summary Rotate the user's API Key // @Description Rotate the user's API key in case the current API Key is compromised // @Security ApiKeyAuth // @Tags Users // @Accept json // @Produce json // @Param userID path string true "ID of the user to update" default(32343a19-da5e-4b1b-a767-3298a73703ca) // @Success 200 {object} responses.UserResponse // @Failure 400 {object} responses.BadRequest // @Failure 401 {object} responses.Unauthorized // @Failure 422 {object} responses.UnprocessableEntity // @Failure 500 {object} responses.InternalServerError // @Router /users/{userID}/api-keys [delete] func (h *UserHandler) DeleteAPIKey(c fiber.Ctx) error { ctx, span := h.tracer.StartFromFiberCtx(c) defer span.End() ctxLogger := h.tracer.CtxLogger(h.logger, span) if c.Params("userID") != string(h.userIDFomContext(c)) { return h.responseUnauthorized(c) } user, err := h.service.RotateAPIKey(ctx, c.OriginalURL(), h.userIDFomContext(c)) if err != nil { msg := fmt.Sprintf("cannot rotate the api key for [%T] with ID [%s]", user, h.userIDFomContext(c)) ctxLogger.Error(h.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))) return h.responseInternalServerError(c) } return h.responseOK(c, "API Key rotated successfully", user) } // subscriptionPayments returns the last 10 payments of the currently authenticated user // @Summary Get the last 10 subscription payments. // @Description Subscription payments are generated throughout the lifecycle of a subscription, typically there is one at the time of purchase and then one for each renewal. // @Security ApiKeyAuth // @Tags Users // @Accept json // @Produce json // @Success 200 {object} responses.UserSubscriptionPaymentsResponse // @Failure 400 {object} responses.BadRequest // @Failure 401 {object} responses.Unauthorized // @Failure 422 {object} responses.UnprocessableEntity // @Failure 500 {object} responses.InternalServerError // @Router /users/subscription/payments [get] func (h *UserHandler) subscriptionPayments(c fiber.Ctx) error { ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger) defer span.End() invoices, err := h.service.GetSubscriptionPayments(ctx, h.userIDFomContext(c)) if err != nil { msg := fmt.Sprintf("cannot get current subscription invoices for user [%s]", h.userFromContext(c)) ctxLogger.Error(stacktrace.Propagate(err, msg)) return h.responseInternalServerError(c) } return h.responseOK(c, "fetched subscription invoices billing usage", invoices) } // subscriptionInvoice generates an invoice for a given subscription invoice ID // @Summary Generate a subscription payment invoice // @Description Generates a new invoice PDF file for the given subscription payment with given parameters. // @Security ApiKeyAuth // @Tags Users // @Accept json // @Produce application/pdf // @Param payload body requests.UserPaymentInvoice true "Generate subscription payment invoice parameters" // @Param subscriptionInvoiceID path string true "ID of the subscription invoice to generate the PDF for" // @Success 200 {file} file // @Failure 400 {object} responses.BadRequest // @Failure 401 {object} responses.Unauthorized // @Failure 422 {object} responses.UnprocessableEntity // @Failure 500 {object} responses.InternalServerError // @Router /users/subscription/invoices/{subscriptionInvoiceID} [post] func (h *UserHandler) subscriptionInvoice(c fiber.Ctx) error { ctx, span, ctxLogger := h.tracer.StartFromFiberCtxWithLogger(c, h.logger) defer span.End() var request requests.UserPaymentInvoice if err := c.Bind().Body(&request); err != nil { msg := fmt.Sprintf("cannot marshall params [%s] into %T", c.Body(), request) ctxLogger.Warn(stacktrace.Propagate(err, msg)) return h.responseBadRequest(c, err) } request.SubscriptionInvoiceID = c.Params("subscriptionInvoiceID") if errors := h.validator.ValidatePaymentInvoice(ctx, h.userIDFomContext(c), request.Sanitize()); len(errors) != 0 { msg := fmt.Sprintf("validation errors [%s], while validating subscription payment invoice request [%s]", spew.Sdump(errors), c.Body()) ctxLogger.Warn(stacktrace.NewError(msg)) return h.responseUnprocessableEntity(c, errors, "validation errors while generating payment invoice") } data, err := h.service.GenerateReceipt(ctx, request.UserInvoiceGenerateParams(h.userIDFomContext(c))) if err != nil { msg := fmt.Sprintf("cannot generate receipt for invoice ID [%s] and user [%s]", request.SubscriptionInvoiceID, h.userFromContext(c)) ctxLogger.Error(stacktrace.Propagate(err, msg)) return h.responseInternalServerError(c) } c.Set(fiber.HeaderContentType, "application/pdf") c.Set(fiber.HeaderContentDisposition, fmt.Sprintf("attachment; filename=\"httpsms.com - %s.pdf\"", request.SubscriptionInvoiceID)) return c.SendStream(data) }