mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-19 09:38:19 +08:00
core: 2fa support
This commit is contained in:
1 parent
f18547ba16
commit
fe1112e652
11 files changed
+200
-6
No files matched your search
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/ui-default",
|
||||
"version": "4.26.15",
|
||||
"version": "4.26.16",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "hydro.js",
|
||||
@@ -16,6 +16,7 @@
|
||||
"@types/jquery": "^3.5.8",
|
||||
"@types/json-schema": "^7.0.9",
|
||||
"@types/katex": "^0.11.1",
|
||||
"@types/qrcode": "^1",
|
||||
"@types/sockjs-client": "^1.5.1",
|
||||
"@undefined-moe/monaco-yaml": "^2.5.0",
|
||||
"ansi_up": "^5.0.1",
|
||||
@@ -64,6 +65,7 @@
|
||||
"prism-themes": "^1.9.0",
|
||||
"prismjs": "^1.25.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"qrcode": "^1.4.4",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-redux": "^7.2.6",
|
||||
@@ -82,6 +84,7 @@
|
||||
"stylus-loader": "^3.0.2",
|
||||
"tether": "^1.4.7",
|
||||
"tether-drop": "^1.4.2",
|
||||
"thirty-two": "^1.0.2",
|
||||
"through2": "^4.0.2",
|
||||
"timeago-react": "^3.0.4",
|
||||
"timeago.js": "^4.0.2",
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { NamedPage } from 'vj/misc/Page';
|
||||
import QRCode from 'qrcode';
|
||||
import b32 from 'thirty-two';
|
||||
import Notification from 'vj/components/notification';
|
||||
import i18n from 'vj/utils/i18n';
|
||||
import tpl from 'vj/utils/tpl';
|
||||
import { ActionDialog } from 'vj/components/dialog';
|
||||
import api, { gql } from 'vj/utils/api';
|
||||
import delay from 'vj/utils/delay';
|
||||
|
||||
export default new NamedPage('home_security', () => {
|
||||
$(document).on('click', '[name="tfa_enable"]', async () => {
|
||||
const result = new ActionDialog({
|
||||
$body: tpl`
|
||||
<div class="typo">
|
||||
<p>${i18n('Please use your two factor authentication app to scan the qrcode below:')}</p>
|
||||
<div style="text-align: center">
|
||||
<canvas id="qrcode"></canvas>
|
||||
<p id="secret">${i18n('Click to show secret')}</p>
|
||||
</div>
|
||||
<label>${i18n('6-Digit Code')}
|
||||
<div class="textbox-container">
|
||||
<input class="textbox" type="text" name="tfa_code" data-autofocus autocomplete="off"></input>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
`,
|
||||
onDispatch(action) {
|
||||
if (action === 'ok' && $('[name="tfa_code"]').val() === null) {
|
||||
$('[name="tfa_code"]').focus();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}).open();
|
||||
const secret = b32.encode(crypto.getRandomValues(new Uint8Array(8)))
|
||||
.toString('utf8').replace(/=/g, '').toUpperCase();
|
||||
$('#secret').on('click', () => $('#secret').html(secret));
|
||||
const uri = `otpauth://totp/Hydro:${UserContext.uname}?secret=${secret}&issuer=Hydro`;
|
||||
const canvas = document.getElementById('qrcode');
|
||||
await QRCode.toCanvas(canvas, uri);
|
||||
const action = await result;
|
||||
if (action !== 'ok') return;
|
||||
try {
|
||||
await api(gql`
|
||||
user {
|
||||
TFA {
|
||||
enable(code: ${$('[name="tfa_code"]').val()}, secret: ${secret})
|
||||
}
|
||||
}
|
||||
`);
|
||||
Notification.success(i18n('Successfully enabled.'));
|
||||
await delay(2000);
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
Notification.error(e.message);
|
||||
}
|
||||
});
|
||||
$(document).on('click', '[name="tfa_enabled"]', async () => {
|
||||
const op = await new ActionDialog({
|
||||
$body: tpl`
|
||||
<div class="typo">
|
||||
<label>${i18n('6-Digit Code')}
|
||||
<div class="textbox-container">
|
||||
<input class="textbox" type="text" name="tfa_code" data-focus autocomplete="off"></input>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
`,
|
||||
onDispatch(action) {
|
||||
if (action === 'ok' && $('[name="tfa_code"]').val() === null) {
|
||||
$('[name="tfa_code"]').focus();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}).open();
|
||||
if (op !== 'ok') return;
|
||||
try {
|
||||
await api(gql`
|
||||
user {
|
||||
TFA {
|
||||
disable(code: ${$('[name="tfa_code"]').val()})
|
||||
}
|
||||
}
|
||||
`);
|
||||
Notification.success(i18n('Successfully disabled.'));
|
||||
await delay(2000);
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
Notification.error(e.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { AutoloadPage } from 'vj/misc/Page';
|
||||
import api, { gql } from 'vj/utils/api';
|
||||
|
||||
export default new AutoloadPage('user_login', (pagename) => {
|
||||
(pagename === 'user_login' ? $(document) : $('.dialog--signin__main')).on('blur', '[name="uname"]', async () => {
|
||||
const uname = $('[name="uname"]').val() as string;
|
||||
if (uname.length > 0) {
|
||||
const tfa = await api(gql`
|
||||
user(uname:${uname}){
|
||||
tfa
|
||||
}
|
||||
`, ['data', 'user', 'tfa']);
|
||||
if (tfa) $('#tfa_div').show();
|
||||
else $('#tfa_div').hide();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -87,6 +87,23 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row nojs--hide">
|
||||
<div class="columns">
|
||||
<div class="section">
|
||||
<div class="section__header">
|
||||
<h1 class="section__title" id="tfa" data-heading>{{ _('Two Factor Authentication') }}</h1>
|
||||
<div class="section__tools">
|
||||
{% if handler.user.tfa %}
|
||||
<button class="primary rounded button" name="tfa_enabled"><span class="icon icon-check"></span> {{ _('Enabled') }}</button>
|
||||
{% else %}
|
||||
<button class="primary rounded button" name="tfa_enable"><span class="icon icon-wrench"></span> {{ _('Enable') }}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="section__body"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="columns">
|
||||
<div class="section">
|
||||
|
||||
@@ -27,6 +27,12 @@
|
||||
<input name="password" type="password">
|
||||
</label>
|
||||
</div></div>
|
||||
<div class="row" style="display: none" id="tfa_div"><div class="columns">
|
||||
<label class="material textbox">
|
||||
{{ _('Two Factor Authentication Code') }}
|
||||
<input name="tfa" type="number">
|
||||
</label>
|
||||
</div></div>
|
||||
<div class="row"><div class="columns">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="rememberme">{{ _('Remember me') }}
|
||||
|
||||
@@ -17,6 +17,12 @@
|
||||
<input name="password" type="password">
|
||||
</label>
|
||||
</div></div>
|
||||
<div class="row" style="display: none" id="tfa_div"><div class="columns">
|
||||
<label class="inverse material textbox">
|
||||
{{ _('Two Factor Authentication Code') }}
|
||||
<input name="tfa" type="number">
|
||||
</label>
|
||||
</div></div>
|
||||
<div class="row"><div class="columns">
|
||||
<label class="inverse material checkbox">
|
||||
<input type="checkbox" name="rememberme">{{ _('Remember me') }}
|
||||
|
||||
Reference in new issue
Block a user