diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/config/SecurityConfig.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/config/SecurityConfig.kt index b54023e..1b080bf 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/config/SecurityConfig.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/config/SecurityConfig.kt @@ -73,8 +73,14 @@ class SecurityConfig { .authorizeHttpRequests { auth -> auth .requestMatchers("/user-queue").authenticated() + .requestMatchers( "/follows", "/follows/**").authenticated() .anyRequest().permitAll() } + .exceptionHandling { + it.authenticationEntryPoint { _, response, _ -> + response.sendError(HttpServletResponse.SC_UNAUTHORIZED) + } + } .oauth2Login { oauthLogin -> oauthLogin.successHandler(CustomAuthenticationSuccessHandler()) } diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/BanlistController.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/BanlistController.kt index 5ee8c4a..0e9d351 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/BanlistController.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/BanlistController.kt @@ -1,16 +1,11 @@ package com.nisemoe.nise.controller import com.nisemoe.generated.tables.references.USERS -import com.nisemoe.generated.tables.references.USER_FOLLOWS -import com.nisemoe.nise.service.AuthService import jakarta.validation.Valid -import jakarta.validation.constraints.Size +import jakarta.validation.constraints.Min import org.jooq.DSLContext import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.DeleteMapping -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RestController import java.time.OffsetDateTime @@ -20,15 +15,84 @@ class BanlistController( private val dslContext: DSLContext ) { - data class BanStatisticsResponse( - val totalUsersBanned: Int + companion object { + + const val MAX_BANLIST_ENTRIES_PER_PAGE = 100 + + } + + data class BanlistRequest( + @field:Min(1) + val page: Int, ) - @GetMapping("banlist/statistics") - fun getBanStatistics(): BanStatisticsResponse { - val totalUsersBanned = dslContext.fetchCount(USERS, USERS.IS_BANNED.eq(true)) + data class BanlistResponse( + val pagination: BanlistPagination, + val users: List + ) - return BanStatisticsResponse(totalUsersBanned) + data class BanlistPagination( + val currentPage: Int, + val pageSize: Int, + val totalResults: Int + ) { + + val totalPages: Int + get() = if (totalResults % pageSize == 0) totalResults / pageSize else totalResults / pageSize + 1 + + } + + data class BanlistEntry( + val userId: Long, + val username: String, + val secondsPlayed: Long?, + val pp: Double?, + val rank: Long?, + val isBanned: Boolean?, + val approximateBanTime: OffsetDateTime?, + val lastUpdate: OffsetDateTime? + ) + + @PostMapping("banlist") + fun banUser(@RequestBody @Valid request: BanlistRequest): ResponseEntity { + // + val pagination = BanlistPagination( + request.page, + MAX_BANLIST_ENTRIES_PER_PAGE, + dslContext.fetchCount(USERS, USERS.IS_BANNED.eq(true)) + ) + + dslContext.select( + USERS.USER_ID, + USERS.USERNAME, + USERS.SECONDS_PLAYED, + USERS.IS_BANNED, + USERS.PP_RAW, + USERS.RANK, + USERS.SYS_LAST_UPDATE, + USERS.APPROX_BAN_DATE + ) + .from(USERS) + .where(USERS.IS_BANNED.eq(true)) + .orderBy(USERS.APPROX_BAN_DATE.desc()) + .limit(MAX_BANLIST_ENTRIES_PER_PAGE) + .offset((request.page - 1) * 10) + .fetch() + .map { + BanlistEntry( + it[USERS.USER_ID]!!, + it[USERS.USERNAME]!!, + it[USERS.SECONDS_PLAYED], + it[USERS.PP_RAW], + it[USERS.RANK], + it[USERS.IS_BANNED], + it[USERS.APPROX_BAN_DATE], + it[USERS.SYS_LAST_UPDATE] + ) + } + .let { + return ResponseEntity.ok(BanlistResponse(pagination, it)) + } } } \ No newline at end of file diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/FollowsController.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/FollowsController.kt index 503d8cd..00bd1fb 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/FollowsController.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/FollowsController.kt @@ -40,10 +40,6 @@ class FollowsController( @GetMapping("follows") fun getFollowsBanStatus(): ResponseEntity { - if(!authService.isLoggedIn()) { - return ResponseEntity.status(401).build() - } - val follows = dslContext.select( USERS.USER_ID, USERS.USERNAME, @@ -70,10 +66,6 @@ class FollowsController( @GetMapping("follows/{followsUserId}") fun getFollowsBanStatusByUserId(@PathVariable followsUserId: Long): ResponseEntity { - if(!authService.isLoggedIn()) { - return ResponseEntity.status(401).build() - } - val userId = authService.getCurrentUser().userId val follows = dslContext.select( @@ -109,10 +101,6 @@ class FollowsController( @PutMapping("follows") fun updateFollowsBanStatus(@RequestBody @Valid request: UpdateFollowsBanStatusRequest): ResponseEntity { - if(!authService.isLoggedIn()) { - return ResponseEntity.status(401).build() - } - // Check if the user already has MAX_FOLLOWS_PER_USER or more if(dslContext.fetchCount(USER_FOLLOWS, USER_FOLLOWS.USER_ID.eq(authService.getCurrentUser().userId)) >= MAX_FOLLOWS_PER_USER) { return ResponseEntity.status(400).build() @@ -138,10 +126,6 @@ class FollowsController( @DeleteMapping("follows") fun deleteFollowsBanStatus(@RequestBody @Valid request: DeleteFollowsBanStatusRequest): ResponseEntity { - if(!authService.isLoggedIn()) { - return ResponseEntity.status(401).build() - } - val userId = authService.getCurrentUser().userId for(userIdToUnban in request.userIds) { diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/ImportScores.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/ImportScores.kt index e7d896a..7f9e907 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/ImportScores.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/ImportScores.kt @@ -344,6 +344,7 @@ class ImportScores( .execute() if(isBanned == true) { + this.messagingTemplate.convertAndSend("/topic/banlist", userId) dslContext.update(SCORES) .set(SCORES.IS_BANNED, true) .where(SCORES.USER_ID.eq(userId)) diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/ImportUsers.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/ImportUsers.kt index 9708525..cce0f02 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/ImportUsers.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/scheduler/ImportUsers.kt @@ -10,6 +10,7 @@ import org.jooq.DSLContext import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Profile +import org.springframework.messaging.simp.SimpMessagingTemplate import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Service import java.time.OffsetDateTime @@ -21,6 +22,7 @@ class ImportUsers( private val userService: UserService, private val discordService: DiscordService, private val osuApi: OsuApi, + private val messagingTemplate: SimpMessagingTemplate ) { private val logger = LoggerFactory.getLogger(javaClass) @@ -103,6 +105,7 @@ class ImportUsers( Thread.sleep(SLEEP_AFTER_API_CALL) if (isUserBanned == true) { this.logger.warn("User $missingId is banned") + this.messagingTemplate.convertAndSend("/topic/banlist", missingId) dslContext.update(SCORES) .set(SCORES.IS_BANNED, true) .where(SCORES.USER_ID.eq(missingId)) diff --git a/nise-frontend/src/app/app-routing.module.ts b/nise-frontend/src/app/app-routing.module.ts index 2f1ad20..9cfd6d7 100644 --- a/nise-frontend/src/app/app-routing.module.ts +++ b/nise-frontend/src/app/app-routing.module.ts @@ -9,6 +9,7 @@ import {ViewReplayPairComponent} from "./view-replay-pair/view-replay-pair.compo import {SearchComponent} from "./search/search.component"; import {ContributeComponent} from "./contribute/contribute.component"; import {BanlistComponent} from "./banlist/banlist.component"; +import {FollowsComponent} from "./follows/follows.component"; const routes: Routes = [ {path: 'sus/:f', component: ViewSuspiciousScoresComponent, title: '/sus/'}, @@ -24,8 +25,10 @@ const routes: Routes = [ {path: 'p/:replay1Id/:replay2Id', component: ViewReplayPairComponent}, + {path: 'follows', component: FollowsComponent, title: '/follows/'}, {path: 'banlist', component: BanlistComponent, title: '/ban/'}, {path: 'contribute', component: ContributeComponent, title: '/contribute/ <3'}, + {path: '**', component: HomeComponent, title: '/nise.moe/'}, ]; diff --git a/nise-frontend/src/app/banlist/banlist.component.html b/nise-frontend/src/app/banlist/banlist.component.html index 93ac4d0..33a6b8a 100644 --- a/nise-frontend/src/app/banlist/banlist.component.html +++ b/nise-frontend/src/app/banlist/banlist.component.html @@ -1,27 +1,71 @@
-

# follow-list

- +

# recent bans

+
+

+ just because an user appears on this list, it doesn't mean they were banned for cheating. +

+

there are more a multitude of reasons osu!support might close an account.

+

+ all we do is check if the user exists (with their unique id) and if we get a "hey, it's missing" response (aka the User not found! ;_; message) we mark it as possibly banned. +

+
+ + +
+

Loading

+

please be patient - the database is working hard!

+
+
+ + + null + +
- + + + + - + - - + + + + + diff --git a/nise-frontend/src/app/banlist/banlist.component.ts b/nise-frontend/src/app/banlist/banlist.component.ts index 6b1c029..188a325 100644 --- a/nise-frontend/src/app/banlist/banlist.component.ts +++ b/nise-frontend/src/app/banlist/banlist.component.ts @@ -1,26 +1,35 @@ import {Component, OnInit} from '@angular/core'; import {HttpClient} from "@angular/common/http"; import {environment} from "../../environments/environment"; -import {JsonPipe, NgForOf, NgIf} from "@angular/common"; -import {calculateTimeAgo} from "../format"; +import {DatePipe, DecimalPipe, JsonPipe, NgForOf, NgIf} from "@angular/common"; +import {calculateTimeAgo, formatDuration} from "../format"; import {RouterLink} from "@angular/router"; +import {CuteProgressbarComponent} from "../../corelib/components/cute-progressbar/cute-progressbar.component"; +import {CuteLoadingComponent} from "../../corelib/components/cute-loading/cute-loading.component"; -interface BanStatisticsResponse { - totalUsersBanned: number; +interface BanlistResponse { + pagination: BanlistPagination; + users: BanlistEntry[]; } -interface FollowsBanStatusResponse { - follows: FollowsBanStatusEntry[]; +interface BanlistPagination { + currentPage: number; + pageSize: number; + totalResults: number; + totalPages: number; } -interface FollowsBanStatusEntry { +interface BanlistEntry { userId: number; username: string; - isBanned: boolean; - lastUpdate: string; + secondsPlayed?: number; + pp?: number; + rank?: number; + isBanned?: boolean; + approximateBanTime?: string; + lastUpdate?: string; } - @Component({ selector: 'app-banlist', standalone: true, @@ -28,34 +37,36 @@ interface FollowsBanStatusEntry { JsonPipe, NgForOf, NgIf, - RouterLink + RouterLink, + DatePipe, + DecimalPipe, + CuteProgressbarComponent, + CuteLoadingComponent ], templateUrl: './banlist.component.html', styleUrl: './banlist.component.css' }) export class BanlistComponent implements OnInit { - banStatistics: BanStatisticsResponse | null = null - follows: FollowsBanStatusResponse | null = null; + isLoading = true; + banlist: BanlistResponse | null = null; constructor(private httpClient: HttpClient) { } ngOnInit(): void { - this.getBanStatistics(); - this.getFollows(); + this.getBanlist(); } - getBanStatistics() { - this.httpClient.get(`${environment.apiUrl}/banlist/statistics`).subscribe(response => { - this.banStatistics = response; - }); - } - - getFollows(): void { - this.httpClient.get(`${environment.apiUrl}/follows`).subscribe(response => { - this.follows = response; + getBanlist(): void { + const body = { + page: 1 + } + this.httpClient.post(`${environment.apiUrl}/banlist`, body).subscribe(response => { + this.banlist = response; + this.isLoading = false; }); } protected readonly calculateTimeAgo = calculateTimeAgo; + protected readonly formatDuration = formatDuration; } diff --git a/nise-frontend/src/app/follows/follows.component.css b/nise-frontend/src/app/follows/follows.component.css new file mode 100644 index 0000000..a54a5f1 --- /dev/null +++ b/nise-frontend/src/app/follows/follows.component.css @@ -0,0 +1,3 @@ +table td { + text-align: center; +} diff --git a/nise-frontend/src/app/follows/follows.component.html b/nise-frontend/src/app/follows/follows.component.html new file mode 100644 index 0000000..93ac4d0 --- /dev/null +++ b/nise-frontend/src/app/follows/follows.component.html @@ -0,0 +1,31 @@ +
+
+

# follow-list

+
UsernameIs banned?Time playedTotal PPRank Last checkApproximate ban date
- + {{ user.username }} {{ user.isBanned }}{{ calculateTimeAgo(user.lastUpdate) }} + + {{ formatDuration(user.secondsPlayed) }} + + + + {{ user.pp | number: '1.0-0' }} + + + + #{{ user.rank | number }} + + + + {{ calculateTimeAgo(user.lastUpdate) }} + + + {{ user.approximateBanTime | date: 'medium' }} +
+ + + + + + + + + + + + + + + + +
UsernameIs banned?Last check
+ + + + {{ user.username }} + + {{ user.isBanned }}{{ calculateTimeAgo(user.lastUpdate) }} +
+ +
+
diff --git a/nise-frontend/src/app/follows/follows.component.ts b/nise-frontend/src/app/follows/follows.component.ts new file mode 100644 index 0000000..cb51047 --- /dev/null +++ b/nise-frontend/src/app/follows/follows.component.ts @@ -0,0 +1,49 @@ +import {Component, OnInit} from '@angular/core'; +import {HttpClient} from "@angular/common/http"; +import {environment} from "../../environments/environment"; +import {JsonPipe, NgForOf, NgIf} from "@angular/common"; +import {calculateTimeAgo} from "../format"; +import {RouterLink} from "@angular/router"; + +interface FollowsBanStatusResponse { + follows: FollowsBanStatusEntry[]; +} + +interface FollowsBanStatusEntry { + userId: number; + username: string; + isBanned: boolean; + lastUpdate: string; +} + + +@Component({ + selector: 'app-follows', + standalone: true, + imports: [ + JsonPipe, + NgForOf, + NgIf, + RouterLink + ], + templateUrl: './follows.component.html', + styleUrl: './follows.component.css' +}) +export class FollowsComponent implements OnInit { + + follows: FollowsBanStatusResponse | null = null; + + constructor(private httpClient: HttpClient) { } + + ngOnInit(): void { + this.getFollows(); + } + + getFollows(): void { + this.httpClient.get(`${environment.apiUrl}/follows`).subscribe(response => { + this.follows = response; + }); + } + + protected readonly calculateTimeAgo = calculateTimeAgo; +} diff --git a/nise-frontend/src/app/search/search.component.css b/nise-frontend/src/app/search/search.component.css index 084322b..97cf837 100644 --- a/nise-frontend/src/app/search/search.component.css +++ b/nise-frontend/src/app/search/search.component.css @@ -19,9 +19,3 @@ .score-entry:hover { background-color: rgba(179, 184, 195, 0.15); } - -code { - background-color: rgba(227, 232, 255, 0.1); - padding: 2px; - border-radius: 3px; -} diff --git a/nise-frontend/src/assets/style.css b/nise-frontend/src/assets/style.css index 5dedcc4..a2ffa73 100644 --- a/nise-frontend/src/assets/style.css +++ b/nise-frontend/src/assets/style.css @@ -14,6 +14,12 @@ src: url(ia-quattro-700-normal.woff2) format('woff2'); } +code { + background-color: rgba(227, 232, 255, 0.1); + padding: 2px; + border-radius: 3px; +} + html { background-color: #151515; @@ -190,7 +196,7 @@ a.btn-success:hover { } .alert { - padding: 5px; + padding: 8px; border: 1px dotted #b3b8c3; }