@@ -60,6 +60,7 @@ export default function DockerContainers() {
60
60
const [ isTestingDeploy , setIsTestingDeploy ] = useState ( false ) ; // placeholder for isTestingDeploy state
61
61
const [ configureModalOpen , setConfigureModalOpen ] = useState ( false ) ;
62
62
const [ selectedContainer , setSelectedContainer ] = useState ( null ) ;
63
+ const [ hasDeployedThisSession , setHasDeployedThisSession ] = useState ( false ) ;
63
64
64
65
// --- IMAGE SEARCH, FILTER, PAGINATION STATE ---
65
66
const [ imageSearch , setImageSearch ] = useState ( '' ) ;
@@ -222,6 +223,12 @@ export default function DockerContainers() {
222
223
223
224
const handleSubmit = async ( e ) => {
224
225
e . preventDefault ( ) ;
226
+ // Enforce frontend container limit
227
+ const MAX_CONTAINERS = 50 ;
228
+ if ( containers . length >= MAX_CONTAINERS ) {
229
+ toast . error ( `You have reached the maximum of ${ MAX_CONTAINERS } containers. Reach out to [email protected] if you need more.` ) ;
230
+ return ;
231
+ }
225
232
try {
226
233
const userId = localStorage . getItem ( 'username' ) ;
227
234
@@ -670,7 +677,7 @@ CMD ["socat", "TCP-LISTEN:9999,fork,reuseaddr", "EXEC:/challenge/challenge"]`
670
677
return (
671
678
< div className = "mt-4 shadow-xl bg-neutral-900/50 p-4 shadow-lg" >
672
679
< div className = "flex justify-between items-center mb-2" >
673
- < h4 className = "text-lg font-bold text-white flex items-center gap-2" >
680
+ < h4 className = "text-lg font-bold text-white mb-6 flex items-center gap-2" >
674
681
< i className = "fas fa-folder-open text-blue-400" > </ i >
675
682
Associated Files
676
683
</ h4 >
@@ -766,7 +773,7 @@ CMD ["socat", "TCP-LISTEN:9999,fork,reuseaddr", "EXEC:/challenge/challenge"]`
766
773
} ;
767
774
768
775
const handleStartDeploy = async ( ) => {
769
- setIsTestingDeploy ( true ) ;
776
+ setIsTestingDeploy ( false ) ;
770
777
setBuildLogs ( [ ] ) ;
771
778
try {
772
779
// Collect relevant data for test deploy
@@ -792,12 +799,29 @@ CMD ["socat", "TCP-LISTEN:9999,fork,reuseaddr", "EXEC:/challenge/challenge"]`
792
799
const reader = response . body . getReader ( ) ;
793
800
let decoder = new TextDecoder ( 'utf-8' ) ;
794
801
let done = false ;
802
+ let foundDomain = false ;
795
803
while ( ! done ) {
796
804
const { value, done : doneReading } = await reader . read ( ) ;
797
805
done = doneReading ;
798
806
if ( value ) {
799
807
const chunk = decoder . decode ( value ) ;
800
- setBuildLogs ( prev => [ ...prev , ...chunk . split ( / \r ? \n / ) . filter ( line => line . trim ( ) ) ] ) ;
808
+ const lines = chunk . split ( / \r ? \n / ) . filter ( line => line . trim ( ) ) ;
809
+ setBuildLogs ( prev => [ ...prev , ...lines ] ) ;
810
+ if ( ! foundDomain ) {
811
+ for ( const line of lines ) {
812
+ // Look for [DOMAIN] https://... or discordapp.com in the line
813
+ let match = line . match ( / \[ D O M A I N \] \s * ( h t t p s ? : \/ \/ \S + ) / ) ;
814
+ if ( match ) {
815
+ foundDomain = true ;
816
+ break ;
817
+ }
818
+ match = line . match ( / h t t p s ? : \/ \/ [ \w . - ] * d i s c o r d a p p \. c o m \S * / ) ;
819
+ if ( match ) {
820
+ foundDomain = true ;
821
+ break ;
822
+ }
823
+ }
824
+ }
801
825
}
802
826
}
803
827
} catch ( error ) {
@@ -841,6 +865,7 @@ CMD ["socat", "TCP-LISTEN:9999,fork,reuseaddr", "EXEC:/challenge/challenge"]`
841
865
useEffect ( ( ) => {
842
866
if ( createContainerModalOpen ) {
843
867
document . body . style . overflow = 'hidden' ;
868
+ setHasDeployedThisSession ( false ) ;
844
869
} else {
845
870
document . body . style . overflow = '' ;
846
871
}
@@ -887,6 +912,17 @@ CMD ["socat", "TCP-LISTEN:9999,fork,reuseaddr", "EXEC:/challenge/challenge"]`
887
912
e . stopPropagation ( ) ;
888
913
} ;
889
914
915
+ // --- Deployment UI logic based on logs ---
916
+ const isDeploySuccess = buildLogs . some ( line => line . includes ( "Discordbot/2.0; +https://discordapp.com" ) ) ;
917
+ const deployedDomain = ( ( ) => {
918
+ let d = "" ;
919
+ buildLogs . forEach ( line => {
920
+ const m = line . match ( / \[ D O M A I N \] \s * ( h t t p s ? : \/ \/ \S + ) / ) ;
921
+ if ( m ) d = m [ 1 ] ;
922
+ } ) ;
923
+ return d ;
924
+ } ) ( ) ;
925
+
890
926
return (
891
927
< >
892
928
< Head >
@@ -1072,7 +1108,7 @@ CMD ["socat", "TCP-LISTEN:9999,fork,reuseaddr", "EXEC:/challenge/challenge"]`
1072
1108
< button
1073
1109
type = "button"
1074
1110
onClick = { createDockerfileFromTemplate }
1075
- className = "bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-none text-sm transition-all duration-200"
1111
+ className = "bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-none transition-all duration-200"
1076
1112
>
1077
1113
Use This Template
1078
1114
</ button >
@@ -1234,7 +1270,7 @@ CMD ["socat", "TCP-LISTEN:9999,fork,reuseaddr", "EXEC:/challenge/challenge"]`
1234
1270
return (
1235
1271
< div
1236
1272
key = { uniqueKey }
1237
- className = { `bg-neutral-850 hover:bg-neutral-800 border-blue-400 shadow-lg p-5 transition-all hover:shadow-xl cursor-pointer ${ expandedImageId === expandedKey ? 'border-t-4 border-blue-500 bg-neutral-800' : '' } ` }
1273
+ className = { `bg-neutral-700/20 hover:bg-neutral-800 shadow-lg p-5 transition-all hover:shadow-xl cursor-pointer ${ expandedImageId === expandedKey ? 'border-t-4 border-blue-500 bg-neutral-800' : '' } ` }
1238
1274
onClick = { ( ) => setExpandedImageId ( expandedImageId === expandedKey ? null : expandedKey ) }
1239
1275
>
1240
1276
< div className = "flex flex-col gap-2" >
@@ -1338,7 +1374,7 @@ CMD ["socat", "TCP-LISTEN:9999,fork,reuseaddr", "EXEC:/challenge/challenge"]`
1338
1374
/>
1339
1375
< button
1340
1376
type = "button"
1341
- className = "ml-2 px-2 py-1 bg-blue-700 text-white rounded text-xs hover:bg-blue-800 transition"
1377
+ className = "ml-2 px-2 py-1 bg-blue-700 text-white rounded hover:bg-blue-800 transition"
1342
1378
onClick = { ( ) => setFormData ( f => ( { ...f , name : generateCoolName ( ) } ) ) }
1343
1379
title = "Generate random name"
1344
1380
>
@@ -1593,22 +1629,22 @@ CMD ["socat", "TCP-LISTEN:9999,fork,reuseaddr", "EXEC:/challenge/challenge"]`
1593
1629
1594
1630
{ /* Deploy Container Modal */ }
1595
1631
{ createContainerModalOpen && (
1596
- < div className = "fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-60" >
1597
- < div className = "bg-neutral-900 rounded-lg shadow-xl p -8 w-full max-w-7xl relative grid grid-cols-6" >
1632
+ < div className = "fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-60 mx-auto " >
1633
+ < div className = "bg-neutral-900 rounded-lg shadow-xl px -8 pb-4 w-full max-w-7xl relative grid grid-cols-6 items-center " >
1598
1634
< div className = "col-span-3" >
1599
1635
< button
1600
1636
className = "absolute top-3 right-3 text-gray-400 hover:text-red-400"
1601
1637
onClick = { ( ) => setCreateContainerModalOpen ( false ) }
1602
1638
>
1603
1639
< XCircleIcon className = "w-6 h-6" />
1604
1640
</ button >
1605
- < h2 className = "text-2xl font-bold text-white mb-4 flex items-center" >
1641
+ < h2 className = "text-2xl font-bold text-white mb-4 flex items-center pb-2 " >
1606
1642
< ServerIcon className = "w-6 h-6 mr-2 text-blue-500" />
1607
1643
Deploy Container
1608
1644
</ h2 >
1609
1645
< div >
1610
1646
< div className = "mb-4" >
1611
- < label className = "block text-gray-300 text-sm mb-1" > Container Name</ label >
1647
+ < label className = "block text-gray-300 text-sm mb-1" > Container Name < button type = "button" className = " ml-1 w-1 hover:text-blue-500 text-white rounded text-xs transition" onClick = { ( ) => setFormData ( f => ( { ... f , name : generateCoolName ( ) } ) ) } title = "Generate random name" > < i className = "fa fa-sync" > </ i > </ button > </ label >
1612
1648
< input
1613
1649
type = "text"
1614
1650
name = "name"
@@ -1659,7 +1695,7 @@ CMD ["socat", "TCP-LISTEN:9999,fork,reuseaddr", "EXEC:/challenge/challenge"]`
1659
1695
</ div >
1660
1696
</ div >
1661
1697
< div className = "mb-4" >
1662
- < label className = "block text-gray-300 text-sm mb-1" > Bind to Challenge</ label >
1698
+ < label className = "block text-gray-300 text-sm mb-1" > Bind to Challenge (Optional) </ label >
1663
1699
< select
1664
1700
value = { challengeId }
1665
1701
onChange = { e => setChallengeId ( e . target . value ) }
@@ -1687,103 +1723,126 @@ CMD ["socat", "TCP-LISTEN:9999,fork,reuseaddr", "EXEC:/challenge/challenge"]`
1687
1723
Test Deployment
1688
1724
</ button >
1689
1725
< button
1690
- className = "flex-1 py-2 px-4 bg-green-600 hover:bg-green-700 text-white font-semibold rounded-none shadow-md transition-all"
1691
- onClick = { handleStartDeploy }
1692
- disabled = { isLoading }
1726
+ className = { `flex-1 py-2 px-4 bg-green-700 text-white font-semibold rounded-none shadow-md transition-all ${ hasDeployedThisSession ? 'bg-green-900 cursor-not-allowed ' : 'hover:bg-green-800' } flex items-center justify-center gap-2` }
1727
+ onClick = { ( ) => {
1728
+ setHasDeployedThisSession ( true ) ;
1729
+ handleStartDeploy ( ) ;
1730
+ } }
1731
+ disabled = { isLoading || hasDeployedThisSession }
1693
1732
>
1694
- { isLoading ? 'Deploying...' : 'Deploy Container' }
1733
+ { isLoading && (
1734
+ < svg className = "animate-spin h-5 w-5 text-white" xmlns = "http://www.w3.org/2000/svg" fill = "none" viewBox = "0 0 24 24" >
1735
+ < circle className = "opacity-25" cx = "12" cy = "12" r = "10" stroke = "currentColor" strokeWidth = "4" > </ circle >
1736
+ < path className = "opacity-75" fill = "currentColor" d = "M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" > </ path >
1737
+ </ svg >
1738
+ ) }
1739
+ { isLoading ? 'Deploying...' : hasDeployedThisSession ? < i className = "fas fa-check" > </ i > : 'Deploy Container' }
1695
1740
</ button >
1696
1741
</ div >
1697
1742
</ div >
1698
1743
</ div >
1699
- < div id = "testdeploy" className = "col-span-3 ml-4 mt-4 h-full w-full" >
1700
- < div className = "w-full h-full" >
1701
1744
1702
- < div className = "bg-black/80 h-[400px] text-white font-mono text-xs rounded p-4 overflow-y-auto border border-neutral-800" id = "build-log-stream" >
1703
- { /* Build logs will be streamed here */ }
1704
- < p > Build logs will show here...</ p >
1705
- < br > </ br >
1706
- < p className = "text-red-400" > This test deployment will be active for 5 minutes.</ p >
1707
- < br > </ br >
1708
- < p > Your deployment will not automatically end, once you see expected behavior click Deploy.</ p >
1709
- < br > </ br >
1710
- { buildLogs . map ( ( line , idx ) => {
1711
- // Remove leading "data: " if present
1712
- const cleanLine = line . replace ( / ^ d a t a : ? / , "" ) ;
1713
- // Regex to match URLs
1714
- const urlRegex = / ( h t t p s ? : \/ \/ [ ^ \s ] + ) / g;
1715
- // Split the line by URLs, keeping the URLs
1716
- const parts = cleanLine . split ( urlRegex ) ;
1717
- return (
1718
- < div key = { idx } >
1719
- { parts . map ( ( part , i ) =>
1720
- urlRegex . test ( part ) ? (
1721
- < a
1722
- key = { i }
1723
- href = { part }
1724
- target = "_blank"
1725
- rel = "noopener noreferrer"
1726
- className = "underline text-blue-400 hover:text-blue-300 break-all"
1727
- >
1728
- { part }
1729
- </ a >
1730
- ) : (
1731
- < span key = { i } > { part } </ span >
1732
- )
1733
- ) }
1734
- </ div >
1735
- ) ;
1736
- } ) }
1737
-
1745
+
1746
+ < div id = "deploysuccess" className = "col-span-3 ml-4 mt-2 justify-center items-center" >
1747
+ { isDeploySuccess ? (
1748
+ < div className = "flex flex-col items-center justify-center h-full px-10" >
1749
+
1750
+ < div className = "mb-4 w-[300px] h-[250px] rounded-xl" >
1751
+ < ComposableMap
1752
+ projection = "geoAlbersUsa"
1753
+ width = { 300 }
1754
+ height = { 250 }
1755
+ projectionConfig = { { center : [ 0 , 0 ] , scale : 400 , rotation : 0 , } }
1756
+ style = { { background : 'transparent' } }
1757
+ >
1758
+ < Geographies geography = "https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json" >
1759
+ { ( { geographies } ) =>
1760
+ geographies . map ( geo => (
1761
+ < Geography
1762
+ key = { geo . rsmKey }
1763
+ geography = { geo }
1764
+ fill = "#23272f"
1765
+ stroke = "#334155"
1766
+ style = { { outline : 'none' , filter : 'drop-shadow(0 2px 8px #0ea5e955)' } }
1767
+ />
1768
+ ) )
1769
+ }
1770
+ </ Geographies >
1771
+ { /* State College, PA marker with pulse */ }
1772
+ < Marker coordinates = { [ - 77.8600 , 40.7934 ] } >
1773
+ < circle r = { 8 } fill = "#34a3ff" stroke = "#34a3ff" strokeWidth = { 0.05 } style = { { filter : 'drop-shadow(0 0 10px #4adeffb3)' } } />
1774
+ </ Marker > </ ComposableMap >
1775
+ </ div >
1776
+ { /* Region label */ }
1777
+ < div className = "text-blue-300 font-semibold text-sm mb-4" >
1778
+ US East (State College, PA)
1779
+ </ div >
1780
+ { /* Domain box */ }
1781
+ < div className = "bg-neutral-800 rounded-lg p-4 w-full max-w-md text-center shadow border border-neutral-700 mb-4" >
1782
+ < div className = "text-gray-400 text-xs mb-1" > Autogenerated Domain</ div >
1783
+ < div className = "text-md font-mono text-blue-400 break-all select-all" > { deployedDomain } </ div >
1784
+ </ div >
1785
+ { /* Description */ }
1786
+ < div className = "text-gray-400 text-xs mt-2 text-center max-w-xs" >
1787
+ { isTestingDeploy
1788
+ ? "This is a test deployment. It will automatically expire in 5 minutes. Use this environment for testing only."
1789
+ : "Your container will be on our servers in State College, PA (US East). " }
1738
1790
</ div >
1739
-
1740
1791
</ div >
1792
+ ) : (
1793
+ < div className = "bg-black/80 h-[400px] text-white font-mono text-xs rounded p-4 overflow-y-auto border border-neutral-800" id = "build-log-stream" >
1794
+ { /* Build logs will be streamed here */ }
1795
+ < p > Build logs will show here...</ p >
1796
+ < br > </ br >
1797
+ { isTestingDeploy && (
1798
+ < >
1799
+ < p className = "text-red-400" > This test deployment will be active for 5 minutes.</ p >
1800
+ < br > </ br >
1801
+ </ >
1802
+ ) }
1741
1803
1742
- </ div >
1743
-
1744
- < div id = "deploysuccess" className = "col-span-3 hidden " >
1745
- < div className = "flex flex-col items-center justify-center h-full px-10" >
1746
- { /* Sexy Interactive Map with react-simple-maps */ }
1747
- < div className = "mb-4 w-[300px] h-[25s0px] rounded-xl" >
1748
- < ComposableMap
1749
- projection = "geoAlbersUsa"
1750
- width = { 300 }
1751
- height = { 250 }
1752
- projectionConfig = { { center : [ 0 , 0 ] , scale : 400 , rotation : 0 , } }
1753
- style = { { background : 'transparent' } }
1754
- >
1755
- < Geographies geography = "https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json" >
1756
- { ( { geographies } ) =>
1757
- geographies . map ( geo => (
1758
- < Geography
1759
- key = { geo . rsmKey }
1760
- geography = { geo }
1761
- fill = "#23272f"
1762
- stroke = "#334155"
1763
- style = { { outline : 'none' , filter : 'drop-shadow(0 2px 8px #0ea5e955)' } }
1764
- />
1765
- ) )
1766
- }
1767
- </ Geographies >
1768
- { /* State College, PA marker with pulse */ }
1769
- < Marker coordinates = { [ - 77.8600 , 40.7934 ] } >
1770
- < circle r = { 8 } fill = "#34a3ff" stroke = "#34a3ff" strokeWidth = { 0.05 } style = { { filter : 'drop-shadow(0 0 10px #4adeffb3)' } } />
1771
- </ Marker > </ ComposableMap >
1772
- </ div >
1773
- { /* Region label */ }
1774
- < div className = "text-blue-300 font-semibold text-sm mb-4" >
1775
- US East (State College, PA)
1776
- </ div >
1777
- { /* Domain box */ }
1778
- < div className = "bg-neutral-800 rounded-lg p-4 w-full max-w-md text-center shadow border border-neutral-700 mb-4" >
1779
- < div className = "text-gray-400 text-xs mb-1" > Autogenerated Domain</ div >
1780
- < div className = "text-md font-mono text-blue-400 break-all select-all" > laphatize333330ujasd.ctfgui.de</ div >
1781
- </ div >
1782
- { /* Description */ }
1783
- < div className = "text-gray-400 text-xs mt-2 text-center max-w-xs" >
1784
- Your container will be on our servers in State College, PA (US East). Domain is autogenerated and will be visible after deployment.
1804
+ < pre className = "text-yellow-400 text-xs" >
1805
+ { `
1806
+ dP\"\"b8 888888 888888 dP\"\"b8 88 88 88 8888b. 888888
1807
+ dP \"\" 88 88__ dP \"\" 88 88 88 8I Yb 88__
1808
+ Yb 88 88\"" Yb \"88 Y8 8P 88 8I dY 88\""
1809
+ YboodP 88 88 YboodP \`YbodP\' 88 8888Y" 888888
1810
+ ` }
1811
+ </ pre >
1812
+ < br > </ br >
1813
+ < p > Note, sometimes we can't automatically detect when a container is ready. You can verify if your container is accessible by clicking on the generated domain.</ p >
1814
+ < br > </ br >
1815
+ < p className = "text-cyan-400" > Log streaming is highly experimental and may not work for all containers.</ p >
1816
+ < br > </ br >
1817
+ { buildLogs . map ( ( line , idx ) => {
1818
+ // Remove leading "data: " if present
1819
+ const cleanLine = line . replace ( / ^ d a t a : ? / , "" ) ;
1820
+ // Regex to match URLs
1821
+ const urlRegex = / ( h t t p s ? : \/ \/ [ ^ \s ] + ) / g;
1822
+ // Split the line by URLs, keeping the URLs
1823
+ const parts = cleanLine . split ( urlRegex ) ;
1824
+ return (
1825
+ < div key = { idx } >
1826
+ { parts . map ( ( part , i ) =>
1827
+ urlRegex . test ( part ) ? (
1828
+ < a
1829
+ key = { i }
1830
+ href = { part }
1831
+ target = "_blank"
1832
+ rel = "noopener noreferrer"
1833
+ className = "underline text-blue-400 hover:text-blue-300 break-all"
1834
+ >
1835
+ { part }
1836
+ </ a >
1837
+ ) : (
1838
+ < span key = { i } > { part } </ span >
1839
+ )
1840
+ ) }
1841
+ </ div >
1842
+ ) ;
1843
+ } ) }
1785
1844
</ div >
1786
- </ div >
1845
+ ) }
1787
1846
</ div >
1788
1847
</ div >
1789
1848
</ div >
0 commit comments