mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 21:24:10 +00:00
feat: add user dashboard with role-based access control
Add a simplified dashboard page for non-admin users showing username, traffic usage, expiry time, and logout button. Implement role-based routing so user-role accounts are redirected to their own dashboard instead of the admin panel. Add getUserInfo API endpoint and i18n translations across all 13 supported locales.
This commit is contained in:
parent
3045b630f0
commit
463b07db52
17 changed files with 305 additions and 2 deletions
|
|
@ -48,6 +48,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
|||
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
|
||||
g.POST("/delDepletedClients/:id", a.delDepletedClients)
|
||||
g.POST("/import", a.importInbound)
|
||||
g.GET("/userInfo", a.getUserInfo)
|
||||
g.POST("/onlines", a.onlines)
|
||||
g.POST("/lastOnline", a.lastOnline)
|
||||
g.POST("/updateClientTraffic/:email", a.updateClientTraffic)
|
||||
|
|
@ -454,3 +455,14 @@ func (a *InboundController) delInboundClientByEmail(c *gin.Context) {
|
|||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
// getUserInfo returns client traffic information for the logged-in user.
|
||||
func (a *InboundController) getUserInfo(c *gin.Context) {
|
||||
user := session.GetLoginUser(c)
|
||||
traffic, err := a.inboundService.GetClientTrafficByEmail(user.Username)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.trafficGetError"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, traffic, nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,12 @@ func (a *IndexController) initRouter(g *gin.RouterGroup) {
|
|||
// index handles the root route, redirecting logged-in users to the panel or showing the login page.
|
||||
func (a *IndexController) index(c *gin.Context) {
|
||||
if session.IsLogin(c) {
|
||||
c.Redirect(http.StatusTemporaryRedirect, "panel/")
|
||||
user := session.GetLoginUser(c)
|
||||
if user.Role == "admin" {
|
||||
c.Redirect(http.StatusTemporaryRedirect, "panel/")
|
||||
} else {
|
||||
c.Redirect(http.StatusTemporaryRedirect, "panel/user")
|
||||
}
|
||||
return
|
||||
}
|
||||
html(c, "login.html", "pages.login.title", nil)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/web/session"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
|
@ -25,6 +29,7 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
|||
g.Use(a.checkLogin)
|
||||
|
||||
g.GET("/", a.index)
|
||||
g.GET("/user", a.user)
|
||||
g.GET("/inbounds", a.inbounds)
|
||||
g.GET("/settings", a.settings)
|
||||
g.GET("/xray", a.xraySettings)
|
||||
|
|
@ -33,11 +38,21 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
|||
a.xraySettingController = NewXraySettingController(g)
|
||||
}
|
||||
|
||||
// index renders the main panel index page.
|
||||
// index renders the main panel index page. Non-admin users are redirected to the user dashboard.
|
||||
func (a *XUIController) index(c *gin.Context) {
|
||||
user := session.GetLoginUser(c)
|
||||
if user.Role != "admin" {
|
||||
c.Redirect(http.StatusTemporaryRedirect, "user")
|
||||
return
|
||||
}
|
||||
html(c, "index.html", "pages.index.title", nil)
|
||||
}
|
||||
|
||||
// user renders the user dashboard page.
|
||||
func (a *XUIController) user(c *gin.Context) {
|
||||
html(c, "user.html", "pages.user.title", nil)
|
||||
}
|
||||
|
||||
// inbounds renders the inbounds management page.
|
||||
func (a *XUIController) inbounds(c *gin.Context) {
|
||||
html(c, "inbounds.html", "pages.inbounds.title", nil)
|
||||
|
|
|
|||
154
web/html/user.html
Normal file
154
web/html/user.html
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
{{ template "page/head_start" .}}
|
||||
{{ template "page/head_end" .}}
|
||||
|
||||
{{ template "page/body_start" .}}
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' login-app'">
|
||||
<transition name="list" appear>
|
||||
<a-layout-content class="under min-h-0">
|
||||
<div class="waves-header">
|
||||
<div class="waves-inner-header"></div>
|
||||
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
|
||||
<defs>
|
||||
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
|
||||
</defs>
|
||||
<g class="parallax">
|
||||
<use xlink:href="#gentle-wave" x="48" y="0" fill="rgba(0, 135, 113, 0.08)" />
|
||||
<use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(0, 135, 113, 0.08)" />
|
||||
<use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(0, 135, 113, 0.08)" />
|
||||
<use xlink:href="#gentle-wave" x="48" y="7" fill="#c7ebe2" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<a-row type="flex" justify="center" align="middle" class="h-100 overflow-y-auto overflow-x-hidden">
|
||||
<a-col :xs="22" :sm="16" :md="12" :lg="10" :xl="8" :xxl="6" class="my-3rem">
|
||||
<template v-if="loading">
|
||||
<div class="text-center">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-card :class="themeSwitcher.currentTheme" class="user-card">
|
||||
<div class="setting-section">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme" title='{{ i18n "menu.settings" }}'
|
||||
placement="bottomRight" trigger="click">
|
||||
<template slot="content">
|
||||
<a-space direction="vertical" :size="10">
|
||||
<a-theme-switch-login></a-theme-switch-login>
|
||||
<span>{{ i18n "pages.settings.language" }}</span>
|
||||
<a-select ref="selectLang" class="w-100" v-model="lang" @change="LanguageManager.setLanguage(lang)"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
|
||||
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
||||
<span v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-button shape="circle" icon="setting"></a-button>
|
||||
</a-popover>
|
||||
</div>
|
||||
<div class="text-center mb-24">
|
||||
<a-icon type="user" style="font-size: 48px; color: #008771;" />
|
||||
<h2 class="mt-8">[[ username ]]</h2>
|
||||
</div>
|
||||
<a-divider />
|
||||
<a-descriptions :column="1" bordered size="small">
|
||||
<a-descriptions-item label='{{ i18n "pages.user.username" }}'>
|
||||
[[ username ]]
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label='{{ i18n "pages.user.upload" }}'>
|
||||
[[ traffic ? SizeFormatter.sizeFormat(traffic.up) : '-' ]]
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label='{{ i18n "pages.user.download" }}'>
|
||||
[[ traffic ? SizeFormatter.sizeFormat(traffic.down) : '-' ]]
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label='{{ i18n "pages.user.totalTraffic" }}'>
|
||||
<template v-if="traffic">
|
||||
<template v-if="traffic.total > 0">
|
||||
[[ SizeFormatter.sizeFormat(traffic.up + traffic.down) ]] / [[ SizeFormatter.sizeFormat(traffic.total) ]]
|
||||
<a-progress :percent="traffic.total > 0 ? NumberFormatter.toFixed((traffic.up + traffic.down) / traffic.total * 100, 1) : 0" size="small" :status="((traffic.up + traffic.down) / traffic.total * 100) >= 90 ? 'exception' : 'normal'" />
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ i18n "unlimited" }}
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>-</template>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label='{{ i18n "pages.user.expiryTime" }}'>
|
||||
<template v-if="traffic">
|
||||
<template v-if="traffic.expiryTime > 0">
|
||||
<span :class="{ 'text-red': traffic.expiryTime < Date.now() }">
|
||||
[[ formatExpiryTime(traffic.expiryTime) ]]
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ i18n "unlimited" }}
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>-</template>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label='{{ i18n "pages.user.status" }}'>
|
||||
<a-badge v-if="traffic" :status="traffic.enable ? 'processing' : 'default'" :text="traffic.enable ? '{{ i18n 'enabled' }}' : '{{ i18n 'disabled' }}'" />
|
||||
<template v-else>-</template>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<div class="mt-24 text-center">
|
||||
<a-button type="primary" icon="logout" @click="logout">
|
||||
{{ i18n "menu.logout" }}
|
||||
</a-button>
|
||||
</div>
|
||||
</a-card>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-layout-content>
|
||||
</transition>
|
||||
</a-layout>
|
||||
{{template "page/body_scripts" .}}
|
||||
{{template "component/aThemeSwitch" .}}
|
||||
<script>
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
themeSwitcher,
|
||||
loading: true,
|
||||
username: '',
|
||||
traffic: null,
|
||||
lang: '',
|
||||
},
|
||||
async mounted() {
|
||||
this.lang = LanguageManager.getLanguage();
|
||||
await this.loadUserInfo();
|
||||
},
|
||||
methods: {
|
||||
async loadUserInfo() {
|
||||
try {
|
||||
const msg = await HttpUtil.get('/panel/api/inbounds/userInfo');
|
||||
if (msg.success) {
|
||||
this.username = msg.obj?.email || '';
|
||||
this.traffic = msg.obj;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to get user info:", e);
|
||||
}
|
||||
this.loading = false;
|
||||
},
|
||||
formatExpiryTime(timestamp) {
|
||||
if (timestamp <= 0) return '{{ i18n "unlimited" }}';
|
||||
const date = new Date(timestamp);
|
||||
const now = Date.now();
|
||||
if (timestamp < now) {
|
||||
return date.toLocaleString() + ' ({{ i18n "depleted" }})';
|
||||
}
|
||||
const diffDays = Math.ceil((timestamp - now) / (1000 * 60 * 60 * 24));
|
||||
return date.toLocaleString() + ' (' + diffDays + ' {{ i18n "day" }})';
|
||||
},
|
||||
logout() {
|
||||
location.href = basePath + 'logout/';
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
{{ template "page/body_end" .}}
|
||||
|
|
@ -312,6 +312,15 @@
|
|||
"requestHeader" = "رأس الطلب"
|
||||
"responseHeader" = "رأس الرد"
|
||||
|
||||
[pages.user]
|
||||
"title" = "لوحة المستخدم"
|
||||
"username" = "اسم المستخدم"
|
||||
"upload" = "الرفع"
|
||||
"download" = "التحميل"
|
||||
"totalTraffic" = "إجمالي حركة المرور"
|
||||
"expiryTime" = "تاريخ الانتهاء"
|
||||
"status" = "الحالة"
|
||||
|
||||
[pages.settings]
|
||||
"title" = "إعدادات البانل"
|
||||
"save" = "حفظ"
|
||||
|
|
|
|||
|
|
@ -317,6 +317,15 @@
|
|||
"requestHeader" = "Request Header"
|
||||
"responseHeader" = "Response Header"
|
||||
|
||||
[pages.user]
|
||||
"title" = "User Dashboard"
|
||||
"username" = "Username"
|
||||
"upload" = "Upload"
|
||||
"download" = "Download"
|
||||
"totalTraffic" = "Total Traffic"
|
||||
"expiryTime" = "Expiry Time"
|
||||
"status" = "Status"
|
||||
|
||||
[pages.settings]
|
||||
"title" = "Panel Settings"
|
||||
"save" = "Save"
|
||||
|
|
|
|||
|
|
@ -312,6 +312,15 @@
|
|||
"requestHeader" = "Encabezado de solicitud"
|
||||
"responseHeader" = "Encabezado de respuesta"
|
||||
|
||||
[pages.user]
|
||||
"title" = "Panel de Usuario"
|
||||
"username" = "Nombre de usuario"
|
||||
"upload" = "Subida"
|
||||
"download" = "Descarga"
|
||||
"totalTraffic" = "Tráfico Total"
|
||||
"expiryTime" = "Fecha de Expiración"
|
||||
"status" = "Estado"
|
||||
|
||||
[pages.settings]
|
||||
"title" = "Configuraciones"
|
||||
"save" = "Guardar"
|
||||
|
|
|
|||
|
|
@ -312,6 +312,15 @@
|
|||
"requestHeader" = "سربرگ درخواست"
|
||||
"responseHeader" = "سربرگ پاسخ"
|
||||
|
||||
[pages.user]
|
||||
"title" = "داشبورد کاربر"
|
||||
"username" = "نام کاربری"
|
||||
"upload" = "آپلود"
|
||||
"download" = "دانلود"
|
||||
"totalTraffic" = "ترافیک کل"
|
||||
"expiryTime" = "زمان انقضا"
|
||||
"status" = "وضعیت"
|
||||
|
||||
[pages.settings]
|
||||
"title" = "تنظیمات پنل"
|
||||
"save" = "ذخیره"
|
||||
|
|
|
|||
|
|
@ -312,6 +312,15 @@
|
|||
"requestHeader" = "Header Permintaan"
|
||||
"responseHeader" = "Header Respons"
|
||||
|
||||
[pages.user]
|
||||
"title" = "Dasbor Pengguna"
|
||||
"username" = "Nama Pengguna"
|
||||
"upload" = "Unggah"
|
||||
"download" = "Unduh"
|
||||
"totalTraffic" = "Total Lalu Lintas"
|
||||
"expiryTime" = "Waktu Kedaluwarsa"
|
||||
"status" = "Status"
|
||||
|
||||
[pages.settings]
|
||||
"title" = "Pengaturan Panel"
|
||||
"save" = "Simpan"
|
||||
|
|
|
|||
|
|
@ -312,6 +312,15 @@
|
|||
"requestHeader" = "リクエストヘッダー"
|
||||
"responseHeader" = "レスポンスヘッダー"
|
||||
|
||||
[pages.user]
|
||||
"title" = "ユーザーダッシュボード"
|
||||
"username" = "ユーザー名"
|
||||
"upload" = "アップロード"
|
||||
"download" = "ダウンロード"
|
||||
"totalTraffic" = "合計トラフィック"
|
||||
"expiryTime" = "有効期限"
|
||||
"status" = "ステータス"
|
||||
|
||||
[pages.settings]
|
||||
"title" = "パネル設定"
|
||||
"save" = "保存"
|
||||
|
|
|
|||
|
|
@ -312,6 +312,15 @@
|
|||
"requestHeader" = "Cabeçalho da Requisição"
|
||||
"responseHeader" = "Cabeçalho da Resposta"
|
||||
|
||||
[pages.user]
|
||||
"title" = "Painel do Usuário"
|
||||
"username" = "Nome de Usuário"
|
||||
"upload" = "Upload"
|
||||
"download" = "Download"
|
||||
"totalTraffic" = "Tráfego Total"
|
||||
"expiryTime" = "Data de Expiração"
|
||||
"status" = "Status"
|
||||
|
||||
[pages.settings]
|
||||
"title" = "Configurações do Painel"
|
||||
"save" = "Salvar"
|
||||
|
|
|
|||
|
|
@ -312,6 +312,15 @@
|
|||
"requestHeader" = "Заголовок запроса"
|
||||
"responseHeader" = "Заголовок ответа"
|
||||
|
||||
[pages.user]
|
||||
"title" = "Панель пользователя"
|
||||
"username" = "Имя пользователя"
|
||||
"upload" = "Загрузка"
|
||||
"download" = "Скачивание"
|
||||
"totalTraffic" = "Общий трафик"
|
||||
"expiryTime" = "Срок действия"
|
||||
"status" = "Статус"
|
||||
|
||||
[pages.settings]
|
||||
"title" = "Настройки"
|
||||
"save" = "Сохранить"
|
||||
|
|
|
|||
|
|
@ -312,6 +312,15 @@
|
|||
"requestHeader" = "İstek Başlığı"
|
||||
"responseHeader" = "Yanıt Başlığı"
|
||||
|
||||
[pages.user]
|
||||
"title" = "Kullanıcı Paneli"
|
||||
"username" = "Kullanıcı Adı"
|
||||
"upload" = "Yükleme"
|
||||
"download" = "İndirme"
|
||||
"totalTraffic" = "Toplam Trafik"
|
||||
"expiryTime" = "Bitiş Tarihi"
|
||||
"status" = "Durum"
|
||||
|
||||
[pages.settings]
|
||||
"title" = "Panel Ayarları"
|
||||
"save" = "Kaydet"
|
||||
|
|
|
|||
|
|
@ -312,6 +312,15 @@
|
|||
"requestHeader" = "Заголовок запиту"
|
||||
"responseHeader" = "Заголовок відповіді"
|
||||
|
||||
[pages.user]
|
||||
"title" = "Панель користувача"
|
||||
"username" = "Ім'я користувача"
|
||||
"upload" = "Завантаження"
|
||||
"download" = "Завантаження"
|
||||
"totalTraffic" = "Загальний трафік"
|
||||
"expiryTime" = "Термін дії"
|
||||
"status" = "Статус"
|
||||
|
||||
[pages.settings]
|
||||
"title" = "Параметри панелі"
|
||||
"save" = "Зберегти"
|
||||
|
|
|
|||
|
|
@ -312,6 +312,15 @@
|
|||
"requestHeader" = "Header yêu cầu"
|
||||
"responseHeader" = "Header phản hồi"
|
||||
|
||||
[pages.user]
|
||||
"title" = "Bảng điều khiển người dùng"
|
||||
"username" = "Tên người dùng"
|
||||
"upload" = "Tải lên"
|
||||
"download" = "Tải xuống"
|
||||
"totalTraffic" = "Tổng lưu lượng"
|
||||
"expiryTime" = "Thời gian hết hạn"
|
||||
"status" = "Trạng thái"
|
||||
|
||||
[pages.settings]
|
||||
"title" = "Cài đặt"
|
||||
"save" = "Lưu"
|
||||
|
|
|
|||
|
|
@ -317,6 +317,15 @@
|
|||
"requestHeader" = "请求头"
|
||||
"responseHeader" = "响应头"
|
||||
|
||||
[pages.user]
|
||||
"title" = "用户面板"
|
||||
"username" = "用户名"
|
||||
"upload" = "上传"
|
||||
"download" = "下载"
|
||||
"totalTraffic" = "总流量"
|
||||
"expiryTime" = "到期时间"
|
||||
"status" = "状态"
|
||||
|
||||
[pages.settings]
|
||||
"title" = "面板设置"
|
||||
"save" = "保存"
|
||||
|
|
|
|||
|
|
@ -312,6 +312,15 @@
|
|||
"requestHeader" = "請求頭"
|
||||
"responseHeader" = "響應頭"
|
||||
|
||||
[pages.user]
|
||||
"title" = "用戶面板"
|
||||
"username" = "用戶名"
|
||||
"upload" = "上傳"
|
||||
"download" = "下載"
|
||||
"totalTraffic" = "總流量"
|
||||
"expiryTime" = "到期時間"
|
||||
"status" = "狀態"
|
||||
|
||||
[pages.settings]
|
||||
"title" = "面板設定"
|
||||
"save" = "儲存"
|
||||
|
|
|
|||
Loading…
Reference in a new issue