mirror of
https://github.com/matrix-org/dendrite.git
synced 2025-12-23 14:53:10 -06:00
Merge branch 'master' of https://github.com/matrix-org/dendrite into add-last-seen
This commit is contained in:
commit
91c6206619
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -4,3 +4,5 @@
|
||||||
|
|
||||||
* [ ] I have added any new tests that need to pass to `sytest-whitelist` as specified in [docs/sytest.md](https://github.com/matrix-org/dendrite/blob/master/docs/sytest.md)
|
* [ ] I have added any new tests that need to pass to `sytest-whitelist` as specified in [docs/sytest.md](https://github.com/matrix-org/dendrite/blob/master/docs/sytest.md)
|
||||||
* [ ] Pull request includes a [sign off](https://github.com/matrix-org/dendrite/blob/master/docs/CONTRIBUTING.md#sign-off)
|
* [ ] Pull request includes a [sign off](https://github.com/matrix-org/dendrite/blob/master/docs/CONTRIBUTING.md#sign-off)
|
||||||
|
|
||||||
|
Signed-off-by: `Your Name <your@email.example.org>`
|
||||||
|
|
|
||||||
55
clientapi/routing/deactivate.go
Normal file
55
clientapi/routing/deactivate.go
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
package routing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Deactivate handles POST requests to /account/deactivate
|
||||||
|
func Deactivate(
|
||||||
|
req *http.Request,
|
||||||
|
userInteractiveAuth *auth.UserInteractive,
|
||||||
|
userAPI api.UserInternalAPI,
|
||||||
|
deviceAPI *api.Device,
|
||||||
|
) util.JSONResponse {
|
||||||
|
ctx := req.Context()
|
||||||
|
defer req.Body.Close() // nolint:errcheck
|
||||||
|
bodyBytes, err := ioutil.ReadAll(req.Body)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON("The request body could not be read: " + err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
login, errRes := userInteractiveAuth.Verify(ctx, bodyBytes, deviceAPI)
|
||||||
|
if errRes != nil {
|
||||||
|
return *errRes
|
||||||
|
}
|
||||||
|
|
||||||
|
localpart, _, err := gomatrixserverlib.SplitID('@', login.User)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
|
||||||
|
var res api.PerformAccountDeactivationResponse
|
||||||
|
err = userAPI.PerformAccountDeactivation(ctx, &api.PerformAccountDeactivationRequest{
|
||||||
|
Localpart: localpart,
|
||||||
|
}, &res)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("userAPI.PerformAccountDeactivation failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusOK,
|
||||||
|
JSON: struct{}{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -435,6 +435,15 @@ func Setup(
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodPost, http.MethodOptions)
|
).Methods(http.MethodPost, http.MethodOptions)
|
||||||
|
|
||||||
|
r0mux.Handle("/account/deactivate",
|
||||||
|
httputil.MakeAuthAPI("deactivate", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
if r := rateLimits.rateLimit(req); r != nil {
|
||||||
|
return *r
|
||||||
|
}
|
||||||
|
return Deactivate(req, userInteractiveAuth, userAPI, device)
|
||||||
|
}),
|
||||||
|
).Methods(http.MethodPost, http.MethodOptions)
|
||||||
|
|
||||||
// Stub endpoints required by Riot
|
// Stub endpoints required by Riot
|
||||||
|
|
||||||
r0mux.Handle("/login",
|
r0mux.Handle("/login",
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/syncapi/storage"
|
"github.com/matrix-org/dendrite/syncapi/storage"
|
||||||
"github.com/matrix-org/dendrite/syncapi/types"
|
"github.com/matrix-org/dendrite/syncapi/types"
|
||||||
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
@ -41,6 +42,7 @@ type messagesReq struct {
|
||||||
from *types.TopologyToken
|
from *types.TopologyToken
|
||||||
to *types.TopologyToken
|
to *types.TopologyToken
|
||||||
fromStream *types.StreamingToken
|
fromStream *types.StreamingToken
|
||||||
|
device *userapi.Device
|
||||||
wasToProvided bool
|
wasToProvided bool
|
||||||
limit int
|
limit int
|
||||||
backwardOrdering bool
|
backwardOrdering bool
|
||||||
|
|
@ -58,7 +60,7 @@ const defaultMessagesLimit = 10
|
||||||
// client-server API.
|
// client-server API.
|
||||||
// See: https://matrix.org/docs/spec/client_server/latest.html#get-matrix-client-r0-rooms-roomid-messages
|
// See: https://matrix.org/docs/spec/client_server/latest.html#get-matrix-client-r0-rooms-roomid-messages
|
||||||
func OnIncomingMessagesRequest(
|
func OnIncomingMessagesRequest(
|
||||||
req *http.Request, db storage.Database, roomID string,
|
req *http.Request, db storage.Database, roomID string, device *userapi.Device,
|
||||||
federation *gomatrixserverlib.FederationClient,
|
federation *gomatrixserverlib.FederationClient,
|
||||||
rsAPI api.RoomserverInternalAPI,
|
rsAPI api.RoomserverInternalAPI,
|
||||||
cfg *config.SyncAPI,
|
cfg *config.SyncAPI,
|
||||||
|
|
@ -151,6 +153,7 @@ func OnIncomingMessagesRequest(
|
||||||
wasToProvided: wasToProvided,
|
wasToProvided: wasToProvided,
|
||||||
limit: limit,
|
limit: limit,
|
||||||
backwardOrdering: backwardOrdering,
|
backwardOrdering: backwardOrdering,
|
||||||
|
device: device,
|
||||||
}
|
}
|
||||||
|
|
||||||
clientEvents, start, end, err := mReq.retrieveEvents()
|
clientEvents, start, end, err := mReq.retrieveEvents()
|
||||||
|
|
@ -238,6 +241,10 @@ func (r *messagesReq) retrieveEvents() (
|
||||||
}
|
}
|
||||||
events = reversed(events)
|
events = reversed(events)
|
||||||
}
|
}
|
||||||
|
events = r.filterHistoryVisible(events)
|
||||||
|
if len(events) == 0 {
|
||||||
|
return []gomatrixserverlib.ClientEvent{}, *r.from, *r.to, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Convert all of the events into client events.
|
// Convert all of the events into client events.
|
||||||
clientEvents = gomatrixserverlib.HeaderedToClientEvents(events, gomatrixserverlib.FormatAll)
|
clientEvents = gomatrixserverlib.HeaderedToClientEvents(events, gomatrixserverlib.FormatAll)
|
||||||
|
|
@ -252,6 +259,90 @@ func (r *messagesReq) retrieveEvents() (
|
||||||
return clientEvents, start, end, err
|
return clientEvents, start, end, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nolint:gocyclo
|
||||||
|
func (r *messagesReq) filterHistoryVisible(events []gomatrixserverlib.HeaderedEvent) []gomatrixserverlib.HeaderedEvent {
|
||||||
|
// TODO FIXME: We don't fully implement history visibility yet. To avoid leaking events which the
|
||||||
|
// user shouldn't see, we check the recent events and remove any prior to the join event of the user
|
||||||
|
// which is equiv to history_visibility: joined
|
||||||
|
joinEventIndex := -1
|
||||||
|
for i, ev := range events {
|
||||||
|
if ev.Type() == gomatrixserverlib.MRoomMember && ev.StateKeyEquals(r.device.UserID) {
|
||||||
|
membership, _ := ev.Membership()
|
||||||
|
if membership == "join" {
|
||||||
|
joinEventIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []gomatrixserverlib.HeaderedEvent
|
||||||
|
var eventsToCheck []gomatrixserverlib.HeaderedEvent
|
||||||
|
if joinEventIndex != -1 {
|
||||||
|
if r.backwardOrdering {
|
||||||
|
result = events[:joinEventIndex+1]
|
||||||
|
eventsToCheck = append(eventsToCheck, result[0])
|
||||||
|
} else {
|
||||||
|
result = events[joinEventIndex:]
|
||||||
|
eventsToCheck = append(eventsToCheck, result[len(result)-1])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eventsToCheck = []gomatrixserverlib.HeaderedEvent{events[0], events[len(events)-1]}
|
||||||
|
result = events
|
||||||
|
}
|
||||||
|
// make sure the user was in the room for both the earliest and latest events, we need this because
|
||||||
|
// some backpagination results will not have the join event (e.g if they hit /messages at the join event itself)
|
||||||
|
wasJoined := true
|
||||||
|
for _, ev := range eventsToCheck {
|
||||||
|
var queryRes api.QueryStateAfterEventsResponse
|
||||||
|
err := r.rsAPI.QueryStateAfterEvents(r.ctx, &api.QueryStateAfterEventsRequest{
|
||||||
|
RoomID: ev.RoomID(),
|
||||||
|
PrevEventIDs: ev.PrevEventIDs(),
|
||||||
|
StateToFetch: []gomatrixserverlib.StateKeyTuple{
|
||||||
|
{EventType: gomatrixserverlib.MRoomMember, StateKey: r.device.UserID},
|
||||||
|
{EventType: gomatrixserverlib.MRoomHistoryVisibility, StateKey: ""},
|
||||||
|
},
|
||||||
|
}, &queryRes)
|
||||||
|
if err != nil {
|
||||||
|
wasJoined = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
var hisVisEvent, membershipEvent *gomatrixserverlib.HeaderedEvent
|
||||||
|
for i := range queryRes.StateEvents {
|
||||||
|
switch queryRes.StateEvents[i].Type() {
|
||||||
|
case gomatrixserverlib.MRoomMember:
|
||||||
|
membershipEvent = &queryRes.StateEvents[i]
|
||||||
|
case gomatrixserverlib.MRoomHistoryVisibility:
|
||||||
|
hisVisEvent = &queryRes.StateEvents[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hisVisEvent == nil {
|
||||||
|
return events // apply no filtering as it defaults to Shared.
|
||||||
|
}
|
||||||
|
hisVis, _ := hisVisEvent.HistoryVisibility()
|
||||||
|
if hisVis == "shared" {
|
||||||
|
return events // apply no filtering
|
||||||
|
}
|
||||||
|
if membershipEvent == nil {
|
||||||
|
wasJoined = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
membership, err := membershipEvent.Membership()
|
||||||
|
if err != nil {
|
||||||
|
wasJoined = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if membership != "join" {
|
||||||
|
wasJoined = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !wasJoined {
|
||||||
|
util.GetLogger(r.ctx).WithField("num_events", len(events)).Warnf("%s was not joined to room during these events, omitting them", r.device.UserID)
|
||||||
|
return []gomatrixserverlib.HeaderedEvent{}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func (r *messagesReq) getStartEnd(events []gomatrixserverlib.HeaderedEvent) (start, end types.TopologyToken, err error) {
|
func (r *messagesReq) getStartEnd(events []gomatrixserverlib.HeaderedEvent) (start, end types.TopologyToken, err error) {
|
||||||
start, err = r.db.EventPositionInTopology(
|
start, err = r.db.EventPositionInTopology(
|
||||||
r.ctx, events[0].EventID(),
|
r.ctx, events[0].EventID(),
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ func Setup(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err)
|
||||||
}
|
}
|
||||||
return OnIncomingMessagesRequest(req, syncDB, vars["roomID"], federation, rsAPI, cfg)
|
return OnIncomingMessagesRequest(req, syncDB, vars["roomID"], device, federation, rsAPI, cfg)
|
||||||
})).Methods(http.MethodGet, http.MethodOptions)
|
})).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
|
||||||
r0mux.Handle("/user/{userId}/filter",
|
r0mux.Handle("/user/{userId}/filter",
|
||||||
|
|
|
||||||
|
|
@ -769,6 +769,33 @@ func (d *Database) getJoinResponseForCompleteSync(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO FIXME: We don't fully implement history visibility yet. To avoid leaking events which the
|
||||||
|
// user shouldn't see, we check the recent events and remove any prior to the join event of the user
|
||||||
|
// which is equiv to history_visibility: joined
|
||||||
|
joinEventIndex := -1
|
||||||
|
for i := len(recentStreamEvents) - 1; i >= 0; i-- {
|
||||||
|
ev := recentStreamEvents[i]
|
||||||
|
if ev.Type() == gomatrixserverlib.MRoomMember && ev.StateKeyEquals(device.UserID) {
|
||||||
|
membership, _ := ev.Membership()
|
||||||
|
if membership == "join" {
|
||||||
|
joinEventIndex = i
|
||||||
|
if i > 0 {
|
||||||
|
// the create event happens before the first join, so we should cut it at that point instead
|
||||||
|
if recentStreamEvents[i-1].Type() == gomatrixserverlib.MRoomCreate && recentStreamEvents[i-1].StateKeyEquals("") {
|
||||||
|
joinEventIndex = i - 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if joinEventIndex != -1 {
|
||||||
|
// cut all events earlier than the join (but not the join itself)
|
||||||
|
recentStreamEvents = recentStreamEvents[joinEventIndex:]
|
||||||
|
limited = false // so clients know not to try to backpaginate
|
||||||
|
}
|
||||||
|
|
||||||
// Retrieve the backward topology position, i.e. the position of the
|
// Retrieve the backward topology position, i.e. the position of the
|
||||||
// oldest event in the room's topology.
|
// oldest event in the room's topology.
|
||||||
var prevBatchStr string
|
var prevBatchStr string
|
||||||
|
|
|
||||||
|
|
@ -689,6 +689,7 @@ func assertInvitedToRooms(t *testing.T, res *types.Response, roomIDs []string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertEventsEqual(t *testing.T, msg string, checkRoomID bool, gots []gomatrixserverlib.ClientEvent, wants []gomatrixserverlib.HeaderedEvent) {
|
func assertEventsEqual(t *testing.T, msg string, checkRoomID bool, gots []gomatrixserverlib.ClientEvent, wants []gomatrixserverlib.HeaderedEvent) {
|
||||||
|
t.Helper()
|
||||||
if len(gots) != len(wants) {
|
if len(gots) != len(wants) {
|
||||||
t.Fatalf("%s response returned %d events, want %d", msg, len(gots), len(wants))
|
t.Fatalf("%s response returned %d events, want %d", msg, len(gots), len(wants))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,8 +90,6 @@ Real non-joined users can get state for world_readable rooms
|
||||||
Real non-joined users can get individual state for world_readable rooms
|
Real non-joined users can get individual state for world_readable rooms
|
||||||
#Real non-joined users can get individual state for world_readable rooms after leaving
|
#Real non-joined users can get individual state for world_readable rooms after leaving
|
||||||
Real non-joined users cannot send messages to guest_access rooms if not joined
|
Real non-joined users cannot send messages to guest_access rooms if not joined
|
||||||
Real users can sync from world_readable guest_access rooms if joined
|
|
||||||
Real users can sync from default guest_access rooms if joined
|
|
||||||
Can't forget room you're still in
|
Can't forget room you're still in
|
||||||
Can get rooms/{roomId}/members
|
Can get rooms/{roomId}/members
|
||||||
Can create filter
|
Can create filter
|
||||||
|
|
@ -221,6 +219,9 @@ Regular users cannot create room aliases within the AS namespace
|
||||||
Deleting a non-existent alias should return a 404
|
Deleting a non-existent alias should return a 404
|
||||||
Users can't delete other's aliases
|
Users can't delete other's aliases
|
||||||
Outbound federation can query room alias directory
|
Outbound federation can query room alias directory
|
||||||
|
Can deactivate account
|
||||||
|
Can't deactivate account with wrong password
|
||||||
|
After deactivating account, can't log in with password
|
||||||
After deactivating account, can't log in with an email
|
After deactivating account, can't log in with an email
|
||||||
Remote room alias queries can handle Unicode
|
Remote room alias queries can handle Unicode
|
||||||
Newly joined room is included in an incremental sync after invite
|
Newly joined room is included in an incremental sync after invite
|
||||||
|
|
@ -236,13 +237,11 @@ Outbound federation can query v2 /send_join
|
||||||
Inbound federation can receive v2 /send_join
|
Inbound federation can receive v2 /send_join
|
||||||
Message history can be paginated
|
Message history can be paginated
|
||||||
Getting messages going forward is limited for a departed room (SPEC-216)
|
Getting messages going forward is limited for a departed room (SPEC-216)
|
||||||
m.room.history_visibility == "world_readable" allows/forbids appropriately for Real users
|
|
||||||
Backfill works correctly with history visibility set to joined
|
Backfill works correctly with history visibility set to joined
|
||||||
Guest user cannot call /events globally
|
Guest user cannot call /events globally
|
||||||
Guest users can join guest_access rooms
|
Guest users can join guest_access rooms
|
||||||
Guest user can set display names
|
Guest user can set display names
|
||||||
Guest user cannot upgrade other users
|
Guest user cannot upgrade other users
|
||||||
m.room.history_visibility == "world_readable" allows/forbids appropriately for Guest users
|
|
||||||
Guest non-joined user cannot call /events on shared room
|
Guest non-joined user cannot call /events on shared room
|
||||||
Guest non-joined user cannot call /events on invited room
|
Guest non-joined user cannot call /events on invited room
|
||||||
Guest non-joined user cannot call /events on joined room
|
Guest non-joined user cannot call /events on joined room
|
||||||
|
|
@ -252,8 +251,6 @@ Guest non-joined users can get individual state for world_readable rooms
|
||||||
Guest non-joined users cannot room initalSync for non-world_readable rooms
|
Guest non-joined users cannot room initalSync for non-world_readable rooms
|
||||||
Guest non-joined users can get individual state for world_readable rooms after leaving
|
Guest non-joined users can get individual state for world_readable rooms after leaving
|
||||||
Guest non-joined users cannot send messages to guest_access rooms if not joined
|
Guest non-joined users cannot send messages to guest_access rooms if not joined
|
||||||
Guest users can sync from world_readable guest_access rooms if joined
|
|
||||||
Guest users can sync from default guest_access rooms if joined
|
|
||||||
Real non-joined users cannot room initalSync for non-world_readable rooms
|
Real non-joined users cannot room initalSync for non-world_readable rooms
|
||||||
Push rules come down in an initial /sync
|
Push rules come down in an initial /sync
|
||||||
Regular users can add and delete aliases in the default room configuration
|
Regular users can add and delete aliases in the default room configuration
|
||||||
|
|
@ -479,3 +476,5 @@ Inbound /make_join rejects attempts to join rooms where all users have left
|
||||||
Inbound federation rejects invites which include invalid JSON for room version 6
|
Inbound federation rejects invites which include invalid JSON for room version 6
|
||||||
Inbound federation rejects invite rejections which include invalid JSON for room version 6
|
Inbound federation rejects invite rejections which include invalid JSON for room version 6
|
||||||
GET /capabilities is present and well formed for registered user
|
GET /capabilities is present and well formed for registered user
|
||||||
|
m.room.history_visibility == "joined" allows/forbids appropriately for Guest users
|
||||||
|
m.room.history_visibility == "joined" allows/forbids appropriately for Real users
|
||||||
|
|
@ -30,6 +30,7 @@ type UserInternalAPI interface {
|
||||||
PerformDeviceCreation(ctx context.Context, req *PerformDeviceCreationRequest, res *PerformDeviceCreationResponse) error
|
PerformDeviceCreation(ctx context.Context, req *PerformDeviceCreationRequest, res *PerformDeviceCreationResponse) error
|
||||||
PerformDeviceDeletion(ctx context.Context, req *PerformDeviceDeletionRequest, res *PerformDeviceDeletionResponse) error
|
PerformDeviceDeletion(ctx context.Context, req *PerformDeviceDeletionRequest, res *PerformDeviceDeletionResponse) error
|
||||||
PerformDeviceUpdate(ctx context.Context, req *PerformDeviceUpdateRequest, res *PerformDeviceUpdateResponse) error
|
PerformDeviceUpdate(ctx context.Context, req *PerformDeviceUpdateRequest, res *PerformDeviceUpdateResponse) error
|
||||||
|
PerformAccountDeactivation(ctx context.Context, req *PerformAccountDeactivationRequest, res *PerformAccountDeactivationResponse) error
|
||||||
QueryProfile(ctx context.Context, req *QueryProfileRequest, res *QueryProfileResponse) error
|
QueryProfile(ctx context.Context, req *QueryProfileRequest, res *QueryProfileResponse) error
|
||||||
QueryAccessToken(ctx context.Context, req *QueryAccessTokenRequest, res *QueryAccessTokenResponse) error
|
QueryAccessToken(ctx context.Context, req *QueryAccessTokenRequest, res *QueryAccessTokenResponse) error
|
||||||
QueryDevices(ctx context.Context, req *QueryDevicesRequest, res *QueryDevicesResponse) error
|
QueryDevices(ctx context.Context, req *QueryDevicesRequest, res *QueryDevicesResponse) error
|
||||||
|
|
@ -203,6 +204,16 @@ type PerformDeviceCreationResponse struct {
|
||||||
Device *Device
|
Device *Device
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PerformAccountDeactivationRequest is the request for PerformAccountDeactivation
|
||||||
|
type PerformAccountDeactivationRequest struct {
|
||||||
|
Localpart string
|
||||||
|
}
|
||||||
|
|
||||||
|
// PerformAccountDeactivationResponse is the response for PerformAccountDeactivation
|
||||||
|
type PerformAccountDeactivationResponse struct {
|
||||||
|
AccountDeactivated bool
|
||||||
|
}
|
||||||
|
|
||||||
// Device represents a client's device (mobile, web, etc)
|
// Device represents a client's device (mobile, web, etc)
|
||||||
type Device struct {
|
type Device struct {
|
||||||
ID string
|
ID string
|
||||||
|
|
|
||||||
|
|
@ -388,3 +388,10 @@ func (a *UserInternalAPI) queryAppServiceToken(ctx context.Context, token, appSe
|
||||||
dev.UserID = appService.SenderLocalpart
|
dev.UserID = appService.SenderLocalpart
|
||||||
return &dev, nil
|
return &dev, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PerformAccountDeactivation deactivates the user's account, removing all ability for the user to login again.
|
||||||
|
func (a *UserInternalAPI) PerformAccountDeactivation(ctx context.Context, req *api.PerformAccountDeactivationRequest, res *api.PerformAccountDeactivationResponse) error {
|
||||||
|
err := a.AccountDB.DeactivateAccount(ctx, req.Localpart)
|
||||||
|
res.AccountDeactivated = err == nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,12 @@ import (
|
||||||
const (
|
const (
|
||||||
InputAccountDataPath = "/userapi/inputAccountData"
|
InputAccountDataPath = "/userapi/inputAccountData"
|
||||||
|
|
||||||
PerformDeviceCreationPath = "/userapi/performDeviceCreation"
|
PerformDeviceCreationPath = "/userapi/performDeviceCreation"
|
||||||
PerformAccountCreationPath = "/userapi/performAccountCreation"
|
PerformAccountCreationPath = "/userapi/performAccountCreation"
|
||||||
PerformPasswordUpdatePath = "/userapi/performPasswordUpdate"
|
PerformPasswordUpdatePath = "/userapi/performPasswordUpdate"
|
||||||
PerformDeviceDeletionPath = "/userapi/performDeviceDeletion"
|
PerformDeviceDeletionPath = "/userapi/performDeviceDeletion"
|
||||||
PerformDeviceUpdatePath = "/userapi/performDeviceUpdate"
|
PerformDeviceUpdatePath = "/userapi/performDeviceUpdate"
|
||||||
|
PerformAccountDeactivationPath = "/userapi/performAccountDeactivation"
|
||||||
|
|
||||||
QueryProfilePath = "/userapi/queryProfile"
|
QueryProfilePath = "/userapi/queryProfile"
|
||||||
QueryAccessTokenPath = "/userapi/queryAccessToken"
|
QueryAccessTokenPath = "/userapi/queryAccessToken"
|
||||||
|
|
@ -126,6 +127,14 @@ func (h *httpUserInternalAPI) PerformDeviceUpdate(ctx context.Context, req *api.
|
||||||
return httputil.PostJSON(ctx, span, h.httpClient, apiURL, req, res)
|
return httputil.PostJSON(ctx, span, h.httpClient, apiURL, req, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *httpUserInternalAPI) PerformAccountDeactivation(ctx context.Context, req *api.PerformAccountDeactivationRequest, res *api.PerformAccountDeactivationResponse) error {
|
||||||
|
span, ctx := opentracing.StartSpanFromContext(ctx, "PerformAccountDeactivation")
|
||||||
|
defer span.Finish()
|
||||||
|
|
||||||
|
apiURL := h.apiURL + PerformAccountDeactivationPath
|
||||||
|
return httputil.PostJSON(ctx, span, h.httpClient, apiURL, req, res)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *httpUserInternalAPI) QueryProfile(
|
func (h *httpUserInternalAPI) QueryProfile(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request *api.QueryProfileRequest,
|
request *api.QueryProfileRequest,
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,19 @@ func AddRoutes(internalAPIMux *mux.Router, s api.UserInternalAPI) {
|
||||||
return util.JSONResponse{Code: http.StatusOK, JSON: &response}
|
return util.JSONResponse{Code: http.StatusOK, JSON: &response}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
internalAPIMux.Handle(PerformAccountDeactivationPath,
|
||||||
|
httputil.MakeInternalAPI("performAccountDeactivation", func(req *http.Request) util.JSONResponse {
|
||||||
|
request := api.PerformAccountDeactivationRequest{}
|
||||||
|
response := api.PerformAccountDeactivationResponse{}
|
||||||
|
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
|
||||||
|
return util.MessageResponse(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
if err := s.PerformAccountDeactivation(req.Context(), &request, &response); err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
return util.JSONResponse{Code: http.StatusOK, JSON: &response}
|
||||||
|
}),
|
||||||
|
)
|
||||||
internalAPIMux.Handle(QueryProfilePath,
|
internalAPIMux.Handle(QueryProfilePath,
|
||||||
httputil.MakeInternalAPI("queryProfile", func(req *http.Request) util.JSONResponse {
|
httputil.MakeInternalAPI("queryProfile", func(req *http.Request) util.JSONResponse {
|
||||||
request := api.QueryProfileRequest{}
|
request := api.QueryProfileRequest{}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ type Database interface {
|
||||||
CheckAccountAvailability(ctx context.Context, localpart string) (bool, error)
|
CheckAccountAvailability(ctx context.Context, localpart string) (bool, error)
|
||||||
GetAccountByLocalpart(ctx context.Context, localpart string) (*api.Account, error)
|
GetAccountByLocalpart(ctx context.Context, localpart string) (*api.Account, error)
|
||||||
SearchProfiles(ctx context.Context, searchString string, limit int) ([]authtypes.Profile, error)
|
SearchProfiles(ctx context.Context, searchString string, limit int) ([]authtypes.Profile, error)
|
||||||
|
DeactivateAccount(ctx context.Context, localpart string) (err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Err3PIDInUse is the error returned when trying to save an association involving
|
// Err3PIDInUse is the error returned when trying to save an association involving
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,9 @@ CREATE TABLE IF NOT EXISTS account_accounts (
|
||||||
-- The password hash for this account. Can be NULL if this is a passwordless account.
|
-- The password hash for this account. Can be NULL if this is a passwordless account.
|
||||||
password_hash TEXT,
|
password_hash TEXT,
|
||||||
-- Identifies which application service this account belongs to, if any.
|
-- Identifies which application service this account belongs to, if any.
|
||||||
appservice_id TEXT
|
appservice_id TEXT,
|
||||||
|
-- If the account is currently active
|
||||||
|
is_deactivated BOOLEAN DEFAULT FALSE
|
||||||
-- TODO:
|
-- TODO:
|
||||||
-- is_guest, is_admin, upgraded_ts, devices, any email reset stuff?
|
-- is_guest, is_admin, upgraded_ts, devices, any email reset stuff?
|
||||||
);
|
);
|
||||||
|
|
@ -51,11 +53,14 @@ const insertAccountSQL = "" +
|
||||||
const updatePasswordSQL = "" +
|
const updatePasswordSQL = "" +
|
||||||
"UPDATE account_accounts SET password_hash = $1 WHERE localpart = $2"
|
"UPDATE account_accounts SET password_hash = $1 WHERE localpart = $2"
|
||||||
|
|
||||||
|
const deactivateAccountSQL = "" +
|
||||||
|
"UPDATE account_accounts SET is_deactivated = TRUE WHERE localpart = $1"
|
||||||
|
|
||||||
const selectAccountByLocalpartSQL = "" +
|
const selectAccountByLocalpartSQL = "" +
|
||||||
"SELECT localpart, appservice_id FROM account_accounts WHERE localpart = $1"
|
"SELECT localpart, appservice_id FROM account_accounts WHERE localpart = $1"
|
||||||
|
|
||||||
const selectPasswordHashSQL = "" +
|
const selectPasswordHashSQL = "" +
|
||||||
"SELECT password_hash FROM account_accounts WHERE localpart = $1"
|
"SELECT password_hash FROM account_accounts WHERE localpart = $1 AND is_deactivated = FALSE"
|
||||||
|
|
||||||
const selectNewNumericLocalpartSQL = "" +
|
const selectNewNumericLocalpartSQL = "" +
|
||||||
"SELECT nextval('numeric_username_seq')"
|
"SELECT nextval('numeric_username_seq')"
|
||||||
|
|
@ -63,6 +68,7 @@ const selectNewNumericLocalpartSQL = "" +
|
||||||
type accountsStatements struct {
|
type accountsStatements struct {
|
||||||
insertAccountStmt *sql.Stmt
|
insertAccountStmt *sql.Stmt
|
||||||
updatePasswordStmt *sql.Stmt
|
updatePasswordStmt *sql.Stmt
|
||||||
|
deactivateAccountStmt *sql.Stmt
|
||||||
selectAccountByLocalpartStmt *sql.Stmt
|
selectAccountByLocalpartStmt *sql.Stmt
|
||||||
selectPasswordHashStmt *sql.Stmt
|
selectPasswordHashStmt *sql.Stmt
|
||||||
selectNewNumericLocalpartStmt *sql.Stmt
|
selectNewNumericLocalpartStmt *sql.Stmt
|
||||||
|
|
@ -80,6 +86,9 @@ func (s *accountsStatements) prepare(db *sql.DB, server gomatrixserverlib.Server
|
||||||
if s.updatePasswordStmt, err = db.Prepare(updatePasswordSQL); err != nil {
|
if s.updatePasswordStmt, err = db.Prepare(updatePasswordSQL); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if s.deactivateAccountStmt, err = db.Prepare(deactivateAccountSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if s.selectAccountByLocalpartStmt, err = db.Prepare(selectAccountByLocalpartSQL); err != nil {
|
if s.selectAccountByLocalpartStmt, err = db.Prepare(selectAccountByLocalpartSQL); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -127,6 +136,13 @@ func (s *accountsStatements) updatePassword(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *accountsStatements) deactivateAccount(
|
||||||
|
ctx context.Context, localpart string,
|
||||||
|
) (err error) {
|
||||||
|
_, err = s.deactivateAccountStmt.ExecContext(ctx, localpart)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (s *accountsStatements) selectPasswordHash(
|
func (s *accountsStatements) selectPasswordHash(
|
||||||
ctx context.Context, localpart string,
|
ctx context.Context, localpart string,
|
||||||
) (hash string, err error) {
|
) (hash string, err error) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
ALTER TABLE account_accounts ADD COLUMN IF NOT EXISTS is_deactivated BOOLEAN DEFAULT FALSE;
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
ALTER TABLE account_accounts DROP COLUMN is_deactivated;
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
@ -317,3 +317,8 @@ func (d *Database) SearchProfiles(ctx context.Context, searchString string, limi
|
||||||
) ([]authtypes.Profile, error) {
|
) ([]authtypes.Profile, error) {
|
||||||
return d.profiles.selectProfilesBySearch(ctx, searchString, limit)
|
return d.profiles.selectProfilesBySearch(ctx, searchString, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeactivateAccount deactivates the user's account, removing all ability for the user to login again.
|
||||||
|
func (d *Database) DeactivateAccount(ctx context.Context, localpart string) (err error) {
|
||||||
|
return d.accounts.deactivateAccount(ctx, localpart)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,9 @@ CREATE TABLE IF NOT EXISTS account_accounts (
|
||||||
-- The password hash for this account. Can be NULL if this is a passwordless account.
|
-- The password hash for this account. Can be NULL if this is a passwordless account.
|
||||||
password_hash TEXT,
|
password_hash TEXT,
|
||||||
-- Identifies which application service this account belongs to, if any.
|
-- Identifies which application service this account belongs to, if any.
|
||||||
appservice_id TEXT
|
appservice_id TEXT,
|
||||||
|
-- If the account is currently active
|
||||||
|
is_deactivated BOOLEAN DEFAULT 0
|
||||||
-- TODO:
|
-- TODO:
|
||||||
-- is_guest, is_admin, upgraded_ts, devices, any email reset stuff?
|
-- is_guest, is_admin, upgraded_ts, devices, any email reset stuff?
|
||||||
);
|
);
|
||||||
|
|
@ -49,11 +51,14 @@ const insertAccountSQL = "" +
|
||||||
const updatePasswordSQL = "" +
|
const updatePasswordSQL = "" +
|
||||||
"UPDATE account_accounts SET password_hash = $1 WHERE localpart = $2"
|
"UPDATE account_accounts SET password_hash = $1 WHERE localpart = $2"
|
||||||
|
|
||||||
|
const deactivateAccountSQL = "" +
|
||||||
|
"UPDATE account_accounts SET is_deactivated = 1 WHERE localpart = $1"
|
||||||
|
|
||||||
const selectAccountByLocalpartSQL = "" +
|
const selectAccountByLocalpartSQL = "" +
|
||||||
"SELECT localpart, appservice_id FROM account_accounts WHERE localpart = $1"
|
"SELECT localpart, appservice_id FROM account_accounts WHERE localpart = $1"
|
||||||
|
|
||||||
const selectPasswordHashSQL = "" +
|
const selectPasswordHashSQL = "" +
|
||||||
"SELECT password_hash FROM account_accounts WHERE localpart = $1"
|
"SELECT password_hash FROM account_accounts WHERE localpart = $1 AND is_deactivated = 0"
|
||||||
|
|
||||||
const selectNewNumericLocalpartSQL = "" +
|
const selectNewNumericLocalpartSQL = "" +
|
||||||
"SELECT COUNT(localpart) FROM account_accounts"
|
"SELECT COUNT(localpart) FROM account_accounts"
|
||||||
|
|
@ -62,6 +67,7 @@ type accountsStatements struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
insertAccountStmt *sql.Stmt
|
insertAccountStmt *sql.Stmt
|
||||||
updatePasswordStmt *sql.Stmt
|
updatePasswordStmt *sql.Stmt
|
||||||
|
deactivateAccountStmt *sql.Stmt
|
||||||
selectAccountByLocalpartStmt *sql.Stmt
|
selectAccountByLocalpartStmt *sql.Stmt
|
||||||
selectPasswordHashStmt *sql.Stmt
|
selectPasswordHashStmt *sql.Stmt
|
||||||
selectNewNumericLocalpartStmt *sql.Stmt
|
selectNewNumericLocalpartStmt *sql.Stmt
|
||||||
|
|
@ -81,6 +87,9 @@ func (s *accountsStatements) prepare(db *sql.DB, server gomatrixserverlib.Server
|
||||||
if s.updatePasswordStmt, err = db.Prepare(updatePasswordSQL); err != nil {
|
if s.updatePasswordStmt, err = db.Prepare(updatePasswordSQL); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if s.deactivateAccountStmt, err = db.Prepare(deactivateAccountSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if s.selectAccountByLocalpartStmt, err = db.Prepare(selectAccountByLocalpartSQL); err != nil {
|
if s.selectAccountByLocalpartStmt, err = db.Prepare(selectAccountByLocalpartSQL); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -128,6 +137,13 @@ func (s *accountsStatements) updatePassword(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *accountsStatements) deactivateAccount(
|
||||||
|
ctx context.Context, localpart string,
|
||||||
|
) (err error) {
|
||||||
|
_, err = s.deactivateAccountStmt.ExecContext(ctx, localpart)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (s *accountsStatements) selectPasswordHash(
|
func (s *accountsStatements) selectPasswordHash(
|
||||||
ctx context.Context, localpart string,
|
ctx context.Context, localpart string,
|
||||||
) (hash string, err error) {
|
) (hash string, err error) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
ALTER TABLE account_accounts RENAME TO account_accounts_tmp;
|
||||||
|
CREATE TABLE account_accounts (
|
||||||
|
localpart TEXT NOT NULL PRIMARY KEY,
|
||||||
|
created_ts BIGINT NOT NULL,
|
||||||
|
password_hash TEXT,
|
||||||
|
appservice_id TEXT,
|
||||||
|
is_deactivated BOOLEAN DEFAULT 0
|
||||||
|
);
|
||||||
|
INSERT
|
||||||
|
INTO account_accounts (
|
||||||
|
localpart, created_ts, password_hash, appservice_id
|
||||||
|
) SELECT
|
||||||
|
localpart, created_ts, password_hash, appservice_id
|
||||||
|
FROM account_accounts_tmp
|
||||||
|
;
|
||||||
|
DROP TABLE account_accounts_tmp;
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
ALTER TABLE account_accounts RENAME TO account_accounts_tmp;
|
||||||
|
CREATE TABLE account_accounts (
|
||||||
|
localpart TEXT NOT NULL PRIMARY KEY,
|
||||||
|
created_ts BIGINT NOT NULL,
|
||||||
|
password_hash TEXT,
|
||||||
|
appservice_id TEXT
|
||||||
|
);
|
||||||
|
INSERT
|
||||||
|
INTO account_accounts (
|
||||||
|
localpart, created_ts, password_hash, appservice_id
|
||||||
|
) SELECT
|
||||||
|
localpart, created_ts, password_hash, appservice_id
|
||||||
|
FROM account_accounts_tmp
|
||||||
|
;
|
||||||
|
DROP TABLE account_accounts_tmp;
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
@ -359,3 +359,8 @@ func (d *Database) SearchProfiles(ctx context.Context, searchString string, limi
|
||||||
) ([]authtypes.Profile, error) {
|
) ([]authtypes.Profile, error) {
|
||||||
return d.profiles.selectProfilesBySearch(ctx, searchString, limit)
|
return d.profiles.selectProfilesBySearch(ctx, searchString, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeactivateAccount deactivates the user's account, removing all ability for the user to login again.
|
||||||
|
func (d *Database) DeactivateAccount(ctx context.Context, localpart string) (err error) {
|
||||||
|
return d.accounts.deactivateAccount(ctx, localpart)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue