1616using XtremeIdiots . Portal . Repository . Abstractions . Models . V1 . LiveStatus ;
1717using XtremeIdiots . Portal . Repository . Abstractions . Models . V1 . Maps ;
1818using XtremeIdiots . Portal . Repository . Abstractions . Models . V1 . Players ;
19+ using XtremeIdiots . Portal . Repository . Abstractions . Models . V1 . Screenshots ;
1920using XtremeIdiots . Portal . Repository . Api . Client . V1 ;
2021using XtremeIdiots . Portal . Web . Auth . Constants ;
2122using XtremeIdiots . Portal . Web . Extensions ;
@@ -155,13 +156,16 @@ public async Task<IActionResult> ServerDetail(Guid id, CancellationToken cancell
155156 var mapRotAuth = authorizationService . AuthorizeAsync ( User , gs . GameType , AuthPolicies . MapRotations_Read ) ;
156157 var statusAuth = authorizationService . AuthorizeAsync ( User , AuthPolicies . GameServers_BanFileMonitors_Read ) ;
157158 var editAuth = authorizationService . AuthorizeAsync ( User , gs . GameType , AuthPolicies . GameServers_Write ) ;
159+ var screenshotsReadAuth = authorizationService . AuthorizeAsync ( User , gs . GameType , AuthPolicies . GameServers_Admin_Screenshots_Read ) ;
160+ var screenshotsDeleteAuth = authorizationService . AuthorizeAsync ( User , gs . GameType , AuthPolicies . GameServers_Admin_Screenshots_Delete ) ;
158161
159162 // Check fine-grained RCON sub-action permissions in parallel
160163 var sayAuth = authorizationService . AuthorizeAsync ( User , gs . GameType , AuthPolicies . GameServers_Admin_Rcon_Say ) ;
161164 var mapCmdAuth = authorizationService . AuthorizeAsync ( User , gs . GameType , AuthPolicies . GameServers_Admin_Rcon_Map ) ;
162165 var restartSrvAuth = authorizationService . AuthorizeAsync ( User , gs . GameType , AuthPolicies . GameServers_Admin_Rcon_Restart ) ;
166+ var screenshotCmdAuth = authorizationService . AuthorizeAsync ( User , gs . GameType , AuthPolicies . GameServers_Admin_Rcon_Screenshot ) ;
163167
164- await Task . WhenAll ( rconAuth , chatAuth , mapRotAuth , statusAuth , editAuth , sayAuth , mapCmdAuth , restartSrvAuth ) . ConfigureAwait ( false ) ;
168+ await Task . WhenAll ( rconAuth , chatAuth , mapRotAuth , statusAuth , editAuth , screenshotsReadAuth , screenshotsDeleteAuth , sayAuth , mapCmdAuth , restartSrvAuth , screenshotCmdAuth ) . ConfigureAwait ( false ) ;
165169
166170 var viewModel = new ServerDetailViewModel
167171 {
@@ -172,9 +176,12 @@ public async Task<IActionResult> ServerDetail(Guid id, CancellationToken cancell
172176 CanViewMapRotation = ( await mapRotAuth . ConfigureAwait ( false ) ) . Succeeded ,
173177 CanViewStatus = ( await statusAuth . ConfigureAwait ( false ) ) . Succeeded ,
174178 CanEditServer = ( await editAuth . ConfigureAwait ( false ) ) . Succeeded ,
179+ CanViewScreenshots = ( await screenshotsReadAuth . ConfigureAwait ( false ) ) . Succeeded ,
175180 CanSay = ( await sayAuth . ConfigureAwait ( false ) ) . Succeeded ,
176181 CanChangeMap = ( await mapCmdAuth . ConfigureAwait ( false ) ) . Succeeded ,
177- CanRestartServer = ( await restartSrvAuth . ConfigureAwait ( false ) ) . Succeeded
182+ CanRestartServer = ( await restartSrvAuth . ConfigureAwait ( false ) ) . Succeeded ,
183+ CanTakeScreenshot = ( await screenshotCmdAuth . ConfigureAwait ( false ) ) . Succeeded ,
184+ CanDeleteScreenshots = ( await screenshotsDeleteAuth . ConfigureAwait ( false ) ) . Succeeded
178185 } ;
179186
180187 // Fetch overview data (non-critical — page renders without it)
@@ -1151,6 +1158,196 @@ await CreateAdminActionForRconOperationAsync(
11511158 } , nameof ( BanRconPlayer ) ) . ConfigureAwait ( false ) ;
11521159 }
11531160
1161+ [ HttpPost ]
1162+ [ ValidateAntiForgeryToken ]
1163+ public async Task < IActionResult > TakeRconScreenshot ( Guid id , string playerIdentifier , string playerName , CancellationToken cancellationToken = default )
1164+ {
1165+ return await ExecuteWithErrorHandlingAsync ( async ( ) =>
1166+ {
1167+ var ( actionResult , gameServerData ) = await GetAuthorizedGameServerAsync ( id , nameof ( TakeRconScreenshot ) , cancellationToken ) . ConfigureAwait ( false ) ;
1168+ if ( actionResult is not null )
1169+ return actionResult ;
1170+
1171+ var screenshotAuthResult = await CheckAuthorizationAsync (
1172+ authorizationService ,
1173+ gameServerData ! . GameType ,
1174+ AuthPolicies . GameServers_Admin_Rcon_Screenshot ,
1175+ nameof ( TakeRconScreenshot ) ,
1176+ "GameServer" ,
1177+ $ "ServerId:{ id } ,GameType:{ gameServerData . GameType } ",
1178+ gameServerData ) . ConfigureAwait ( false ) ;
1179+
1180+ if ( screenshotAuthResult is not null )
1181+ return Json ( new { success = false , message = "You don't have permission to request screenshots" } ) ;
1182+
1183+ if ( string . IsNullOrWhiteSpace ( playerIdentifier ) )
1184+ {
1185+ return Json ( new { success = false , message = "Player identifier is required" } ) ;
1186+ }
1187+
1188+ var result = await serversApiClient . Rcon . V1 . TakeScreenshot ( id , new TakeScreenshotRequestDto
1189+ {
1190+ PlayerIdentifier = playerIdentifier . Trim ( )
1191+ } , cancellationToken ) . ConfigureAwait ( false ) ;
1192+
1193+ if ( ! result . IsSuccess )
1194+ {
1195+ return Json ( new { success = false , message = "Failed to request screenshot from game server" } ) ;
1196+ }
1197+
1198+ TrackSuccessTelemetry ( "RconScreenshotRequested" , nameof ( TakeRconScreenshot ) , new Dictionary < string , string >
1199+ {
1200+ { "ServerId" , id . ToString ( ) } ,
1201+ { "GameType" , gameServerData . GameType . ToString ( ) } ,
1202+ { "PlayerIdentifier" , playerIdentifier . Trim ( ) }
1203+ } ) ;
1204+
1205+ return Json ( new { success = true , message = $ "Screenshot requested for { ( string . IsNullOrWhiteSpace ( playerName ) ? "player" : playerName ) } . It may take a short time to appear." } ) ;
1206+ } , nameof ( TakeRconScreenshot ) ) . ConfigureAwait ( false ) ;
1207+ }
1208+
1209+ [ HttpGet ]
1210+ public async Task < IActionResult > GetScreenshots ( Guid id , int skipEntries = 0 , int takeEntries = 1000 , CancellationToken cancellationToken = default )
1211+ {
1212+ return await ExecuteWithErrorHandlingAsync ( async ( ) =>
1213+ {
1214+ var gameServerResponse = await repositoryApiClient . GameServers . V1 . GetGameServer ( id , cancellationToken ) . ConfigureAwait ( false ) ;
1215+ if ( gameServerResponse . IsNotFound || gameServerResponse . Result ? . Data is null )
1216+ {
1217+ return NotFound ( ) ;
1218+ }
1219+
1220+ var gameServer = gameServerResponse . Result . Data ;
1221+ var authResult = await CheckAuthorizationAsync (
1222+ authorizationService ,
1223+ gameServer . GameType ,
1224+ AuthPolicies . GameServers_Admin_Screenshots_Read ,
1225+ nameof ( GetScreenshots ) ,
1226+ "GameServer" ,
1227+ $ "ServerId:{ id } ,GameType:{ gameServer . GameType } ",
1228+ gameServer ) . ConfigureAwait ( false ) ;
1229+
1230+ if ( authResult is not null )
1231+ return authResult ;
1232+
1233+ var screenshotsResponse = await repositoryApiClient . Screenshots . V1 . GetScreenshots (
1234+ id ,
1235+ Math . Max ( skipEntries , 0 ) ,
1236+ Math . Clamp ( takeEntries , 1 , 2000 ) ,
1237+ ScreenshotOrder . CapturedUtcDesc ,
1238+ cancellationToken ) . ConfigureAwait ( false ) ;
1239+
1240+ if ( ! screenshotsResponse . IsSuccess || screenshotsResponse . Result ? . Data ? . Items is null )
1241+ {
1242+ return Json ( new { data = Array . Empty < object > ( ) } ) ;
1243+ }
1244+
1245+ var data = screenshotsResponse . Result . Data . Items . Select ( s => new
1246+ {
1247+ screenshotId = s . ScreenshotId ,
1248+ capturedUtc = s . CapturedUtc ,
1249+ playerIdentifier = s . PlayerIdentifier ,
1250+ playerName = s . PlayerName ,
1251+ sourceFileName = s . SourceFileName ,
1252+ sizeBytes = s . SizeBytes ,
1253+ contentType = s . ContentType
1254+ } ) ;
1255+
1256+ return Json ( new { data } ) ;
1257+ } , nameof ( GetScreenshots ) ) . ConfigureAwait ( false ) ;
1258+ }
1259+
1260+ [ HttpGet ]
1261+ public async Task < IActionResult > GetScreenshotContent ( Guid id , Guid screenshotId , CancellationToken cancellationToken = default )
1262+ {
1263+ return await ExecuteWithErrorHandlingAsync ( async ( ) =>
1264+ {
1265+ var screenshotResponse = await repositoryApiClient . Screenshots . V1 . GetScreenshot ( screenshotId , cancellationToken ) . ConfigureAwait ( false ) ;
1266+ if ( screenshotResponse . IsNotFound || screenshotResponse . Result ? . Data is null )
1267+ {
1268+ return NotFound ( ) ;
1269+ }
1270+
1271+ var screenshot = screenshotResponse . Result . Data ;
1272+ if ( screenshot . GameServerId != id )
1273+ {
1274+ return NotFound ( ) ;
1275+ }
1276+
1277+ if ( ! Enum . TryParse < GameType > ( screenshot . GameType , true , out var gameType ) || gameType == GameType . Unknown )
1278+ {
1279+ return BadRequest ( ) ;
1280+ }
1281+
1282+ var authResult = await CheckAuthorizationAsync (
1283+ authorizationService ,
1284+ gameType ,
1285+ AuthPolicies . GameServers_Admin_Screenshots_Read ,
1286+ nameof ( GetScreenshotContent ) ,
1287+ "Screenshot" ,
1288+ $ "ScreenshotId:{ screenshotId } ,ServerId:{ id } ,GameType:{ gameType } ",
1289+ screenshot ) . ConfigureAwait ( false ) ;
1290+
1291+ if ( authResult is not null )
1292+ return authResult ;
1293+
1294+ var contentResponse = await repositoryApiClient . Screenshots . V1 . GetScreenshotContent ( screenshotId , cancellationToken ) . ConfigureAwait ( false ) ;
1295+ if ( ! contentResponse . IsSuccess || contentResponse . Result ? . Data is null )
1296+ {
1297+ return NotFound ( ) ;
1298+ }
1299+
1300+ var content = contentResponse . Result . Data ;
1301+ var fileName = string . IsNullOrWhiteSpace ( content . FileName ) ? $ "{ screenshotId } .jpg" : content . FileName ;
1302+ return File ( content . Content , content . ContentType , fileName ) ;
1303+ } , nameof ( GetScreenshotContent ) ) . ConfigureAwait ( false ) ;
1304+ }
1305+
1306+ [ HttpPost ]
1307+ [ ValidateAntiForgeryToken ]
1308+ public async Task < IActionResult > DeleteScreenshot ( Guid id , Guid screenshotId , CancellationToken cancellationToken = default )
1309+ {
1310+ return await ExecuteWithErrorHandlingAsync ( async ( ) =>
1311+ {
1312+ var screenshotResponse = await repositoryApiClient . Screenshots . V1 . GetScreenshot ( screenshotId , cancellationToken ) . ConfigureAwait ( false ) ;
1313+ if ( screenshotResponse . IsNotFound || screenshotResponse . Result ? . Data is null )
1314+ {
1315+ return Json ( new { success = false , message = "Screenshot not found" } ) ;
1316+ }
1317+
1318+ var screenshot = screenshotResponse . Result . Data ;
1319+ if ( screenshot . GameServerId != id )
1320+ {
1321+ return Json ( new { success = false , message = "Screenshot not found" } ) ;
1322+ }
1323+
1324+ if ( ! Enum . TryParse < GameType > ( screenshot . GameType , true , out var gameType ) || gameType == GameType . Unknown )
1325+ {
1326+ return Json ( new { success = false , message = "Invalid screenshot game type" } ) ;
1327+ }
1328+
1329+ var authResult = await CheckAuthorizationAsync (
1330+ authorizationService ,
1331+ gameType ,
1332+ AuthPolicies . GameServers_Admin_Screenshots_Delete ,
1333+ nameof ( DeleteScreenshot ) ,
1334+ "Screenshot" ,
1335+ $ "ScreenshotId:{ screenshotId } ,ServerId:{ id } ,GameType:{ gameType } ",
1336+ screenshot ) . ConfigureAwait ( false ) ;
1337+
1338+ if ( authResult is not null )
1339+ return Json ( new { success = false , message = "You don't have permission to delete screenshots" } ) ;
1340+
1341+ var deleteResponse = await repositoryApiClient . Screenshots . V1 . DeleteScreenshot ( screenshotId , cancellationToken ) . ConfigureAwait ( false ) ;
1342+ if ( ! deleteResponse . IsSuccess )
1343+ {
1344+ return Json ( new { success = false , message = "Failed to delete screenshot" } ) ;
1345+ }
1346+
1347+ return Json ( new { success = true , message = "Screenshot deleted" } ) ;
1348+ } , nameof ( DeleteScreenshot ) ) . ConfigureAwait ( false ) ;
1349+ }
1350+
11541351 /// <summary>
11551352 /// Creates an admin action record for an RCON operation (kick/ban)
11561353 /// </summary>
0 commit comments