diff --git a/doc/api.oa3 b/doc/api.oa3 index db6bc0b9..acd8b5d5 100644 --- a/doc/api.oa3 +++ b/doc/api.oa3 @@ -1894,6 +1894,35 @@ paths: $ref: '#/components/schemas/Subject' /content/channels/{channelId}: + get: + tags: + - content + description: Get details of channel. + operationId: get-channel + security: + - bearerAuth: [] + parameters: + - name: channelId + in: path + description: specified channel id + required: true + schema: + type: string + responses: + '200': + description: success + content: + application/json: + schema: + $ref: '#/components/schemas/Channel' + '401': + description: invalid password + '404': + description: channel not found + '410': + description: account disabled + '500': + description: internal server error delete: tags: - content @@ -1919,37 +1948,6 @@ paths: description: account disabled '500': description: internal server error - - /content/channels/{channelId}/detail: - get: - tags: - - content - description: Get detail object of channel. - operationId: get-channel-detail - security: - - bearerAuth: [] - parameters: - - name: channelId - in: path - description: specified channel id - required: true - schema: - type: string - responses: - '200': - description: success - content: - application/json: - schema: - $ref: '#/components/schemas/ChannelDetail' - '401': - description: invalid password - '404': - description: channel not found - '410': - description: account disabled - '500': - description: internal server error /content/channels/{channelId}/subject/{field}: get: @@ -2247,6 +2245,41 @@ paths: $ref: '#/components/schemas/Subject' /content/channels/{channelId}/topics/{topicId}: + get: + tags: + - content + description: Get full object of topic. + operationId: get-channel-topic + security: + - bearerAuth: [] + parameters: + - name: channelId + in: path + description: specified channel id + required: true + schema: + type: string + - name: topicId + in: path + description: specified topic id + required: true + schema: + type: string + responses: + '200': + description: success + content: + application/json: + schema: + $ref: '#/components/schemas/Topic' + '401': + description: invalid password + '404': + description: channel not found + '410': + description: account disabled + '500': + description: internal server error delete: tags: - content @@ -2303,6 +2336,10 @@ paths: responses: '200': description: success + content: + application/json: + schema: + $ref: '#/components/schemas/TopicDetail' '401': description: invalid password '404': @@ -2312,12 +2349,12 @@ paths: '500': description: internal server error - /content/channels/{channelId}/topics/{topicId}/size: + /content/channels/{channelId}/topics/{topicId}/count: get: tags: - content - description: Get detail object of topic. - operationId: get-channel-topic-size + description: Get count of associated taghs. + operationId: get-channel-topic-count security: - bearerAuth: [] parameters: @@ -2336,6 +2373,10 @@ paths: responses: '200': description: success + content: + application/json: + schema: + $ref: '#/components/schemas/TagCount' '401': description: invalid password '404': @@ -3236,7 +3277,7 @@ components: topicDetail: $ref: '#/components/schemas/TopicDetail' topicTags:: - $ref: '#/components/schemas/TopicTags' + $ref: '#/components/schemas/TagCount' TopicDetail: type: object @@ -3264,16 +3305,16 @@ components: type: string enum: [ unconfirmed, confirmed, complete, error ] - TopicTags: + TagCount: type: object required: - - tagCount - - tagUpdated + - count + - updated properties: - tagCount: + count: type: integer format: int32 - tagUpdated: + updated: type: integer format: int64 diff --git a/net/server/internal/api_addChannelTopic.go b/net/server/internal/api_addChannelTopic.go index e5c55c1f..14580e7f 100644 --- a/net/server/internal/api_addChannelTopic.go +++ b/net/server/internal/api_addChannelTopic.go @@ -1,81 +1,34 @@ package databag import ( - "errors" "net/http" "gorm.io/gorm" - "github.com/gorilla/mux" "github.com/google/uuid" "databag/internal/store" ) func AddChannelTopic(w http.ResponseWriter, r *http.Request) { - // scan parameters - params := mux.Vars(r) - channelId := params["channelId"] - var subject Subject if err := ParseRequest(r, w, &subject); err != nil { ErrResponse(w, http.StatusBadRequest, err) return } - var guid string - var act *store.Account - tokenType := r.Header.Get("TokenType") - if tokenType == APP_TOKENAPP { - account, code, err := BearerAppToken(r, false); - if err != nil { - ErrResponse(w, code, err) - return - } - act = account - guid = account.Guid - } else if tokenType == APP_TOKENCONTACT { - card, code, err := BearerContactToken(r, true) - if err != nil { - ErrResponse(w, code, err) - return - } - act = &card.Account - guid = card.Guid - } else { - ErrResponse(w, http.StatusBadRequest, errors.New("unknown token type")) + channelSlot, guid, err, code := getChannelSlot(r, false) + if err != nil { + ErrResponse(w, code, err) return } - - // load channel - var channelSlot store.ChannelSlot - if err := store.DB.Preload("Channel.Cards").Preload("Channel.Groups.Cards").Where("account_id = ? AND channel_slot_id = ?", act.ID, channelId).First(&channelSlot).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - ErrResponse(w, http.StatusNotFound, err) - } else { - ErrResponse(w, http.StatusInternalServerError, err) - } - return - } - if channelSlot.Channel == nil { - ErrResponse(w, http.StatusNotFound, errors.New("referenced empty channel")) - return - } - - // check if a member - if tokenType == APP_TOKENCONTACT { - if !isMember(guid, channelSlot.Channel.Cards) { - ErrResponse(w, http.StatusUnauthorized, errors.New("not a member of channel")) - return - } - } + act := &channelSlot.Account topicSlot := &store.TopicSlot{} - err := store.DB.Transaction(func(tx *gorm.DB) error { + err = store.DB.Transaction(func(tx *gorm.DB) error { // add new record topic := &store.Topic{} topic.Data = subject.Data topic.DataType = subject.DataType - topic.Channel = channelSlot.Channel topic.TagCount = 0 topic.Guid = guid topic.DetailRevision = act.ChannelRevision + 1 @@ -86,6 +39,7 @@ func AddChannelTopic(w http.ResponseWriter, r *http.Request) { } topicSlot.TopicSlotId = uuid.New().String() topicSlot.AccountID = act.ID + topicSlot.ChannelID = channelSlot.Channel.ID topicSlot.TopicID = topic.ID topicSlot.Revision = act.ChannelRevision + 1 topicSlot.Topic = topic @@ -97,7 +51,7 @@ func AddChannelTopic(w http.ResponseWriter, r *http.Request) { if res := tx.Model(&channelSlot).Update("revision", act.ChannelRevision + 1).Error; res != nil { return res } - if res := tx.Model(&act).Update("channel_revision", act.ChannelRevision + 1).Error; res != nil { + if res := tx.Model(act).Update("channel_revision", act.ChannelRevision + 1).Error; res != nil { return res } return nil @@ -122,15 +76,7 @@ func AddChannelTopic(w http.ResponseWriter, r *http.Request) { for _, card := range cards { SetContactChannelNotification(act, &card) } - WriteResponse(w, getTopicModel(topicSlot, true, true)) + WriteResponse(w, getTopicModel(topicSlot)) } -func isMember(guid string, cards []store.Card) bool { - for _, card := range cards { - if guid == card.Guid { - return true - } - } - return false -} diff --git a/net/server/internal/api_content.go b/net/server/internal/api_content.go index aedfc0b0..798b8164 100644 --- a/net/server/internal/api_content.go +++ b/net/server/internal/api_content.go @@ -33,21 +33,6 @@ func GetChannelAssets(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } -func GetChannelSize(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) -} - -func GetChannelTopicDetail(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) -} - -func GetChannelTopicSize(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) -} - func GetChannelTopicSubjectField(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(http.StatusOK) diff --git a/net/server/internal/api_getChannelTopic.go b/net/server/internal/api_getChannelTopic.go new file mode 100644 index 00000000..65cf3aa3 --- /dev/null +++ b/net/server/internal/api_getChannelTopic.go @@ -0,0 +1,122 @@ +package databag + +import ( + "errors" + "net/http" + "gorm.io/gorm" + "github.com/gorilla/mux" + "databag/internal/store" +) + +func GetChannelTopic(w http.ResponseWriter, r *http.Request) { + + // scan parameters + params := mux.Vars(r) + topicId := params["topicId"] + + var subject Subject + if err := ParseRequest(r, w, &subject); err != nil { + ErrResponse(w, http.StatusBadRequest, err) + return + } + + channelSlot, _, err, code := getChannelSlot(r, false) + if err != nil { + ErrResponse(w, code, err) + return + } + + // load topic + var topicSlot store.TopicSlot + if err = store.DB.Where("channel_id = ? AND topic_slot_id = ?", channelSlot.Channel.ID, topicId).First(&topicSlot).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + code = http.StatusNotFound + } else { + code = http.StatusInternalServerError + } + return + } + + WriteResponse(w, getTopicModel(&topicSlot)) +} + +func isMember(guid string, cards []store.Card) bool { + for _, card := range cards { + if guid == card.Guid { + return true + } + } + return false +} + +func isViewer(guid string, groups []store.Group) bool { + for _, group := range groups { + for _, card := range group.Cards { + if guid == card.Guid { + return true + } + } + } + return false +} + +func getChannelSlot(r *http.Request, member bool) (slot store.ChannelSlot, guid string, err error, code int) { + + // scan parameters + params := mux.Vars(r) + channelId := params["channelId"] + + // validate contact access + var account *store.Account + tokenType := r.Header.Get("TokenType") + if tokenType == APP_TOKENAPP { + account, code, err = BearerAppToken(r, false); + if err != nil { + return + } + guid = account.Guid + } else if tokenType == APP_TOKENCONTACT { + var card *store.Card + card, code, err = BearerContactToken(r, true) + if err != nil { + return + } + account = &card.Account + guid = card.Guid + } else { + err = errors.New("unknown token type") + code = http.StatusBadRequest + return + } + + // load channel + if err = store.DB.Preload("Account").Preload("Channel.Cards").Preload("Channel.Groups.Cards").Where("account_id = ? AND channel_slot_id = ?", account.ID, channelId).First(&slot).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + code = http.StatusNotFound + } else { + code = http.StatusInternalServerError + } + return + } + if slot.Channel == nil { + err = errors.New("referenced empty channel") + code = http.StatusNotFound + return + } + + // validate access to channel + if tokenType == APP_TOKENCONTACT { + if member && !isMember(guid, slot.Channel.Cards) { + err = errors.New("contact is not a channel member") + code = http.StatusUnauthorized + return + } else if !isViewer(guid, slot.Channel.Groups) && !isMember(guid, slot.Channel.Cards) { + err = errors.New("contact is not a channel viewer") + code = http.StatusUnauthorized + return + } + } + + return +} + diff --git a/net/server/internal/api_getChannelTopicCount.go b/net/server/internal/api_getChannelTopicCount.go new file mode 100644 index 00000000..0b2861a3 --- /dev/null +++ b/net/server/internal/api_getChannelTopicCount.go @@ -0,0 +1,42 @@ +package databag + +import ( + "errors" + "net/http" + "gorm.io/gorm" + "github.com/gorilla/mux" + "databag/internal/store" +) + +func GetChannelTopicCount(w http.ResponseWriter, r *http.Request) { + + // scan parameters + params := mux.Vars(r) + topicId := params["topicId"] + + var subject Subject + if err := ParseRequest(r, w, &subject); err != nil { + ErrResponse(w, http.StatusBadRequest, err) + return + } + + channelSlot, _, err, code := getChannelSlot(r, false) + if err != nil { + ErrResponse(w, code, err) + return + } + + // load topic + var topicSlot store.TopicSlot + if err = store.DB.Where("channel_id = ? AND topic_slot_id = ?", channelSlot.Channel.ID, topicId).First(&topicSlot).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + code = http.StatusNotFound + } else { + code = http.StatusInternalServerError + } + return + } + + WriteResponse(w, getTopicCountModel(&topicSlot)) +} + diff --git a/net/server/internal/api_getChannelTopicDetail.go b/net/server/internal/api_getChannelTopicDetail.go new file mode 100644 index 00000000..766d02e1 --- /dev/null +++ b/net/server/internal/api_getChannelTopicDetail.go @@ -0,0 +1,42 @@ +package databag + +import ( + "errors" + "net/http" + "gorm.io/gorm" + "github.com/gorilla/mux" + "databag/internal/store" +) + +func GetChannelTopicDetail(w http.ResponseWriter, r *http.Request) { + + // scan parameters + params := mux.Vars(r) + topicId := params["topicId"] + + var subject Subject + if err := ParseRequest(r, w, &subject); err != nil { + ErrResponse(w, http.StatusBadRequest, err) + return + } + + channelSlot, _, err, code := getChannelSlot(r, false) + if err != nil { + ErrResponse(w, code, err) + return + } + + // load topic + var topicSlot store.TopicSlot + if err = store.DB.Where("channel_id = ? AND topic_slot_id = ?", channelSlot.Channel.ID, topicId).First(&topicSlot).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + code = http.StatusNotFound + } else { + code = http.StatusInternalServerError + } + return + } + + WriteResponse(w, getTopicDetailModel(&topicSlot)) +} + diff --git a/net/server/internal/modelUtil.go b/net/server/internal/modelUtil.go index 01692905..31860b1c 100644 --- a/net/server/internal/modelUtil.go +++ b/net/server/internal/modelUtil.go @@ -213,8 +213,6 @@ func getChannelModel(slot *store.ChannelSlot, showData bool, showList bool) *Cha } } - - func getTopicRevisionModel(slot *store.TopicSlot, showData bool) *Topic { if !showData || slot.Topic == nil { @@ -234,9 +232,37 @@ func getTopicRevisionModel(slot *store.TopicSlot, showData bool) *Topic { } } -func getTopicModel(slot *store.TopicSlot, showData bool, showList bool) *Topic { +func getTopicDetailModel(slot *store.TopicSlot) *TopicDetail { - if !showData || slot.Topic == nil { + if slot.Topic == nil { + return nil + } + + return &TopicDetail{ + Guid: slot.Topic.Guid, + DataType: slot.Topic.DataType, + Data: slot.Topic.Data, + Created: slot.Topic.Created, + Updated: slot.Topic.Updated, + Status: slot.Topic.Status, + } +} + +func getTopicCountModel(slot *store.TopicSlot) *TagCount { + + if slot.Topic == nil { + return nil + } + + return &TagCount{ + Count: slot.Topic.TagCount, + Updated: slot.Topic.TagUpdated, + } +} + +func getTopicModel(slot *store.TopicSlot) *Topic { + + if slot.Topic == nil { return &Topic{ Id: slot.TopicSlotId, Revision: slot.Revision, @@ -248,22 +274,11 @@ func getTopicModel(slot *store.TopicSlot, showData bool, showList bool) *Topic { Revision: slot.Revision, Data: &TopicData { DetailRevision: slot.Topic.DetailRevision, - TopicDetail: &TopicDetail{ - Guid: slot.Topic.Guid, - DataType: slot.Topic.DataType, - Data: slot.Topic.Data, - Created: slot.Topic.Created, - Updated: slot.Topic.Updated, - Status: slot.Topic.Status, - }, + TopicDetail: getTopicDetailModel(slot), TagRevision: slot.Topic.TagRevision, - TopicTags: &TopicTags{ - TagCount: slot.Topic.TagCount, - TagUpdated: slot.Topic.TagUpdated, - }, + TopicTags: getTopicCountModel(slot), }, } } - diff --git a/net/server/internal/models.go b/net/server/internal/models.go index 49163873..df2760c4 100644 --- a/net/server/internal/models.go +++ b/net/server/internal/models.go @@ -368,6 +368,13 @@ type Tag struct { Data *TagData `json:"data"` } +type TagCount struct { + + Count int32 `json:"count"` + + Updated int64 `json:"updated"` +} + type TagData struct { Guid string `json:"guid"` @@ -398,7 +405,7 @@ type TopicData struct { TopicDetail *TopicDetail `json:"topicDetail,omitempty"` - TopicTags *TopicTags `json:"topicTags:,omitempty"` + TopicTags *TagCount `json:"topicTags:,omitempty"` } type TopicDetail struct { diff --git a/net/server/internal/routers.go b/net/server/internal/routers.go index b20450dc..734a1c0d 100644 --- a/net/server/internal/routers.go +++ b/net/server/internal/routers.go @@ -558,6 +558,13 @@ var routes = Routes{ GetChannelSubjectField, }, + Route{ + "GetChannelTopic", + strings.ToUpper("Get"), + "/content/channels/{channelId}/topics/{topicId}", + GetChannelTopic, + }, + Route{ "GetChannelTopicDetail", strings.ToUpper("Get"), @@ -566,10 +573,10 @@ var routes = Routes{ }, Route{ - "GetChannelTopicSize", + "GetChannelTopicCount", strings.ToUpper("Get"), - "/content/channels/{channelId}/topics/{topicId}/size", - GetChannelTopicSize, + "/content/channels/{channelId}/topics/{topicId}/count", + GetChannelTopicCount, }, Route{ diff --git a/net/server/internal/store/schema.go b/net/server/internal/store/schema.go index 5db58d0b..3804f94d 100644 --- a/net/server/internal/store/schema.go +++ b/net/server/internal/store/schema.go @@ -206,17 +206,18 @@ type Channel struct { type TopicSlot struct { ID uint - TopicSlotId string `gorm:"not null;index:topicslot,unique"` - AccountID uint `gorm:"not null;index:topicslot,unique"` + TopicSlotId string `gorm:"not null;index:topicaccount,unique;index:topicchannel,unique"` + AccountID uint `gorm:"not null;index:topicaccount,unique"` + ChannelID uint `gorm:"not null;index:topicchannel,unique"` Revision int64 `gorm:"not null"` TopicID uint `gorm:"not null;default:0"` Topic *Topic + Channel *Channel Account Account } type Topic struct { ID uint `gorm:"primaryKey;not null;unique;autoIncrement"` - ChannelID uint DetailRevision int64 `gorm:"not null"` Guid string DataType string `gorm:"index"` @@ -227,7 +228,6 @@ type Topic struct { TagCount int32 `gorm:"not null"` TagUpdated int64 `gorm:"autoUpdateTime"` TagRevision int64 `gorm:"not null"` - Channel *Channel Assets []Asset Tags []Tag }