mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-11 01:36:37 +08:00
登录状态记录
This commit is contained in:
@@ -2,13 +2,27 @@
|
|||||||
|
|
||||||
import {useEffect, useState} from "react"
|
import {useEffect, useState} from "react"
|
||||||
import {useMutation, useQuery, useQueryClient, type UseQueryResult} from "@tanstack/react-query"
|
import {useMutation, useQuery, useQueryClient, type UseQueryResult} from "@tanstack/react-query"
|
||||||
import {Fingerprint, Globe, Loader2, Lock, Mail, Pencil, Plus, Settings, Shield, Trash2, UserPlus} from "lucide-react"
|
import {
|
||||||
|
Clock,
|
||||||
|
Fingerprint,
|
||||||
|
Globe,
|
||||||
|
Loader2,
|
||||||
|
Lock,
|
||||||
|
Mail,
|
||||||
|
Pencil,
|
||||||
|
Plus,
|
||||||
|
Settings,
|
||||||
|
Shield,
|
||||||
|
Trash2,
|
||||||
|
UserPlus
|
||||||
|
} from "lucide-react"
|
||||||
|
|
||||||
import {Button} from "@/components/ui/button"
|
import {Button} from "@/components/ui/button"
|
||||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card"
|
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card"
|
||||||
import {Switch} from "@/components/ui/switch"
|
import {Switch} from "@/components/ui/switch"
|
||||||
import {Input} from "@/components/ui/input"
|
import {Input} from "@/components/ui/input"
|
||||||
import {Label} from "@/components/ui/label"
|
import {Label} from "@/components/ui/label"
|
||||||
|
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select"
|
||||||
import {AuthSourceModal} from "@/components/common/settings/auth-source-modal"
|
import {AuthSourceModal} from "@/components/common/settings/auth-source-modal"
|
||||||
import {AdminService} from "@/lib/services"
|
import {AdminService} from "@/lib/services"
|
||||||
import type {AuthSource, SystemConfig} from "@/lib/services/admin"
|
import type {AuthSource, SystemConfig} from "@/lib/services/admin"
|
||||||
@@ -70,6 +84,9 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
|
|||||||
const [capTokenTTL, setCapTokenTTL] = useState("")
|
const [capTokenTTL, setCapTokenTTL] = useState("")
|
||||||
const [capAutoSolve, setCapAutoSolve] = useState(true)
|
const [capAutoSolve, setCapAutoSolve] = useState(true)
|
||||||
|
|
||||||
|
const [sessionTTL, setSessionTTL] = useState("168")
|
||||||
|
const [customHours, setCustomHours] = useState("")
|
||||||
|
|
||||||
const authSourcesQuery = useQuery({
|
const authSourcesQuery = useQuery({
|
||||||
queryKey: ["auth", "sources"],
|
queryKey: ["auth", "sources"],
|
||||||
queryFn: () => AdminService.listAuthSources(),
|
queryFn: () => AdminService.listAuthSources(),
|
||||||
@@ -84,9 +101,58 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
|
|||||||
setCapTTL(cfgMap["cap_challenge_ttl_seconds"]?.value || "600")
|
setCapTTL(cfgMap["cap_challenge_ttl_seconds"]?.value || "600")
|
||||||
setCapTokenTTL(cfgMap["cap_token_ttl_seconds"]?.value || "1200")
|
setCapTokenTTL(cfgMap["cap_token_ttl_seconds"]?.value || "1200")
|
||||||
setCapAutoSolve(cfgMap["cap_auto_solve"]?.value !== "false")
|
setCapAutoSolve(cfgMap["cap_auto_solve"]?.value !== "false")
|
||||||
|
|
||||||
|
// 初始化登录保持设置
|
||||||
|
const ttlVal = cfgMap["login_session_ttl_hours"]?.value || "0"
|
||||||
|
if (ttlVal === "0" || ttlVal === "168" || ttlVal === "720" || ttlVal === "-1") {
|
||||||
|
setSessionTTL(ttlVal)
|
||||||
|
setCustomHours("")
|
||||||
|
} else {
|
||||||
|
setSessionTTL("custom")
|
||||||
|
setCustomHours(ttlVal)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [systemConfigsQuery.data, configs])
|
}, [systemConfigsQuery.data, configs])
|
||||||
|
|
||||||
|
const updateTTLMutation = useMutation({
|
||||||
|
mutationFn: async (value: string) => {
|
||||||
|
const config = configs["login_session_ttl_hours"]
|
||||||
|
if (!config) {
|
||||||
|
throw new Error("缺少配置项: login_session_ttl_hours")
|
||||||
|
}
|
||||||
|
await AdminService.updateSystemConfig("login_session_ttl_hours", {
|
||||||
|
value: value,
|
||||||
|
description: config.description,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["admin", "system-configs"] })
|
||||||
|
toast.success("登录状态保持时间已更新")
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
toast.error(error.message || "更新配置失败")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleTTLChange = (val: string) => {
|
||||||
|
setSessionTTL(val)
|
||||||
|
if (val !== "custom") {
|
||||||
|
updateTTLMutation.mutate(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCustomBlur = () => {
|
||||||
|
const parsed = parseInt(customHours, 10)
|
||||||
|
if (isNaN(parsed) || parsed <= 0) {
|
||||||
|
toast.error("请输入有效的过期小时数(正整数)")
|
||||||
|
// 重置为原本的值
|
||||||
|
const originalVal = configs["login_session_ttl_hours"]?.value || "0"
|
||||||
|
setCustomHours(originalVal === "custom" || ["0", "168", "720", "-1"].includes(originalVal) ? "" : originalVal)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateTTLMutation.mutate(parsed.toString())
|
||||||
|
}
|
||||||
|
|
||||||
const updateConfigMutation = useMutation({
|
const updateConfigMutation = useMutation({
|
||||||
mutationFn: async ({ key, value }: { key: string; value: boolean }) => {
|
mutationFn: async ({ key, value }: { key: string; value: boolean }) => {
|
||||||
const config = configs[key]
|
const config = configs[key]
|
||||||
@@ -183,7 +249,7 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
|
|||||||
<Settings className="size-4" />
|
<Settings className="size-4" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-base font-semibold">系统安全与注册控制</CardTitle>
|
<CardTitle className="text-base font-semibold">系统登录/注册设置</CardTitle>
|
||||||
<CardDescription className="text-xs">配置系统的登录限制与用户自主注册权限</CardDescription>
|
<CardDescription className="text-xs">配置系统的登录限制与用户自主注册权限</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -214,6 +280,58 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{/* 登录状态保持时间 (选择后立即更改) */}
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-between gap-4 rounded-xl border border-dashed p-4 bg-card hover:bg-muted/10 hover:border-indigo-500/30 transition-all duration-300 shadow-sm md:col-span-2"
|
||||||
|
>
|
||||||
|
<div className="space-y-1 pr-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Clock className="size-4 text-indigo-500" />
|
||||||
|
<span className="font-medium text-sm text-foreground">登录状态保持时间</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground leading-relaxed pr-2">
|
||||||
|
配置用户登录会话在浏览器中的保持期限。设置为“关闭”则在浏览器关闭后自动退登。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
|
<Select
|
||||||
|
value={sessionTTL}
|
||||||
|
disabled={updateTTLMutation.isPending}
|
||||||
|
onValueChange={handleTTLChange}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[180px] bg-card border-dashed text-xs h-8">
|
||||||
|
<SelectValue placeholder="选择保留时间" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="0">关闭 (浏览器关闭自动退登)</SelectItem>
|
||||||
|
<SelectItem value="168">7 天</SelectItem>
|
||||||
|
<SelectItem value="720">30 天</SelectItem>
|
||||||
|
<SelectItem value="-1">永不过期</SelectItem>
|
||||||
|
<SelectItem value="custom">自定义时长</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{sessionTTL === "custom" && (
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={customHours}
|
||||||
|
onChange={(e) => setCustomHours(e.target.value)}
|
||||||
|
onBlur={handleCustomBlur}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
handleCustomBlur()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="小时"
|
||||||
|
disabled={updateTTLMutation.isPending}
|
||||||
|
className="w-20 bg-card border-dashed text-xs h-8 px-2"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const expectedDefaultConfigsCount = 28
|
||||||
|
|
||||||
func setupTestRouter(authUser *model.User) *gin.Engine {
|
func setupTestRouter(authUser *model.User) *gin.Engine {
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
@@ -137,9 +139,9 @@ func TestListSystemConfigs(t *testing.T) {
|
|||||||
var configs []model.SystemConfig
|
var configs []model.SystemConfig
|
||||||
_ = json.Unmarshal(dataBytes, &configs)
|
_ = json.Unmarshal(dataBytes, &configs)
|
||||||
|
|
||||||
// Defaults seed 27 configurations
|
// Defaults seed configurations
|
||||||
if len(configs) != 27 {
|
if len(configs) != expectedDefaultConfigsCount {
|
||||||
t.Errorf("expected 27 default configs, got %d", len(configs))
|
t.Errorf("expected %d default configs, got %d", expectedDefaultConfigsCount, len(configs))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -165,10 +165,24 @@ func containsScope(scopes []string, scope string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func setLoginSession(c *gin.Context, user *model.User) error {
|
func setLoginSession(ctx context.Context, c *gin.Context, user *model.User) error {
|
||||||
session := sessions.Default(c)
|
session := sessions.Default(c)
|
||||||
session.Set(UserIDKey, user.ID)
|
session.Set(UserIDKey, user.ID)
|
||||||
session.Set(UserNameKey, user.Username)
|
session.Set(UserNameKey, user.Username)
|
||||||
|
|
||||||
|
// 根据系统配置动态设置 Session 过期时间
|
||||||
|
maxAge := 0
|
||||||
|
ttlHours, err := model.GetIntByKey(ctx, model.ConfigKeyLoginSessionTTLHours)
|
||||||
|
if err == nil {
|
||||||
|
if ttlHours == -1 {
|
||||||
|
// 永不过期,设置为 10 年
|
||||||
|
maxAge = 10 * 365 * 24 * 3600
|
||||||
|
} else if ttlHours > 0 {
|
||||||
|
maxAge = ttlHours * 3600
|
||||||
|
}
|
||||||
|
}
|
||||||
|
session.Options(util.GetSessionOptions(maxAge))
|
||||||
|
|
||||||
return session.Save()
|
return session.Save()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,7 +522,7 @@ func handleCallbackLogin(ctx context.Context, c *gin.Context, source *model.Auth
|
|||||||
|
|
||||||
user.LastLoginAt = time.Now()
|
user.LastLoginAt = time.Now()
|
||||||
_ = db.DB(ctx).Model(&user).Update("last_login_at", user.LastLoginAt).Error
|
_ = db.DB(ctx).Model(&user).Update("last_login_at", user.LastLoginAt).Error
|
||||||
if err := setLoginSession(c, &user); err != nil {
|
if err := setLoginSession(ctx, c, &user); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
c.JSON(http.StatusInternalServerError, util.Err(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,10 +58,24 @@ func isRegistrationEnabled() bool {
|
|||||||
return enabled
|
return enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
func setLoginSession(c *gin.Context, user *model.User) error {
|
func setLoginSession(ctx context.Context, c *gin.Context, user *model.User) error {
|
||||||
session := sessions.Default(c)
|
session := sessions.Default(c)
|
||||||
session.Set(oauth.UserIDKey, user.ID)
|
session.Set(oauth.UserIDKey, user.ID)
|
||||||
session.Set(oauth.UserNameKey, user.Username)
|
session.Set(oauth.UserNameKey, user.Username)
|
||||||
|
|
||||||
|
// 根据系统配置动态设置 Session 过期时间
|
||||||
|
maxAge := 0
|
||||||
|
ttlHours, err := model.GetIntByKey(ctx, model.ConfigKeyLoginSessionTTLHours)
|
||||||
|
if err == nil {
|
||||||
|
if ttlHours == -1 {
|
||||||
|
// 永不过期,设置为 10 年
|
||||||
|
maxAge = 10 * 365 * 24 * 3600
|
||||||
|
} else if ttlHours > 0 {
|
||||||
|
maxAge = ttlHours * 3600
|
||||||
|
}
|
||||||
|
}
|
||||||
|
session.Options(util.GetSessionOptions(maxAge))
|
||||||
|
|
||||||
if err := session.Save(); err != nil {
|
if err := session.Save(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -134,7 +148,7 @@ func Login(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, util.Err(err.Error()))
|
c.JSON(http.StatusOK, util.Err(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := setLoginSession(c, &user); err != nil {
|
if err := setLoginSession(ctx, c, &user); err != nil {
|
||||||
c.JSON(http.StatusOK, util.Err(errSaveSessionFailed))
|
c.JSON(http.StatusOK, util.Err(errSaveSessionFailed))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -218,7 +232,7 @@ func Register(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := setLoginSession(c, &user); err != nil {
|
if err := setLoginSession(ctx, c, &user); err != nil {
|
||||||
c.JSON(http.StatusOK, util.Err(errSaveSessionFailed))
|
c.JSON(http.StatusOK, util.Err(errSaveSessionFailed))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- +goose Up
|
||||||
|
INSERT INTO w_system_configs (key, value, type, visibility, description, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
('login_session_ttl_hours', '0', 'system', 0, '登录会话过期时间 (小时,0表示浏览器关闭后自动退出,-1表示永不过期)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT (key) DO NOTHING;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DELETE FROM w_system_configs WHERE key = 'login_session_ttl_hours';
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- +goose Up
|
||||||
|
INSERT INTO w_system_configs (key, value, type, visibility, description, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
('login_session_ttl_hours', '0', 'system', 0, '登录会话过期时间 (小时,0表示浏览器关闭后自动退出,-1表示永不过期)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT (key) DO NOTHING;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DELETE FROM w_system_configs WHERE key = 'login_session_ttl_hours';
|
||||||
@@ -16,6 +16,8 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const expectedMigratedSystemConfigCount = 28
|
||||||
|
|
||||||
func TestMigrateInitializesSQLiteDatabase(t *testing.T) {
|
func TestMigrateInitializesSQLiteDatabase(t *testing.T) {
|
||||||
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
sqliteDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||||
DisableForeignKeyConstraintWhenMigrating: true,
|
DisableForeignKeyConstraintWhenMigrating: true,
|
||||||
@@ -38,8 +40,8 @@ func TestMigrateInitializesSQLiteDatabase(t *testing.T) {
|
|||||||
if err := sqliteDB.Table("w_system_configs").Count(&systemConfigCount).Error; err != nil {
|
if err := sqliteDB.Table("w_system_configs").Count(&systemConfigCount).Error; err != nil {
|
||||||
t.Fatalf("Migrate() count w_system_configs error = %v", err)
|
t.Fatalf("Migrate() count w_system_configs error = %v", err)
|
||||||
}
|
}
|
||||||
if systemConfigCount != 27 {
|
if systemConfigCount != expectedMigratedSystemConfigCount {
|
||||||
t.Errorf("Migrate() w_system_configs count = %d, want %d", systemConfigCount, 27)
|
t.Errorf("Migrate() w_system_configs count = %d, want %d", systemConfigCount, expectedMigratedSystemConfigCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
var adminCount int64
|
var adminCount int64
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const (
|
|||||||
ConfigKeyDiskCacheMaxSizeMB = "disk_cache_max_size_mb" // 磁盘缓存最大空间大小 (MB)
|
ConfigKeyDiskCacheMaxSizeMB = "disk_cache_max_size_mb" // 磁盘缓存最大空间大小 (MB)
|
||||||
ConfigKeyDiskCacheTTLMinutes = "disk_cache_ttl_minutes" // 磁盘缓存默认有效期 (分钟)
|
ConfigKeyDiskCacheTTLMinutes = "disk_cache_ttl_minutes" // 磁盘缓存默认有效期 (分钟)
|
||||||
ConfigKeyDiskCacheLRUEnabled = "disk_cache_lru_enabled" // 是否启用 LRU 淘汰机制
|
ConfigKeyDiskCacheLRUEnabled = "disk_cache_lru_enabled" // 是否启用 LRU 淘汰机制
|
||||||
|
ConfigKeyLoginSessionTTLHours = "login_session_ttl_hours" // 登录会话过期时间 (小时,0表示浏览器关闭后自动退出登录,-1表示永不过期)
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -249,6 +249,12 @@ func getSeedConfigsPart2() []model.SystemConfig {
|
|||||||
Type: configTypeSystem,
|
Type: configTypeSystem,
|
||||||
Description: "是否启用 LRU 淘汰机制",
|
Description: "是否启用 LRU 淘汰机制",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Key: model.ConfigKeyLoginSessionTTLHours,
|
||||||
|
Value: "0",
|
||||||
|
Type: configTypeSystem,
|
||||||
|
Description: "登录会话过期时间 (小时,0表示浏览器关闭后自动退出,-1表示永不过期)",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user