mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-07 05:34:17 +00:00
feat: add registration tab with Cloudflare Turnstile support
Add a register tab on the login page with username, password, confirm password fields and Cloudflare Turnstile widget. The site key is configurable via x-ui.json and exposed through a public endpoint.
This commit is contained in:
parent
82d93da36e
commit
5103d57879
6 changed files with 168 additions and 37 deletions
|
|
@ -44,6 +44,7 @@ func (a *IndexController) initRouter(g *gin.RouterGroup) {
|
|||
|
||||
g.POST("/login", a.login)
|
||||
g.POST("/getTwoFactorEnable", a.getTwoFactorEnable)
|
||||
g.POST("/getTurnstileSiteKey", a.getTurnstileSiteKey)
|
||||
}
|
||||
|
||||
// index handles the root route, redirecting logged-in users to the panel or showing the login page.
|
||||
|
|
@ -131,3 +132,11 @@ func (a *IndexController) getTwoFactorEnable(c *gin.Context) {
|
|||
jsonObj(c, status, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// getTurnstileSiteKey returns the Cloudflare Turnstile site key for the registration form.
|
||||
func (a *IndexController) getTurnstileSiteKey(c *gin.Context) {
|
||||
siteKey, err := a.settingService.GetTurnstileSiteKey()
|
||||
if err == nil {
|
||||
jsonObj(c, siteKey, nil)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,7 +103,9 @@ type AllSetting struct {
|
|||
LdapDefaultTotalGB int `json:"ldapDefaultTotalGB" form:"ldapDefaultTotalGB"`
|
||||
LdapDefaultExpiryDays int `json:"ldapDefaultExpiryDays" form:"ldapDefaultExpiryDays"`
|
||||
LdapDefaultLimitIP int `json:"ldapDefaultLimitIP" form:"ldapDefaultLimitIP"`
|
||||
// JSON subscription routing rules
|
||||
|
||||
// Registration settings
|
||||
TurnstileSiteKey string `json:"turnstileSiteKey" form:"turnstileSiteKey"`
|
||||
}
|
||||
|
||||
// CheckValid validates all settings in the AllSetting struct, checking IP addresses, ports, SSL certificates, and other configuration values.
|
||||
|
|
|
|||
|
|
@ -59,39 +59,83 @@
|
|||
</a-row>
|
||||
<a-row type="flex" justify="center">
|
||||
<a-col span="24">
|
||||
<a-form @submit.prevent="login">
|
||||
<a-space direction="vertical" size="middle">
|
||||
<a-form-item>
|
||||
<a-input autocomplete="username" name="username" v-model.trim="user.username"
|
||||
placeholder='{{ i18n "username" }}' autofocus required>
|
||||
<a-icon slot="prefix" type="user" class="fs-1rem"></a-icon>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-input-password autocomplete="current-password" name="password" v-model.trim="user.password"
|
||||
placeholder='{{ i18n "password" }}' required>
|
||||
<a-icon slot="prefix" type="lock" class="fs-1rem"></a-icon>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="twoFactorEnable">
|
||||
<a-input autocomplete="one-time-code" name="twoFactorCode" v-model.trim="user.twoFactorCode"
|
||||
placeholder='{{ i18n "twoFactorCode" }}' required>
|
||||
<a-icon slot="prefix" type="key" class="fs-1rem"></a-icon>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-row justify="center" class="centered">
|
||||
<div class="wave-btn-bg wave-btn-bg-cl h-50px mt-1rem"
|
||||
:style="loadingStates.spinning ? 'width: 52px' : 'display: inline-block'">
|
||||
<a-button class="ant-btn-primary-login" type="primary" :loading="loadingStates.spinning"
|
||||
:icon="loadingStates.spinning ? 'poweroff' : undefined" html-type="submit">
|
||||
[[ loadingStates.spinning ? '' : '{{ i18n "login" }}' ]]
|
||||
</a-button>
|
||||
</div>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-space>
|
||||
</a-form>
|
||||
<a-tabs :active-key="activeTab" @change="switchTab" centered>
|
||||
<a-tab-pane key="login" tab='{{ i18n "pages.login.loginTab" }}'>
|
||||
<a-form @submit.prevent="doLogin">
|
||||
<a-space direction="vertical" size="middle">
|
||||
<a-form-item>
|
||||
<a-input autocomplete="username" name="username" v-model.trim="user.username"
|
||||
placeholder='{{ i18n "username" }}' autofocus required>
|
||||
<a-icon slot="prefix" type="user" class="fs-1rem"></a-icon>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-input-password autocomplete="current-password" name="password" v-model.trim="user.password"
|
||||
placeholder='{{ i18n "password" }}' required>
|
||||
<a-icon slot="prefix" type="lock" class="fs-1rem"></a-icon>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="twoFactorEnable">
|
||||
<a-input autocomplete="one-time-code" name="twoFactorCode" v-model.trim="user.twoFactorCode"
|
||||
placeholder='{{ i18n "twoFactorCode" }}' required>
|
||||
<a-icon slot="prefix" type="key" class="fs-1rem"></a-icon>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-row justify="center" class="centered">
|
||||
<div class="wave-btn-bg wave-btn-bg-cl h-50px mt-1rem"
|
||||
:style="loadingStates.spinning ? 'width: 52px' : 'display: inline-block'">
|
||||
<a-button class="ant-btn-primary-login" type="primary" :loading="loadingStates.spinning"
|
||||
:icon="loadingStates.spinning ? 'poweroff' : undefined" html-type="submit">
|
||||
[[ loadingStates.spinning ? '' : '{{ i18n "login" }}' ]]
|
||||
</a-button>
|
||||
</div>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-space>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="register" tab='{{ i18n "pages.login.registerTab" }}'>
|
||||
<a-form @submit.prevent="doRegister">
|
||||
<a-space direction="vertical" size="middle">
|
||||
<a-form-item>
|
||||
<a-input autocomplete="username" name="reg-username" v-model.trim="regUser.username"
|
||||
placeholder='{{ i18n "username" }}' required>
|
||||
<a-icon slot="prefix" type="user" class="fs-1rem"></a-icon>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-input-password autocomplete="new-password" name="reg-password" v-model.trim="regUser.password"
|
||||
placeholder='{{ i18n "password" }}' required>
|
||||
<a-icon slot="prefix" type="lock" class="fs-1rem"></a-icon>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-input-password autocomplete="new-password" name="reg-confirm-password" v-model.trim="regUser.confirmPassword"
|
||||
placeholder='{{ i18n "pages.login.confirmPassword" }}' required>
|
||||
<a-icon slot="prefix" type="lock" class="fs-1rem"></a-icon>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<div class="cf-turnstile-wrapper">
|
||||
<div class="cf-turnstile" :data-sitekey="turnstileSiteKey" data-callback="onTurnstileCallback"></div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-row justify="center" class="centered">
|
||||
<div class="wave-btn-bg wave-btn-bg-cl h-50px mt-1rem"
|
||||
:style="loadingStates.registerSpinning ? 'width: 52px' : 'display: inline-block'">
|
||||
<a-button class="ant-btn-primary-login" type="primary" :loading="loadingStates.registerSpinning"
|
||||
:icon="loadingStates.registerSpinning ? 'poweroff' : undefined" html-type="submit">
|
||||
[[ loadingStates.registerSpinning ? '' : '{{ i18n "pages.login.registerTab" }}' ]]
|
||||
</a-button>
|
||||
</div>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-space>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
|
@ -103,23 +147,39 @@
|
|||
{{template "page/body_scripts" .}}
|
||||
{{template "component/aThemeSwitch" .}}
|
||||
<script>
|
||||
var turnstileToken = '';
|
||||
|
||||
function onTurnstileCallback(token) {
|
||||
turnstileToken = token;
|
||||
}
|
||||
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
themeSwitcher,
|
||||
loadingStates: { fetched: false, spinning: false },
|
||||
loadingStates: { fetched: false, spinning: false, registerSpinning: false },
|
||||
activeTab: 'login',
|
||||
user: { username: "", password: "", twoFactorCode: "" },
|
||||
regUser: { username: "", password: "", confirmPassword: "" },
|
||||
twoFactorEnable: false,
|
||||
lang: "",
|
||||
animationStarted: false
|
||||
animationStarted: false,
|
||||
turnstileSiteKey: '',
|
||||
},
|
||||
async mounted() {
|
||||
this.lang = LanguageManager.getLanguage();
|
||||
this.twoFactorEnable = await this.getTwoFactorEnable();
|
||||
this.turnstileSiteKey = await this.getTurnstileSiteKey();
|
||||
if (this.turnstileSiteKey) {
|
||||
this.loadTurnstileScript();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async login() {
|
||||
switchTab(key) {
|
||||
this.activeTab = key;
|
||||
},
|
||||
async doLogin() {
|
||||
this.loadingStates.spinning = true;
|
||||
const msg = await HttpUtil.post('/login', this.user);
|
||||
if (msg.success) {
|
||||
|
|
@ -127,6 +187,47 @@
|
|||
}
|
||||
this.loadingStates.spinning = false;
|
||||
},
|
||||
async doRegister() {
|
||||
if (this.regUser.password !== this.regUser.confirmPassword) {
|
||||
this.$message.error('{{ i18n "pages.login.passwordMismatch" }}');
|
||||
return;
|
||||
}
|
||||
if (!turnstileToken) {
|
||||
this.$message.error('{{ i18n "pages.login.turnstileRequired" }}');
|
||||
return;
|
||||
}
|
||||
this.loadingStates.registerSpinning = true;
|
||||
const msg = await HttpUtil.post('/register', {
|
||||
username: this.regUser.username,
|
||||
password: this.regUser.password,
|
||||
turnstileToken: turnstileToken,
|
||||
});
|
||||
if (msg.success) {
|
||||
this.$message.success('{{ i18n "pages.login.toasts.successRegister" }}');
|
||||
this.activeTab = 'login';
|
||||
this.user.username = this.regUser.username;
|
||||
this.regUser = { username: "", password: "", confirmPassword: "" };
|
||||
turnstileToken = '';
|
||||
if (window.turnstile) {
|
||||
turnstile.reset('.cf-turnstile');
|
||||
}
|
||||
}
|
||||
this.loadingStates.registerSpinning = false;
|
||||
},
|
||||
loadTurnstileScript() {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js';
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
document.head.appendChild(script);
|
||||
},
|
||||
async getTurnstileSiteKey() {
|
||||
const msg = await HttpUtil.post('/getTurnstileSiteKey');
|
||||
if (msg.success) {
|
||||
return msg.obj;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
async getTwoFactorEnable() {
|
||||
const msg = await HttpUtil.post('/getTwoFactorEnable');
|
||||
if (msg.success) {
|
||||
|
|
|
|||
|
|
@ -104,6 +104,9 @@ var defaultValueMap = map[string]string{
|
|||
"ldapDefaultTotalGB": "0",
|
||||
"ldapDefaultExpiryDays": "0",
|
||||
"ldapDefaultLimitIP": "0",
|
||||
|
||||
// Registration settings
|
||||
"turnstileSiteKey": "",
|
||||
}
|
||||
|
||||
// loadSettings reads the JSON settings file into a map.
|
||||
|
|
@ -746,6 +749,10 @@ func (s *SettingService) GetLdapDefaultLimitIP() (int, error) {
|
|||
return s.getInt("ldapDefaultLimitIP")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetTurnstileSiteKey() (string, error) {
|
||||
return s.getString("turnstileSiteKey")
|
||||
}
|
||||
|
||||
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
|
||||
if err := allSetting.CheckValid(); err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -101,6 +101,11 @@
|
|||
"hello" = "Hello"
|
||||
"title" = "Welcome"
|
||||
"loginAgain" = "Your session has expired, please log in again"
|
||||
"loginTab" = "Login"
|
||||
"registerTab" = "Register"
|
||||
"confirmPassword" = "Confirm Password"
|
||||
"passwordMismatch" = "Passwords do not match"
|
||||
"turnstileRequired" = "Please complete the verification"
|
||||
|
||||
[pages.login.toasts]
|
||||
"invalidFormData" = "The Input data format is invalid."
|
||||
|
|
@ -108,6 +113,7 @@
|
|||
"emptyPassword" = "Password is required"
|
||||
"wrongUsernameOrPassword" = "Invalid username or password or two-factor code."
|
||||
"successLogin" = " You have successfully logged into your account."
|
||||
"successRegister" = "Registration successful, please log in."
|
||||
|
||||
[pages.index]
|
||||
"title" = "Overview"
|
||||
|
|
|
|||
|
|
@ -101,6 +101,11 @@
|
|||
"hello" = "你好"
|
||||
"title" = "欢迎"
|
||||
"loginAgain" = "登录时效已过,请重新登录"
|
||||
"loginTab" = "登录"
|
||||
"registerTab" = "注册"
|
||||
"confirmPassword" = "确认密码"
|
||||
"passwordMismatch" = "两次输入的密码不一致"
|
||||
"turnstileRequired" = "请完成验证"
|
||||
|
||||
[pages.login.toasts]
|
||||
"invalidFormData" = "数据格式错误"
|
||||
|
|
@ -108,6 +113,7 @@
|
|||
"emptyPassword" = "请输入密码"
|
||||
"wrongUsernameOrPassword" = "用户名、密码或双重验证码无效。"
|
||||
"successLogin" = "您已成功登录您的账户。"
|
||||
"successRegister" = "注册成功,请登录。"
|
||||
|
||||
[pages.index]
|
||||
"title" = "系统状态"
|
||||
|
|
|
|||
Loading…
Reference in a new issue