feat: add api token call
This commit is contained in:
@@ -38,8 +38,9 @@ const (
|
||||
KeyTitle = "title"
|
||||
KeySubtitle = "subtitle"
|
||||
KeyIcon = "icon"
|
||||
KeyPageSize = "page_size"
|
||||
KeyCookieDays = "cookie_days"
|
||||
KeyPageSize = "page_size"
|
||||
KeyCookieDays = "cookie_days"
|
||||
KeyApiToken = "api_token"
|
||||
|
||||
// System Settings Key 常量
|
||||
KeyInitialized = "initialized"
|
||||
|
||||
@@ -5,8 +5,10 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/engigu/baihu-panel/internal/constant"
|
||||
@@ -82,6 +84,16 @@ func (sc *SettingsController) ChangePassword(c *gin.Context) {
|
||||
// GetSiteSettings 获取站点设置
|
||||
func (sc *SettingsController) GetSiteSettings(c *gin.Context) {
|
||||
settings := sc.settingsService.GetSection(constant.SectionSite)
|
||||
|
||||
// 解析 JSON 格式的 API Token
|
||||
if tokenJson, ok := settings[constant.KeyApiToken]; ok && tokenJson != "" {
|
||||
var tokenData map[string]string
|
||||
if err := json.Unmarshal([]byte(tokenJson), &tokenData); err == nil {
|
||||
settings["api_token"] = tokenData["token"]
|
||||
settings["api_token_expire"] = tokenData["expire_at"]
|
||||
}
|
||||
}
|
||||
|
||||
utils.Success(c, settings)
|
||||
}
|
||||
|
||||
@@ -100,11 +112,13 @@ func (sc *SettingsController) GetPublicSiteSettings(c *gin.Context) {
|
||||
// UpdateSiteSettings 更新站点设置
|
||||
func (sc *SettingsController) UpdateSiteSettings(c *gin.Context) {
|
||||
var req struct {
|
||||
Title string `json:"title"`
|
||||
Subtitle string `json:"subtitle"`
|
||||
Icon string `json:"icon"`
|
||||
PageSize string `json:"page_size"`
|
||||
CookieDays string `json:"cookie_days"`
|
||||
Title string `json:"title"`
|
||||
Subtitle string `json:"subtitle"`
|
||||
Icon string `json:"icon"`
|
||||
PageSize string `json:"page_size"`
|
||||
CookieDays string `json:"cookie_days"`
|
||||
ApiToken string `json:"api_token"`
|
||||
ApiTokenExpire string `json:"api_token_expire"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -112,12 +126,24 @@ func (sc *SettingsController) UpdateSiteSettings(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
apiTokenJson := ""
|
||||
if req.ApiToken != "" || req.ApiTokenExpire != "" {
|
||||
tokenData := map[string]string{
|
||||
"token": req.ApiToken,
|
||||
"expire_at": req.ApiTokenExpire,
|
||||
}
|
||||
if b, err := json.Marshal(tokenData); err == nil {
|
||||
apiTokenJson = string(b)
|
||||
}
|
||||
}
|
||||
|
||||
values := map[string]string{
|
||||
constant.KeyTitle: req.Title,
|
||||
constant.KeySubtitle: req.Subtitle,
|
||||
constant.KeyIcon: req.Icon,
|
||||
constant.KeyPageSize: req.PageSize,
|
||||
constant.KeyCookieDays: req.CookieDays,
|
||||
constant.KeyApiToken: apiTokenJson,
|
||||
}
|
||||
|
||||
if err := sc.settingsService.SetSection(constant.SectionSite, values); err != nil {
|
||||
@@ -128,6 +154,13 @@ func (sc *SettingsController) UpdateSiteSettings(c *gin.Context) {
|
||||
utils.SuccessMsg(c, "保存成功")
|
||||
}
|
||||
|
||||
// GenerateApiToken 随机生成API Token
|
||||
func (sc *SettingsController) GenerateApiToken(c *gin.Context) {
|
||||
utils.Success(c, gin.H{
|
||||
"token": strings.ToLower(utils.RandomString(32)),
|
||||
})
|
||||
}
|
||||
|
||||
// GetSchedulerSettings 获取调度设置
|
||||
func (sc *SettingsController) GetSchedulerSettings(c *gin.Context) {
|
||||
settings := sc.settingsService.GetSection(constant.SectionScheduler)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/engigu/baihu-panel/internal/constant"
|
||||
"github.com/engigu/baihu-panel/internal/services"
|
||||
"github.com/engigu/baihu-panel/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -9,7 +13,13 @@ import (
|
||||
|
||||
// AuthRequired 认证中间件
|
||||
func AuthRequired() gin.HandlerFunc {
|
||||
settingsSvc := services.NewSettingsService()
|
||||
return func(c *gin.Context) {
|
||||
// 校验 API Token (实验特性)
|
||||
if checkApiToken(c, settingsSvc) {
|
||||
return
|
||||
}
|
||||
|
||||
token, err := c.Cookie(constant.CookieName)
|
||||
if err != nil || token == "" {
|
||||
utils.Unauthorized(c, "请先登录")
|
||||
@@ -32,6 +42,49 @@ func AuthRequired() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// checkApiToken 校验 API Token (实验特性,后续可能移除或重构)
|
||||
// 返回 true 表示校验通过并已放行请求
|
||||
func checkApiToken(c *gin.Context, settingsSvc *services.SettingsService) bool {
|
||||
apiToken := c.GetHeader("X-API-Token")
|
||||
if apiToken == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
siteConfig := settingsSvc.GetSection(constant.SectionSite)
|
||||
tokenJson, ok := siteConfig[constant.KeyApiToken]
|
||||
if !ok || tokenJson == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
var tokenData map[string]string
|
||||
if err := json.Unmarshal([]byte(tokenJson), &tokenData); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
expectedToken, ok := tokenData["token"]
|
||||
if !ok || expectedToken == "" || apiToken != expectedToken {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查过期时间
|
||||
if expireStr, ok := tokenData["expire_at"]; ok && expireStr != "" {
|
||||
// 前端传来的时间格式是 YYYY-MM-DD,使用 2006-01-02 解析
|
||||
expireDate, err := time.Parse("2006-01-02", expireStr)
|
||||
if err == nil {
|
||||
// 将过期时间设为当天的 23:59:59
|
||||
expireDate = expireDate.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
|
||||
if time.Now().After(expireDate) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.Set("userID", uint(1)) // 模拟 Admin 角色
|
||||
c.Set("username", "api_token_user")
|
||||
c.Next()
|
||||
return true
|
||||
}
|
||||
|
||||
// SetAuthCookie 设置认证 Cookie,expireDays 为过期天数
|
||||
func SetAuthCookie(c *gin.Context, token string, expireDays int) {
|
||||
maxAge := 86400 * expireDays
|
||||
|
||||
@@ -189,6 +189,7 @@ func Setup(c *Controllers) *gin.Engine {
|
||||
settings.POST("/password", c.Settings.ChangePassword)
|
||||
settings.GET("/site", c.Settings.GetSiteSettings)
|
||||
settings.PUT("/site", c.Settings.UpdateSiteSettings)
|
||||
settings.POST("/site/api-token/generate", c.Settings.GenerateApiToken)
|
||||
settings.GET("/paths", c.Settings.GetPaths)
|
||||
settings.GET("/scheduler", c.Settings.GetSchedulerSettings)
|
||||
settings.PUT("/scheduler", c.Settings.UpdateSchedulerSettings)
|
||||
|
||||
@@ -125,6 +125,7 @@ export const api = {
|
||||
getPublicSite: () => request<{ title: string; subtitle: string; icon: string; demo_mode: boolean }>('/settings/public'),
|
||||
updateSite: (data: SiteSettings) =>
|
||||
request('/settings/site', { method: 'PUT', body: JSON.stringify(data) }),
|
||||
generateApiToken: () => request<{ token: string }>('/settings/site/api-token/generate', { method: 'POST' }),
|
||||
getScheduler: () => request<SchedulerSettings>('/settings/scheduler'),
|
||||
updateScheduler: (data: SchedulerSettings) =>
|
||||
request('/settings/scheduler', { method: 'PUT', body: JSON.stringify(data) }),
|
||||
@@ -401,6 +402,8 @@ export interface SiteSettings {
|
||||
icon: string
|
||||
page_size: string
|
||||
cookie_days: string
|
||||
api_token?: string
|
||||
api_token_expire?: string
|
||||
}
|
||||
|
||||
export interface SchedulerSettings {
|
||||
|
||||
@@ -6,6 +6,8 @@ import { Button } from '@/components/ui/button'
|
||||
import { api, type SiteSettings } from '@/api'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { useSiteSettings } from '@/composables/useSiteSettings'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { RefreshCw, Copy } from 'lucide-vue-next'
|
||||
|
||||
const { refreshSettings } = useSiteSettings()
|
||||
|
||||
@@ -14,7 +16,9 @@ const form = ref<SiteSettings>({
|
||||
subtitle: '',
|
||||
icon: '',
|
||||
page_size: '10',
|
||||
cookie_days: '7'
|
||||
cookie_days: '7',
|
||||
api_token: '',
|
||||
api_token_expire: ''
|
||||
})
|
||||
const loading = ref(false)
|
||||
|
||||
@@ -51,6 +55,32 @@ async function saveSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
async function generateToken() {
|
||||
try {
|
||||
const res = await api.settings.generateApiToken()
|
||||
form.value.api_token = res.token
|
||||
|
||||
// 如果没有设置过期时间,默认给一年后
|
||||
if (!form.value.api_token_expire) {
|
||||
const d = new Date()
|
||||
d.setFullYear(d.getFullYear() + 1)
|
||||
form.value.api_token_expire = d.toISOString().split('T')[0]
|
||||
}
|
||||
} catch {
|
||||
toast.error('生成 Token 失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function copyToken() {
|
||||
if (!form.value.api_token) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(form.value.api_token)
|
||||
toast.success('Token 已复制到剪贴板')
|
||||
} catch {
|
||||
toast.error('复制失败,请手动复制')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadSettings)
|
||||
</script>
|
||||
|
||||
@@ -84,6 +114,35 @@ onMounted(loadSettings)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-6 border-t mt-6">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<h3 class="text-lg font-medium text-foreground">API Token</h3>
|
||||
<Badge variant="secondary" class="font-normal text-xs bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20">实验特性,可能变更</Badge>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground mb-4">开启全局 API 直接访问能力,配置后可通过请求头 <code class="bg-muted px-1.5 py-0.5 rounded text-xs select-all font-sans">X-API-Token: <在此生成的Token></code> 无需登录直接调用系统的所有接口,请妥善保管并设置合理的有效期。</p>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-4 items-center gap-2 sm:gap-4 mb-4">
|
||||
<Label class="sm:text-right text-muted-foreground">Token 密钥</Label>
|
||||
<div class="sm:col-span-3 flex w-full max-w-sm items-center space-x-2">
|
||||
<Input v-model="form.api_token" placeholder="点击右侧按钮生成 32 位随机 Token" class="text-sm" />
|
||||
<Button type="button" variant="outline" size="icon" @click="generateToken" title="随机生成">
|
||||
<RefreshCw class="w-4 h-4" />
|
||||
</Button>
|
||||
<Button type="button" variant="outline" size="icon" @click="copyToken" title="复制" :disabled="!form.api_token">
|
||||
<Copy class="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-4 items-center gap-2 sm:gap-4">
|
||||
<Label class="sm:text-right text-muted-foreground">截止有效期</Label>
|
||||
<div class="sm:col-span-3">
|
||||
<Input v-model="form.api_token_expire" type="date" class="w-full max-w-xs dark:[color-scheme:dark]" />
|
||||
<div class="text-xs text-muted-foreground mt-1.5 ml-1">超过此日期后该 Token 将失效,置空代表该特性完全关闭。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end pt-2">
|
||||
<Button @click="saveSettings" :disabled="loading">
|
||||
{{ loading ? '保存中...' : '保存设置' }}
|
||||
|
||||
Reference in New Issue
Block a user