diff --git a/app/lib/admin/actions/moderate_user.dart b/app/lib/admin/actions/moderate_user.dart index ee6f1d278a..15db91cf2d 100644 --- a/app/lib/admin/actions/moderate_user.dart +++ b/app/lib/admin/actions/moderate_user.dart @@ -2,9 +2,15 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -import 'package:pub_dev/account/agent.dart'; -import 'package:pub_dev/account/backend.dart'; -import 'package:pub_dev/account/models.dart'; +import 'package:clock/clock.dart'; + +import '../../account/agent.dart'; +import '../../account/backend.dart'; +import '../../account/models.dart'; +import '../../package/backend.dart'; +import '../../package/models.dart'; +import '../../publisher/backend.dart'; +import '../../shared/datastore.dart'; import 'actions.dart'; @@ -56,6 +62,60 @@ The active web sessions of the user will be expired. if (valueToSet != null) { await accountBackend.updateModeratedFlag(user!.userId, valueToSet); user2 = await accountBackend.lookupUserById(user.userId); + + if (valueToSet) { + await for (final p + in packageBackend.streamPackagesWhereUserIsUploader(user.userId)) { + await withRetryTransaction(dbService, (tx) async { + final key = dbService.emptyKey.append(Package, id: p); + final pkg = await tx.lookupOrNull(key); + if (pkg == null || pkg.isDiscontinued || pkg.uploaderCount != 1) { + return; + } + pkg.isDiscontinued = true; + pkg.updated = clock.now().toUtc(); + tx.insert(pkg); + }); + } + + final publishers = + await publisherBackend.listPublishersForUser(user.userId); + for (final e in publishers.publishers!) { + final p = await publisherBackend.getPublisher(e.publisherId); + if (p == null) { + continue; + } + // Only restrict publishers where the user was a single active admin. + // Note: at this point the User.isModerated flag is already set. + final members = + await publisherBackend.listPublisherMembers(e.publisherId); + var nonBlockedCount = 0; + for (final member in members) { + final mu = await accountBackend.lookupUserById(member.userId); + if (mu?.isVisible ?? false) { + nonBlockedCount++; + } + } + if (nonBlockedCount > 0) { + continue; + } + + final query = dbService.query() + ..filter('publisherId =', e.publisherId); + await for (final p in query.run()) { + if (p.isDiscontinued) continue; + await withRetryTransaction(dbService, (tx) async { + final pkg = await tx.lookupOrNull(p.key); + if (pkg == null || pkg.isDiscontinued) { + return; + } + pkg.isDiscontinued = true; + pkg.updated = clock.now().toUtc(); + tx.insert(pkg); + }); + } + } + } } return { diff --git a/app/lib/package/backend.dart b/app/lib/package/backend.dart index d7e8b13573..f75f67a4b8 100644 --- a/app/lib/package/backend.dart +++ b/app/lib/package/backend.dart @@ -208,6 +208,19 @@ class PackageBackend { ); } + /// Streams package names where the [userId] is an uploader. + Stream streamPackagesWhereUserIsUploader(String userId) async* { + var page = await listPackagesForUser(userId); + while (page.packages.isNotEmpty) { + yield* Stream.fromIterable(page.packages); + if (page.nextPackage == null) { + break; + } else { + page = await listPackagesForUser(userId, next: page.nextPackage); + } + } + } + /// Returns the latest releases info of a package. Future latestReleases(Package package) async { // TODO: implement runtimeVersion-specific release calculation diff --git a/app/test/account/moderate_user_test.dart b/app/test/account/moderate_user_test.dart index 30ac87ba53..463897aec6 100644 --- a/app/test/account/moderate_user_test.dart +++ b/app/test/account/moderate_user_test.dart @@ -10,6 +10,7 @@ import 'package:pub_dev/account/auth_provider.dart'; import 'package:pub_dev/account/backend.dart'; import 'package:pub_dev/account/models.dart'; import 'package:pub_dev/fake/backend/fake_auth_provider.dart'; +import 'package:pub_dev/package/backend.dart'; import 'package:pub_dev/shared/configuration.dart'; import 'package:pub_dev/shared/datastore.dart'; import 'package:test/test.dart'; @@ -163,8 +164,43 @@ void main() { expect(rs.websiteUrl, 'https://other.com/'); }); - // TODO(https://github.com/dart-lang/pub-dev/issues/7535): - // - single packages owned exclusively by the user are marked discontinued - // - publisher packages owned exclusively by the user are marked discontinued + testWithProfile('single packages marked discontinued', fn: () async { + final pubspecContent = generatePubspecYaml('foo', '1.0.0'); + final bytes = await packageArchiveBytes(pubspecContent: pubspecContent); + await createPubApiClient(authToken: userClientToken) + .uploadPackageBytes(bytes); + + await _moderate('user@pub.dev', state: true); + final p1 = await packageBackend.lookupPackage('foo'); + expect(p1!.isDiscontinued, true); + + await _moderate('user@pub.dev', state: false); + final p2 = await packageBackend.lookupPackage('foo'); + expect(p2!.isDiscontinued, true); + }); + + testWithProfile('publisher packages marked discontinued', fn: () async { + final client = await createFakeAuthPubApiClient( + email: 'user@pub.dev', scopes: [webmasterScope]); + await client.createPublisher('verified.com'); + + final pubspecContent = generatePubspecYaml('foo', '1.0.0'); + final bytes = await packageArchiveBytes(pubspecContent: pubspecContent); + await createPubApiClient(authToken: userClientToken) + .uploadPackageBytes(bytes); + await (await createFakeAuthPubApiClient(email: 'user@pub.dev')) + .setPackagePublisher( + 'foo', PackagePublisherInfo(publisherId: 'verified.com')); + + await _moderate('user@pub.dev', state: true); + final p1 = await packageBackend.lookupPackage('foo'); + expect(p1!.publisherId, isNotEmpty); + expect(p1.isDiscontinued, true); + + await _moderate('user@pub.dev', state: false); + final p2 = await packageBackend.lookupPackage('foo'); + expect(p2!.publisherId, isNotEmpty); + expect(p2.isDiscontinued, true); + }); }); }