API: follower lists, blocking, reports, admin; drop Spotify sign-in

- Followers/following lists (paginated), 'follows you', remove a follower
- Blocking severs follows both ways and hides each user from the other
  (profile 404s, search, follows); the blocker sees activity hidden
- Reports for moderation (App Store guideline 1.2)
- First account on a server becomes admin; admins see the report queue,
  resolve reports, and suspend accounts (signs out, blocks login, hides
  the profile, stops sharing). set-role script to promote/demote
- users.onboardedAt drives first-run onboarding; existing accounts are
  backfilled as already onboarded. Empty PATCH /me no longer 500s
- Remove Spotify account sign-in and polling: Spotify caps new apps at 5
  accounts, so Spotify arrives via its Last.fm/ListenBrainz link or the
  Android reader. App-level catalog search stays for exact links
- Shared: QR code generation and profile-link parsing (verified with jsQR)
- Integration test timeout raised; password hashing is slow by design

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
2026-10-05 19:16:40 -04:00
co-authored by Claude Opus 5.5
parent 63d618f0e3
commit 586acdbeab
44 changed files with 4559 additions and 220 deletions
+36
View File
@@ -23,6 +23,9 @@ export interface MeDTO extends UserSummaryDTO {
email: string;
bio: string | null;
preferredService: StreamingService | null;
role: 'user' | 'admin';
/** When onboarding was finished; null means show it. */
onboardedAt: string | null;
/** ISO time sharing resumes, or null when sharing. Far future means "until turned back on". */
sharingPausedUntil: string | null;
plan: Plan;
@@ -77,6 +80,10 @@ export interface ProfileDTO {
isFollowing: boolean;
isPinned: boolean;
isSelf: boolean;
/** They follow the viewer. */
followsYou: boolean;
/** The viewer has blocked them (activity is hidden; they can be unblocked). */
isBlocked: boolean;
featured: FeaturedSongDTO | null;
nowPlaying: NowPlayingDTO | null;
/** Most recent first, not including the track playing right now. */
@@ -85,6 +92,35 @@ export interface ProfileDTO {
attribution: Partial<Record<'lastfm' | 'listenbrainz', string>>;
}
export interface AdminReportDTO {
id: string;
reason: string;
note: string | null;
status: 'open' | 'resolved';
createdAt: string;
reporter: UserSummaryDTO | null;
reported: UserSummaryDTO & { suspended: boolean };
}
export interface AdminStatsDTO {
users: number;
listeningNow: number;
openReports: number;
suspended: number;
}
/** A row in a followers/following list. */
export interface FollowListItemDTO extends UserSummaryDTO {
/** Whether the viewer follows this person (for Follow/Following buttons). */
isFollowing: boolean;
}
export interface FollowListDTO {
users: FollowListItemDTO[];
/** Pass back as ?cursor= for the next page; null when there are no more. */
nextCursor: string | null;
}
export interface ConnectionDTO {
provider: NowPlayingProvider;
externalUserId: string;