@@ -43,6 +43,8 @@ import { AdminStatsService } from './admin-stats.service';
4343import { AdminAnalyticsService } from './admin-analytics.service' ;
4444import { PrismaService } from '../prisma/prisma.service' ;
4545import { SorobanService } from '../rpc/soroban.service' ;
46+ import { TokenBlacklistService } from '../auth/token-blacklist.service' ;
47+ import { SupportService } from '../support/support.service' ;
4648
4749class BatchRegisterVotersDto {
4850 @IsArray ( )
@@ -76,6 +78,15 @@ class SetClaimSeverityDto {
7678 severity ! : ClaimSeverity ;
7779}
7880
81+ class RevokeTokenDto {
82+ @IsString ( ) jti ! : string ;
83+ @IsInt ( ) expiresAt ! : number ;
84+ }
85+
86+ class AssignTicketDto {
87+ @IsOptional ( ) @IsString ( ) assignee ?: string | null ;
88+ }
89+
7990type AdminRequest = Request & {
8091 user ?: {
8192 walletAddress ?: string ;
@@ -111,6 +122,8 @@ export class AdminController {
111122 private readonly adminAnalyticsService : AdminAnalyticsService ,
112123 private readonly prisma : PrismaService ,
113124 private readonly sorobanService : SorobanService ,
125+ private readonly tokenBlacklist : TokenBlacklistService ,
126+ private readonly supportService : SupportService ,
114127 ) { }
115128
116129 // ── Governance: Voters ────────────────────────────────────────────
@@ -926,4 +939,68 @@ export class AdminController {
926939 } ) ;
927940 return result ;
928941 }
942+
943+ // ── Auth: Token Management ─────────────────────────────────────────
944+
945+ /**
946+ * POST /admin/auth/revoke
947+ *
948+ * Revoke a JWT token immediately by adding to Redis blacklist.
949+ * Token remains blacklisted until its expiry time.
950+ */
951+ @Post ( 'auth/revoke' )
952+ @HttpCode ( HttpStatus . NO_CONTENT )
953+ @ApiOperation ( { summary : 'Revoke a JWT token' } )
954+ async revokeToken ( @Body ( ) dto : RevokeTokenDto , @Req ( ) req : AdminRequest ) {
955+ if ( ! dto . jti || dto . jti . length === 0 ) {
956+ throw new BadRequestException ( 'jti must be a non-empty string' ) ;
957+ }
958+ if ( ! Number . isInteger ( dto . expiresAt ) || dto . expiresAt <= 0 ) {
959+ throw new BadRequestException ( 'expiresAt must be a positive integer (Unix timestamp)' ) ;
960+ }
961+
962+ await this . tokenBlacklist . revokeToken ( dto . jti , dto . expiresAt ) ;
963+
964+ const actor = req . adminIdentity ?. staffId || req . adminIdentity ?. email || 'unknown' ;
965+ await this . auditService . write ( {
966+ actor,
967+ action : 'auth_token_revoke' ,
968+ payload : { jti : dto . jti } ,
969+ ipAddress : req . ip ,
970+ } ) ;
971+ }
972+
973+ // ── Support: Ticket Management ─────────────────────────────────────
974+
975+ /**
976+ * GET /admin/support/tickets
977+ *
978+ * List all support tickets with optional filtering.
979+ */
980+ @Get ( 'support/tickets' )
981+ @MinAdminRole ( 'viewer' )
982+ @ApiOperation ( { summary : 'List support tickets' } )
983+ async listSupportTickets (
984+ @Query ( 'limit' , new ParseIntPipe ( { optional : true } ) ) limit ?: number ,
985+ @Query ( 'offset' , new ParseIntPipe ( { optional : true } ) ) offset ?: number ,
986+ @Query ( 'assignedTo' ) assignedTo ?: string ,
987+ ) {
988+ return this . supportService . listTickets ( limit || 50 , offset || 0 , assignedTo ) ;
989+ }
990+
991+ /**
992+ * PATCH /admin/support/tickets/:id/assign
993+ *
994+ * Assign a support ticket to a staff member or unassign it.
995+ */
996+ @Patch ( 'support/tickets/:id/assign' )
997+ @ApiOperation ( { summary : 'Assign support ticket to staff member' } )
998+ async assignSupportTicket (
999+ @Param ( 'id' ) ticketId : string ,
1000+ @Body ( ) dto : AssignTicketDto ,
1001+ @Req ( ) req : AdminRequest ,
1002+ ) {
1003+ const actor = req . adminIdentity ?. staffId || req . adminIdentity ?. email || 'unknown' ;
1004+ return this . supportService . assignTicket ( ticketId , dto . assignee ?? null , actor , req . ip ) ;
1005+ }
9291006}
0 commit comments