unbind device push token on logout
This commit is contained in:
@@ -364,7 +364,7 @@ func (a *audit) run() {
|
|||||||
if alice == nil || bob == nil || a.groupTopic == "" {
|
if alice == nil || bob == nil || a.groupTopic == "" {
|
||||||
return "FAIL: websocket client or group missing"
|
return "FAIL: websocket client or group missing"
|
||||||
}
|
}
|
||||||
text := "WS 群消息 " + suffix
|
text := "group audit message " + suffix
|
||||||
seq, topic, err := alice.pubWithHeadAndPeer(bob, a.groupTopic, map[string]any{"mime": "text/plain"}, text)
|
seq, topic, err := alice.pubWithHeadAndPeer(bob, a.groupTopic, map[string]any{"mime": "text/plain"}, text)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "FAIL: " + err.Error()
|
return "FAIL: " + err.Error()
|
||||||
@@ -377,11 +377,11 @@ func (a *audit) run() {
|
|||||||
return "FAIL: websocket client missing"
|
return "FAIL: websocket client missing"
|
||||||
}
|
}
|
||||||
head := map[string]any{"mime": "text/plain", "x-im-client-mid": "dedup-" + suffix}
|
head := map[string]any{"mime": "text/plain", "x-im-client-mid": "dedup-" + suffix}
|
||||||
first, err := alice.pubCtrl(a.groupTopic, head, "群消息幂等测试 "+suffix)
|
first, err := alice.pubCtrl(a.groupTopic, head, "group dedup audit "+suffix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "FAIL: first pub: " + err.Error()
|
return "FAIL: first pub: " + err.Error()
|
||||||
}
|
}
|
||||||
second, err := alice.pubCtrl(a.groupTopic, head, "群消息幂等测试 "+suffix)
|
second, err := alice.pubCtrl(a.groupTopic, head, "group dedup audit "+suffix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "FAIL: duplicate pub: " + err.Error()
|
return "FAIL: duplicate pub: " + err.Error()
|
||||||
}
|
}
|
||||||
@@ -406,7 +406,7 @@ func (a *audit) run() {
|
|||||||
return fmt.Sprintf("anchor_seq=%d items=%d", anchor, count)
|
return fmt.Sprintf("anchor_seq=%d items=%d", anchor, count)
|
||||||
})
|
})
|
||||||
a.step("WS-22G", "群聊天记录搜索", "HTTP", "/v1/im/topics/{topic}/messages/search", func() string {
|
a.step("WS-22G", "群聊天记录搜索", "HTTP", "/v1/im/topics/{topic}/messages/search", func() string {
|
||||||
status, r, err := a.call("GET", "/v1/im/topics/"+url.PathEscape(a.groupTopic)+"/messages/search?q="+url.QueryEscape("WS 群消息")+"&type=text", nil, a.tokenB)
|
status, r, err := a.call("GET", "/v1/im/topics/"+url.PathEscape(a.groupTopic)+"/messages/search?q="+url.QueryEscape("group audit message")+"&type=text", nil, a.tokenB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "FAIL: " + err.Error()
|
return "FAIL: " + err.Error()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3672,6 +3672,34 @@ func (a *adapter) TenantDevicePushTokenUpsert(id t.TenantID, token *t.DevicePush
|
|||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *adapter) TenantDevicePushTokenDelete(id t.TenantID, uid t.Uid, deviceID string) error {
|
||||||
|
if !a.tenantScopedDB {
|
||||||
|
tenantAdp, err := a.routedTenantAdapter(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tenantAdp != a {
|
||||||
|
return tenantAdp.TenantDevicePushTokenDelete(id, uid, deviceID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deviceID = strings.TrimSpace(deviceID)
|
||||||
|
if err := validTenant(id); err != nil || uid.IsZero() || deviceID == "" {
|
||||||
|
return t.ErrMalformed
|
||||||
|
}
|
||||||
|
ctx, cancel := a.getContext()
|
||||||
|
if cancel != nil {
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
res, err := a.db.ExecContext(ctx, "DELETE FROM im_device_push_tokens WHERE tenant_id=? AND user_id=? AND device_id=?", id, decodeUID(uid), deviceID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if affected, _ := res.RowsAffected(); affected == 0 {
|
||||||
|
return t.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *adapter) TenantDevicePushTokenGetAll(id t.TenantID, uids ...t.Uid) (map[t.Uid][]t.DeviceDef, int, error) {
|
func (a *adapter) TenantDevicePushTokenGetAll(id t.TenantID, uids ...t.Uid) (map[t.Uid][]t.DeviceDef, int, error) {
|
||||||
return a.tenantDevicePushTokenGetAllByProviders(id, []string{"fcm", "firebase"}, uids...)
|
return a.tenantDevicePushTokenGetAllByProviders(id, []string{"fcm", "firebase"}, uids...)
|
||||||
}
|
}
|
||||||
|
|||||||
+66
-3
@@ -447,6 +447,10 @@ type imDevicePushTokenRequest struct {
|
|||||||
NotificationPermission string `json:"notification_permission,omitempty"`
|
NotificationPermission string `json:"notification_permission,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type imAuthLogoutRequest struct {
|
||||||
|
DeviceID string `json:"device_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type imP2PConversationRequest struct {
|
type imP2PConversationRequest struct {
|
||||||
PeerUserID string `json:"peer_user_id"`
|
PeerUserID string `json:"peer_user_id"`
|
||||||
}
|
}
|
||||||
@@ -4369,8 +4373,7 @@ func serveIMGroupDetail(wrt http.ResponseWriter, req *http.Request, topic string
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.Method == http.MethodGet {
|
if req.Method == http.MethodGet {
|
||||||
if settings, err := store.GroupBusiness.GetSettings(principal.TenantID, topic); err == nil && settings != nil && settings.GroupStatus == types.GroupStatusDissolved {
|
if writeIMGroupDissolvedIfGone(wrt, req, principal.TenantID, topic) {
|
||||||
writeIMError(wrt, req, http.StatusGone, 30004, imReasonGroupDissolved, "group dissolved", map[string]any{"topic": topic})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4529,6 +4532,19 @@ func serveIMGroupDetail(wrt http.ResponseWriter, req *http.Request, topic string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeIMGroupDissolvedIfGone(wrt http.ResponseWriter, req *http.Request, tenantID types.TenantID, topic string) bool {
|
||||||
|
if settings, err := store.GroupBusiness.GetSettings(tenantID, topic); err == nil && settings != nil && settings.GroupStatus == types.GroupStatusDissolved {
|
||||||
|
writeIMError(wrt, req, http.StatusGone, 30004, imReasonGroupDissolved, "group dissolved", map[string]any{"topic": topic})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
group, err := store.Topics.Get(tenantID, topic)
|
||||||
|
if err != nil || group == nil || group.State != types.StateDeleted {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
writeIMError(wrt, req, http.StatusGone, 30004, imReasonGroupDissolved, "group dissolved", map[string]any{"topic": topic})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func serveIMGroupSettings(wrt http.ResponseWriter, req *http.Request, topic string) {
|
func serveIMGroupSettings(wrt http.ResponseWriter, req *http.Request, topic string) {
|
||||||
if req.Method != http.MethodGet {
|
if req.Method != http.MethodGet {
|
||||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||||
@@ -6583,6 +6599,10 @@ func serveIMAuthLogout(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30000, imReasonMethodNotAllowed, "method not allowed", nil)
|
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30000, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
deviceID, ok := imLogoutDeviceID(wrt, req)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
principal, token, ttl, err := resolveIMHTTPBearerToken(req)
|
principal, token, ttl, err := resolveIMHTTPBearerToken(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeIMError(wrt, req, http.StatusUnauthorized, 30043, imReasonAuthRequired, "authentication required", nil)
|
writeIMError(wrt, req, http.StatusUnauthorized, 30043, imReasonAuthRequired, "authentication required", nil)
|
||||||
@@ -6597,7 +6617,50 @@ func serveIMAuthLogout(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeIMSuccess(wrt, req, map[string]any{"status": "ok"})
|
pushUnbound := false
|
||||||
|
if deviceID != "" {
|
||||||
|
if deleter, ok := store.DevicePushTokens.(interface {
|
||||||
|
Delete(types.TenantID, types.Uid, string) error
|
||||||
|
}); ok {
|
||||||
|
if err = deleter.Delete(principal.TenantID, principal.Uid, deviceID); err != nil && !errors.Is(err, types.ErrNotFound) {
|
||||||
|
logs.Warn.Println("im logout: push token unbind failed", principal.TenantID, principal.Uid, deviceID, err)
|
||||||
|
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pushUnbound = err == nil
|
||||||
|
}
|
||||||
|
if store.Devices != nil {
|
||||||
|
if err = store.Devices.Delete(principal.TenantID, principal.Uid, deviceID); err != nil && !errors.Is(err, types.ErrNotFound) {
|
||||||
|
logs.Warn.Println("im logout: legacy device unbind failed", principal.TenantID, principal.Uid, deviceID, err)
|
||||||
|
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeIMSuccess(wrt, req, map[string]any{"status": "ok", "device_id": deviceID, "push_unbound": pushUnbound})
|
||||||
|
}
|
||||||
|
|
||||||
|
func imLogoutDeviceID(wrt http.ResponseWriter, req *http.Request) (string, bool) {
|
||||||
|
deviceID := strings.TrimSpace(req.Header.Get("X-Device-ID"))
|
||||||
|
if deviceID == "" {
|
||||||
|
deviceID = strings.TrimSpace(req.Header.Get("X-IM-Device-ID"))
|
||||||
|
}
|
||||||
|
if req.Body != nil && req.Body != http.NoBody {
|
||||||
|
var body imAuthLogoutRequest
|
||||||
|
dec := json.NewDecoder(req.Body)
|
||||||
|
if err := dec.Decode(&body); err != nil && !errors.Is(err, io.EOF) {
|
||||||
|
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "body"})
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(body.DeviceID) != "" {
|
||||||
|
deviceID = strings.TrimSpace(body.DeviceID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(deviceID, "/") {
|
||||||
|
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "device_id"})
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return deviceID, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func currentUserPhone(tenantID types.TenantID, uid types.Uid) (string, error) {
|
func currentUserPhone(tenantID types.TenantID, uid types.Uid) (string, error) {
|
||||||
|
|||||||
+113
-2
@@ -235,8 +235,11 @@ type testAppReleaseStore struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type testDevicePushTokenStore struct {
|
type testDevicePushTokenStore struct {
|
||||||
token *types.DevicePushToken
|
token *types.DevicePushToken
|
||||||
tenantID types.TenantID
|
tenantID types.TenantID
|
||||||
|
deletedTenantID types.TenantID
|
||||||
|
deletedUserID types.Uid
|
||||||
|
deletedDeviceID string
|
||||||
}
|
}
|
||||||
|
|
||||||
type testSafetyStore struct {
|
type testSafetyStore struct {
|
||||||
@@ -752,6 +755,13 @@ func (s *testDevicePushTokenStore) Upsert(tenantID types.TenantID, token *types.
|
|||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *testDevicePushTokenStore) Delete(tenantID types.TenantID, uid types.Uid, deviceID string) error {
|
||||||
|
s.deletedTenantID = tenantID
|
||||||
|
s.deletedUserID = uid
|
||||||
|
s.deletedDeviceID = deviceID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *testSafetyStore) CreateRiskChallenge(tenantID types.TenantID, challenge *types.RiskChallenge) (*types.RiskChallenge, error) {
|
func (s *testSafetyStore) CreateRiskChallenge(tenantID types.TenantID, challenge *types.RiskChallenge) (*types.RiskChallenge, error) {
|
||||||
now := time.Now().UTC().Round(time.Millisecond)
|
now := time.Now().UTC().Round(time.Millisecond)
|
||||||
challenge.CreatedAt = now
|
challenge.CreatedAt = now
|
||||||
@@ -2907,6 +2917,30 @@ func TestServeIMDevicePushToken(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIMLogoutDeviceID(t *testing.T) {
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/logout", bytes.NewBufferString(`{"device_id":"device-1","ignored":true}`))
|
||||||
|
req.Header.Set("X-Device-ID", "header-device")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
deviceID, ok := imLogoutDeviceID(rec, req)
|
||||||
|
if !ok || deviceID != "device-1" || rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("unexpected body device id: ok=%v device_id=%q status=%d body=%s", ok, deviceID, rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/v1/im/auth/logout", nil)
|
||||||
|
req.Header.Set("X-IM-Device-ID", "header-device")
|
||||||
|
rec = httptest.NewRecorder()
|
||||||
|
deviceID, ok = imLogoutDeviceID(rec, req)
|
||||||
|
if !ok || deviceID != "header-device" || rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("unexpected header device id: ok=%v device_id=%q status=%d body=%s", ok, deviceID, rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
req = httptest.NewRequest(http.MethodPost, "/v1/im/auth/logout", bytes.NewBufferString(`{"device_id":"bad/device"}`))
|
||||||
|
rec = httptest.NewRecorder()
|
||||||
|
if deviceID, ok = imLogoutDeviceID(rec, req); ok || rec.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("invalid device id accepted: ok=%v device_id=%q status=%d body=%s", ok, deviceID, rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestServeIMConversationSearch(t *testing.T) {
|
func TestServeIMConversationSearch(t *testing.T) {
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
@@ -3780,6 +3814,83 @@ func TestServeIMGroupDetailPatchUsesBusinessSettingsVersion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServeIMGroupDetailReturnsGoneForDissolvedSettingsWithoutSubscription(t *testing.T) {
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
oldStore := store.Store
|
||||||
|
oldGroupBusiness := store.GroupBusiness
|
||||||
|
mockStore := mock_store.NewMockPersistentStorageInterface(ctrl)
|
||||||
|
tokenAuth := mock_auth.NewMockAuthHandler(ctrl)
|
||||||
|
mockStore.EXPECT().GetLogicalAuthHandler("token").Return(tokenAuth)
|
||||||
|
tokenAuth.EXPECT().IsInitialized().Return(true)
|
||||||
|
tokenAuth.EXPECT().Authenticate(auth.AuthContext{}, []byte("token-1")).
|
||||||
|
Return(&auth.Rec{TenantID: 7, Uid: 42, AuthLevel: auth.LevelAuth}, nil, nil)
|
||||||
|
store.Store = mockStore
|
||||||
|
store.GroupBusiness = &testGroupBusinessStore{settings: &types.GroupSettings{
|
||||||
|
Topic: "grpAlpha", OwnerUserID: types.Uid(42), Name: "Alpha",
|
||||||
|
GroupStatus: types.GroupStatusDissolved, Version: 2,
|
||||||
|
}}
|
||||||
|
defer func() {
|
||||||
|
store.Store = oldStore
|
||||||
|
store.GroupBusiness = oldGroupBusiness
|
||||||
|
}()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/v1/im/groups/grpAlpha", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer token-1")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
serveIMGroupResource(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusGone {
|
||||||
|
t.Fatalf("detail status: got %d body=%s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
if reason := decodeIMHTTPResponse(t, rec).Reason; reason != imReasonGroupDissolved {
|
||||||
|
t.Fatalf("unexpected reason: %q", reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServeIMGroupDetailReturnsGoneForDeletedTopicWithoutSubscription(t *testing.T) {
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
oldStore := store.Store
|
||||||
|
oldGroupBusiness := store.GroupBusiness
|
||||||
|
oldTopics := store.Topics
|
||||||
|
mockStore := mock_store.NewMockPersistentStorageInterface(ctrl)
|
||||||
|
tokenAuth := mock_auth.NewMockAuthHandler(ctrl)
|
||||||
|
mockStore.EXPECT().GetLogicalAuthHandler("token").Return(tokenAuth)
|
||||||
|
tokenAuth.EXPECT().IsInitialized().Return(true)
|
||||||
|
tokenAuth.EXPECT().Authenticate(auth.AuthContext{}, []byte("token-1")).
|
||||||
|
Return(&auth.Rec{TenantID: 7, Uid: 42, AuthLevel: auth.LevelAuth}, nil, nil)
|
||||||
|
topics := mock_store.NewMockTopicsPersistenceInterface(ctrl)
|
||||||
|
topics.EXPECT().Get(types.TenantID(7), "grpAlpha").Return(&types.Topic{
|
||||||
|
ObjHeader: types.ObjHeader{Id: "grpAlpha"},
|
||||||
|
TenantID: 7,
|
||||||
|
State: types.StateDeleted,
|
||||||
|
}, nil)
|
||||||
|
store.Store = mockStore
|
||||||
|
store.Topics = topics
|
||||||
|
store.GroupBusiness = &testGroupBusinessStore{settings: &types.GroupSettings{
|
||||||
|
Topic: "grpAlpha", OwnerUserID: types.Uid(42), Name: "Alpha",
|
||||||
|
GroupStatus: types.GroupStatusActive, Version: 1,
|
||||||
|
}}
|
||||||
|
defer func() {
|
||||||
|
store.Store = oldStore
|
||||||
|
store.Topics = oldTopics
|
||||||
|
store.GroupBusiness = oldGroupBusiness
|
||||||
|
}()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/v1/im/groups/grpAlpha", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer token-1")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
serveIMGroupResource(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusGone {
|
||||||
|
t.Fatalf("detail status: got %d body=%s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
if reason := decodeIMHTTPResponse(t, rec).Reason; reason != imReasonGroupDissolved {
|
||||||
|
t.Fatalf("unexpected reason: %q", reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestServeIMGroupMyProfileKeepsSubscriptionNicknameWhenBusinessMemberMissing(t *testing.T) {
|
func TestServeIMGroupMyProfileKeepsSubscriptionNicknameWhenBusinessMemberMissing(t *testing.T) {
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|||||||
@@ -2618,6 +2618,24 @@ func (devicePushTokenMapper) Upsert(tenantID types.TenantID, token *types.Device
|
|||||||
return tenantAdp.TenantDevicePushTokenUpsert(tenantID, token)
|
return tenantAdp.TenantDevicePushTokenUpsert(tenantID, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (devicePushTokenMapper) Delete(tenantID types.TenantID, uid types.Uid, deviceID string) error {
|
||||||
|
deviceID = strings.TrimSpace(deviceID)
|
||||||
|
if uid.IsZero() || deviceID == "" {
|
||||||
|
return types.ErrMalformed
|
||||||
|
}
|
||||||
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
deleter, ok := tenantAdp.(interface {
|
||||||
|
TenantDevicePushTokenDelete(types.TenantID, types.Uid, string) error
|
||||||
|
})
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return deleter.TenantDevicePushTokenDelete(tenantID, uid, deviceID)
|
||||||
|
}
|
||||||
|
|
||||||
func (devicePushTokenMapper) GetAll(tenantID types.TenantID, uids ...types.Uid) (map[types.Uid][]types.DeviceDef, int, error) {
|
func (devicePushTokenMapper) GetAll(tenantID types.TenantID, uids ...types.Uid) (map[types.Uid][]types.DeviceDef, int, error) {
|
||||||
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
tenantAdp, err := tenantBusinessAdapter(tenantID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user