mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 20:54:14 +00:00
Revert "feat(auth): block panel with default admin/admin credentials and guide credential change"
This reverts commit 56ce6073ce.
This commit is contained in:
parent
11db4f0d29
commit
d028d390eb
10 changed files with 47 additions and 234 deletions
|
|
@ -52,9 +52,7 @@ async function login() {
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
const msg = await HttpUtil.post('/login', user);
|
const msg = await HttpUtil.post('/login', user);
|
||||||
if (msg.success) {
|
if (msg.success) window.location.href = basePath + 'panel/';
|
||||||
window.location.href = basePath + (msg.obj?.mustChangeCredentials ? 'panel/settings' : 'panel/');
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false;
|
submitting.value = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,9 @@ const tfa = reactive({
|
||||||
description: '',
|
description: '',
|
||||||
token: '',
|
token: '',
|
||||||
type: 'set',
|
type: 'set',
|
||||||
// resolveConfirm is called by the modal's @confirm with the success bool
|
// resolveConfirm is called by the modal's @confirm with the success bool;
|
||||||
// and, for redacted-token confirm flows, the code entered by the user.
|
|
||||||
// it then routes the value back to whichever flow opened the modal.
|
// it then routes the value back to whichever flow opened the modal.
|
||||||
resolveConfirm: (_success, _code) => { },
|
resolveConfirm: (_success) => { },
|
||||||
});
|
});
|
||||||
|
|
||||||
function openTfa({ title, description = '', token = '', type, onConfirm }) {
|
function openTfa({ title, description = '', token = '', type, onConfirm }) {
|
||||||
|
|
@ -36,8 +35,8 @@ function openTfa({ title, description = '', token = '', type, onConfirm }) {
|
||||||
tfa.open = true;
|
tfa.open = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTfaConfirm(success, code = '') {
|
function onTfaConfirm(success) {
|
||||||
tfa.resolveConfirm(success, code);
|
tfa.resolveConfirm(success);
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = reactive({
|
const user = reactive({
|
||||||
|
|
@ -53,23 +52,16 @@ async function sendUpdateUser() {
|
||||||
try {
|
try {
|
||||||
const msg = await HttpUtil.post('/panel/setting/updateUser', user);
|
const msg = await HttpUtil.post('/panel/setting/updateUser', user);
|
||||||
if (msg?.success) {
|
if (msg?.success) {
|
||||||
await logoutAndReturn();
|
// Force re-login at the standard logout path; basePath is handled
|
||||||
|
// by the Go router so a relative redirect is correct here.
|
||||||
|
const basePath = window.X_UI_BASE_PATH || '';
|
||||||
|
window.location.replace(`${basePath}logout`);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
updating.value = false;
|
updating.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logoutAndReturn() {
|
|
||||||
await HttpUtil.post('/logout');
|
|
||||||
window.location.replace(window.X_UI_BASE_PATH || '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function verifyTwoFactor(code) {
|
|
||||||
const msg = await HttpUtil.post('/panel/setting/verifyTwoFactor', { code });
|
|
||||||
return !!(msg?.success && msg.obj === true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateUser() {
|
function updateUser() {
|
||||||
if (props.allSetting.twoFactorEnable) {
|
if (props.allSetting.twoFactorEnable) {
|
||||||
openTfa({
|
openTfa({
|
||||||
|
|
@ -77,11 +69,7 @@ function updateUser() {
|
||||||
description: t('pages.settings.security.twoFactorModalChangeCredentialsStep'),
|
description: t('pages.settings.security.twoFactorModalChangeCredentialsStep'),
|
||||||
token: props.allSetting.twoFactorToken,
|
token: props.allSetting.twoFactorToken,
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
onConfirm: async (ok, code) => {
|
onConfirm: (ok) => { if (ok) sendUpdateUser(); },
|
||||||
if (!ok) return;
|
|
||||||
const verified = props.allSetting.twoFactorToken ? ok : await verifyTwoFactor(code);
|
|
||||||
if (verified) sendUpdateUser();
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
sendUpdateUser();
|
sendUpdateUser();
|
||||||
|
|
@ -100,10 +88,7 @@ async function loadApiToken() {
|
||||||
apiTokenLoading.value = true;
|
apiTokenLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const msg = await HttpUtil.get('/panel/setting/getApiToken');
|
const msg = await HttpUtil.get('/panel/setting/getApiToken');
|
||||||
if (msg?.success) {
|
if (msg?.success) apiToken.value = msg.obj || '';
|
||||||
apiToken.value = msg.obj || '';
|
|
||||||
props.allSetting.hasApiToken = !!apiToken.value;
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
apiTokenLoading.value = false;
|
apiTokenLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
@ -139,7 +124,6 @@ function regenerateApiToken() {
|
||||||
const msg = await HttpUtil.post('/panel/setting/regenerateApiToken');
|
const msg = await HttpUtil.post('/panel/setting/regenerateApiToken');
|
||||||
if (msg?.success) {
|
if (msg?.success) {
|
||||||
apiToken.value = msg.obj || '';
|
apiToken.value = msg.obj || '';
|
||||||
props.allSetting.hasApiToken = !!apiToken.value;
|
|
||||||
message.success(t('success'));
|
message.success(t('success'));
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -163,7 +147,6 @@ function toggleTwoFactor() {
|
||||||
if (ok) {
|
if (ok) {
|
||||||
message.success(t('pages.settings.security.twoFactorModalSetSuccess'));
|
message.success(t('pages.settings.security.twoFactorModalSetSuccess'));
|
||||||
props.allSetting.twoFactorToken = newToken;
|
props.allSetting.twoFactorToken = newToken;
|
||||||
props.allSetting.hasTwoFactorToken = true;
|
|
||||||
}
|
}
|
||||||
props.allSetting.twoFactorEnable = ok;
|
props.allSetting.twoFactorEnable = ok;
|
||||||
},
|
},
|
||||||
|
|
@ -174,14 +157,11 @@ function toggleTwoFactor() {
|
||||||
description: t('pages.settings.security.twoFactorModalRemoveStep'),
|
description: t('pages.settings.security.twoFactorModalRemoveStep'),
|
||||||
token: props.allSetting.twoFactorToken,
|
token: props.allSetting.twoFactorToken,
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
onConfirm: async (ok, code) => {
|
onConfirm: (ok) => {
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
const verified = props.allSetting.twoFactorToken ? ok : await verifyTwoFactor(code);
|
|
||||||
if (!verified) return;
|
|
||||||
message.success(t('pages.settings.security.twoFactorModalDeleteSuccess'));
|
message.success(t('pages.settings.security.twoFactorModalDeleteSuccess'));
|
||||||
props.allSetting.twoFactorEnable = false;
|
props.allSetting.twoFactorEnable = false;
|
||||||
props.allSetting.twoFactorToken = '';
|
props.allSetting.twoFactorToken = '';
|
||||||
props.allSetting.hasTwoFactorToken = false;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,6 @@ const { t } = useI18n();
|
||||||
const { fetched, spinning, saveDisabled, allSetting, saveAll } = useAllSetting();
|
const { fetched, spinning, saveDisabled, allSetting, saveAll } = useAllSetting();
|
||||||
const { isMobile } = useMediaQuery();
|
const { isMobile } = useMediaQuery();
|
||||||
|
|
||||||
const mustChangeCredentials = window.X_UI_MUST_CHANGE_CREDENTIALS === true
|
|
||||||
const activeTab = ref(mustChangeCredentials ? '2' : '1')
|
|
||||||
|
|
||||||
const basePath = window.X_UI_BASE_PATH || '';
|
const basePath = window.X_UI_BASE_PATH || '';
|
||||||
const requestUri = window.location.pathname;
|
const requestUri = window.location.pathname;
|
||||||
|
|
||||||
|
|
@ -120,68 +117,39 @@ function restartPanel() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const securityChecklist = computed(() => {
|
// Conf alerts mirror the legacy banner — pure derivation off allSetting.
|
||||||
const segs = window.location.pathname.split('/').length < 4;
|
const confAlerts = computed(() => {
|
||||||
const out = []
|
const out = [];
|
||||||
if (mustChangeCredentials) {
|
if (window.location.protocol !== 'https:') {
|
||||||
out.push({
|
out.push('Panel is served over plain HTTP — set up TLS for production.');
|
||||||
label: 'Default credentials',
|
}
|
||||||
ok: false,
|
if (allSetting.webPort === 2053) {
|
||||||
action: 'Change the default admin/admin credentials in Authentication settings.',
|
out.push('Default port 2053 is well-known — change it to a random port.');
|
||||||
})
|
}
|
||||||
|
const segs = window.location.pathname.split('/').length < 4;
|
||||||
|
if (segs && allSetting.webBasePath === '/') {
|
||||||
|
out.push('Default base path "/" is well-known — change it to a random path.');
|
||||||
}
|
}
|
||||||
out.push(
|
|
||||||
{
|
|
||||||
label: 'TLS',
|
|
||||||
ok: window.location.protocol === 'https:',
|
|
||||||
action: 'Set certificate and key paths, then restart.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Base path',
|
|
||||||
ok: !(segs && allSetting.webBasePath === '/'),
|
|
||||||
action: 'Change the panel URL path from "/".',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Panel port',
|
|
||||||
ok: allSetting.webPort !== 2053,
|
|
||||||
action: 'Use a non-default listening port.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Two-factor authentication',
|
|
||||||
ok: allSetting.twoFactorEnable && allSetting.hasTwoFactorToken,
|
|
||||||
action: 'Enable 2FA in Security.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'API token',
|
|
||||||
ok: allSetting.hasApiToken,
|
|
||||||
action: 'Generate or rotate the API token in Security.',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (allSetting.subEnable) {
|
if (allSetting.subEnable) {
|
||||||
let subPath = allSetting.subPath;
|
let subPath = allSetting.subPath;
|
||||||
if (allSetting.subURI) {
|
if (allSetting.subURI) {
|
||||||
try { subPath = new URL(allSetting.subURI).pathname; } catch (_e) { }
|
try { subPath = new URL(allSetting.subURI).pathname; } catch (_e) { }
|
||||||
}
|
}
|
||||||
out.push({
|
if (subPath === '/sub/') {
|
||||||
label: 'Subscription path',
|
out.push('Default subscription path "/sub/" is well-known — change it.');
|
||||||
ok: subPath !== '/sub/',
|
}
|
||||||
action: 'Change the default subscription path.',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (allSetting.subJsonEnable) {
|
if (allSetting.subJsonEnable) {
|
||||||
let p = allSetting.subJsonPath;
|
let p = allSetting.subJsonPath;
|
||||||
if (allSetting.subJsonURI) {
|
if (allSetting.subJsonURI) {
|
||||||
try { p = new URL(allSetting.subJsonURI).pathname; } catch (_e) { }
|
try { p = new URL(allSetting.subJsonURI).pathname; } catch (_e) { }
|
||||||
}
|
}
|
||||||
out.push({
|
if (p === '/json/') {
|
||||||
label: 'JSON subscription path',
|
out.push('Default JSON subscription path "/json/" is well-known — change it.');
|
||||||
ok: p !== '/json/',
|
}
|
||||||
action: 'Change the default JSON subscription path.',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
const hasSecurityGaps = computed(() => securityChecklist.value.some((item) => !item.ok));
|
|
||||||
|
|
||||||
const alertVisible = ref(true);
|
const alertVisible = ref(true);
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -197,31 +165,14 @@ const alertVisible = ref(true);
|
||||||
<div v-if="!fetched" class="loading-spacer" />
|
<div v-if="!fetched" class="loading-spacer" />
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<a-alert
|
<a-alert v-if="confAlerts.length > 0 && alertVisible" type="error" show-icon closable class="conf-alert"
|
||||||
v-if="mustChangeCredentials"
|
|
||||||
type="error"
|
|
||||||
show-icon
|
|
||||||
banner
|
|
||||||
message="Change your default admin credentials to unlock the panel"
|
|
||||||
description="All other panel sections are blocked until you set a unique username and password in the Authentication tab."
|
|
||||||
class="conf-alert"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<a-alert v-if="hasSecurityGaps && alertVisible" type="error" show-icon closable class="conf-alert"
|
|
||||||
@close="alertVisible = false">
|
@close="alertVisible = false">
|
||||||
<template #message>Security posture checklist</template>
|
<template #message>Security warnings</template>
|
||||||
<template #description>
|
<template #description>
|
||||||
<a-list size="small" :data-source="securityChecklist">
|
<b>Your panel may be exposed:</b>
|
||||||
<template #renderItem="{ item }">
|
<ul>
|
||||||
<a-list-item class="checklist-item">
|
<li v-for="(msg, i) in confAlerts" :key="i">{{ msg }}</li>
|
||||||
<a-space :size="8" wrap>
|
</ul>
|
||||||
<a-tag :color="item.ok ? 'green' : 'red'">{{ item.ok ? 'OK' : 'Action' }}</a-tag>
|
|
||||||
<strong>{{ item.label }}</strong>
|
|
||||||
<span>{{ item.ok ? 'Configured' : item.action }}</span>
|
|
||||||
</a-space>
|
|
||||||
</a-list-item>
|
|
||||||
</template>
|
|
||||||
</a-list>
|
|
||||||
</template>
|
</template>
|
||||||
</a-alert>
|
</a-alert>
|
||||||
|
|
||||||
|
|
@ -248,7 +199,7 @@ const alertVisible = ref(true);
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|
||||||
<a-col :span="24">
|
<a-col :span="24">
|
||||||
<a-tabs v-model:activeKey="activeTab">
|
<a-tabs default-active-key="1">
|
||||||
<a-tab-pane key="1" class="tab-pane">
|
<a-tab-pane key="1" class="tab-pane">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<SettingOutlined />
|
<SettingOutlined />
|
||||||
|
|
@ -335,11 +286,6 @@ const alertVisible = ref(true);
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checklist-item {
|
|
||||||
padding-left: 0 !important;
|
|
||||||
padding-right: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-row {
|
.header-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,8 @@ package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v3/logger"
|
"github.com/mhsanaei/3x-ui/v3/logger"
|
||||||
"github.com/mhsanaei/3x-ui/v3/util/crypto"
|
|
||||||
"github.com/mhsanaei/3x-ui/v3/web/locale"
|
"github.com/mhsanaei/3x-ui/v3/web/locale"
|
||||||
"github.com/mhsanaei/3x-ui/v3/web/session"
|
"github.com/mhsanaei/3x-ui/v3/web/session"
|
||||||
|
|
||||||
|
|
@ -19,8 +17,7 @@ type BaseController struct{}
|
||||||
|
|
||||||
// checkLogin is a middleware that verifies user authentication and handles unauthorized access.
|
// checkLogin is a middleware that verifies user authentication and handles unauthorized access.
|
||||||
func (a *BaseController) checkLogin(c *gin.Context) {
|
func (a *BaseController) checkLogin(c *gin.Context) {
|
||||||
user := session.GetLoginUser(c)
|
if !session.IsLogin(c) {
|
||||||
if user == nil {
|
|
||||||
if isAjax(c) {
|
if isAjax(c) {
|
||||||
pureJsonMsg(c, http.StatusUnauthorized, false, I18nWeb(c, "pages.login.loginAgain"))
|
pureJsonMsg(c, http.StatusUnauthorized, false, I18nWeb(c, "pages.login.loginAgain"))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -28,41 +25,11 @@ func (a *BaseController) checkLogin(c *gin.Context) {
|
||||||
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
|
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
|
||||||
}
|
}
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
|
||||||
}
|
|
||||||
if isDefaultAdminCredential(user.Username, user.Password) && !credentialChangeRouteAllowed(c) {
|
|
||||||
if isAjax(c) {
|
|
||||||
pureJsonMsg(c, http.StatusForbidden, false, "Change the default admin credentials before continuing.")
|
|
||||||
} else {
|
|
||||||
c.Header("Cache-Control", "no-store")
|
|
||||||
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path")+"panel/settings")
|
|
||||||
}
|
|
||||||
c.Abort()
|
|
||||||
} else {
|
} else {
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isDefaultAdminCredential(username string, hashedPassword string) bool {
|
|
||||||
return username == "admin" && crypto.CheckPasswordHash(hashedPassword, "admin")
|
|
||||||
}
|
|
||||||
|
|
||||||
func credentialChangeRouteAllowed(c *gin.Context) bool {
|
|
||||||
basePath := c.GetString("base_path")
|
|
||||||
path := c.Request.URL.Path
|
|
||||||
allowedPrefixes := []string{
|
|
||||||
basePath + "panel/settings",
|
|
||||||
basePath + "panel/setting/",
|
|
||||||
basePath + "panel/csrf-token",
|
|
||||||
}
|
|
||||||
for _, prefix := range allowedPrefixes {
|
|
||||||
if strings.HasPrefix(path, prefix) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// I18nWeb retrieves an internationalized message for the web interface based on the current locale.
|
// I18nWeb retrieves an internationalized message for the web interface based on the current locale.
|
||||||
func I18nWeb(c *gin.Context, name string, params ...string) string {
|
func I18nWeb(c *gin.Context, name string, params ...string) string {
|
||||||
anyfunc, funcExists := c.Get("I18n")
|
anyfunc, funcExists := c.Get("I18n")
|
||||||
|
|
|
||||||
|
|
@ -57,18 +57,11 @@ func serveDistPage(c *gin.Context, name string) {
|
||||||
}
|
}
|
||||||
csrfMeta := []byte(`<meta name="csrf-token" content="` + htmlpkg.EscapeString(csrfToken) + `">`)
|
csrfMeta := []byte(`<meta name="csrf-token" content="` + htmlpkg.EscapeString(csrfToken) + `">`)
|
||||||
|
|
||||||
nonceAttr := ""
|
script := `<script>window.X_UI_BASE_PATH="` + escapedBase + `"`
|
||||||
if nonce := c.GetString("csp_nonce"); nonce != "" {
|
|
||||||
nonceAttr = ` nonce="` + htmlpkg.EscapeString(nonce) + `"`
|
|
||||||
}
|
|
||||||
script := `<script` + nonceAttr + `>window.X_UI_BASE_PATH="` + escapedBase + `"`
|
|
||||||
if name != "login.html" {
|
if name != "login.html" {
|
||||||
escapedVer := jsEscape.Replace(config.GetVersion())
|
escapedVer := jsEscape.Replace(config.GetVersion())
|
||||||
script += `;window.X_UI_CUR_VER="` + escapedVer + `"`
|
script += `;window.X_UI_CUR_VER="` + escapedVer + `"`
|
||||||
}
|
}
|
||||||
if u := session.GetLoginUser(c); u != nil && isDefaultAdminCredential(u.Username, u.Password) {
|
|
||||||
script += `;window.X_UI_MUST_CHANGE_CREDENTIALS=true`
|
|
||||||
}
|
|
||||||
script += `;</script>`
|
script += `;</script>`
|
||||||
inject := []byte(script)
|
inject := []byte(script)
|
||||||
inject = append(inject, csrfMeta...)
|
inject = append(inject, csrfMeta...)
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ func NewIndexController(g *gin.RouterGroup) *IndexController {
|
||||||
// initRouter sets up the routes for index, login, logout, and two-factor authentication.
|
// initRouter sets up the routes for index, login, logout, and two-factor authentication.
|
||||||
func (a *IndexController) initRouter(g *gin.RouterGroup) {
|
func (a *IndexController) initRouter(g *gin.RouterGroup) {
|
||||||
g.GET("/", a.index)
|
g.GET("/", a.index)
|
||||||
g.GET("/logout", a.logoutGet)
|
g.GET("/logout", a.logout)
|
||||||
// Public CSRF endpoint — the SPA login page (served by Vite in
|
// Public CSRF endpoint — the SPA login page (served by Vite in
|
||||||
// dev or by serveDistPage in prod) needs a token to POST /login,
|
// dev or by serveDistPage in prod) needs a token to POST /login,
|
||||||
// but the panel-side /panel/csrf-token sits behind checkLogin.
|
// but the panel-side /panel/csrf-token sits behind checkLogin.
|
||||||
|
|
@ -48,7 +48,6 @@ func (a *IndexController) initRouter(g *gin.RouterGroup) {
|
||||||
g.GET("/csrf-token", a.csrfToken)
|
g.GET("/csrf-token", a.csrfToken)
|
||||||
|
|
||||||
g.POST("/login", middleware.CSRFMiddleware(), a.login)
|
g.POST("/login", middleware.CSRFMiddleware(), a.login)
|
||||||
g.POST("/logout", middleware.CSRFMiddleware(), a.logout)
|
|
||||||
g.POST("/getTwoFactorEnable", middleware.CSRFMiddleware(), a.getTwoFactorEnable)
|
g.POST("/getTwoFactorEnable", middleware.CSRFMiddleware(), a.getTwoFactorEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,9 +130,7 @@ func (a *IndexController) login(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Infof("%s logged in successfully", safeUser)
|
logger.Infof("%s logged in successfully", safeUser)
|
||||||
jsonMsgObj(c, I18nWeb(c, "pages.login.toasts.successLogin"), gin.H{
|
jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), nil)
|
||||||
"mustChangeCredentials": user.Username == "admin" && form.Password == "admin",
|
|
||||||
}, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loginFailureReason(err error) string {
|
func loginFailureReason(err error) string {
|
||||||
|
|
@ -153,18 +150,9 @@ func (a *IndexController) logout(c *gin.Context) {
|
||||||
logger.Warning("Unable to clear session on logout:", err)
|
logger.Warning("Unable to clear session on logout:", err)
|
||||||
}
|
}
|
||||||
c.Header("Cache-Control", "no-store")
|
c.Header("Cache-Control", "no-store")
|
||||||
if isAjax(c) {
|
|
||||||
jsonMsg(c, "", nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
|
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *IndexController) logoutGet(c *gin.Context) {
|
|
||||||
c.Header("Allow", http.MethodPost)
|
|
||||||
c.AbortWithStatus(http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
|
|
||||||
// csrfToken returns the session CSRF token. Public — the login page
|
// csrfToken returns the session CSRF token. Public — the login page
|
||||||
// needs a token before authenticating.
|
// needs a token before authenticating.
|
||||||
func (a *IndexController) csrfToken(c *gin.Context) {
|
func (a *IndexController) csrfToken(c *gin.Context) {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v3/util/crypto"
|
"github.com/mhsanaei/3x-ui/v3/util/crypto"
|
||||||
|
|
@ -21,15 +20,6 @@ type updateUserForm struct {
|
||||||
NewPassword string `json:"newPassword" form:"newPassword"`
|
NewPassword string `json:"newPassword" form:"newPassword"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type verifyTwoFactorForm struct {
|
|
||||||
Code string `json:"code" form:"code"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type updateSecretForm struct {
|
|
||||||
Key string `json:"key" form:"key"`
|
|
||||||
Value string `json:"value" form:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SettingController handles settings and user management operations.
|
// SettingController handles settings and user management operations.
|
||||||
type SettingController struct {
|
type SettingController struct {
|
||||||
settingService service.SettingService
|
settingService service.SettingService
|
||||||
|
|
@ -51,9 +41,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
|
||||||
g.POST("/all", a.getAllSetting)
|
g.POST("/all", a.getAllSetting)
|
||||||
g.POST("/defaultSettings", a.getDefaultSettings)
|
g.POST("/defaultSettings", a.getDefaultSettings)
|
||||||
g.POST("/update", a.updateSetting)
|
g.POST("/update", a.updateSetting)
|
||||||
g.POST("/secret", a.updateSecret)
|
|
||||||
g.POST("/updateUser", a.updateUser)
|
g.POST("/updateUser", a.updateUser)
|
||||||
g.POST("/verifyTwoFactor", a.verifyTwoFactor)
|
|
||||||
g.POST("/restartPanel", a.restartPanel)
|
g.POST("/restartPanel", a.restartPanel)
|
||||||
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
||||||
g.GET("/getApiToken", a.getApiToken)
|
g.GET("/getApiToken", a.getApiToken)
|
||||||
|
|
@ -62,7 +50,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
|
||||||
|
|
||||||
// getAllSetting retrieves all current settings.
|
// getAllSetting retrieves all current settings.
|
||||||
func (a *SettingController) getAllSetting(c *gin.Context) {
|
func (a *SettingController) getAllSetting(c *gin.Context) {
|
||||||
allSetting, err := a.settingService.GetAllSettingView()
|
allSetting, err := a.settingService.GetAllSetting()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||||
return
|
return
|
||||||
|
|
@ -92,16 +80,6 @@ func (a *SettingController) updateSetting(c *gin.Context) {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *SettingController) updateSecret(c *gin.Context) {
|
|
||||||
form := &updateSecretForm{}
|
|
||||||
if err := c.ShouldBind(form); err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err := a.settingService.UpdateSecret(form.Key, form.Value)
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateUser updates the current user's username and password.
|
// updateUser updates the current user's username and password.
|
||||||
func (a *SettingController) updateUser(c *gin.Context) {
|
func (a *SettingController) updateUser(c *gin.Context) {
|
||||||
form := &updateUserForm{}
|
form := &updateUserForm{}
|
||||||
|
|
@ -115,18 +93,10 @@ func (a *SettingController) updateUser(c *gin.Context) {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUserError"), errors.New(I18nWeb(c, "pages.settings.toasts.originalUserPassIncorrect")))
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUserError"), errors.New(I18nWeb(c, "pages.settings.toasts.originalUserPassIncorrect")))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(form.NewUsername) == "" || form.NewPassword == "" {
|
if form.NewUsername == "" || form.NewPassword == "" {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUserError"), errors.New(I18nWeb(c, "pages.settings.toasts.userPassMustBeNotEmpty")))
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUserError"), errors.New(I18nWeb(c, "pages.settings.toasts.userPassMustBeNotEmpty")))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(form.NewPassword) < 10 {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUserError"), errors.New("new password must be at least 10 characters"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(form.NewUsername) == "admin" && form.NewPassword == "admin" {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUserError"), errors.New("default admin/admin credentials are not allowed"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword)
|
err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
user.Username = form.NewUsername
|
user.Username = form.NewUsername
|
||||||
|
|
@ -138,19 +108,6 @@ func (a *SettingController) updateUser(c *gin.Context) {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *SettingController) verifyTwoFactor(c *gin.Context) {
|
|
||||||
form := &verifyTwoFactorForm{}
|
|
||||||
if err := c.ShouldBind(form); err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ok, err := a.userService.VerifyTwoFactorCode(form.Code)
|
|
||||||
if err == nil && !ok {
|
|
||||||
err = errors.New("invalid 2fa code")
|
|
||||||
}
|
|
||||||
jsonObj(c, ok, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// restartPanel restarts the panel service after a delay.
|
// restartPanel restarts the panel service after a delay.
|
||||||
func (a *SettingController) restartPanel(c *gin.Context) {
|
func (a *SettingController) restartPanel(c *gin.Context) {
|
||||||
err := a.panelService.RestartPanel(time.Second * 3)
|
err := a.panelService.RestartPanel(time.Second * 3)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
|
||||||
|
|
@ -102,21 +102,6 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *UserService) VerifyTwoFactorCode(code string) (bool, error) {
|
|
||||||
twoFactorEnable, err := s.settingService.GetTwoFactorEnable()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if !twoFactorEnable {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
twoFactorToken, err := s.settingService.GetTwoFactorToken()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return gotp.NewDefaultTOTP(twoFactorToken).Now() == code, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *UserService) UpdateUser(id int, username string, password string) error {
|
func (s *UserService) UpdateUser(id int, username string, password string) error {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
hashedPassword, err := crypto.HashPasswordAsBcrypt(password)
|
hashedPassword, err := crypto.HashPasswordAsBcrypt(password)
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,10 @@ type ObsTagSnapshot struct {
|
||||||
type XrayMetricsService struct {
|
type XrayMetricsService struct {
|
||||||
settingService SettingService
|
settingService SettingService
|
||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
state xrayMetricsState
|
state xrayMetricsState
|
||||||
client *http.Client
|
client *http.Client
|
||||||
obsByTag map[string]ObsTagSnapshot
|
obsByTag map[string]ObsTagSnapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
var validObsTag = regexp.MustCompile(`^[a-zA-Z0-9._\-]+$`)
|
var validObsTag = regexp.MustCompile(`^[a-zA-Z0-9._\-]+$`)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue