diff --git a/scnserver/README.md b/scnserver/README.md index 1500a8e..d3cdad1 100644 --- a/scnserver/README.md +++ b/scnserver/README.md @@ -19,12 +19,16 @@ - Pagination for ListChannels / ListSubscriptions / ListClients / ListChannelSubscriptions / ListUserSubscriptions + - cannot open sqlite in dbbrowsr (cannot parse schema?) + ------------------------------------------------------------------------------------------------------------------------------- - in my script: use (backupname || hostname) for sendername ------------------------------------------------------------------------------------------------------------------------------- + - (?) use uuid ids (also prevents wrong-joins?) + - (?) default-priority for channels - (?) ack/read deliveries && return ack-count (? or not, how to query?) diff --git a/scnserver/api/handler/api.go b/scnserver/api/handler/api.go index 5565825..50c9be8 100644 --- a/scnserver/api/handler/api.go +++ b/scnserver/api/handler/api.go @@ -1268,14 +1268,15 @@ func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse { // @ID api-subscriptions-update // @Tags API-v2 // -// @Param uid path int true "UserID" -// @Param sid path int true "SubscriptionID" +// @Param uid path int true "UserID" +// @Param sid path int true "SubscriptionID" +// @Param post_data body handler.UpdateSubscription.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 404 {object} ginresp.apiError "subscription not found" -// @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 404 {object} ginresp.apiError "subscription not found" +// @Failure 500 {object} ginresp.apiError "internal server error" // // @Router /api/users/{uid}/subscriptions/{sid} [PATCH] func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse { @@ -1299,6 +1300,8 @@ func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse { return *permResp } + userid := *ctx.GetPermissionUserID() + subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID) if err == sql.ErrNoRows { return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) @@ -1306,11 +1309,14 @@ 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.SubscriberUserID != u.UserID { + if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != u.UserID { return ginresp.APIError(g, 404, apierr.SUBSCRIPTION_USER_MISMATCH, "Subscription not found", nil) } if b.Confirmed != nil { + if subscription.ChannelOwnerUserID != userid { + return ginresp.APIError(g, 401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) + } err = h.database.UpdateSubscriptionConfirmed(ctx, u.SubscriptionID, *b.Confirmed) if err != nil { return ginresp.APIError(g, 500, apierr.DATABASE_ERROR, "Failed to update subscription", err) diff --git a/scnserver/api/handler/common.go b/scnserver/api/handler/common.go index d9aeedd..4246cf1 100644 --- a/scnserver/api/handler/common.go +++ b/scnserver/api/handler/common.go @@ -69,7 +69,7 @@ func (h CommonHandler) Ping(g *gin.Context) ginresp.HTTPResponse { // DatabaseTest swaggerdoc // -// @Summary Check for a wroking database connection +// @Summary Check for a working database connection // @ID api-common-dbtest // @Tags Common // diff --git a/scnserver/api/router.go b/scnserver/api/router.go index 8065f98..0c37318 100644 --- a/scnserver/api/router.go +++ b/scnserver/api/router.go @@ -127,10 +127,10 @@ func (r *Router) Init(e *gin.Engine) { apiv2.GET("/users/:uid/channels/:cid/subscriptions", ginresp.Wrap(r.apiHandler.ListChannelSubscriptions)) apiv2.GET("/users/:uid/subscriptions", ginresp.Wrap(r.apiHandler.ListUserSubscriptions)) + apiv2.POST("/users/:uid/subscriptions", ginresp.Wrap(r.apiHandler.CreateSubscription)) apiv2.GET("/users/:uid/subscriptions/:sid", ginresp.Wrap(r.apiHandler.GetSubscription)) apiv2.DELETE("/users/:uid/subscriptions/:sid", ginresp.Wrap(r.apiHandler.CancelSubscription)) - apiv2.POST("/users/:uid/subscriptions", ginresp.Wrap(r.apiHandler.CreateSubscription)) - apiv2.PATCH("/users/:uid/subscriptions", ginresp.Wrap(r.apiHandler.UpdateSubscription)) + apiv2.PATCH("/users/:uid/subscriptions/:sid", ginresp.Wrap(r.apiHandler.UpdateSubscription)) apiv2.GET("/messages", ginresp.Wrap(r.apiHandler.ListMessages)) apiv2.GET("/messages/:mid", ginresp.Wrap(r.apiHandler.GetMessage)) diff --git a/scnserver/db/channels.go b/scnserver/db/channels.go index 32e7e9e..c371739 100644 --- a/scnserver/db/channels.go +++ b/scnserver/db/channels.go @@ -175,11 +175,11 @@ func (db *Database) ListChannelsByAccess(ctx TxContext, userid models.UserID, co return nil, err } - confCond := "" + confCond := "OR (sub.subscription_id IS NOT NULL)" if confirmed != nil && *confirmed { - confCond = "OR sub.confirmed = 1" + confCond = "OR (sub.subscription_id IS NOT NULL AND sub.confirmed = 1)" } else if confirmed != nil && !*confirmed { - confCond = "OR sub.confirmed = 0" + confCond = "OR (sub.subscription_id IS NOT NULL AND sub.confirmed = 0)" } rows, err := tx.Query(ctx, "SELECT channels.*, sub.* FROM channels LEFT JOIN subscriptions AS sub on channels.channel_id = sub.channel_id AND sub.subscriber_user_id = :subuid WHERE owner_user_id = :ouid "+confCond, sq.PP{ diff --git a/scnserver/swagger/swagger.json b/scnserver/swagger/swagger.json index 32b1b9d..bc93c7a 100644 --- a/scnserver/swagger/swagger.json +++ b/scnserver/swagger/swagger.json @@ -257,7 +257,7 @@ "tags": [ "Common" ], - "summary": "Check for a wroking database connection", + "summary": "Check for a working database connection", "operationId": "api-common-dbtest", "responses": { "200": { @@ -1742,7 +1742,7 @@ }, "/api/users/{uid}/subscriptions": { "get": { - "description": "The possible values for 'selector' are:\n- \"owner_all\" All subscriptions (confirmed/unconfirmed) with the user as owner (= subscriptions he can use to read channels)\n- \"owner_confirmed\" Confirmed subscriptions with the user as owner\n- \"owner_unconfirmed\" Unconfirmed (Pending) subscriptions with the user as owner\n- \"incoming_all\" All subscriptions (confirmed/unconfirmed) from other users to channels of this user (= incoming subscriptions and subscription requests)\n- \"incoming_confirmed\" Confirmed subscriptions from other users to channels of this user\n- \"incoming_unconfirmed\" Unconfirmed subscriptions from other users to channels of this user (= requests)", + "description": "The possible values for 'selector' are:\n- \"outgoing_all\" All subscriptions (confirmed/unconfirmed) with the user as subscriber (= subscriptions he can use to read channels)\n- \"outgoing_confirmed\" Confirmed subscriptions with the user as subscriber\n- \"outgoing_unconfirmed\" Unconfirmed (Pending) subscriptions with the user as subscriber\n- \"incoming_all\" All subscriptions (confirmed/unconfirmed) from other users to channels of this user (= incoming subscriptions and subscription requests)\n- \"incoming_confirmed\" Confirmed subscriptions from other users to channels of this user\n- \"incoming_unconfirmed\" Unconfirmed subscriptions from other users to channels of this user (= requests)", "tags": [ "API-v2" ], @@ -1758,9 +1758,9 @@ }, { "enum": [ - "owner_all", - "owner_confirmed", - "owner_unconfirmed", + "outgoing_all", + "outgoing_confirmed", + "outgoing_unconfirmed", "incoming_all", "incoming_confirmed", "incoming_unconfirmed" @@ -1987,6 +1987,14 @@ "name": "sid", "in": "path", "required": true + }, + { + "description": " ", + "name": "post_data", + "in": "body", + "schema": { + "$ref": "#/definitions/handler.UpdateSubscription.body" + } } ], "responses": { @@ -2410,11 +2418,6 @@ }, "handler.CreateSubscription.body": { "type": "object", - "required": [ - "channel_id", - "channel_internal_name", - "channel_owner_user_id" - ], "properties": { "channel_id": { "type": "integer" @@ -2738,6 +2741,14 @@ } } }, + "handler.UpdateSubscription.body": { + "type": "object", + "properties": { + "confirmed": { + "type": "boolean" + } + } + }, "handler.Upgrade.response": { "type": "object", "properties": { diff --git a/scnserver/swagger/swagger.yaml b/scnserver/swagger/swagger.yaml index fff7d6f..17eeecf 100644 --- a/scnserver/swagger/swagger.yaml +++ b/scnserver/swagger/swagger.yaml @@ -66,10 +66,6 @@ definitions: type: string channel_owner_user_id: type: integer - required: - - channel_id - - channel_internal_name - - channel_owner_user_id type: object handler.CreateUser.body: properties: @@ -277,6 +273,11 @@ definitions: user_key: type: string type: object + handler.UpdateSubscription.body: + properties: + confirmed: + type: boolean + type: object handler.Upgrade.response: properties: is_pro: @@ -695,7 +696,7 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/ginresp.apiError' - summary: Check for a wroking database connection + summary: Check for a working database connection tags: - Common /api/expand.php: @@ -1701,9 +1702,9 @@ paths: get: description: |- The possible values for 'selector' are: - - "owner_all" All subscriptions (confirmed/unconfirmed) with the user as owner (= subscriptions he can use to read channels) - - "owner_confirmed" Confirmed subscriptions with the user as owner - - "owner_unconfirmed" Unconfirmed (Pending) subscriptions with the user as owner + - "outgoing_all" All subscriptions (confirmed/unconfirmed) with the user as subscriber (= subscriptions he can use to read channels) + - "outgoing_confirmed" Confirmed subscriptions with the user as subscriber + - "outgoing_unconfirmed" Unconfirmed (Pending) subscriptions with the user as subscriber - "incoming_all" All subscriptions (confirmed/unconfirmed) from other users to channels of this user (= incoming subscriptions and subscription requests) - "incoming_confirmed" Confirmed subscriptions from other users to channels of this user - "incoming_unconfirmed" Unconfirmed subscriptions from other users to channels of this user (= requests) @@ -1716,9 +1717,9 @@ paths: type: integer - description: 'Filter subscriptions (default: owner_all)' enum: - - owner_all - - owner_confirmed - - owner_unconfirmed + - outgoing_all + - outgoing_confirmed + - outgoing_unconfirmed - incoming_all - incoming_confirmed - incoming_unconfirmed @@ -1872,6 +1873,11 @@ paths: name: sid required: true type: integer + - description: ' ' + in: body + name: post_data + schema: + $ref: '#/definitions/handler.UpdateSubscription.body' responses: "200": description: OK diff --git a/scnserver/test/channel_test.go b/scnserver/test/channel_test.go index e071a1a..4a8b9ff 100644 --- a/scnserver/test/channel_test.go +++ b/scnserver/test/channel_test.go @@ -150,6 +150,41 @@ func TestChannelNameNormalization(t *testing.T) { } +func TestListChannelsDefault(t *testing.T) { + ws, baseUrl, stop := tt.StartSimpleWebserver(t) + defer stop() + + data := tt.InitDefaultData(t, ws) + + type chanlist struct { + Channels []gin.H `json:"channels"` + } + + testdata := map[int][]string{ + 0: {"main", "chatting chamber", "unicôdé häll \U0001f92a", "promotions", "reminders"}, + 1: {"main", "private"}, + 2: {"main", "ü", "ö", "ä"}, + 3: {"main", "\U0001f5ff", "innovations", "reminders"}, + 4: {"main"}, + 5: {"main", "test1", "test2", "test3", "test4", "test5"}, + 6: {"main", "security", "lipsum"}, + 7: {"main"}, + 8: {"main"}, + 9: {"main", "manual@chan"}, + 10: {"main"}, + 11: {"promotions"}, + 12: {}, + 13: {}, + 14: {"main", "chan_self_subscribed", "chan_self_unsub"}, + 15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, + } + + for k, v := range testdata { + r0 := tt.RequestAuthGet[chanlist](t, data.User[k].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels", data.User[k].UID)) + tt.AssertMappedSet(t, fmt.Sprintf("%d->chanlist", k), v, r0.Channels, "internal_name") + } +} + func TestListChannelsOwned(t *testing.T) { ws, baseUrl, stop := tt.StartSimpleWebserver(t) defer stop() @@ -175,12 +210,12 @@ func TestListChannelsOwned(t *testing.T) { 11: {"promotions"}, 12: {}, 13: {}, - 14: {"main", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases - 15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases + 14: {"main", "chan_self_subscribed", "chan_self_unsub"}, + 15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, } for k, v := range testdata { - r0 := tt.RequestAuthGet[chanlist](t, data.User[k].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels", data.User[k].UID)) + r0 := tt.RequestAuthGet[chanlist](t, data.User[k].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels?selector=%s", data.User[k].UID, "owned")) tt.AssertMappedSet(t, fmt.Sprintf("%d->chanlist", k), v, r0.Channels, "internal_name") } } @@ -210,12 +245,12 @@ func TestListChannelsSubscribedAny(t *testing.T) { 11: {"promotions"}, 12: {}, 13: {}, - 14: {"main", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases - 15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases + 14: {"main", "chan_self_subscribed", "chan_other_request", "chan_other_accepted"}, + 15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, } for k, v := range testdata { - r0 := tt.RequestAuthGet[chanlist](t, data.User[k].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels", data.User[k].UID)) + r0 := tt.RequestAuthGet[chanlist](t, data.User[k].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels?selector=%s", data.User[k].UID, "subscribed_any")) tt.AssertMappedSet(t, fmt.Sprintf("%d->chanlist", k), v, r0.Channels, "internal_name") } } @@ -245,12 +280,12 @@ func TestListChannelsAllAny(t *testing.T) { 11: {"promotions"}, 12: {}, 13: {}, - 14: {"main", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases - 15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases + 14: {"main", "chan_self_subscribed", "chan_self_unsub", "chan_other_request", "chan_other_accepted"}, + 15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, } for k, v := range testdata { - r0 := tt.RequestAuthGet[chanlist](t, data.User[k].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels", data.User[k].UID)) + r0 := tt.RequestAuthGet[chanlist](t, data.User[k].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels?selector=%s", data.User[k].UID, "all_any")) tt.AssertMappedSet(t, fmt.Sprintf("%d->chanlist", k), v, r0.Channels, "internal_name") } } @@ -280,12 +315,12 @@ func TestListChannelsSubscribed(t *testing.T) { 11: {"promotions"}, 12: {}, 13: {}, - 14: {"main", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases - 15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases + 14: {"main", "chan_self_subscribed", "chan_other_accepted"}, + 15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, } for k, v := range testdata { - r0 := tt.RequestAuthGet[chanlist](t, data.User[k].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels", data.User[k].UID)) + r0 := tt.RequestAuthGet[chanlist](t, data.User[k].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels?selector=%s", data.User[k].UID, "subscribed")) tt.AssertMappedSet(t, fmt.Sprintf("%d->chanlist", k), v, r0.Channels, "internal_name") } } @@ -315,12 +350,12 @@ func TestListChannelsAll(t *testing.T) { 11: {"promotions"}, 12: {}, 13: {}, - 14: {"main", "chan_self_subscribed", "chan_self_unsub"}, //TODO these two have the interesting cases - 15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, //TODO these two have the interesting cases + 14: {"main", "chan_self_subscribed", "chan_self_unsub", "chan_other_accepted"}, + 15: {"main", "chan_other_nosub", "chan_other_request", "chan_other_accepted"}, } for k, v := range testdata { - r0 := tt.RequestAuthGet[chanlist](t, data.User[k].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels", data.User[k].UID)) + r0 := tt.RequestAuthGet[chanlist](t, data.User[k].AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/channels?selector=%s", data.User[k].UID, "all")) tt.AssertMappedSet(t, fmt.Sprintf("%d->chanlist", k), v, r0.Channels, "internal_name") } } diff --git a/scnserver/test/util/factory.go b/scnserver/test/util/factory.go index 9a77907..b8c70af 100644 --- a/scnserver/test/util/factory.go +++ b/scnserver/test/util/factory.go @@ -463,7 +463,9 @@ func doAcceptSub(t *testing.T, baseUrl string, user Userdat, subscriber Userdat, } } - RequestAuthDelete[Void](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/subscriptions/%v", user.UID, subdat["subscription_id"]), gin.H{}) + RequestAuthPatch[Void](t, user.AdminKey, baseUrl, fmt.Sprintf("/api/users/%d/subscriptions/%v", user.UID, subdat["subscription_id"]), gin.H{ + "confirmed": true, + }) }