Browse Source

完成登陆的前端,修改api

pull/6/head
hole-thu 5 years ago
parent
commit
6f7ad41591
  1. 5
      README.md
  2. 1
      package.json
  3. 2
      public/index.html
  4. 19
      src/App.js
  5. 3
      src/Common.js
  6. 1
      src/Flows.js
  7. 4
      src/Message.js
  8. 6
      src/Title.js
  9. 2
      src/UserAction.css
  10. 73
      src/UserAction.js
  11. 3
      src/flows_api.js
  12. 2
      src/infrastructure/const.js
  13. 14
      src/infrastructure/widgets.css
  14. 359
      src/infrastructure/widgets.js
  15. 18
      yarn.lock

5
README.md

@ -28,6 +28,11 @@
`num+` 指符合版本号 `num` 的最新版本及后续所有版本。`最新版` 以 stable 分支为准。
## 接口
+ /login/...
+ /api/v1/...
## 问题反馈
对 新T树洞 网页版的 bug 反馈请在后端仓库提交 Issue。

1
package.json

@ -14,7 +14,6 @@
"pressure": "^2.1.2",
"react": "^16.13.1",
"react-dom": "^16.13.0",
"react-google-recaptcha-v3": "^1.5.2",
"react-scripts": "^3.4.1",
"react-timeago": "^4.4.0",
"typescript": "^4.0.2"

2
public/index.html

@ -26,6 +26,6 @@
<title>新T树洞</title>
</head>
<body>
<div id="root"></div>
<div id="root">开启javascript,或刷新重试</div>
</body>
</html>

19
src/App.js

@ -33,12 +33,6 @@ class App extends Component {
this.show_sidebar_bound = this.show_sidebar.bind(this);
this.set_mode_bound = this.set_mode.bind(this);
this.on_pressure_bound = this.on_pressure.bind(this);
// a silly self-deceptive approach to ban guests, enough to fool those muggles
// document cookie 'pku_ip_flag=yes'
this.inthu_flag =
window[atob('ZG9jdW1lbnQ')][atob('Y29va2ll')].indexOf(
atob('dGh1X2lwX2ZsYWc9eWVz'),
) !== -1;
}
static is_darkmode() {
@ -102,6 +96,15 @@ class App extends Component {
});
}
componentDidMount() {
let arg = window.location.search;
console.log(arg);
if (arg.startsWith('?token=')) {
localStorage['TOKEN'] = arg.substr(7);
window.location.search = '';
}
}
render() {
return (
<TokenCtx.Provider
@ -129,7 +132,6 @@ class App extends Component {
{!token.value && (
<div className="flow-item-row aux-margin">
<div className="box box-tip">
<p>
<LoginPopup token_callback={token.set_value}>
{(do_popup) => (
<a onClick={do_popup}>
@ -138,11 +140,10 @@ class App extends Component {
</a>
)}
</LoginPopup>
</p>
</div>
</div>
)}
{this.inthu_flag || token.value ? (
{token.value ? (
<Flow
key={this.state.flow_render_key}
show_sidebar={this.show_sidebar_bound}

3
src/Common.js

@ -1,6 +1,5 @@
import React, { Component, PureComponent } from 'react';
import { format_time, Time, TitleLine } from './infrastructure/widgets';
import { THUHOLE_API_ROOT } from './flows_api';
import HtmlToReact from 'html-to-react';
@ -17,7 +16,7 @@ import renderMd from './Markdown';
export { format_time, Time, TitleLine };
export const API_BASE = THUHOLE_API_ROOT + 'services/thuhole';
export const API_BASE = '/api/v1/';
// https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
function escape_regex(string) {

1
src/Flows.js

@ -25,7 +25,6 @@ import { API } from './flows_api';
const IMAGE_BASE = 'https://thimg.yecdn.com/';
const IMAGE_BAK_BASE = 'https://img2.thuhole.com/';
// const AUDIO_BASE=THUHOLE_API_ROOT+'services/thuhole/audios/';
const CLICKABLE_TAGS = { a: true, audio: true };
const PREVIEW_REPLY_COUNT = 10;

4
src/Message.js

@ -1,5 +1,4 @@
import React, { PureComponent } from 'react';
import { THUHOLE_API_ROOT, get_json, API_VERSION_PARAM } from './flows_api';
import { Time } from './Common';
export class MessageViewer extends PureComponent {
@ -23,8 +22,7 @@ export class MessageViewer extends PureComponent {
},
() => {
fetch(
THUHOLE_API_ROOT +
'api_xmcp/hole/system_msg?user_token=' +
'/api/v1/system_msg?user_token=' +
encodeURIComponent(this.props.token) +
API_VERSION_PARAM(),
)

6
src/Title.js

@ -126,7 +126,7 @@ class ControlBar extends PureComponent {
className="no-underline control-btn"
onClick={() => {
this.props.show_sidebar(
'新树洞',
'新T树洞',
<InfoSidebar show_sidebar={this.props.show_sidebar} />,
);
}}
@ -173,12 +173,12 @@ export function Title(props) {
<span
onClick={() =>
props.show_sidebar(
'新树洞',
'新T树洞',
<InfoSidebar show_sidebar={props.show_sidebar} />,
)
}
>
树洞
T树洞
</span>
</p>
</div>

2
src/UserAction.css

@ -3,7 +3,7 @@
text-align: center;
}
.login-form button {
width: 6rem;
min-width: 6rem;
}
.reply-form {

73
src/UserAction.js

@ -13,8 +13,6 @@ import fixOrientation from 'fix-orientation';
import copy from 'copy-to-clipboard';
import { cache } from './cache';
import {
API_VERSION_PARAM,
THUHOLE_API_ROOT,
API,
get_json,
token_param,
@ -107,7 +105,7 @@ export function InfoSidebar(props) {
开源
</p>
<p>
树洞 网页版基于
T树洞 网页版基于
<a
href="https://github.com/pkuhelper-web/webhole"
target="_blank"
@ -130,71 +128,6 @@ export function InfoSidebar(props) {
);
}
class ResetUsertokenWidget extends Component {
constructor(props) {
super(props);
this.state = {
loading_status: 'done',
};
}
do_reset() {
if (
window.confirm(
'您正在重置 UserToken!\n您的账号将会在【所有设备】上注销,您需要手动重新登录!',
)
) {
let uid = window.prompt(
'您正在重置 UserToken!\n请输入您的学号以确认身份:',
);
if (uid)
this.setState(
{
loading_status: 'loading',
},
() => {
fetch(THUHOLE_API_ROOT + 'api_xmcp/hole/reset_usertoken', {
method: 'post',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
user_token: this.props.token,
uid: uid,
}),
})
.then(get_json)
.then((json) => {
if (json.error) throw new Error(json.error);
else alert('重置成功!您需要在所有设备上重新登录。');
this.setState({
loading_status: 'done',
});
})
.catch((e) => {
alert('重置失败:' + e);
this.setState({
loading_status: 'done',
});
});
},
);
}
}
render() {
if (this.state.loading_status === 'done')
return <a onClick={this.do_reset.bind(this)}>重置</a>;
else if (this.state.loading_status === 'loading')
return (
<a>
<span className="icon icon-loading" />
</a>
);
}
}
export class LoginForm extends Component {
copy_token(token) {
if (copy(token)) alert('复制成功!\n请一定不要泄露哦');
@ -242,9 +175,7 @@ export class LoginForm extends Component {
复制 User Token
</a>
<br />
复制 User Token
可以在新设备登录切勿告知他人若怀疑被盗号请重置Token
{/*,若怀疑被盗号请尽快 <ResetUsertokenWidget token={token.value} />*/}
User Token仅用于开发bot切勿告知他人若怀疑被盗号请刷新Token
</p>
</div>
) : (

3
src/flows_api.js

@ -1,10 +1,7 @@
import { get_json, API_VERSION_PARAM } from './infrastructure/functions';
import { THUHOLE_API_ROOT } from './infrastructure/const';
import { API_BASE } from './Common';
import { cache } from './cache';
export { THUHOLE_API_ROOT, API_VERSION_PARAM };
export function token_param(token) {
return API_VERSION_PARAM() + (token ? '&user_token=' + token : '');
}

2
src/infrastructure/const.js

@ -1,2 +0,0 @@
// export const THUHOLE_API_ROOT='//localhost:5001/';
export const THUHOLE_API_ROOT = 'https://thuhole.com/'

14
src/infrastructure/widgets.css

@ -241,7 +241,7 @@ a.app-switcher-item, .app-switcher-item a {
line-height: 2em;
}
.thuhole-login-popup button {
width: 6rem;
min-width: 6rem;
color: black;
background-color: rgba(235,235,235,.5);
border-radius: 5px;
@ -299,3 +299,15 @@ a.app-switcher-item, .app-switcher-item a {
.time-str {
color: #999999;
}
a.button {
-webkit-appearance: button;
-moz-appearance: button;
appearance: button;
text-decoration: none;
color: initial;
min-width: 6em;
font-size: 0.85em;
}

359
src/infrastructure/widgets.js

@ -17,16 +17,8 @@ import appicon_course_survey from './appicon/course_survey.png';
import appicon_dropdown from './appicon/dropdown.png';
import appicon_dropdown_rev from './appicon/dropdown_rev.png';
import appicon_homepage from './appicon/homepage.png';
import {THUHOLE_API_ROOT} from './const';
import {get_json, API_VERSION_PARAM} from './functions';
import {
GoogleReCaptchaProvider,
GoogleReCaptcha
} from 'react-google-recaptcha-v3';
const LOGIN_POPUP_ANCHOR_ID='pkuhelper_login_popup_anchor';
function pad2(x) {
return x<10 ? '0'+x : ''+x;
}
@ -66,365 +58,48 @@ export function GlobalTitle(props) {
);
}
const FALLBACK_APPS={
// id, text, url, icon_normal, icon_hover, new_tab
bar: [
['hole', '树洞', '/hole', appicon_hole, null, false],
['imasugu', '教室', '/spare_classroom', appicon_imasugu, appicon_imasugu_rev, false],
['syllabus', '课表', '/syllabus', appicon_syllabus, null, false],
['score', '成绩', '/my_score', appicon_score, null, false],
],
dropdown: [
['course_survey', '课程测评', 'https://courses.pinzhixiaoyuan.com/', appicon_course_survey, null, true],
['homepage', '客户端', '/', appicon_homepage, null, true],
],
fix: {},
};
// const SWITCHER_DATA_VER='switcher_2';
// const SWITCHER_DATA_URL=THUHOLE_API_ROOT+'web_static/appswitcher_items.json';
// export class AppSwitcher extends Component {
// constructor(props) {
// super(props);
// this.state={
// apps: this.get_apps_from_localstorage(),
// }
// }
//
// get_apps_from_localstorage() {
// let ret=FALLBACK_APPS;
// if(localStorage['APPSWITCHER_ITEMS'])
// try {
// let content=JSON.parse(localStorage['APPSWITCHER_ITEMS'])[SWITCHER_DATA_VER];
// if(!content || !content.bar)
// throw new Error('content is empty');
//
// ret=content;
// } catch(e) {
// console.error('load appswitcher items from localstorage failed');
// console.trace(e);
// }
//
// return ret;
// }
//
// check_fix() {
// if(this.state.apps && this.state.apps.fix && this.state.apps.fix[this.props.appid])
// setTimeout(()=>{
// window.HOTFIX_CONTEXT={
// build_info: process.env.REACT_APP_BUILD_INFO || '---',
// build_env: process.env.NODE_ENV,
// };
// eval(this.state.apps.fix[this.props.appid]);
// },1); // make it async so failures won't be critical
// }
//
// componentDidMount() {
// this.check_fix();
// setTimeout(()=>{
// fetch(SWITCHER_DATA_URL)
// .then((res)=>{
// if(!res.ok) throw Error(`网络错误 ${res.status} ${res.statusText}`);
// return res.text();
// })
// .then((txt)=>{
// if(txt!==localStorage['APPSWITCHER_ITEMS']) {
// console.log('loaded new appswitcher items',txt);
// localStorage['APPSWITCHER_ITEMS']=txt;
//
// this.setState({
// apps: this.get_apps_from_localstorage(),
// });
// } else {
// console.log('appswitcher items unchanged');
// }
// })
// .catch((e)=>{
// console.error('loading appswitcher items failed');
// console.trace(e);
// });
// },500);
// }
//
// componentDidUpdate(prevProps, prevState) {
// if(this.state.apps!==prevState.apps)
// this.check_fix();
// }
//
// render() {
// let cur_id=this.props.appid;
//
// function app_elem([id,title,url,icon_normal,icon_hover,new_tab],no_class=false,ref=null) {
// return (
// <a ref={ref} key={id} className={no_class ? null : ('app-switcher-item'+(id===cur_id ? ' app-switcher-item-current' : ''))}
// href={url} target={new_tab ? '_blank' : '_self'}>
// {!!icon_normal && [
// <img key="normal" src={icon_normal} className="app-switcher-logo-normal" />,
// <img key="hover" src={icon_hover||icon_normal} className="app-switcher-logo-hover" />
// ]}
// <span>{title}</span>
// </a>
// );
// }
//
// let dropdown_cur_app=null;
// this.state.apps.dropdown.forEach((app)=>{
// if(app[0]===cur_id)
// dropdown_cur_app=app;
// });
//
// //console.log(JSON.stringify(this.state.apps));
//
// return (
// <div className="app-switcher">
// <span className="app-switcher-desc app-switcher-left">PKUHelper</span>
// {this.state.apps.bar.map((app)=>
// app_elem(app)
// )}
// {!!this.state.apps.dropdown.length &&
// <div className={
// 'app-switcher-item app-switcher-dropdown '
// +(dropdown_cur_app ? ' app-switcher-item-current' : '')
// }>
// <p className="app-switcher-dropdown-title">
// {!!dropdown_cur_app ?
// app_elem((()=>{
// let [id,title,_url,icon_normal,icon_hover,_new_tab]=dropdown_cur_app;
// return [id,title+'▾',null,icon_normal,icon_hover,false];
// })(),true) :
// app_elem(['-placeholder-elem','更多▾',null,appicon_dropdown,appicon_dropdown_rev,false],true)
// }
// </p>
// {this.state.apps.dropdown.map((app)=>{
// let ref=React.createRef();
// return (
// <p key={app[0]} className="app-switcher-dropdown-item" onClick={(e)=>{
// if(!e.target.closest('a') && ref.current)
// ref.current.click();
// }}>
// {app_elem(app,true,ref)}
// </p>
// );
// })}
// </div>
// }
// <span className="app-switcher-desc app-switcher-right">网页版</span>
// </div>
// );
// }
// }
class LoginPopupSelf extends Component {
constructor(props) {
super(props);
this.state={
loading_status: 'idle',
recaptcha_verified: false
// excluded_scopes: [],
};
this.username_ref=React.createRef();
this.password_ref=React.createRef();
this.input_token_ref=React.createRef();
this.popup_anchor=document.getElementById(LOGIN_POPUP_ANCHOR_ID);
if(!this.popup_anchor) {
this.popup_anchor=document.createElement('div');
this.popup_anchor.id=LOGIN_POPUP_ANCHOR_ID;
document.body.appendChild(this.popup_anchor);
}
}
do_sendcode(type) {
if(!this.state.recaptcha_verified) {
alert("reCAPTCHA风控系统正在评估您的浏览器安全状态,请稍后重试。")
return
}
if(this.state.loading_status==='loading')
return;
this.setState({
loading_status: 'loading',
},()=>{
fetch(
THUHOLE_API_ROOT+'api_xmcp/login/send_code'
+'?user='+encodeURIComponent(this.username_ref.current.value)
+'&code_type='+encodeURIComponent(type)
+"&recaptcha_token="+localStorage["recaptcha"]
+API_VERSION_PARAM(), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
excluded_scopes: [],
}),
}
)
.then(get_json)
.then((json)=>{
console.log(json);
if(!json.success)
throw new Error(JSON.stringify(json));
alert(json.msg);
this.setState({
loading_status: 'done',
});
})
.catch((e)=>{
console.error(e);
alert('发送失败\n'+e);
this.setState({
loading_status: 'done',
});
});
});
}
do_login(set_token) {
if(this.state.loading_status==='loading')
return;
this.setState({
loading_status: 'loading',
},()=>{
fetch(
THUHOLE_API_ROOT+'api_xmcp/login/login'
+'?user='+encodeURIComponent(this.username_ref.current.value)
+'&valid_code='+encodeURIComponent(this.password_ref.current.value)
+API_VERSION_PARAM(), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
excluded_scopes: [],
}),
}
)
.then(get_json)
.then((json)=>{
if(json.code!==0) {
if(json.msg) throw new Error(json.msg);
throw new Error(JSON.stringify(json));
}
set_token(json.user_token);
alert(`登录成功`);
this.setState({
loading_status: 'done',
});
this.props.on_close();
})
.catch((e)=>{
console.error(e);
alert('登录失败\n'+e);
this.setState({
loading_status: 'done',
});
});
});
}
do_input_token(set_token) {
if(this.state.loading_status==='loading')
return;
let token=this.input_token_ref.current.value;
this.setState({
loading_status: 'loading',
},()=>{
fetch(THUHOLE_API_ROOT+'api_xmcp/hole/system_msg?user_token='+encodeURIComponent(token)+API_VERSION_PARAM())
.then((res)=>res.json())
.then((json)=>{
if(json.error)
throw new Error(json.error);
if(json.result.length===0)
throw new Error('result check failed');
this.setState({
loading_status: 'done',
});
set_token(token);
this.props.on_close();
})
.catch((e)=>{
alert('Token检验失败\n'+e);
this.setState({
loading_status: 'done',
});
console.error(e);
});
});
}
// perm_alert() {
// alert('如果你不需要 PKU Helper 的某项功能,可以取消相应权限。\n其中【状态信息】包括你的网费、校园卡余额等。\n该设置应用到你的【所有】设备,取消后如需再次启用相应功能需要重新登录。');
// }
};
render() {
// let PERM_SCOPES=[
// ['score','成绩查询'],
// ['syllabus','课表查询'],
// ['my_info','状态信息'],
// ];
return ReactDOM.createPortal(
<GoogleReCaptchaProvider reCaptchaKey={"6Leq0a0ZAAAAAHEStocsqtJfKEs9APB0LdgzTNfZ"} useRecaptchaNet={true}>
<GoogleReCaptcha onVerify={(token) => {
this.setState({
recaptcha_verified: true,
});
localStorage["recaptcha"] = token
}} />
return (
<div>
<div className="thuhole-login-popup-shadow" />
<div className="thuhole-login-popup">
<p>
<b>接收验证码来登录 T大树洞</b>
<b>通过第三方验证登陆T大树洞</b>
</p>
<p>
<label>
 邮箱&nbsp;
<input ref={this.username_ref} type="email" autoFocus={true} defaultValue="@mails.tsinghua.edu.cn" />
</label>
<span className="thuhole-login-type">
{/*<a onClick={(e)=>this.do_sendcode('sms')}>*/}
{/* &nbsp;短信&nbsp;*/}
{/*</a>*/}
{/*/*/}
<a onClick={(e)=>this.do_sendcode('mail')}>
&nbsp;发送邮件&nbsp;
<a className="button" href="/login/request?p=cs" target="_blank"
>
闭社
</a>
</span>
</p>
<p>
<label>
验证码&nbsp;
<input ref={this.password_ref} type="tel" />
</label>
<button type="button" disabled={this.state.loading_status==='loading'}
onClick={(e)=>this.do_login(this.props.token_callback)}>
登录
<button type="button" disabled
>
T大树洞
</button>
</p>
<hr />
<p>
<b>从其他设备导入登录状态</b>
<button type="button" disabled
>
未名bbs
</button>
</p>
<p>
<input ref={this.input_token_ref} placeholder="User Token" />
<button type="button" disabled={this.state.loading_status==='loading'}
onClick={(e)=>this.do_input_token(this.props.token_callback)}>
导入
<button type="button" disabled
>
清华统一身份认证
</button>
</p>
<hr />
<p style={{fontSize:11}}>
This site is protected by reCAPTCHA and the Google <a
href="https://policies.google.com/privacy">Privacy Policy</a> and <a
href="https://policies.google.com/terms">Terms of Service</a> apply.
</p>
<p>
<button onClick={this.props.on_close}>
取消
@ -432,8 +107,6 @@ class LoginPopupSelf extends Component {
</p>
</div>
</div>
</GoogleReCaptchaProvider>,
this.popup_anchor,
);
}
}

18
yarn.lock

@ -1110,7 +1110,7 @@
"@babel/runtime@^7.0.0", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4":
version "7.11.2"
resolved "https://registry.npm.taobao.org/@babel/runtime/download/@babel/runtime-7.11.2.tgz?cache=0&sync_timestamp=1596637820375&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40babel%2Fruntime%2Fdownload%2F%40babel%2Fruntime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
resolved "https://registry.npm.taobao.org/@babel/runtime/download/@babel/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
integrity sha1-9UnBPHVMxAuHZEufqfCaapX+BzY=
dependencies:
regenerator-runtime "^0.13.4"
@ -5167,13 +5167,6 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
[email protected]:
version "3.3.2"
resolved "https://registry.npm.taobao.org/hoist-non-react-statics/download/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha1-7OCsr3HWLClpwuxZ/v9CpLGoW0U=
dependencies:
react-is "^16.7.0"
hosted-git-info@^2.1.4:
version "2.8.8"
resolved "https://registry.npm.taobao.org/hosted-git-info/download/hosted-git-info-2.8.8.tgz?cache=0&sync_timestamp=1594427993800&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fhosted-git-info%2Fdownload%2Fhosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
@ -8820,14 +8813,7 @@ react-error-overlay@^6.0.7:
resolved "https://registry.npm.taobao.org/react-error-overlay/download/react-error-overlay-6.0.7.tgz?cache=0&sync_timestamp=1596670540895&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-error-overlay%2Fdownload%2Freact-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108"
integrity sha1-Hc+0WatnHVP2YKmRUTyy8KBVMQg=
react-google-recaptcha-v3@^1.5.2:
version "1.5.2"
resolved "https://registry.npm.taobao.org/react-google-recaptcha-v3/download/react-google-recaptcha-v3-1.5.2.tgz#0a20cb133270bd4fbd90a641937142e45d0ff9ae"
integrity sha1-CiDLEzJwvU+9kKZBk3FC5F0P+a4=
dependencies:
hoist-non-react-statics "3.3.2"
react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4:
react-is@^16.8.1, react-is@^16.8.4:
version "16.13.1"
resolved "https://registry.npm.taobao.org/react-is/download/react-is-16.13.1.tgz?cache=0&sync_timestamp=1598612913326&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Freact-is%2Fdownload%2Freact-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha1-eJcppNw23imZ3BVt1sHZwYzqVqQ=

Loading…
Cancel
Save