Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/MHSanaei/3x-ui.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/web/html
diff options
context:
space:
mode:
Diffstat (limited to 'web/html')
-rw-r--r--web/html/login.html20
-rw-r--r--web/html/modals/two_factor_modal.html118
-rw-r--r--web/html/settings.html64
-rw-r--r--web/html/settings/panel/security.html24
4 files changed, 163 insertions, 63 deletions
diff --git a/web/html/login.html b/web/html/login.html
index 9f292d84..515ea2b3 100644
--- a/web/html/login.html
+++ b/web/html/login.html
@@ -512,11 +512,11 @@
<a-icon slot="prefix" type="lock" :style="{ fontSize: '1rem' }"></a-icon>
</a-input-password>
</a-form-item>
- <a-form-item v-if="secretEnable">
- <a-input-password autocomplete="secret" name="secret" v-model.trim="user.loginSecret"
- placeholder='{{ i18n "secretToken" }}' @keydown.enter.native="login">
+ <a-form-item v-if="twoFactorEnable">
+ <a-input autocomplete="totp" name="twoFactorCode" v-model.trim="user.twoFactorCode"
+ placeholder='{{ i18n "twoFactorCode" }}' @keydown.enter.native="login">
<a-icon slot="prefix" type="key" :style="{ fontSize: '1rem' }"></a-icon>
- </a-input-password>
+ </a-input>
</a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
@@ -549,14 +549,14 @@
user: {
username: "",
password: "",
- loginSecret: ""
+ twoFactorCode: ""
},
- secretEnable: false,
+ twoFactorEnable: false,
lang: ""
},
async mounted() {
this.lang = LanguageManager.getLanguage();
- this.secretEnable = await this.getSecretStatus();
+ this.twoFactorEnable = await this.getTwoFactorEnable();
},
methods: {
async login() {
@@ -567,12 +567,12 @@
location.href = basePath + 'panel/';
}
},
- async getSecretStatus() {
+ async getTwoFactorEnable() {
this.loading = true;
- const msg = await HttpUtil.post('/getSecretStatus');
+ const msg = await HttpUtil.post('/getTwoFactorEnable');
this.loading = false;
if (msg.success) {
- this.secretEnable = msg.obj;
+ this.twoFactorEnable = msg.obj;
return msg.obj;
}
},
diff --git a/web/html/modals/two_factor_modal.html b/web/html/modals/two_factor_modal.html
new file mode 100644
index 00000000..d2f8c442
--- /dev/null
+++ b/web/html/modals/two_factor_modal.html
@@ -0,0 +1,118 @@
+{{define "modals/twoFactorModal"}}
+<a-modal id="two-factor-modal" v-model="twoFactorModal.visible" :title="twoFactorModal.title" :closable="true"
+ :class="themeSwitcher.currentTheme">
+ <template v-if="twoFactorModal.type === 'set'">
+ <p>{{ i18n "pages.settings.security.twoFactorModalSteps" }}</p>
+ <a-divider></a-divider>
+ <p>{{ i18n "pages.settings.security.twoFactorModalFirstStep" }}</p>
+ <div :style="{ display: 'flex', alignItems: 'center', flexDirection: 'column', gap: '12px' }">
+ <div
+ :style="{ border: '1px solid', borderRadius: '1rem', borderColor: themeSwitcher.isDarkTheme ? 'var(--dark-color-surface-300)' : '#d9d9d9', padding: 0 }">
+ <img :src="twoFactorModal.qrImage"
+ :style="{ filter: themeSwitcher.isDarkTheme ? 'invert(1)' : 'none'}"
+ :alt="twoFactorModal.token">
+ </div>
+ <span :style="{ fontSize: '12px', fontFamily: 'monospace' }">[[ twoFactorModal.token ]]</span>
+ </div>
+ <a-divider></a-divider>
+ <p>{{ i18n "pages.settings.security.twoFactorModalSecondStep" }}</p>
+ <a-input v-model.trim="twoFactorModal.enteredCode" :style="{ width: '100%' }"></a-input>
+ </template>
+ <template v-if="twoFactorModal.type === 'remove'">
+ <p>{{ i18n "pages.settings.security.twoFactorModalRemoveStep" }}</p>
+ <a-input v-model.trim="twoFactorModal.enteredCode" :style="{ width: '100%' }"></a-input>
+ </template>
+ <template slot="footer">
+ <a-button @click="twoFactorModal.cancel">
+ <span>{{ i18n "cancel" }}</span>
+ </a-button>
+ <a-button type="primary" :disabled="twoFactorModal.enteredCode.length < 6" @click="twoFactorModal.ok">
+ <span>{{ i18n "confirm" }}</span>
+ </a-button>
+ </template>
+</a-modal>
+
+<script>
+ const twoFactorModal = {
+ title: '',
+ fileName: '',
+ token: '',
+ enteredCode: '',
+ visible: false,
+ type: 'set',
+ confirm: null,
+ totpObject: null,
+ qrImage: "",
+ ok() {
+ if (twoFactorModal.totpObject.generate() === twoFactorModal.enteredCode) {
+ ObjectUtil.execute(twoFactorModal.confirm, true)
+
+ twoFactorModal.close()
+
+ switch (twoFactorModal.type) {
+ case 'set':
+ Vue.prototype.$message['success']('{{ i18n "pages.settings.security.twoFactorModalSetSuccess" }}')
+ break;
+ case 'remove':
+ Vue.prototype.$message['success']('{{ i18n "pages.settings.security.twoFactorModalDeleteSuccess" }}')
+ break;
+ default:
+ break;
+ }
+ } else {
+ Vue.prototype.$message['error']('{{ i18n "pages.settings.security.twoFactorModalError" }}')
+ }
+ },
+ cancel() {
+ ObjectUtil.execute(twoFactorModal.confirm, false)
+
+ twoFactorModal.close()
+ },
+ show: function ({
+ title = '',
+ token = '',
+ type = 'set',
+ confirm = (success) => { }
+ }) {
+ this.title = title;
+ this.token = token;
+ this.visible = true;
+ this.confirm = confirm;
+ this.type = type;
+
+ this.totpObject = new OTPAuth.TOTP({
+ issuer: "3x-ui",
+ label: "Administrator",
+ algorithm: "SHA1",
+ digits: 6,
+ period: 30,
+ secret: twoFactorModal.token,
+ });
+
+ if (type === 'set') {
+ this.qrImage = new QRious({
+ size: 150,
+ value: twoFactorModal.totpObject.toString(),
+ background: 'white',
+ backgroundAlpha: 0,
+ foreground: 'black',
+ padding: 12,
+ level: 'L'
+ }).toDataURL()
+ }
+ },
+ close: function () {
+ twoFactorModal.enteredCode = "";
+ twoFactorModal.visible = false;
+ },
+ };
+
+ const twoFactorModalApp = new Vue({
+ delimiters: ['[[', ']]'],
+ el: '#two-factor-modal',
+ data: {
+ twoFactorModal: twoFactorModal,
+ },
+ });
+</script>
+{{end}} \ No newline at end of file
diff --git a/web/html/settings.html b/web/html/settings.html
index 82e636d9..dec40de7 100644
--- a/web/html/settings.html
+++ b/web/html/settings.html
@@ -122,10 +122,13 @@
</a-layout>
</a-layout>
{{template "js" .}}
+<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
+<script src="{{ .base_path }}assets/otpauth/otpauth.umd.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/setting.js?{{ .cur_ver }}"></script>
{{template "component/aSidebar" .}}
{{template "component/aThemeSwitch" .}}
{{template "component/aSettingListItem" .}}
+{{template "modals/twoFactorModal"}}
<script>
const app = new Vue({
delimiters: ['[[', ']]'],
@@ -133,7 +136,6 @@
data: {
themeSwitcher,
spinning: false,
- changeSecret: false,
oldAllSetting: new AllSetting(),
allSetting: new AllSetting(),
saveBtnDisable: true,
@@ -258,7 +260,6 @@
app.changeRemarkSample();
this.saveBtnDisable = true;
}
- await this.fetchUserSecret();
},
async updateAllSetting() {
this.loading(true);
@@ -302,38 +303,34 @@
window.location.replace(url);
}
},
- async fetchUserSecret() {
- this.loading(true);
- const userMessage = await HttpUtil.post("/panel/setting/getUserSecret", this.user);
- if (userMessage.success) {
- this.user = userMessage.obj;
- }
- this.loading(false);
- },
- async updateSecret() {
- this.loading(true);
- const msg = await HttpUtil.post("/panel/setting/updateUserSecret", this.user);
- if (msg && msg.obj) {
- this.user = msg.obj;
- }
- this.loading(false);
- await this.updateAllSetting();
- },
- async getNewSecret() {
- if (!this.changeSecret) {
- this.changeSecret = true;
- this.user.loginSecret = '';
- const newSecret = RandomUtil.randomSeq(64);
- await PromiseUtil.sleep(1000);
- this.user.loginSecret = newSecret;
- this.changeSecret = false;
- }
- },
- async toggleToken(value) {
- if (value) {
- await this.getNewSecret();
+ toggleTwoFactor(newValue) {
+ if (newValue) {
+ const newTwoFactorToken = RandomUtil.randomBase32String()
+
+ twoFactorModal.show({
+ title: '{{ i18n "pages.settings.security.twoFactorModalSetTitle" }}',
+ token: newTwoFactorToken,
+ type: 'set',
+ confirm: (success) => {
+ if (success) {
+ this.allSetting.twoFactorToken = newTwoFactorToken
+ }
+
+ this.allSetting.twoFactorEnable = success
+ }
+ })
} else {
- this.user.loginSecret = "";
+ twoFactorModal.show({
+ title: '{{ i18n "pages.settings.security.twoFactorModalDeleteTitle" }}',
+ token: this.allSetting.twoFactorToken,
+ type: 'remove',
+ confirm: (success) => {
+ if (success) {
+ this.allSetting.twoFactorEnable = false
+ this.allSetting.twoFactorToken = ""
+ }
+ }
+ })
}
},
addNoise() {
@@ -526,6 +523,7 @@
},
async mounted() {
await this.getAllSetting();
+
while (true) {
await PromiseUtil.sleep(1000);
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
diff --git a/web/html/settings/panel/security.html b/web/html/settings/panel/security.html
index 48e7796f..2a570faa 100644
--- a/web/html/settings/panel/security.html
+++ b/web/html/settings/panel/security.html
@@ -31,30 +31,14 @@
</a-space>
</a-list-item>
</a-collapse-panel>
- <a-collapse-panel key="2" header='{{ i18n "pages.settings.security.secret"}}'>
+ <a-collapse-panel key="2" header='{{ i18n "pages.settings.security.twoFactor" }}'>
<a-setting-list-item paddings="small">
- <template #title>{{ i18n "pages.settings.security.loginSecurity" }}</template>
- <template #description>{{ i18n "pages.settings.security.loginSecurityDesc" }}</template>
+ <template #title>{{ i18n "pages.settings.security.twoFactorEnable" }}</template>
+ <template #description>{{ i18n "pages.settings.security.twoFactorEnableDesc" }}</template>
<template #control>
- <a-switch @change="toggleToken(allSetting.secretEnable)" v-model="allSetting.secretEnable"></a-switch>
- <a-icon :style="{ marginLeft: '1rem' }" v-if="allSetting.secretEnable" :spin="this.changeSecret" type="sync"
- @click="getNewSecret"></a-icon>
+ <a-switch @click="toggleTwoFactor" :checked="allSetting.twoFactorEnable"></a-switch>
</template>
</a-setting-list-item>
- <a-setting-list-item paddings="small">
- <template #title>{{ i18n "pages.settings.security.secretToken" }}</template>
- <template #description>{{ i18n "pages.settings.security.secretTokenDesc" }}</template>
- <template #control>
- <a-textarea type="text" :disabled="!allSetting.secretEnable" v-model="user.loginSecret"></a-textarea>
- </template>
- </a-setting-list-item>
- <a-list-item>
- <a-space direction="horizontal" :style="{ padding: '0 20px' }">
- <a-button type="primary" :loading="this.changeSecret" @click="updateSecret">
- <span>{{ i18n "confirm"}}</span>
- </a-button>
- </a-space>
- </a-list-item>
</a-collapse-panel>
</a-collapse>
{{end}} \ No newline at end of file