Private
Public Access
1
0

chg: usr: add ReCaptcha overlay again to protect the game #7

This commit is contained in:
2026-04-19 21:31:08 +02:00
parent db37ab45b2
commit 51bd909879
5 changed files with 135 additions and 90 deletions

View File

@@ -676,3 +676,96 @@
} }
} }
// CaptchaOverlay Styles
.captcha-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(7, 9, 13, 0.95);
backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.captcha-content {
text-align: center;
color: #fff;
max-width: 400px;
padding: 40px;
}
.captcha-icon {
font-size: 64px;
color: #236f87;
margin-bottom: 24px;
}
.captcha-title {
font: 800 32px 'Rajdhani', sans-serif;
margin: 0 0 16px;
letter-spacing: 1px;
}
.captcha-description {
color: rgba(149, 207, 245, 0.7);
font: 400 16px 'Rajdhani', sans-serif;
margin: 0 0 32px;
letter-spacing: 0.5px;
}
.captcha-button {
background: linear-gradient(#236f87 0%, #1a5068 100%);
border: 2px solid #2e7a9a;
border-radius: 8px;
color: #e0f4ff;
cursor: pointer;
font: 800 18px 'Rajdhani', sans-serif;
letter-spacing: 2px;
padding: 16px 40px;
text-transform: uppercase;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 12px;
opacity: 1;
i {
font-size: 16px;
}
&:disabled {
opacity: 0.7;
cursor: wait;
}
&:hover:not(:disabled) {
background: linear-gradient(#2d8aa8 0%, #236f87 100%);
border-color: #5ba4d4;
color: #fff;
box-shadow: 0 8px 24px rgba(35, 111, 135, 0.4);
transform: translateY(-2px);
}
&:active:not(:disabled) {
transform: translateY(0);
}
&.captcha-button--error {
background: linear-gradient(#8a2323 0%, #681a1a 100%);
border-color: #9a2e2e;
&:hover {
background: linear-gradient(#a82d2d 0%, #872323 100%);
border-color: #d45b5b;
box-shadow: 0 8px 24px rgba(135, 35, 35, 0.4);
}
}
&.captcha-button--loading {
opacity: 0.7;
}
}

View File

@@ -6,7 +6,8 @@
* For the full copyright and license information, please view the LICENSE * For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
const CAPTCHA_STORAGE_KEY = 'mineseeker_captcha_verified'; const CAPTCHA_STORAGE_KEY = 'mineseeker_captcha_verified';
const CAPTCHA_TOKEN_KEY = 'mineseeker_captcha_token'; const CAPTCHA_TOKEN_KEY = 'mineseeker_captcha_token';
@@ -17,6 +18,23 @@ const CaptchaOverlay = ({ siteKey, onVerified, children }) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(false); const [error, setError] = useState(false);
const handleToken = useCallback(token => {
const wrapper = document.getElementById('mine-wrapper');
if (wrapper) {
wrapper.dataset.captchaToken = token;
}
sessionStorage.setItem(CAPTCHA_TOKEN_KEY, token);
sessionStorage.setItem(CAPTCHA_STORAGE_KEY, Date.now().toString());
setVerified(true);
onVerified?.();
}, [onVerified]);
const buttonClasses = useMemo(() => [
'captcha-button',
error && 'captcha-button--error',
loading && 'captcha-button--loading',
].filter(Boolean).join(' '), [error, loading]);
useEffect(() => { useEffect(() => {
const storedToken = sessionStorage.getItem(CAPTCHA_TOKEN_KEY); const storedToken = sessionStorage.getItem(CAPTCHA_TOKEN_KEY);
const storedTime = sessionStorage.getItem(CAPTCHA_STORAGE_KEY); const storedTime = sessionStorage.getItem(CAPTCHA_STORAGE_KEY);
@@ -48,16 +66,6 @@ const CaptchaOverlay = ({ siteKey, onVerified, children }) => {
} }
}, [siteKey, onVerified, handleToken]); }, [siteKey, onVerified, handleToken]);
const handleToken = useCallback(token => {
const wrapper = document.getElementById('mine-wrapper');
if (wrapper) {
wrapper.dataset.captchaToken = token;
}
sessionStorage.setItem(CAPTCHA_TOKEN_KEY, token);
sessionStorage.setItem(CAPTCHA_STORAGE_KEY, Date.now().toString());
setVerified(true);
onVerified?.();
}, [onVerified]);
const handleClick = () => { const handleClick = () => {
setLoading(true); setLoading(true);
@@ -82,79 +90,18 @@ const CaptchaOverlay = ({ siteKey, onVerified, children }) => {
return <>{children}</>; return <>{children}</>;
} }
const overlayStyles = {
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
background: 'rgba(7, 9, 13, 0.95)',
backdropFilter: 'blur(8px)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
};
const contentStyles = {
textAlign: 'center',
color: '#fff',
maxWidth: '400px',
padding: '40px',
};
const iconStyles = {
fontSize: '64px',
color: '#236f87',
marginBottom: '24px',
};
const h1Styles = {
font: '800 32px Rajdhani, sans-serif',
margin: '0 0 16px',
letterSpacing: '1px',
};
const pStyles = {
color: 'rgba(149, 207, 245, 0.7)',
font: '400 16px Rajdhani, sans-serif',
margin: '0 0 32px',
letterSpacing: '0.5px',
};
const buttonStyles = {
background: error
? 'linear-gradient(#8a2323 0%, #681a1a 100%)'
: loading
? 'linear-gradient(#236f87 0%, #1a5068 100%)'
: 'linear-gradient(#236f87 0%, #1a5068 100%)',
border: `2px solid ${error ? '#9a2e2e' : loading ? '#2e7a9a' : '#2e7a9a'}`,
borderRadius: '8px',
color: '#e0f4ff',
cursor: loading ? 'wait' : 'pointer',
font: '800 18px Rajdhani, sans-serif',
letterSpacing: '2px',
padding: '16px 40px',
textTransform: 'uppercase',
transition: 'all 0.3s ease',
display: 'inline-flex',
alignItems: 'center',
gap: '12px',
opacity: loading ? 0.7 : 1,
};
return ( return (
<div style={overlayStyles}> <div className="captcha-overlay">
<div style={contentStyles}> <div className="captcha-content">
<div style={iconStyles}> <div className="captcha-icon">
<i className="fa fa-shield-halved" /> <i className="fa fa-shield-halved" />
</div> </div>
<h1 style={h1Styles}>Ready to Play?</h1> <h1 className="captcha-title">Ready to Play?</h1>
<p style={pStyles}> <p className="captcha-description">
Click below to verify you&apos;re human and start playing. Click below to verify you&apos;re human and start playing.
</p> </p>
<button <button
style={buttonStyles} className={buttonClasses}
onClick={handleClick} onClick={handleClick}
disabled={loading} disabled={loading}
> >

View File

@@ -7,14 +7,18 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
import React from 'react'; import React, { useState } from 'react';
import { useGame } from '@mine-contexts'; import { useGame } from '@mine-contexts';
import { useServerCommunication } from '@mine-hooks'; import { useServerCommunication } from '@mine-hooks';
import CaptchaOverlay from './CaptchaOverlay';
import GridControl from './grid/GridControl'; import GridControl from './grid/GridControl';
export const GameBoard = ({ gameAssoc, gameInherited, opponentName = '', isEnvDev }) => { export const GameBoard = ({ gameAssoc, gameInherited, opponentName = '', isEnvDev }) => {
const { gridReady } = useGame(); const { gridReady } = useGame();
const { onClick, resign } = useServerCommunication(gameAssoc, gameInherited, opponentName, isEnvDev); const { onClick, resign } = useServerCommunication(gameAssoc, gameInherited, opponentName, isEnvDev);
const [captchaVerified, setCaptchaVerified] = useState(false);
const siteKey = document.getElementById('mine-wrapper')?.dataset.recaptchaSiteKey;
if (!gridReady) { if (!gridReady) {
return ( return (
@@ -24,6 +28,12 @@ export const GameBoard = ({ gameAssoc, gameInherited, opponentName = '', isEnvDe
); );
} }
if (!captchaVerified && siteKey) {
return (
<CaptchaOverlay siteKey={siteKey} onVerified={() => setCaptchaVerified(true)} />
);
}
return ( return (
<GridControl <GridControl
gameAssoc={gameAssoc} gameAssoc={gameAssoc}

View File

@@ -54,15 +54,11 @@ const GameTimer = () => {
}, [activePlayer, overlay]); }, [activePlayer, overlay]);
useEffect(() => { useEffect(() => {
if (endRef.current) { if (endRef.current) setIsRunning(false);
setIsRunning(false);
}
}, [endRef]); }, [endRef]);
useEffect(() => { useEffect(() => {
if (connectionLost) { if (connectionLost) setIsRunning(false);
setIsRunning(false);
}
}, [connectionLost]); }, [connectionLost]);
useEffect(() => { useEffect(() => {
@@ -140,9 +136,7 @@ const GameTimer = () => {
}, [isRunning, activePlayer]); }, [isRunning, activePlayer]);
useEffect(() => () => { useEffect(() => () => {
if (timerIntervalRef.current) { if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
clearInterval(timerIntervalRef.current);
}
}, []); }, []);
const formatTime = seconds => { const formatTime = seconds => {

View File

@@ -13,11 +13,12 @@ import User from './User';
import BonusStatsDialog from '../BonusStatsDialog'; import BonusStatsDialog from '../BonusStatsDialog';
const UserControl = ({ resign }) => { const UserControl = ({ resign }) => {
const { webPlayer, activePlayer, mines, foundMines, red, blue, onBombToggle } = useGame(); const { webPlayer, activePlayer, foundMines, red, blue, onBombToggle } = useGame();
const [bonusDialogOpen, setBonusDialogOpen] = useState(false); const [bonusDialogOpen, setBonusDialogOpen] = useState(false);
const activeColor = activePlayer ? 'blue' : 'red'; const activeColor = activePlayer ? 'blue' : 'red';
const resignClass = 'resign' + (activeColor !== webPlayer ? ' disabled' : ''); const resignClass = 'resign' + (activeColor !== webPlayer ? ' disabled' : '');
const minesClass = 'active-mines' + (foundMines ? ' found-mine' : ''); const minesClass = 'active-mines' + (foundMines ? ' found-mine' : '');
const remainingMines = 51 - red.mines - blue.mines;
const handleBombClick = (color, player) => { const handleBombClick = (color, player) => {
const p = 'red' === color ? red : blue; const p = 'red' === color ? red : blue;
@@ -41,7 +42,7 @@ const UserControl = ({ resign }) => {
<div className="active-mines-container"> <div className="active-mines-container">
<i className="fa fa-star" /> <i className="fa fa-star" />
<div className={minesClass}> <div className={minesClass}>
<div className="active-mines-nbr">{mines}</div> <div className="active-mines-nbr">{remainingMines}</div>
<div className="active-mines-shine" /> <div className="active-mines-shine" />
</div> </div>
<i className="fa fa-star" /> <i className="fa fa-star" />