@@ -50,7 +50,7 @@ function getSafeRelativeFilePath(base: string, relPath: string): string | null {
5050import { SocData } from '../models/socData.entity' ;
5151import { ApplicationForm } from '../models/applicationForm.entity' ;
5252import { ApplicationSubmission } from '../models/applicationSubmission.entity' ;
53- import { notifyServerOwnerSuspended , notifyServerOwnerUnsuspended } from '../utils/suspensionNotice' ;
53+ import { notifyServerOwnerDmca , notifyServerOwnerSuspended , notifyServerOwnerUnsuspended } from '../utils/suspensionNotice' ;
5454import { createActivityLog } from './logHandler' ;
5555
5656const adminRoles = [ 'admin' , 'rootAdmin' , '*' ] ;
@@ -332,6 +332,41 @@ function sanitizeForDb(s: string | null | undefined) {
332332 }
333333}
334334
335+ function normalizeTicketMessages ( ticket : Ticket ) {
336+ if ( ! ticket ) return ;
337+ if ( Array . isArray ( ticket . messages ) ) return ;
338+
339+ try {
340+ if ( typeof ticket . messages === 'string' ) {
341+ const parsed = JSON . parse ( ticket . messages ) ;
342+ if ( Array . isArray ( parsed ) ) {
343+ ticket . messages = parsed ;
344+ return ;
345+ }
346+ }
347+
348+ if ( ticket . messages && typeof ticket . messages === 'object' ) {
349+ if ( Array . isArray ( ( ticket . messages as any ) . messages ) ) {
350+ ticket . messages = ( ticket . messages as any ) . messages ;
351+ return ;
352+ }
353+
354+ const keys = Object . keys ( ticket . messages ) ;
355+ const numericKeys = keys . filter ( ( k ) => / ^ \d + $ / . test ( k ) ) ;
356+ if ( numericKeys . length === keys . length && numericKeys . length > 0 ) {
357+ ticket . messages = numericKeys
358+ . sort ( ( a , b ) => Number ( a ) - Number ( b ) )
359+ . map ( ( k ) => ( ticket . messages as any ) [ k ] ) ;
360+ return ;
361+ }
362+ }
363+ } catch {
364+ // skippy
365+ }
366+
367+ ticket . messages = [ ] ;
368+ }
369+
335370function getTicketResponseDurations ( ticket : Ticket ) : number [ ] {
336371 const records = Array . isArray ( ticket . messages ) ? ticket . messages : [ ] ;
337372 const sorted = records
@@ -1077,6 +1112,12 @@ export async function adminRoutes(app: any, prefix = '') {
10771112 ctx . set . status = 400 ;
10781113 return { error : 'invalid_date_of_birth' , message : 'dateOfBirth must be a valid date string in YYYY-MM-DD format.' } ;
10791114 }
1115+ const updatedAge = getAgeFromDate ( dob ) ;
1116+ if ( updatedAge !== null && updatedAge < 14 ) {
1117+ user . suspended = true ;
1118+ user . fraudFlag = true ;
1119+ user . fraudReason = 'Underage account (<14 years)' ;
1120+ }
10801121 user . dateOfBirth = dob ;
10811122 }
10821123 if ( parentId !== undefined ) {
@@ -1390,6 +1431,7 @@ export async function adminRoutes(app: any, prefix = '') {
13901431 const nextStatus = normalizeTicketStatus ( status ) ;
13911432 if ( nextStatus !== ticket . status ) {
13921433 ticket . status = nextStatus ;
1434+ normalizeTicketMessages ( ticket ) ;
13931435 if ( ! Array . isArray ( ticket . messages ) ) ticket . messages = [ ] ;
13941436 ticket . messages . push ( {
13951437 sender : 'staff' ,
@@ -1399,6 +1441,7 @@ export async function adminRoutes(app: any, prefix = '') {
13991441 }
14001442 }
14011443
1444+ normalizeTicketMessages ( ticket ) ;
14021445 if ( ! Array . isArray ( ticket . messages ) ) ticket . messages = [ ] ;
14031446
14041447 if ( typeof reply === 'string' && reply . trim ( ) ) {
@@ -2583,12 +2626,17 @@ export async function adminRoutes(app: any, prefix = '') {
25832626 if ( ! requireAdminCtx ( ctx ) ) return ;
25842627 const serverId = ctx . params . id as string ;
25852628 const body = ( ctx . body || { } ) as any ;
2629+ const dmcaMark = Boolean ( body . dmca ) ;
25862630 const reason = typeof body . reason === 'string' && body . reason . trim ( )
25872631 ? body . reason . trim ( )
2588- : 'Suspended by administrator' ;
2632+ : dmcaMark
2633+ ? 'DMCA takedown'
2634+ : 'Suspended by administrator' ;
25892635 const adminUser = ctx . user as any ;
25902636 const adminName = [ adminUser ?. firstName , adminUser ?. lastName ] . filter ( Boolean ) . join ( ' ' ) . trim ( ) ;
25912637 const suspendedBy = adminName || adminUser ?. email || 'admin panel' ;
2638+ const dmcaAt = dmcaMark ? new Date ( ) : undefined ;
2639+ const dmcaDeletionAt = dmcaMark ? new Date ( Date . now ( ) + 30 * 24 * 60 * 60 * 1000 ) : undefined ;
25922640
25932641 const nodeRepo = AppDataSource . getRepository ( Node ) ;
25942642 const cfgRepo = AppDataSource . getRepository ( require ( '../models/serverConfig.entity' ) . ServerConfig ) ;
@@ -2605,9 +2653,23 @@ export async function adminRoutes(app: any, prefix = '') {
26052653 const svc = new WingsApiService ( base , n . token ) ;
26062654 await svc . getServer ( serverId ) ;
26072655 const alreadySuspended = ! ! existingCfg ?. suspended ;
2656+ const alreadyDmca = ! ! existingCfg ?. dmca ;
2657+ const updateData : any = {
2658+ suspended : true ,
2659+ suspendedBy,
2660+ suspendedReason : reason ,
2661+ suspendedAt : new Date ( ) ,
2662+ } ;
2663+ if ( dmcaMark ) {
2664+ updateData . dmca = true ;
2665+ updateData . dmcaBy = suspendedBy ;
2666+ updateData . dmcaReason = reason ;
2667+ updateData . dmcaAt = dmcaAt ;
2668+ updateData . dmcaDeletionAt = dmcaDeletionAt ;
2669+ }
26082670 await cfgRepo . update (
26092671 { uuid : serverId } ,
2610- { suspended : true , suspendedBy , suspendedReason : reason , suspendedAt : new Date ( ) } ,
2672+ updateData ,
26112673 ) ;
26122674 await svc . powerServer ( serverId , 'kill' ) . catch ( ( ) => { } ) ;
26132675 await svc . syncServer ( serverId , { } ) ;
@@ -2624,7 +2686,19 @@ export async function adminRoutes(app: any, prefix = '') {
26242686 recipient : undefined ,
26252687 } ;
26262688
2627- if ( ! alreadySuspended && existingCfg ) {
2689+ const shouldSendDmcaNotice = dmcaMark && ! alreadyDmca ;
2690+ if ( shouldSendDmcaNotice && existingCfg ) {
2691+ notice = await notifyServerOwnerDmca ( {
2692+ cfg : existingCfg ,
2693+ actor : suspendedBy ,
2694+ reason,
2695+ dmcaAt,
2696+ deletionAt : dmcaDeletionAt ,
2697+ } ) ;
2698+ if ( ! notice . sent && ! notice . skipped ) {
2699+ console . warn ( '[admin:server:suspend] failed to notify owner by email:' , notice . reason || 'unknown error' ) ;
2700+ }
2701+ } else if ( ! alreadySuspended && ! dmcaMark && existingCfg ) {
26282702 notice = await notifyServerOwnerSuspended ( {
26292703 cfg : existingCfg ,
26302704 actor : suspendedBy ,
@@ -2634,8 +2708,10 @@ export async function adminRoutes(app: any, prefix = '') {
26342708 if ( ! notice . sent && ! notice . skipped ) {
26352709 console . warn ( '[admin:server:suspend] failed to notify owner by email:' , notice . reason || 'unknown error' ) ;
26362710 }
2637- } else if ( alreadySuspended ) {
2711+ } else if ( alreadySuspended && ! dmcaMark ) {
26382712 notice . reason = 'server already suspended' ;
2713+ } else if ( alreadyDmca && dmcaMark ) {
2714+ notice . reason = 'server already marked DMCA' ;
26392715 }
26402716
26412717 return {
@@ -2653,7 +2729,7 @@ export async function adminRoutes(app: any, prefix = '') {
26532729 beforeHandle : authenticate ,
26542730 schema : {
26552731 params : t . Object ( { id : t . String ( ) } ) ,
2656- body : t . Optional ( t . Object ( { reason : t . Optional ( t . String ( ) ) } ) ) ,
2732+ body : t . Optional ( t . Object ( { reason : t . Optional ( t . String ( ) ) , dmca : t . Optional ( t . Boolean ( ) ) } ) ) ,
26572733 response : {
26582734 200 : t . Object ( {
26592735 success : t . Boolean ( ) ,
@@ -3649,7 +3725,7 @@ export async function adminRoutes(app: any, prefix = '') {
36493725 const apiKeys = await apiKeyRepo . find ( { where : { user : { id : userId } } } ) ;
36503726 const idVerifications = await idVerificationRepo . find ( { where : { userId } } ) ;
36513727 const tickets = await ticketRepo . find ( { where : { userId } } ) ;
3652- const userLogs = await userLogRepo . find ( { where : { userId } } ) ;
3728+ const userLogs = await userLogRepo . find ( { where : { userId } , order : { timestamp : 'DESC' } , take : 10 } ) ;
36533729 const organisationsOwned = await organisationRepo . find ( { where : { ownerId : userId } , relations : [ 'invites' ] } ) ;
36543730 const membershipRows = await orgMemberRepo . find ( { where : { userId } , relations : [ 'organisation' ] } ) ;
36553731 const organisations = membershipRows
@@ -3925,6 +4001,28 @@ export async function adminRoutes(app: any, prefix = '') {
39254001 detail : { summary : 'Export all user and owned object data (admin)' , tags : [ 'Admin' ] } ,
39264002 } ) ;
39274003
4004+ app . get ( prefix + '/admin/users/:id/address-change-logs' , async ( ctx : any ) => {
4005+ if ( ! requireAdminCtx ( ctx ) ) return ;
4006+ const userId = Number ( ctx . params . id ) ;
4007+ const userRepo = AppDataSource . getRepository ( User ) ;
4008+ const user = await userRepo . findOneBy ( { id : userId } ) ;
4009+ if ( ! user ) {
4010+ ctx . set . status = 404 ;
4011+ return { error : 'User not found' } ;
4012+ }
4013+
4014+ const userLogRepo = AppDataSource . getRepository ( UserLog ) ;
4015+ const logs = await userLogRepo . find ( { where : { userId, action : 'update-address' } , order : { timestamp : 'DESC' } , take : 10 } ) ;
4016+ return { success : true , logs } ;
4017+ } , {
4018+ beforeHandle : authenticate ,
4019+ schema : {
4020+ params : t . Object ( { id : t . String ( ) } ) ,
4021+ response : { 200 : t . Object ( { success : t . Boolean ( ) , logs : t . Array ( t . Any ( ) ) } ) , 401 : t . Object ( { error : t . String ( ) } ) , 403 : t . Object ( { error : t . String ( ) } ) , 404 : t . Object ( { error : t . String ( ) } ) } ,
4022+ } ,
4023+ detail : { summary : 'Get last address change logs for a user' , tags : [ 'Admin' ] } ,
4024+ } ) ;
4025+
39284026 app . delete ( '/admin/users/:id/ai/:linkId' , async ( ctx ) => {
39294027 if ( ! requireAdminCtx ( ctx ) ) return ;
39304028 const AIModelUser = require ( '../models/aiModelUser.entity' ) . AIModelUser ;
@@ -4481,6 +4579,7 @@ isSuspicious: true if fraudScore >= 50`;
44814579 portalDescriptions : portalDescriptions || null ,
44824580 codeInstancesEnabled,
44834581 geoBlockCountries : map [ 'geoBlockCountries' ] || '' ,
4582+ countryAgeRules : map [ 'countryAgeRules' ] || '' ,
44844583 billingCurrency : ( map [ 'billingCurrency' ] || 'USD' ) . toUpperCase ( ) ,
44854584 billingTaxRules : map [ 'billingTaxRules' ] || '' ,
44864585 gamblingEnabled : gamblingConfig . gamblingEnabled ,
@@ -4497,6 +4596,7 @@ isSuspicious: true if fraudScore >= 50`;
44974596 codeInstancesEnabled : t . Boolean ( ) ,
44984597 featureToggles : t . Record ( t . String ( ) , t . Boolean ( ) ) ,
44994598 geoBlockCountries : t . String ( ) ,
4599+ countryAgeRules : t . Optional ( t . String ( ) ) ,
45004600 billingCurrency : t . String ( ) ,
45014601 billingTaxRules : t . String ( ) ,
45024602 gamblingEnabled : t . Boolean ( ) ,
@@ -4535,6 +4635,7 @@ isSuspicious: true if fraudScore >= 50`;
45354635 codeInstancesEnabled,
45364636 portalDescriptions : portalDescriptions || null ,
45374637 geoBlockCountries : map [ 'geoBlockCountries' ] || '' ,
4638+ countryAgeRules : map [ 'countryAgeRules' ] || '' ,
45384639 billingCurrency : ( map [ 'billingCurrency' ] || 'USD' ) . toUpperCase ( ) ,
45394640 billingTaxRules : map [ 'billingTaxRules' ] || '' ,
45404641 gamblingEnabled : gamblingConfig . gamblingEnabled ,
@@ -4551,6 +4652,7 @@ isSuspicious: true if fraudScore >= 50`;
45514652 codeInstancesEnabled : t . Boolean ( ) ,
45524653 portalDescriptions : t . Optional ( t . Any ( ) ) ,
45534654 geoBlockCountries : t . String ( ) ,
4655+ countryAgeRules : t . Optional ( t . String ( ) ) ,
45544656 billingCurrency : t . String ( ) ,
45554657 billingTaxRules : t . String ( ) ,
45564658 gamblingEnabled : t . Boolean ( ) ,
@@ -4649,7 +4751,7 @@ isSuspicious: true if fraudScore >= 50`;
46494751 if ( ! requireAdminCtx ( ctx ) ) return ;
46504752 const repo = AppDataSource . getRepository ( PanelSetting ) ;
46514753 const body = ctx . body as any ;
4652- const allowed = [ 'registrationEnabled' , 'registrationNotice' , 'codeInstancesEnabled' , 'geoBlockCountries' , 'billingCurrency' , 'billingTaxRules' , 'gamblingEnabled' , 'gamblingResourceLuckyChance' , 'gamblingPowerDenyChance' ] ;
4754+ const allowed = [ 'registrationEnabled' , 'registrationNotice' , 'codeInstancesEnabled' , 'geoBlockCountries' , 'countryAgeRules' , ' billingCurrency', 'billingTaxRules' , 'gamblingEnabled' , 'gamblingResourceLuckyChance' , 'gamblingPowerDenyChance' ] ;
46534755 for ( const key of allowed ) {
46544756 if ( body [ key ] !== undefined ) {
46554757 let value = typeof body [ key ] === 'boolean' ? String ( body [ key ] ) : String ( body [ key ] ) ;
@@ -4689,6 +4791,7 @@ isSuspicious: true if fraudScore >= 50`;
46894791 portalDescriptions : portalDescriptions || null ,
46904792 codeInstancesEnabled : map [ 'codeInstancesEnabled' ] !== 'false' ,
46914793 geoBlockCountries : map [ 'geoBlockCountries' ] || '' ,
4794+ countryAgeRules : map [ 'countryAgeRules' ] || '' ,
46924795 billingCurrency : ( map [ 'billingCurrency' ] || 'USD' ) . toUpperCase ( ) ,
46934796 billingTaxRules : map [ 'billingTaxRules' ] || '' ,
46944797 gamblingEnabled : gamblingConfig . gamblingEnabled ,
@@ -4708,6 +4811,7 @@ isSuspicious: true if fraudScore >= 50`;
47084811 geoBlockCountries : t . Optional ( t . String ( ) ) ,
47094812 billingCurrency : t . Optional ( t . String ( ) ) ,
47104813 billingTaxRules : t . Optional ( t . String ( ) ) ,
4814+ countryAgeRules : t . Optional ( t . String ( ) ) ,
47114815 gamblingEnabled : t . Optional ( t . Boolean ( ) ) ,
47124816 gamblingResourceLuckyChance : t . Optional ( t . Number ( ) ) ,
47134817 gamblingPowerDenyChance : t . Optional ( t . Number ( ) ) ,
0 commit comments