Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Account delete #16779

Merged
merged 28 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d82b1a7
account delete WIP
ornicar Dec 18, 2024
fadd818
Merge branch 'master' into account-delete
ornicar Dec 19, 2024
c42b343
Merge branch 'master' into account-delete
ornicar Jan 15, 2025
e827318
revoke all oauth tokens on account closure
ornicar Jan 15, 2025
50ddd69
document account termination
ornicar Jan 15, 2025
a0dc197
add missing dependency
ornicar Jan 15, 2025
93946e0
mongo scheduler and progress on account termination strategies
ornicar Jan 15, 2025
72a9ffd
Merge branch 'master' into account-delete
ornicar Jan 16, 2025
43ed35e
rename MongoScheduler.nextLookup
ornicar Jan 16, 2025
c3974f1
delete MongoScheduler
ornicar Jan 16, 2025
45468c8
account termination WIP
ornicar Jan 16, 2025
89aa111
account termination WIP, schedule deletion
ornicar Jan 16, 2025
c8d7dfe
Merge branch 'master' into account-delete
ornicar Jan 16, 2025
495f561
cleanup analysis_requester collection
ornicar Jan 16, 2025
944019f
account termination WIP
ornicar Jan 16, 2025
a58a961
account termination WIP
ornicar Jan 16, 2025
16790db
account termination WIP
ornicar Jan 16, 2025
dfe1ac0
account termination WIP
ornicar Jan 16, 2025
80a9c75
fix compilation
ornicar Jan 17, 2025
491241f
account deletion UI
ornicar Jan 17, 2025
091923b
only allow missing important indexes when gdpr erasure is requested
ornicar Jan 17, 2025
ec6f47b
detect weird UAs
ornicar Jan 17, 2025
5ee5c06
user trust and pool join tweaking
ornicar Jan 17, 2025
2c80390
better pair provisional players
ornicar Jan 17, 2025
5b0bb8f
use computed trust in playban calculations
ornicar Jan 17, 2025
489b98f
faster and better user trust
ornicar Jan 17, 2025
dc99ac4
better delete/anonymize studies
ornicar Jan 17, 2025
26e018b
log user deletion
ornicar Jan 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 31 additions & 6 deletions app/controllers/Account.scala
Original file line number Diff line number Diff line change
Expand Up @@ -239,23 +239,48 @@ final class Account(
}

def close = Auth { _ ?=> me ?=>
env.clas.api.student.isManaged(me).flatMap { managed =>
env.security.forms.closeAccount.flatMap: form =>
Ok.page(pages.close(form, managed))
}
for
managed <- env.clas.api.student.isManaged(me)
form <- env.security.forms.closeAccount
res <- Ok.page(pages.close(form, managed))
yield res
}

def closeConfirm = AuthBody { ctx ?=> me ?=>
NotManaged:
auth.HasherRateLimit:
env.security.forms.closeAccount.flatMap: form =>
FormFuResult(form)(err => renderPage(pages.close(err, managed = false))): _ =>
env.api.accountClosure
.close(me.value)
env.api.accountTermination
.disable(me.value)
.inject:
Redirect(routes.User.show(me.username)).withCookies(env.security.lilaCookie.newSession)
}

def delete = Auth { _ ?=> me ?=>
for
managed <- env.clas.api.student.isManaged(me)
form <- env.security.forms.deleteAccount
res <- Ok.page(pages.delete(form, managed))
yield res
}

def deleteConfirm = AuthBody { ctx ?=> me ?=>
NotManaged:
auth.HasherRateLimit:
env.security.forms.deleteAccount.flatMap: form =>
FormFuResult(form)(err => renderPage(pages.delete(err, managed = false))): _ =>
env.api.accountTermination
.scheduleDelete(me.value)
.inject:
Redirect(routes.Account.deleteDone).withCookies(env.security.lilaCookie.newSession)
}

def deleteDone = Open { ctx ?=>
if ctx.isAuth then Redirect(routes.Lobby.home)
else FoundPage(env.cms.renderKey("delete-done"))(views.site.page.lone)
}

def kid = Auth { _ ?=> me ?=>
for
managed <- env.clas.api.student.isManaged(me)
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/Clas.scala
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ final class Clas(env: Env, authC: Auth) extends LilaController(env):
WithStudent(clas, username): s =>
if s.student.managed then
(env.clas.api.student.closeAccount(s) >>
env.api.accountClosure.close(s.user)).inject(redirectTo(clas).flashSuccess)
env.api.accountTermination.disable(s.user)).inject(redirectTo(clas).flashSuccess)
else if s.student.isArchived then
env.clas.api.student.closeAccount(s) >>
redirectTo(clas).flashSuccess
Expand Down
19 changes: 8 additions & 11 deletions app/controllers/Mod.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ final class Mod(
withSuspect(username): sus =>
for
_ <- api.setAlt(sus, v)
_ <- (v && sus.user.enabled.yes).so(env.api.accountClosure.close(sus.user))
_ <- (v && sus.user.enabled.yes).so(env.api.accountTermination.disable(sus.user))
_ <- (!v && sus.user.enabled.no).so(api.reopenAccount(sus.user.id))
yield sus.some
}(reportC.onModAction)
Expand All @@ -40,7 +40,7 @@ final class Mod(
Source(ctx.body.body.split(' ').toList.flatMap(UserStr.read))
.mapAsync(1): username =>
withSuspect(username): sus =>
api.setAlt(sus, true) >> (sus.user.enabled.yes.so(env.api.accountClosure.close(sus.user)))
api.setAlt(sus, true) >> (sus.user.enabled.yes.so(env.api.accountTermination.disable(sus.user)))
.runWith(Sink.ignore)
.void
.inject(NoContent)
Expand Down Expand Up @@ -114,7 +114,7 @@ final class Mod(

def closeAccount(username: UserStr) = OAuthMod(_.CloseAccount) { _ ?=> me ?=>
meOrFetch(username).flatMapz: user =>
env.api.accountClosure.close(user).map(some)
env.api.accountTermination.disable(user).map(some)
}(actionResult(username))

def reopenAccount(username: UserStr) = OAuthMod(_.CloseAccount) { _ ?=> me ?=>
Expand Down Expand Up @@ -380,13 +380,10 @@ final class Mod(
env.user.noteApi.search(q.trim, page, withDox = true).map(views.mod.search.notes(q, _))
}

def gdprErase(username: UserStr) = Secure(_.GdprErase) { _ ?=> me ?=>
val res = Redirect(routes.User.show(username))
env.api.accountClosure
.closeThenErase(username)
.map:
case Right(msg) => res.flashSuccess(msg)
case Left(err) => res.flashFailure(err)
def gdprErase(username: UserStr) = Secure(_.GdprErase) { _ ?=> _ ?=>
Found(env.user.repo.byId(username)): user =>
for _ <- env.api.accountTermination.scheduleErase(user)
yield Redirect(routes.User.show(username)).flashSuccess("Erasure scheduled")
}

protected[controllers] def searchTerm(query: String)(using Context, Me) =
Expand All @@ -411,7 +408,7 @@ final class Mod(

def printBan(v: Boolean, fh: String) = Secure(_.PrintBan) { _ ?=> me ?=>
val hash = FingerHash(fh)
env.security.printBan.toggle(hash, v).inject(Redirect(routes.Mod.print(fh)))
for _ <- env.security.printBan.toggle(hash, v) yield Redirect(routes.Mod.print(fh))
}

def singleIp(ip: String) = SecureBody(_.ViewPrintNoIP) { ctx ?=> me ?=>
Expand Down
8 changes: 4 additions & 4 deletions app/controllers/User.scala
Original file line number Diff line number Diff line change
Expand Up @@ -396,8 +396,8 @@ final class User(
.map(ui.showRageSitAndPlaybans)
)

val actions = env.user.repo.isErased(user).map { erased =>
ui.actions(user, emails, erased, env.mod.presets.getPmPresets)
val actions = env.user.repo.isDeleted(user).map { deleted =>
ui.actions(user, emails, deleted, env.mod.presets.getPmPresets)
}

val userLoginsFu = env.security.userLogins(user, nbOthers)
Expand Down Expand Up @@ -458,12 +458,12 @@ final class User(
protected[controllers] def renderModZoneActions(username: UserStr)(using ctx: Context) =
env.user.api.withPerfsAndEmails(username).orFail(s"No such user $username").flatMap {
case WithPerfsAndEmails(user, emails) =>
env.user.repo.isErased(user).flatMap { erased =>
env.user.repo.isDeleted(user).flatMap { deleted =>
Ok.snip:
views.mod.user.actions(
user,
emails,
erased,
deleted,
env.mod.presets.getPmPresetsOpt
)
}
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/UserTournament.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ final class UserTournament(env: Env, apiC: => Api) extends LilaController(env):
apiC.jsonDownload:
env.tournament.leaderboardApi
.byPlayerStream(
user,
user.id,
withPerformance = getBool("performance"),
maxPerSecond(name),
getInt("nb") | Int.MaxValue
Expand Down
2 changes: 1 addition & 1 deletion app/views/user/show/page.scala
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ object page:
h1(cls := "box__top")("No such player"),
div(
p("This username doesn't match any Lichess player."),
(!canCreate).option(p("It cannot be used to create a new account."))
canCreate.not.option(p("It cannot be used to create a new account."))
)
)

Expand Down
26 changes: 26 additions & 0 deletions bin/mongodb/analysis-requester-cleanup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
count = 0;
db.analysis_requester.find().forEach(function(doc) {
const entries = Object.entries(doc);
const reduced = entries.filter(([key]) => !key.includes('-') || key.startsWith('2025-'));
if (entries.length !== reduced.length) {
const newDoc = Object.fromEntries(reduced);
db.analysis_requester.replaceOne({ _id: doc._id }, newDoc);
}
if (count % 1000 === 0) print(count);
count++;
});

print(count, "documents processed");

/*
size: 2710719734,
count: 6218537,
storageSize: 627077120,
totalIndexSize: 178106368,
totalSize: 805183488,
indexSizes: { 'total_-1': 51482624, _id_: 126623744 },
avgObjSize: 435,
ns: 'lichess.analysis_requester',
nindexes: 2,
scaleFactor: 1
*/
2 changes: 1 addition & 1 deletion bin/mongodb/indexes.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ db.user4.createIndex({ title: 1 }, { sparse: true });
db.user4.createIndex({ email: 1 }, { unique: true, sparse: 1 });
db.user4.createIndex({ roles: 1 }, { background: 1, partialFilterExpression: { roles: { $exists: 1 } } });
db.user4.createIndex({ prevEmail: 1 }, { sparse: 1, background: 1 });
db.user4.createIndex({ eraseAt: 1 }, { partialFilterExpression: { eraseAt: { $exists: true } } });
db.user4.createIndex({ 'delete.scheduled': 1 }, { partialFilterExpression: { 'delete.scheduled': { $exists: 1 }, 'delete.done': false } })
db.f_topic.createIndex({ categId: 1, troll: 1 });
db.f_topic.createIndex({ categId: 1, updatedAt: -1, troll: 1 });
db.f_topic.createIndex({ categId: 1, slug: 1 });
Expand Down
5 changes: 4 additions & 1 deletion conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -774,7 +774,10 @@ POST /account/email controllers.Account.emailApply
GET /contact/email-confirm/help controllers.Account.emailConfirmHelp
GET /account/email/confirm/:token controllers.Account.emailConfirm(token)
GET /account/close controllers.Account.close
POST /account/closeConfirm controllers.Account.closeConfirm
POST /account/close controllers.Account.closeConfirm
GET /account/delete controllers.Account.delete
POST /account/delete controllers.Account.deleteConfirm
GET /account/delete/done controllers.Account.deleteDone
GET /account/profile controllers.Account.profile
POST /account/profile controllers.Account.profileApply
GET /account/username controllers.Account.username
Expand Down
2 changes: 2 additions & 0 deletions modules/activity/src/main/ActivityWriteApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ final class ActivityWriteApi(
import BSONHandlers.{ *, given }
import activities.*

def deleteAll(u: User): Funit = withColl(_.delete.one(regexId(u.id)).void)

def game(game: Game): Funit =
(for
userId <- game.userIds
Expand Down
4 changes: 3 additions & 1 deletion modules/analyse/src/main/AnalyseBsonHandlers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import lila.tree.{ Analysis, Info }

object AnalyseBsonHandlers:

given BSONWriter[Analysis.Id] = BSONWriter(id => BSONString(id.value))

given BSON[Analysis] with
def reads(r: BSON.Reader) =
val startPly = Ply(r.intD("ply"))
Expand All @@ -28,7 +30,7 @@ object AnalyseBsonHandlers:
)
def writes(w: BSON.Writer, a: Analysis) =
BSONDocument(
"_id" -> a.id.value,
"_id" -> a.id,
"studyId" -> a.studyId,
"data" -> Info.encodeList(a.infos),
"ply" -> w.intO(a.startPly.value),
Expand Down
11 changes: 7 additions & 4 deletions modules/analyse/src/main/AnalysisRepo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,31 @@ package lila.analyse

import lila.db.dsl.*
import lila.tree.Analysis
import reactivemongo.api.bson.*

final class AnalysisRepo(val coll: Coll)(using Executor):

import AnalyseBsonHandlers.given

def save(analysis: Analysis) = coll.insert.one(analysis).void

def byId(id: Analysis.Id): Fu[Option[Analysis]] = coll.byId[Analysis](id.value)
def byId(id: Analysis.Id): Fu[Option[Analysis]] = coll.byId[Analysis](id)

def byGame(game: Game): Fu[Option[Analysis]] =
game.metadata.analysed.so(byId(Analysis.Id(game.id)))

def byIds(ids: Seq[Analysis.Id]): Fu[Seq[Option[Analysis]]] =
coll.optionsByOrderedIds[Analysis, String](ids.map(_.value))(_.id.value)
coll.optionsByOrderedIds[Analysis, Analysis.Id](ids)(_.id)

def associateToGames(games: List[Game]): Fu[List[(Game, Analysis)]] =
byIds(games.map(g => Analysis.Id(g.id))).map: as =>
games.zip(as).collect { case (game, Some(analysis)) =>
game -> analysis
}

def remove(id: GameId) = coll.delete.one($id(Analysis.Id(id).value))
def remove(id: GameId) = coll.delete.one($id(Analysis.Id(id)))

def exists(id: GameId) = coll.exists($id(id.value))
def remove(ids: List[GameId]) = coll.delete.one($inIds(ids.map(Analysis.Id(_))))

def exists(id: GameId) = coll.exists($id(Analysis.Id(id)))
def chapterExists(id: StudyChapterId) = coll.exists($id(id.value))
9 changes: 5 additions & 4 deletions modules/analyse/src/main/RequesterApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,16 @@ final class RequesterApi(coll: Coll)(using Executor):
coll
.one(
$id(userId),
$doc {
$doc:
(7 to 0 by -1).toList.map(now.minusDays).map(formatter.print).map(_ -> BSONBoolean(true))
}
)
.map { doc =>
.map: doc =>
val daily = doc.flatMap(_.int(formatter.print(now)))
val weekly = doc.so:
_.values.foldLeft(0):
case (acc, BSONInteger(v)) => acc + v
case (acc, _) => acc
(~daily, weekly)
}

lila.common.Bus.sub[lila.core.user.UserDelete]: del =>
coll.delete.one($id(del.id)).void
82 changes: 0 additions & 82 deletions modules/api/src/main/AccountClosure.scala

This file was deleted.

Loading
Loading