feat: 登录管理设备列表显示会话类型和剩余有效期
- 临时会话显示橙色'临时会话'徽章 - 非当前设备显示剩余有效时间(临时/长期均显示) - 当前设备不显示剩余时间(持续续期,显示无意义) - 新增 formatTTL 模板函数,分钟数转人类可读格式 - sessionConfig 接口增加 GetRememberTimeout 方法
This commit is contained in:
@ -129,6 +129,16 @@ func bindEnvOverride(v *viper.Viper) {
|
|||||||
_ = v.BindEnv("database.password", "DATABASE_PASSWORD")
|
_ = v.BindEnv("database.password", "DATABASE_PASSWORD")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetIdleTimeout 返回临时会话空闲超时(分钟),实现 controller.sessionConfig 接口
|
||||||
|
func (c *Config) GetIdleTimeout() int {
|
||||||
|
return c.Session.IdleTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRememberTimeout 返回记住我会话空闲超时(分钟)
|
||||||
|
func (c *Config) GetRememberTimeout() int {
|
||||||
|
return c.Session.RememberTimeout
|
||||||
|
}
|
||||||
|
|
||||||
// DSN 返回 PostgreSQL 连接字符串
|
// DSN 返回 PostgreSQL 连接字符串
|
||||||
func (d DatabaseConfig) DSN() string {
|
func (d DatabaseConfig) DSN() string {
|
||||||
return "host=" + d.Host +
|
return "host=" + d.Host +
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"metazone.cc/metalab/internal/common"
|
"metazone.cc/metalab/internal/common"
|
||||||
"metazone.cc/metalab/internal/model"
|
"metazone.cc/metalab/internal/model"
|
||||||
@ -33,17 +34,24 @@ type auditSubmittable interface {
|
|||||||
GetPendingTypes(userID uint) ([]string, error)
|
GetPendingTypes(userID uint) ([]string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sessionConfig 登录管理对配置的最小依赖(ISP:2 个方法)
|
||||||
|
type sessionConfig interface {
|
||||||
|
GetIdleTimeout() int // 分钟
|
||||||
|
GetRememberTimeout() int // 分钟
|
||||||
|
}
|
||||||
|
|
||||||
// SettingsController 个人设置控制器
|
// SettingsController 个人设置控制器
|
||||||
type SettingsController struct {
|
type SettingsController struct {
|
||||||
authService profileProvider
|
authService profileProvider
|
||||||
avatarService avatarProvider
|
avatarService avatarProvider
|
||||||
auditService auditSubmittable
|
auditService auditSubmittable
|
||||||
sessionMgr sessionManager
|
sessionMgr sessionManager
|
||||||
|
sessionCfg sessionConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSettingsController 构造函数
|
// NewSettingsController 构造函数
|
||||||
func NewSettingsController(authService profileProvider, avatarService avatarProvider, auditService auditSubmittable, sessionMgr sessionManager) *SettingsController {
|
func NewSettingsController(authService profileProvider, avatarService avatarProvider, auditService auditSubmittable, sessionMgr sessionManager, sessionCfg sessionConfig) *SettingsController {
|
||||||
return &SettingsController{authService: authService, avatarService: avatarService, auditService: auditService, sessionMgr: sessionMgr}
|
return &SettingsController{authService: authService, avatarService: avatarService, auditService: auditService, sessionMgr: sessionMgr, sessionCfg: sessionCfg}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SettingsPage 个人设置页面(需登录)
|
// SettingsPage 个人设置页面(需登录)
|
||||||
@ -94,13 +102,24 @@ func (sc *SettingsController) SettingsPage(c *gin.Context) {
|
|||||||
*session.Session
|
*session.Session
|
||||||
DeviceInfo session.DeviceInfo
|
DeviceInfo session.DeviceInfo
|
||||||
IsCurrent bool
|
IsCurrent bool
|
||||||
|
TTLMinutes int // 剩余有效分钟数(仅非当前设备显示)
|
||||||
}
|
}
|
||||||
var views []sessionView
|
var views []sessionView
|
||||||
for _, s := range sessions {
|
for _, s := range sessions {
|
||||||
|
timeout := sc.sessionCfg.GetIdleTimeout() // 临时会话超时(分钟)
|
||||||
|
if s.RememberMe {
|
||||||
|
timeout = sc.sessionCfg.GetRememberTimeout() // 记住我超时(分钟)
|
||||||
|
}
|
||||||
|
elapsed := int(time.Since(s.LastAccess).Minutes())
|
||||||
|
ttl := timeout - elapsed
|
||||||
|
if ttl < 0 {
|
||||||
|
ttl = 0
|
||||||
|
}
|
||||||
views = append(views, sessionView{
|
views = append(views, sessionView{
|
||||||
Session: s,
|
Session: s,
|
||||||
DeviceInfo: session.ParseUA(s.UserAgent),
|
DeviceInfo: session.ParseUA(s.UserAgent),
|
||||||
IsCurrent: s.ID == currentSID,
|
IsCurrent: s.ID == currentSID,
|
||||||
|
TTLMinutes: ttl,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
data["Sessions"] = views
|
data["Sessions"] = views
|
||||||
|
|||||||
@ -22,7 +22,7 @@ func buildDeps(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSetting
|
|||||||
|
|
||||||
messageController := controller.NewMessageController(notificationService)
|
messageController := controller.NewMessageController(notificationService)
|
||||||
|
|
||||||
settingsCtrl := controller.NewSettingsController(authService, avatarSvc, auditService, sessionMgr)
|
settingsCtrl := controller.NewSettingsController(authService, avatarSvc, auditService, sessionMgr, cfg)
|
||||||
siteSettingController := adminCtrl.NewSiteSettingController(
|
siteSettingController := adminCtrl.NewSiteSettingController(
|
||||||
service.NewSiteSettingService(siteSettings),
|
service.NewSiteSettingService(siteSettings),
|
||||||
&service.AuditDefaults{
|
&service.AuditDefaults{
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package theme
|
package theme
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -17,8 +18,9 @@ type TemplateRoot struct {
|
|||||||
// 模板名 = prefix + 相对于 root.Dir 的相对路径
|
// 模板名 = prefix + 相对于 root.Dir 的相对路径
|
||||||
func LoadTemplates(roots ...TemplateRoot) (*template.Template, error) {
|
func LoadTemplates(roots ...TemplateRoot) (*template.Template, error) {
|
||||||
funcMap := template.FuncMap{
|
funcMap := template.FuncMap{
|
||||||
"add": func(a, b int) int { return a + b },
|
"add": func(a, b int) int { return a + b },
|
||||||
"subtract": func(a, b int) int { return a - b },
|
"subtract": func(a, b int) int { return a - b },
|
||||||
|
"formatTTL": formatTTL,
|
||||||
}
|
}
|
||||||
t := template.New("").Funcs(funcMap)
|
t := template.New("").Funcs(funcMap)
|
||||||
for _, root := range roots {
|
for _, root := range roots {
|
||||||
@ -55,3 +57,23 @@ func LoadContent(path string) (template.HTML, error) {
|
|||||||
}
|
}
|
||||||
return template.HTML(content), nil
|
return template.HTML(content), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// formatTTL 将剩余分钟数格式化为人类可读的时间(如 "23 小时"、"3 天"、"15 分钟")
|
||||||
|
func formatTTL(minutes int) string {
|
||||||
|
if minutes <= 0 {
|
||||||
|
return "即将过期"
|
||||||
|
}
|
||||||
|
if minutes < 60 {
|
||||||
|
return fmt.Sprintf("%d 分钟", minutes)
|
||||||
|
}
|
||||||
|
hours := minutes / 60
|
||||||
|
if hours < 24 {
|
||||||
|
return fmt.Sprintf("%d 小时", hours)
|
||||||
|
}
|
||||||
|
days := hours / 24
|
||||||
|
remainHours := hours % 24
|
||||||
|
if remainHours == 0 {
|
||||||
|
return fmt.Sprintf("%d 天", days)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d 天 %d 小时", days, remainHours)
|
||||||
|
}
|
||||||
|
|||||||
@ -118,11 +118,13 @@
|
|||||||
<span class="session-browser">{{.DeviceInfo.Browser}}</span>
|
<span class="session-browser">{{.DeviceInfo.Browser}}</span>
|
||||||
<span class="session-os">/ {{.DeviceInfo.OS}}</span>
|
<span class="session-os">/ {{.DeviceInfo.OS}}</span>
|
||||||
{{if .IsCurrent}}<span class="session-badge">当前设备</span>{{end}}
|
{{if .IsCurrent}}<span class="session-badge">当前设备</span>{{end}}
|
||||||
|
{{if not .RememberMe}}<span class="session-badge session-badge-temp">临时会话</span>{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="session-meta">
|
<div class="session-meta">
|
||||||
<span class="session-ip">IP: {{.IP}}</span>
|
<span class="session-ip">IP: {{.IP}}</span>
|
||||||
<span class="session-time">登录于 {{.CreatedAt.Format "2006-01-02 15:04"}}</span>
|
<span class="session-time">登录于 {{.CreatedAt.Format "2006-01-02 15:04"}}</span>
|
||||||
<span class="session-time">最后活动 {{.LastAccess.Format "2006-01-02 15:04"}}</span>
|
<span class="session-time">最后活动 {{.LastAccess.Format "2006-01-02 15:04"}}</span>
|
||||||
|
{{if not .IsCurrent}}<span class="session-ttl">剩余约 {{formatTTL .TTLMinutes}}</span>{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="session-remark-row">
|
<div class="session-remark-row">
|
||||||
<input type="text" class="session-remark-input" value="{{.Remark}}" placeholder="添加备注(如 我的笔记本)" maxlength="32" data-sid="{{.ID}}" data-current="{{.IsCurrent}}">
|
<input type="text" class="session-remark-input" value="{{.Remark}}" placeholder="添加备注(如 我的笔记本)" maxlength="32" data-sid="{{.ID}}" data-current="{{.IsCurrent}}">
|
||||||
|
|||||||
@ -649,6 +649,17 @@ body {
|
|||||||
margin-left: .2rem;
|
margin-left: .2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.session-badge.session-badge-temp {
|
||||||
|
background: #fff3e0;
|
||||||
|
color: #e65100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-ttl {
|
||||||
|
color: #e65100;
|
||||||
|
font-size: .78rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.session-meta {
|
.session-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: .8rem;
|
gap: .8rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user