This commit is contained in:
		@@ -9,30 +9,30 @@
 | 
			
		||||
    "preview": "vite preview"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@emotion/react": "^11.10.5",
 | 
			
		||||
    "@emotion/styled": "^11.10.5",
 | 
			
		||||
    "@fontsource/roboto": "^4.5.8",
 | 
			
		||||
    "@mui/icons-material": "^5.11.0",
 | 
			
		||||
    "@mui/material": "^5.11.6",
 | 
			
		||||
    "@react-three/drei": "^9.56.12",
 | 
			
		||||
    "@react-three/fiber": "^8.10.1",
 | 
			
		||||
    "@react-three/postprocessing": "^2.7.0",
 | 
			
		||||
    "axios": "^1.2.6",
 | 
			
		||||
    "notistack": "^2.0.8",
 | 
			
		||||
    "postprocessing": "^6.29.3",
 | 
			
		||||
    "react": "^18.2.0",
 | 
			
		||||
    "react-dom": "^18.2.0",
 | 
			
		||||
    "react-hook-form": "^7.43.0",
 | 
			
		||||
    "three": "^0.148.0",
 | 
			
		||||
    "three-stdlib": "^2.21.8"
 | 
			
		||||
    "@emotion/react": "^11.14.0",
 | 
			
		||||
    "@emotion/styled": "^11.14.0",
 | 
			
		||||
    "@fontsource/roboto": "^5.2.5",
 | 
			
		||||
    "@mui/icons-material": "^7.0.1",
 | 
			
		||||
    "@mui/material": "^7.0.1",
 | 
			
		||||
    "@react-three/drei": "^10.0.5",
 | 
			
		||||
    "@react-three/fiber": "^9.1.0",
 | 
			
		||||
    "@react-three/postprocessing": "^3.0.4",
 | 
			
		||||
    "axios": "^1.8.4",
 | 
			
		||||
    "notistack": "^3.0.2",
 | 
			
		||||
    "postprocessing": "^6.37.2",
 | 
			
		||||
    "react": "^19.1.0",
 | 
			
		||||
    "react-dom": "^19.1.0",
 | 
			
		||||
    "react-hook-form": "^7.55.0",
 | 
			
		||||
    "three": "^0.175.0",
 | 
			
		||||
    "three-stdlib": "^2.35.14"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@types/react": "^18.0.26",
 | 
			
		||||
    "@types/react-dom": "^18.0.9",
 | 
			
		||||
    "@types/three": "^0.148.0",
 | 
			
		||||
    "@vitejs/plugin-react": "^3.0.0",
 | 
			
		||||
    "typescript": "^4.9.3",
 | 
			
		||||
    "vite": "^4.0.0",
 | 
			
		||||
    "vite-plugin-mock-dev-server": "^0.3.16"
 | 
			
		||||
    "@types/react": "^19.0.12",
 | 
			
		||||
    "@types/react-dom": "^19.0.4",
 | 
			
		||||
    "@types/three": "^0.175.0",
 | 
			
		||||
    "@vitejs/plugin-react": "^4.3.4",
 | 
			
		||||
    "typescript": "^5.8.2",
 | 
			
		||||
    "vite": "^6.2.3",
 | 
			
		||||
    "vite-plugin-mock-dev-server": "^1.8.4"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2023. Gardel <sunxinao@hotmail.com> and contributors
 | 
			
		||||
 * Copyright (C) 2023-2025. Gardel <sunxinao@hotmail.com> and contributors
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
@@ -18,6 +18,7 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import './app.css';
 | 
			
		||||
import Login from './login';
 | 
			
		||||
import PasswordReset from './reset';
 | 
			
		||||
import {Container} from '@mui/material';
 | 
			
		||||
import {AppState} from './types';
 | 
			
		||||
import User from './user';
 | 
			
		||||
@@ -29,12 +30,13 @@ function App() {
 | 
			
		||||
    const [appData, setAppData] = React.useState(() => {
 | 
			
		||||
        const saved = localStorage.getItem('appData');
 | 
			
		||||
        return (saved ? JSON.parse(saved) : {
 | 
			
		||||
            login: false,
 | 
			
		||||
            login: 'register',
 | 
			
		||||
            accessToken: '',
 | 
			
		||||
            tokenValid: false,
 | 
			
		||||
            loginTime: 0,
 | 
			
		||||
            profileName: '',
 | 
			
		||||
            uuid: ''
 | 
			
		||||
            uuid: '',
 | 
			
		||||
            passwordReset: false
 | 
			
		||||
        }) as AppState;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@@ -95,11 +97,52 @@ function App() {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Container maxWidth={'lg'}>
 | 
			
		||||
            {appData.tokenValid ? <User appData={appData} setAppData={setAppData}/> : <Login appData={appData} setAppData={setAppData}/>}
 | 
			
		||||
        </Container>
 | 
			
		||||
    );
 | 
			
		||||
    const path = window.location.pathname;
 | 
			
		||||
    const hash = window.location.hash;
 | 
			
		||||
    if (hash && hash.length > 1) {
 | 
			
		||||
        const params = new URLSearchParams(hash.substring(1));
 | 
			
		||||
        if (params.has('emailVerifyToken')) {
 | 
			
		||||
            const token = params.get('emailVerifyToken');
 | 
			
		||||
            axios.get('/authserver/verifyEmail?access_token=' + token)
 | 
			
		||||
                .then(() => {
 | 
			
		||||
                    window.location.replace('/profile/')
 | 
			
		||||
                    enqueueSnackbar('邮箱验证通过', {variant: 'success'});
 | 
			
		||||
                })
 | 
			
		||||
                .catch(e => {
 | 
			
		||||
                    const response = e.response;
 | 
			
		||||
                    if (response && response.status >= 400 && response.status < 500) {
 | 
			
		||||
                        let errorMessage = response.data.errorMessage ?? response.data;
 | 
			
		||||
                        enqueueSnackbar('邮箱验证失败: ' + errorMessage, {variant: 'error'});
 | 
			
		||||
                    } else {
 | 
			
		||||
                        enqueueSnackbar('网络错误:' + e.message, {variant: 'error'});
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if (path === '/profile/resetPassword' || path === '/resetPassword') {
 | 
			
		||||
        appData.passwordReset = true;
 | 
			
		||||
        // setAppData(appData);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (appData.tokenValid) {
 | 
			
		||||
        return (
 | 
			
		||||
            <Container maxWidth={'lg'}>
 | 
			
		||||
                <User appData={appData} setAppData={setAppData}/>
 | 
			
		||||
            </Container>
 | 
			
		||||
        );
 | 
			
		||||
    } else if (appData.passwordReset) {
 | 
			
		||||
        return (
 | 
			
		||||
            <Container maxWidth={'lg'}>
 | 
			
		||||
                <PasswordReset appData={appData} setAppData={setAppData}/>
 | 
			
		||||
            </Container>
 | 
			
		||||
        )
 | 
			
		||||
    } else {
 | 
			
		||||
        return (
 | 
			
		||||
            <Container maxWidth={'lg'}>
 | 
			
		||||
                <Login appData={appData} setAppData={setAppData}/>
 | 
			
		||||
            </Container>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default App;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2023. Gardel <sunxinao@hotmail.com> and contributors
 | 
			
		||||
 * Copyright (C) 2023-2025. Gardel <sunxinao@hotmail.com> and contributors
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
@@ -48,9 +48,29 @@ function Login(props: { appData: AppState, setAppData: React.Dispatch<React.SetS
 | 
			
		||||
    const {enqueueSnackbar} = useSnackbar();
 | 
			
		||||
    const {register, handleSubmit, formState: {errors}} = useForm<Inputs>();
 | 
			
		||||
    const [submitting, setSubmitting] = React.useState(false);
 | 
			
		||||
    const [submitText, setSubmitText] = React.useState('提交');
 | 
			
		||||
    React.useEffect(() => {
 | 
			
		||||
        if (submitting) {
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
        switch (appData.login) {
 | 
			
		||||
            case 'login':
 | 
			
		||||
                setSubmitText('登录');
 | 
			
		||||
                break;
 | 
			
		||||
            case 'register':
 | 
			
		||||
                setSubmitText('注册');
 | 
			
		||||
                break;
 | 
			
		||||
            case 'reset':
 | 
			
		||||
                setSubmitText('重置');
 | 
			
		||||
                break;
 | 
			
		||||
            default:
 | 
			
		||||
                setSubmitText('提交');
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
    }, [appData, submitting])
 | 
			
		||||
    const onSubmit: SubmitHandler<Inputs> = data => {
 | 
			
		||||
        setSubmitting(true)
 | 
			
		||||
        if (appData.login) {
 | 
			
		||||
        setSubmitting(true);
 | 
			
		||||
        if (appData.login === 'login') {
 | 
			
		||||
            axios.post('/authserver/authenticate', {
 | 
			
		||||
                username: data.username,
 | 
			
		||||
                password: data.password
 | 
			
		||||
@@ -80,7 +100,7 @@ function Login(props: { appData: AppState, setAppData: React.Dispatch<React.SetS
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                .finally(() => setSubmitting(false))
 | 
			
		||||
        } else {
 | 
			
		||||
        } else if (appData.login === 'register') {
 | 
			
		||||
            axios.post('/authserver/register', {
 | 
			
		||||
                username: data.username,
 | 
			
		||||
                password: data.password,
 | 
			
		||||
@@ -90,7 +110,7 @@ function Login(props: { appData: AppState, setAppData: React.Dispatch<React.SetS
 | 
			
		||||
                    let data = response.data
 | 
			
		||||
                    if (data && data.id) {
 | 
			
		||||
                        enqueueSnackbar("注册成功,uuid:" + data.id, {variant: 'success'});
 | 
			
		||||
                        setLogin(true)
 | 
			
		||||
                        setLogin('login')
 | 
			
		||||
                    } else {
 | 
			
		||||
                        enqueueSnackbar(data && data.errorMessage ? "注册失败: " + data.errorMessage: "注册失败", {variant: 'error'});
 | 
			
		||||
                    }
 | 
			
		||||
@@ -98,7 +118,7 @@ function Login(props: { appData: AppState, setAppData: React.Dispatch<React.SetS
 | 
			
		||||
                .catch(e => {
 | 
			
		||||
                    const response = e.response;
 | 
			
		||||
                    if (response && response.data) {
 | 
			
		||||
                        let errorMessage = response.data.errorMessage;
 | 
			
		||||
                        let errorMessage = response.data.errorMessage ?? response.data;
 | 
			
		||||
                        let message =  "注册失败: " + errorMessage;
 | 
			
		||||
                        if (errorMessage === "profileName exist") {
 | 
			
		||||
                            message = "注册失败: 角色名已存在";
 | 
			
		||||
@@ -111,6 +131,38 @@ function Login(props: { appData: AppState, setAppData: React.Dispatch<React.SetS
 | 
			
		||||
                    }
 | 
			
		||||
                })
 | 
			
		||||
                .finally(() => setSubmitting(false))
 | 
			
		||||
        } else if (appData.login === 'reset') {
 | 
			
		||||
            const countdown = {
 | 
			
		||||
                timeout: 60,
 | 
			
		||||
            }
 | 
			
		||||
            setSubmitText(`${countdown.timeout}`);
 | 
			
		||||
            const timer = setInterval(() => {
 | 
			
		||||
                countdown.timeout = countdown.timeout - 1;
 | 
			
		||||
                if (countdown.timeout <= 0) {
 | 
			
		||||
                    clearInterval(timer);
 | 
			
		||||
                    setSubmitting(false);
 | 
			
		||||
                    return
 | 
			
		||||
                }
 | 
			
		||||
                setSubmitting(true);
 | 
			
		||||
                setSubmitText(`${countdown.timeout}`);
 | 
			
		||||
            }, 1000);
 | 
			
		||||
            axios.post('/authserver/sendEmail', {
 | 
			
		||||
                email: data.username,
 | 
			
		||||
                emailType: "resetPassword"
 | 
			
		||||
            })
 | 
			
		||||
                .then(() => {
 | 
			
		||||
                    enqueueSnackbar("重置链接发送成功,请检查垃圾邮箱", {variant: 'success'});
 | 
			
		||||
                })
 | 
			
		||||
                .catch(e => {
 | 
			
		||||
                    const response = e.response;
 | 
			
		||||
                    if (response && response.data) {
 | 
			
		||||
                        let errorMessage = response.data.errorMessage ?? response.data;
 | 
			
		||||
                        enqueueSnackbar("发送失败: " + errorMessage, {variant: 'error'});
 | 
			
		||||
                    } else {
 | 
			
		||||
                        enqueueSnackbar('网络错误:' + e.message, {variant: 'error'});
 | 
			
		||||
                    }
 | 
			
		||||
                    countdown.timeout = 0;
 | 
			
		||||
                });
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
@@ -122,7 +174,7 @@ function Login(props: { appData: AppState, setAppData: React.Dispatch<React.SetS
 | 
			
		||||
        event.preventDefault();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const setLogin = (login: boolean) => setAppData((oldData: AppState) => {
 | 
			
		||||
    const setLogin = (login: string) => setAppData((oldData: AppState) => {
 | 
			
		||||
        return {
 | 
			
		||||
            ...oldData,
 | 
			
		||||
            login
 | 
			
		||||
@@ -146,19 +198,21 @@ function Login(props: { appData: AppState, setAppData: React.Dispatch<React.SetS
 | 
			
		||||
                            required
 | 
			
		||||
                            error={errors.username && true}
 | 
			
		||||
                            type='email'
 | 
			
		||||
                            inputProps={{
 | 
			
		||||
                                ...register('username', {required: true})
 | 
			
		||||
                            slotProps={{
 | 
			
		||||
                                htmlInput: {
 | 
			
		||||
                                    ...register('username', {required: true})
 | 
			
		||||
                                }
 | 
			
		||||
                            }}
 | 
			
		||||
                        />
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <Collapse in={!appData.login} className='profileName'>
 | 
			
		||||
                        <FormControl fullWidth variant="filled" required={!appData.login} error={errors.profileName && true}>
 | 
			
		||||
                    <Collapse in={appData.login === 'register'} className='profileName'>
 | 
			
		||||
                        <FormControl fullWidth variant="filled" required={appData.login === 'register'} error={errors.profileName && true}>
 | 
			
		||||
                            <InputLabel htmlFor="profileName-input">角色名</InputLabel>
 | 
			
		||||
                            <FilledInput
 | 
			
		||||
                                id="profileName-input"
 | 
			
		||||
                                name="profileName"
 | 
			
		||||
                                required={!appData.login}
 | 
			
		||||
                                inputProps={appData.login ? {} : {
 | 
			
		||||
                                required={appData.login === 'register'}
 | 
			
		||||
                                inputProps={appData.login !== 'register' ? {} : {
 | 
			
		||||
                                    minLength: '2', maxLength: 16,
 | 
			
		||||
                                    ...register('profileName', {required: true, minLength: 2, pattern: /^[a-zA-Z0-9_]{1,16}$/, maxLength: 16})
 | 
			
		||||
                                }}
 | 
			
		||||
@@ -166,13 +220,13 @@ function Login(props: { appData: AppState, setAppData: React.Dispatch<React.SetS
 | 
			
		||||
                            <FocusedShowHelperText id="profileName-input-helper-text">字母,数字或下划线</FocusedShowHelperText>
 | 
			
		||||
                        </FormControl>
 | 
			
		||||
                    </Collapse>
 | 
			
		||||
                    <div className='password'>
 | 
			
		||||
                        <FormControl fullWidth variant="filled" required error={errors.password && true}>
 | 
			
		||||
                    <Collapse in={appData.login !== 'reset'} className='password'>
 | 
			
		||||
                        <FormControl fullWidth variant="filled" required={appData.login !== 'reset'} error={errors.password && true}>
 | 
			
		||||
                            <InputLabel htmlFor="password-input">密码</InputLabel>
 | 
			
		||||
                            <FilledInput
 | 
			
		||||
                                id="password-input"
 | 
			
		||||
                                name="password"
 | 
			
		||||
                                required
 | 
			
		||||
                                required={appData.login !== 'reset'}
 | 
			
		||||
                                type={showPassword ? 'text' : 'password'}
 | 
			
		||||
                                endAdornment={
 | 
			
		||||
                                    <InputAdornment position="end">
 | 
			
		||||
@@ -185,17 +239,19 @@ function Login(props: { appData: AppState, setAppData: React.Dispatch<React.SetS
 | 
			
		||||
                                        </IconButton>
 | 
			
		||||
                                    </InputAdornment>
 | 
			
		||||
                                }
 | 
			
		||||
                                inputProps={{
 | 
			
		||||
                                inputProps={appData.login === 'reset' ? {} : {
 | 
			
		||||
                                    minLength: '6',
 | 
			
		||||
                                    ...register('password', {required: true, minLength: 6})
 | 
			
		||||
                                }}
 | 
			
		||||
                            />
 | 
			
		||||
                            <FocusedShowHelperText id="password-input-helper-text">警告: 暂无重置密码接口,请妥善保管密码</FocusedShowHelperText>
 | 
			
		||||
                            <FocusedShowHelperText id="password-input-helper-text">请妥善保管密码</FocusedShowHelperText>
 | 
			
		||||
                        </FormControl>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    </Collapse>
 | 
			
		||||
                    <div className='button-container'>
 | 
			
		||||
                        <Button variant='contained' onClick={() => setLogin(!appData.login)} disabled={submitting}>{appData.login ? '注册' : '已有帐号登录'}</Button>
 | 
			
		||||
                        <Button variant='contained' type='submit' disabled={submitting}>{appData.login ? '登录' : '注册'}</Button>
 | 
			
		||||
                        {appData.login !== 'reset' && <Button variant='contained' onClick={() => setLogin('reset')}>忘记密码</Button>}
 | 
			
		||||
                        {appData.login !== 'login' && <Button variant='contained' onClick={() => setLogin('login')}>已有账号登录</Button>}
 | 
			
		||||
                        {appData.login !== 'register' && <Button variant='contained' onClick={() => setLogin('register')}>注册</Button>}
 | 
			
		||||
                        <Button variant='contained' type='submit' disabled={submitting}>{submitText}</Button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </Box>
 | 
			
		||||
            </Paper>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										43
									
								
								frontend/src/reset.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								frontend/src/reset.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2023-2025. Gardel <sunxinao@hotmail.com> and contributors
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU Affero General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
.header {
 | 
			
		||||
    text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.reset-card {
 | 
			
		||||
    padding: 14px 24px;
 | 
			
		||||
    margin: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.username,
 | 
			
		||||
.password {
 | 
			
		||||
    display: block;
 | 
			
		||||
    width: 87%;
 | 
			
		||||
    margin: 20px auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.button-container {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: flex-end;
 | 
			
		||||
    width: 87%;
 | 
			
		||||
    margin: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.button-container button {
 | 
			
		||||
    margin: 3px;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										155
									
								
								frontend/src/reset.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								frontend/src/reset.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,155 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2023-2025. Gardel <sunxinao@hotmail.com> and contributors
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU Affero General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU Affero General Public License
 | 
			
		||||
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import Button from '@mui/material/Button';
 | 
			
		||||
import {
 | 
			
		||||
    Box,
 | 
			
		||||
    Container,
 | 
			
		||||
    FilledInput,
 | 
			
		||||
    FormControl,
 | 
			
		||||
    IconButton,
 | 
			
		||||
    InputAdornment,
 | 
			
		||||
    InputLabel,
 | 
			
		||||
    Paper,
 | 
			
		||||
    TextField
 | 
			
		||||
} from '@mui/material';
 | 
			
		||||
import {Visibility, VisibilityOff} from '@mui/icons-material';
 | 
			
		||||
import {AppState} from './types';
 | 
			
		||||
import './reset.css';
 | 
			
		||||
import {SubmitHandler, useForm} from 'react-hook-form';
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
import {useSnackbar} from 'notistack';
 | 
			
		||||
 | 
			
		||||
type Inputs = {
 | 
			
		||||
    username: string,
 | 
			
		||||
    profileName: string,
 | 
			
		||||
    password: string
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function PasswordReset(props: { appData: AppState, setAppData: React.Dispatch<React.SetStateAction<AppState>> }) {
 | 
			
		||||
    const {appData, setAppData} = props;
 | 
			
		||||
    const {enqueueSnackbar} = useSnackbar();
 | 
			
		||||
    const {register, handleSubmit, formState: {errors}} = useForm<Inputs>();
 | 
			
		||||
    const [submitting, setSubmitting] = React.useState(false);
 | 
			
		||||
    const onSubmit: SubmitHandler<Inputs> = data => {
 | 
			
		||||
        setSubmitting(true);
 | 
			
		||||
        const hash = window.location.hash;
 | 
			
		||||
        if (!hash) {
 | 
			
		||||
            setSubmitting(false);
 | 
			
		||||
            enqueueSnackbar('链接失效,请重新打开', {variant: 'error'});
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        const params = new URLSearchParams(hash.substring(1));
 | 
			
		||||
        axios.post('/authserver/resetPassword', {
 | 
			
		||||
            email: data.username,
 | 
			
		||||
            password: data.password,
 | 
			
		||||
            accessToken: params.get('passwordResetToken'),
 | 
			
		||||
        })
 | 
			
		||||
            .then(() => {
 | 
			
		||||
                toLogin();
 | 
			
		||||
                window.location.replace('/profile/')
 | 
			
		||||
                enqueueSnackbar("重置成功", {variant: 'success'});
 | 
			
		||||
            })
 | 
			
		||||
            .catch(e => {
 | 
			
		||||
                const response = e.response;
 | 
			
		||||
                if (response && response.status >= 400 && response.status < 500) {
 | 
			
		||||
                    let errorMessage = response.data.errorMessage ?? response.data;
 | 
			
		||||
                    enqueueSnackbar('重置失败: ' + errorMessage, {variant: 'error'});
 | 
			
		||||
                } else {
 | 
			
		||||
                    enqueueSnackbar('网络错误:' + e.message, {variant: 'error'});
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            .finally(() => setSubmitting(false))
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const [showPassword, setShowPassword] = React.useState(false);
 | 
			
		||||
 | 
			
		||||
    const handleClickShowPassword = () => setShowPassword((show) => !show);
 | 
			
		||||
 | 
			
		||||
    const handleMouseDownPassword = (event: React.MouseEvent<HTMLButtonElement>) => {
 | 
			
		||||
        event.preventDefault();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const toLogin = () => {
 | 
			
		||||
        setAppData({
 | 
			
		||||
            ...appData,
 | 
			
		||||
            passwordReset: false
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Container maxWidth={'sm'}>
 | 
			
		||||
            <Paper className={'reset-card'}>
 | 
			
		||||
                <section className="header">
 | 
			
		||||
                    <h1>简陋重置密码页</h1>
 | 
			
		||||
                </section>
 | 
			
		||||
                <Box component="form" autoComplete="off" onSubmit={handleSubmit(onSubmit)}>
 | 
			
		||||
                    <div className='username'>
 | 
			
		||||
                        <TextField
 | 
			
		||||
                            id="username-input"
 | 
			
		||||
                            name='username'
 | 
			
		||||
                            fullWidth
 | 
			
		||||
                            label="邮箱"
 | 
			
		||||
                            variant="filled"
 | 
			
		||||
                            required
 | 
			
		||||
                            error={errors.username && true}
 | 
			
		||||
                            type='email'
 | 
			
		||||
                            slotProps={{
 | 
			
		||||
                                htmlInput: {
 | 
			
		||||
                                    ...register('username', {required: true})
 | 
			
		||||
                                }
 | 
			
		||||
                            }}
 | 
			
		||||
                        />
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className='password'>
 | 
			
		||||
                        <FormControl fullWidth variant="filled" required error={errors.password && true}>
 | 
			
		||||
                            <InputLabel htmlFor="password-input">新密码</InputLabel>
 | 
			
		||||
                            <FilledInput
 | 
			
		||||
                                id="password-input"
 | 
			
		||||
                                name="password"
 | 
			
		||||
                                required
 | 
			
		||||
                                type={showPassword ? 'text' : 'password'}
 | 
			
		||||
                                endAdornment={
 | 
			
		||||
                                    <InputAdornment position="end">
 | 
			
		||||
                                        <IconButton
 | 
			
		||||
                                            aria-label="显示密码"
 | 
			
		||||
                                            onClick={handleClickShowPassword}
 | 
			
		||||
                                            onMouseDown={handleMouseDownPassword}
 | 
			
		||||
                                            edge="end">
 | 
			
		||||
                                            {showPassword ? <VisibilityOff/> : <Visibility/>}
 | 
			
		||||
                                        </IconButton>
 | 
			
		||||
                                    </InputAdornment>
 | 
			
		||||
                                }
 | 
			
		||||
                                inputProps={{
 | 
			
		||||
                                    minLength: '6',
 | 
			
		||||
                                    ...register('password', {required: true, minLength: 6})
 | 
			
		||||
                                }}
 | 
			
		||||
                            />
 | 
			
		||||
                        </FormControl>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className='button-container'>
 | 
			
		||||
                        <Button variant='contained' onClick={toLogin}>登录</Button>
 | 
			
		||||
                        <Button variant='contained' type='submit' disabled={submitting}>重置</Button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </Box>
 | 
			
		||||
            </Paper>
 | 
			
		||||
        </Container>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default PasswordReset;
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2023. Gardel <sunxinao@hotmail.com> and contributors
 | 
			
		||||
 * Copyright (C) 2023-2025. Gardel <sunxinao@hotmail.com> and contributors
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
@@ -21,7 +21,7 @@ import {Canvas, RootState, useFrame, useLoader} from '@react-three/fiber';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import createPlayerModel from './utils';
 | 
			
		||||
import {OrbitControls} from '@react-three/drei';
 | 
			
		||||
import {EffectComposer, Vignette, SMAA, SSAO, SSR} from '@react-three/postprocessing';
 | 
			
		||||
import {EffectComposer, SSAO} from '@react-three/postprocessing';
 | 
			
		||||
import {BlendFunction} from 'postprocessing';
 | 
			
		||||
 | 
			
		||||
function PlayerModel(props: { skinUrl: string, capeUrl?: string, slim?: boolean }) {
 | 
			
		||||
@@ -84,7 +84,7 @@ function SkinRender(props: { skinUrl: string, capeUrl?: string, slim?: boolean }
 | 
			
		||||
                        rangeFalloff={0.1}
 | 
			
		||||
                        luminanceInfluence={0.9}
 | 
			
		||||
                        radius={20}
 | 
			
		||||
                        scale={0.5}
 | 
			
		||||
                        resolutionScale={0.5}
 | 
			
		||||
                        bias={0.5}
 | 
			
		||||
                    />
 | 
			
		||||
                </EffectComposer>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2023. Gardel <sunxinao@hotmail.com> and contributors
 | 
			
		||||
 * Copyright (C) 2023-2025. Gardel <sunxinao@hotmail.com> and contributors
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
@@ -99,7 +99,7 @@ function createCube(texture: THREE.Texture, width: number, height: number, depth
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default function createPlayerModel(skinTexture: THREE.Texture, capeTexture: THREE.Texture | null | undefined, v: number, slim: boolean = false, capeType?: string): THREE.Object3D<THREE.Event> {
 | 
			
		||||
export default function createPlayerModel(skinTexture: THREE.Texture, capeTexture: THREE.Texture | null | undefined, v: number, slim: boolean = false, capeType?: string): THREE.Object3D<THREE.Object3DEventMap> {
 | 
			
		||||
    let headGroup = new THREE.Object3D();
 | 
			
		||||
    headGroup.name = 'headGroup';
 | 
			
		||||
    headGroup.position.x = 0;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								frontend/src/types.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								frontend/src/types.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -1,5 +1,5 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright (C) 2023. Gardel <sunxinao@hotmail.com> and contributors
 | 
			
		||||
 * Copyright (C) 2023-2025. Gardel <sunxinao@hotmail.com> and contributors
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU Affero General Public License as published by
 | 
			
		||||
@@ -16,10 +16,11 @@
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export type AppState = {
 | 
			
		||||
    login: boolean
 | 
			
		||||
    login: string
 | 
			
		||||
    accessToken: string
 | 
			
		||||
    tokenValid: boolean
 | 
			
		||||
    loginTime: number
 | 
			
		||||
    profileName: string
 | 
			
		||||
    uuid: string
 | 
			
		||||
    passwordReset: boolean
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2699
									
								
								frontend/yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										2699
									
								
								frontend/yarn.lock
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user