Fix SQL unmarshalling of optional nested structs (LEFT JOIN)

This commit is contained in:
2022-12-22 12:43:40 +01:00
parent 0cb2a977a0
commit 0112d681ac
20 changed files with 403 additions and 135 deletions

View File

@@ -18,6 +18,7 @@ const (
BINDFAIL_QUERY_PARAM APIError = 1151
BINDFAIL_BODY_PARAM APIError = 1152
BINDFAIL_URI_PARAM APIError = 1153
INVALID_BODY_PARAM APIError = 1161
INVALID_ENUM_VALUE APIError = 1171
NO_TITLE APIError = 1201
@@ -29,12 +30,13 @@ const (
CHANNEL_TOO_LONG APIError = 1207
CHANNEL_NAME_WOULD_CHANGE APIError = 1207
USER_NOT_FOUND APIError = 1301
CLIENT_NOT_FOUND APIError = 1302
CHANNEL_NOT_FOUND APIError = 1303
SUBSCRIPTION_NOT_FOUND APIError = 1304
MESSAGE_NOT_FOUND APIError = 1305
USER_AUTH_FAILED APIError = 1311
USER_NOT_FOUND APIError = 1301
CLIENT_NOT_FOUND APIError = 1302
CHANNEL_NOT_FOUND APIError = 1303
SUBSCRIPTION_NOT_FOUND APIError = 1304
MESSAGE_NOT_FOUND APIError = 1305
SUBSCRIPTION_USER_MISMATCH APIError = 1306
USER_AUTH_FAILED APIError = 1311
NO_DEVICE_LINKED APIError = 1401

View File

@@ -2,7 +2,6 @@ package handler
import (
"blackforestbytes.com/simplecloudnotifier/api/apierr"
hl "blackforestbytes.com/simplecloudnotifier/api/apihighlight"
"blackforestbytes.com/simplecloudnotifier/api/ginresp"
"blackforestbytes.com/simplecloudnotifier/db"
"blackforestbytes.com/simplecloudnotifier/db/cursortoken"
@@ -501,8 +500,8 @@ func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse {
// @ID api-channels-list
// @Tags API-v2
//
// @Param uid path int true "UserID"
// @Param selector query string true "Filter channels (default: owned)" Enums(owned, subscribed, all, subscribed_any, all_any)
// @Param uid path int true "UserID"
// @Param selector query string false "Filter channels (default: owned)" Enums(owned, subscribed, all, subscribed_any, all_any)
//
// @Success 200 {object} handler.ListChannels.response
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
@@ -667,6 +666,10 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
return *permResp
}
if b.Name == "" {
return ginresp.APIError(g, 400, apierr.INVALID_BODY_PARAM, "Missing parameter: name", nil)
}
channelDisplayName := h.app.NormalizeChannelDisplayName(b.Name)
channelInternalName := h.app.NormalizeChannelInternalName(b.Name)
@@ -677,17 +680,17 @@ func (h APIHandler) CreateChannel(g *gin.Context) ginresp.HTTPResponse {
user, err := h.database.GetUser(ctx, u.UserID)
if err == sql.ErrNoRows {
return ginresp.SendAPIError(g, 400, apierr.USER_NOT_FOUND, hl.USER_ID, "User not found", nil)
return ginresp.APIError(g, 400, apierr.USER_NOT_FOUND, "User not found", nil)
}
if err != nil {
return ginresp.SendAPIError(g, 500, apierr.DATABASE_ERROR, hl.NONE, "Failed to query user", err)
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query user", err)
}
if len(channelDisplayName) > user.MaxChannelNameLength() {
return ginresp.SendAPIError(g, 400, apierr.CHANNEL_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
return ginresp.APIError(g, 400, apierr.CHANNEL_TOO_LONG, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
}
if len(channelInternalName) > user.MaxChannelNameLength() {
return ginresp.SendAPIError(g, 400, apierr.CHANNEL_TOO_LONG, hl.CHANNEL, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
return ginresp.APIError(g, 400, apierr.CHANNEL_TOO_LONG, fmt.Sprintf("Channel too long (max %d characters)", user.MaxChannelNameLength()), nil)
}
if channelExisting != nil {
@@ -915,27 +918,27 @@ func (h APIHandler) ListChannelMessages(g *gin.Context) ginresp.HTTPResponse {
// ListUserSubscriptions swaggerdoc
//
// @Summary List all subscriptions of a user (incoming/owned)
// // @Description The possible values for 'selector' are:
// // @Description - "owner_all" All subscriptions (confirmed/unconfirmed) with the user as owner (= subscriptions he can use to read channels)
// // @Description - "owner_confirmed" Confirmed subscriptions with the user as owner
// // @Description - "owner_unconfirmed" Unconfirmed (Pending) subscriptions with the user as owner
// // @Description - "incoming_all" All subscriptions (confirmed/unconfirmed) from other users to channels of this user (= incoming subscriptions and subscription requests)
// // @Description - "incoming_confirmed" Confirmed subscriptions from other users to channels of this user
// // @Description - "incoming_unconfirmed" Unconfirmed subscriptions from other users to channels of this user (= requests)
// //
// @ID api-user-subscriptions-list
// @Tags API-v2
// @Summary List all subscriptions of a user (incoming/owned)
// @Description The possible values for 'selector' are:
// @Description - "outgoing_all" All subscriptions (confirmed/unconfirmed) with the user as subscriber (= subscriptions he can use to read channels)
// @Description - "outgoing_confirmed" Confirmed subscriptions with the user as subscriber
// @Description - "outgoing_unconfirmed" Unconfirmed (Pending) subscriptions with the user as subscriber
// @Description - "incoming_all" All subscriptions (confirmed/unconfirmed) from other users to channels of this user (= incoming subscriptions and subscription requests)
// @Description - "incoming_confirmed" Confirmed subscriptions from other users to channels of this user
// @Description - "incoming_unconfirmed" Unconfirmed subscriptions from other users to channels of this user (= requests)
//
// @Param uid path int true "UserID"
// @Param selector query string true "Filter subscribptions (default: owner_all)" Enums(owner_all, owner_confirmed, owner_unconfirmed, incoming_all, incoming_confirmed, incoming_unconfirmed)
// @ID api-user-subscriptions-list
// @Tags API-v2
//
// @Success 200 {object} handler.ListUserSubscriptions.response
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 500 {object} ginresp.apiError "internal server error"
// @Param uid path int true "UserID"
// @Param selector query string true "Filter subscriptions (default: owner_all)" Enums(outgoing_all, outgoing_confirmed, outgoing_unconfirmed, incoming_all, incoming_confirmed, incoming_unconfirmed)
//
// @Router /api/users/{uid}/subscriptions [GET]
// @Success 200 {object} handler.ListUserSubscriptions.response
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 500 {object} ginresp.apiError "internal server error"
//
// @Router /api/users/{uid}/subscriptions [GET]
func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid"`
@@ -964,48 +967,48 @@ func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
var res []models.Subscription
var err error
if sel == "owner_all" {
res, err = h.database.ListSubscriptionsByOwner(ctx, u.UserID, nil)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
} else if sel == "owner_confirmed" {
res, err = h.database.ListSubscriptionsByOwner(ctx, u.UserID, langext.Ptr(true))
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
} else if sel == "owner_unconfirmed" {
res, err = h.database.ListSubscriptionsByOwner(ctx, u.UserID, langext.Ptr(false))
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
} else if sel == "incoming_all" {
if sel == "outgoing_all" {
res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, nil)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
} else if sel == "incoming_confirmed" {
} else if sel == "outgoing_confirmed" {
res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, langext.Ptr(true))
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
} else if sel == "incoming_unconfirmed" {
} else if sel == "outgoing_unconfirmed" {
res, err = h.database.ListSubscriptionsBySubscriber(ctx, u.UserID, langext.Ptr(false))
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
} else if sel == "incoming_all" {
res, err = h.database.ListSubscriptionsByChannelOwner(ctx, u.UserID, nil)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
} else if sel == "incoming_confirmed" {
res, err = h.database.ListSubscriptionsByChannelOwner(ctx, u.UserID, langext.Ptr(true))
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
} else if sel == "incoming_unconfirmed" {
res, err = h.database.ListSubscriptionsByChannelOwner(ctx, u.UserID, langext.Ptr(false))
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscriptions", err)
}
} else {
return ginresp.APIError(g, 400, apierr.INVALID_ENUM_VALUE, "Invalid value for the [selector] parameter", nil)
@@ -1111,9 +1114,8 @@ func (h APIHandler) GetSubscription(g *gin.Context) ginresp.HTTPResponse {
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
}
if subscription.SubscriberUserID != u.UserID {
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != u.UserID {
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", nil)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, subscription.JSON()))
@@ -1159,9 +1161,8 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse {
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
}
if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != u.UserID {
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", nil)
}
err = h.database.DeleteSubscription(ctx, u.SubscriptionID)
@@ -1174,27 +1175,29 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse {
// CreateSubscription swaggerdoc
//
// @Summary Creare/Request a subscription
// @ID api-subscriptions-create
// @Tags API-v2
// @Summary Create/Request a subscription
// @Description Either [channel_owner_user_id, channel_internal_name] or [channel_id] must be supplied in the request body
// @ID api-subscriptions-create
// @Tags API-v2
//
// @Param uid path int true "UserID"
// @Param query_data query handler.CreateSubscription.query false " "
// @Param post_data body handler.CreateSubscription.body false " "
// @Param uid path int true "UserID"
// @Param query_data query handler.CreateSubscription.query false " "
// @Param post_data body handler.CreateSubscription.body false " "
//
// @Success 200 {object} models.SubscriptionJSON
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 500 {object} ginresp.apiError "internal server error"
// @Success 200 {object} models.SubscriptionJSON
// @Failure 400 {object} ginresp.apiError "supplied values/parameters cannot be parsed / are invalid"
// @Failure 401 {object} ginresp.apiError "user is not authorized / has missing permissions"
// @Failure 500 {object} ginresp.apiError "internal server error"
//
// @Router /api/users/{uid}/subscriptions [POST]
// @Router /api/users/{uid}/subscriptions [POST]
func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse {
type uri struct {
UserID models.UserID `uri:"uid"`
}
type body struct {
ChannelOwnerUserID models.UserID `form:"channel_owner_user_id" binding:"required"`
Channel string `form:"channel_name" binding:"required"`
ChannelOwnerUserID *models.UserID `json:"channel_owner_user_id"`
ChannelInternalName *string `json:"channel_internal_name"`
ChannelID *models.ChannelID `json:"channel_id"`
}
type query struct {
ChanSubscribeKey *string `json:"chan_subscribe_key" form:"chan_subscribe_key"`
@@ -1213,21 +1216,45 @@ func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse {
return *permResp
}
channelInternalName := h.app.NormalizeChannelInternalName(b.Channel)
var channel models.Channel
if b.ChannelOwnerUserID != nil && b.ChannelInternalName != nil && b.ChannelID == nil {
channelInternalName := h.app.NormalizeChannelInternalName(*b.ChannelInternalName)
outchannel, err := h.database.GetChannelByName(ctx, *b.ChannelOwnerUserID, channelInternalName)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
if outchannel == nil {
return ginresp.APIError(g, 400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
}
channel = *outchannel
} else if b.ChannelOwnerUserID == nil && b.ChannelInternalName == nil && b.ChannelID != nil {
outchannel, err := h.database.GetChannelByID(ctx, *b.ChannelID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
if outchannel == nil {
return ginresp.APIError(g, 400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
}
channel = *outchannel
} else {
return ginresp.APIError(g, 400, apierr.INVALID_BODY_PARAM, "Must either supply [channel_owner_user_id, channel_internal_name] or [channel_id]", nil)
channel, err := h.database.GetChannelByName(ctx, b.ChannelOwnerUserID, channelInternalName)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
if channel == nil {
return ginresp.APIError(g, 400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
}
if channel.OwnerUserID != u.UserID && (q.ChanSubscribeKey == nil || *q.ChanSubscribeKey != channel.SubscribeKey) {
ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
}
sub, err := h.database.CreateSubscription(ctx, u.UserID, *channel, channel.OwnerUserID == u.UserID)
sub, err := h.database.CreateSubscription(ctx, u.UserID, channel, channel.OwnerUserID == u.UserID)
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to create subscription", err)
}
@@ -1279,9 +1306,8 @@ func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse {
if err != nil {
return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
}
if subscription.ChannelOwnerUserID != u.UserID {
return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
if subscription.SubscriberUserID != u.UserID {
return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", nil)
}
if b.Confirmed != nil {

View File

@@ -124,7 +124,7 @@ func (h CommonHandler) Health(g *gin.Context) ginresp.HTTPResponse {
_, libVersionNumber, _ := sqlite3.Version()
if libVersionNumber < 3039000 {
ginresp.InternalError(errors.New("sqlite version too low"))
return ginresp.InternalError(errors.New("sqlite version too low"))
}
err := h.app.Database.Ping(ctx)