update pwd change
This commit is contained in:
parent
b91daad8ad
commit
47b6f4903f
13 changed files with 572 additions and 90 deletions
24
api/api.go
24
api/api.go
|
|
@ -31,6 +31,24 @@ func ApiGroup(v0 touka.IRouter, cdb *db.ConfigDB, cfg *config.Config) {
|
|||
|
||||
api.GET("/config/templates", GetTemplates(cdb)) // 获取可用模板名称
|
||||
|
||||
api.GET("/config/headers-presets", func(c *touka.Context) {
|
||||
c.JSON(200, GetHeaderSetMetadataList())
|
||||
})
|
||||
|
||||
api.GET("/config/headers-presets/:name", func(c *touka.Context) {
|
||||
presetName := c.Param("name")
|
||||
if presetName == "" {
|
||||
c.JSON(400, touka.H{"error": "presetName is required"})
|
||||
return
|
||||
}
|
||||
preset, found := GetHeaderSetByID(presetName)
|
||||
if !found {
|
||||
c.JSON(404, touka.H{"error": "preset not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(200, preset)
|
||||
})
|
||||
|
||||
// caddy实例相关
|
||||
api.POST("/caddy/stop", apic.StopCaddy()) // 无需payload
|
||||
api.POST("/caddy/run", apic.StartCaddy(cfg))
|
||||
|
|
@ -46,10 +64,7 @@ func ApiGroup(v0 touka.IRouter, cdb *db.ConfigDB, cfg *config.Config) {
|
|||
AuthLogout(c)
|
||||
})
|
||||
auth.GET("/logout", func(c *touka.Context) {
|
||||
// 重定向到/
|
||||
c.Redirect(302, "/")
|
||||
c.Abort()
|
||||
return
|
||||
AuthLogout(c)
|
||||
})
|
||||
auth.GET("/init", func(c *touka.Context) {
|
||||
// 返回是否init管理员
|
||||
|
|
@ -76,6 +91,7 @@ func ApiGroup(v0 touka.IRouter, cdb *db.ConfigDB, cfg *config.Config) {
|
|||
}
|
||||
c.JSON(200, touka.H{"message": "admin initialized"})
|
||||
})
|
||||
auth.POST("resetpwd", ResetPassword(cdb))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
74
api/auth.go
74
api/auth.go
|
|
@ -13,12 +13,13 @@ import (
|
|||
|
||||
var (
|
||||
exactMatchPaths = map[string]struct{}{
|
||||
"/login": {},
|
||||
"/login.html": {},
|
||||
"/v0/api/auth/login": {},
|
||||
"/v0/api/auth/init": {},
|
||||
"/init.html": {},
|
||||
"/favicon.ico": {},
|
||||
"/login": {},
|
||||
"/login.html": {},
|
||||
"/v0/api/auth/login": {},
|
||||
"/v0/api/auth/logout": {},
|
||||
"/v0/api/auth/init": {},
|
||||
"/init.html": {},
|
||||
"/favicon.ico": {},
|
||||
}
|
||||
prefixMatchPaths = []string{ // 保持前缀匹配,因为数量少
|
||||
"/js/",
|
||||
|
|
@ -98,6 +99,7 @@ func AuthLogin(c *touka.Context, cfg *config.Config, cdb *db.ConfigDB) {
|
|||
c.JSON(http.StatusBadRequest, touka.H{"error": "Need username and password"})
|
||||
return
|
||||
}
|
||||
c.Infof("user login: %s password: %s", username, password)
|
||||
|
||||
// 验证账户密码
|
||||
pass, err := user.CheckLogin(username, password, cdb)
|
||||
|
|
@ -119,9 +121,65 @@ func AuthLogin(c *touka.Context, cfg *config.Config, cdb *db.ConfigDB) {
|
|||
}
|
||||
|
||||
func AuthLogout(c *touka.Context) {
|
||||
|
||||
session := sessions.Default(c)
|
||||
session.Clear()
|
||||
session.Set("authenticated", false)
|
||||
session.Save()
|
||||
session.Clear()
|
||||
err := session.Save()
|
||||
if err != nil {
|
||||
c.Errorf("Failed to save session: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, touka.H{"error": "Failed to save session"})
|
||||
return
|
||||
}
|
||||
c.Redirect(http.StatusFound, "/login.html")
|
||||
|
||||
}
|
||||
|
||||
func ResetPassword(cdb *db.ConfigDB) touka.HandlerFunc {
|
||||
return func(c *touka.Context) {
|
||||
username := c.PostForm("username")
|
||||
oldPassword := c.PostForm("old_password")
|
||||
newPassword := c.PostForm("new_password")
|
||||
// 验证是否为空
|
||||
if username == "" || oldPassword == "" || newPassword == "" {
|
||||
c.JSON(400, touka.H{"error": "username and password are required"})
|
||||
return
|
||||
}
|
||||
// 验证用户是否存在
|
||||
exist, err := cdb.IsUserExists(username)
|
||||
if err != nil {
|
||||
c.JSON(500, touka.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
//不正确的参数
|
||||
c.JSON(400, touka.H{"error": "user not exist"})
|
||||
return
|
||||
}
|
||||
// 是否可以重置
|
||||
ok, err := user.CheckLogin(username, oldPassword, cdb)
|
||||
if err != nil {
|
||||
c.JSON(500, touka.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
// 错误的密码
|
||||
c.JSON(400, touka.H{"error": "current password is not correct"})
|
||||
return
|
||||
}
|
||||
// 更新密码
|
||||
hashpwd, err := user.HashPassword(newPassword)
|
||||
if err != nil {
|
||||
c.Errorf("Failed to hash password: %v", err)
|
||||
c.JSON(500, touka.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
err = cdb.UpdateUserPassword(username, hashpwd)
|
||||
if err != nil {
|
||||
c.JSON(500, touka.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
// 进行logout
|
||||
AuthLogout(c)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
112
api/headers_presets.go
Normal file
112
api/headers_presets.go
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
package api
|
||||
|
||||
// HeaderSet 定义了一个可复用的、完整的HTTP头预设。
|
||||
// JSON标签用于API响应的序列化。
|
||||
type HeaderSet struct {
|
||||
ID string `json:"id"` // 唯一ID, e.g., "real_ip_cloudflare"
|
||||
Name string `json:"name"` // UI上显示的名称 (为简化, 此处不使用i18n key)
|
||||
Description string `json:"description"` // UI上的提示文本
|
||||
Target string `json:"target"` // 目标: "global" 或 "upstream"
|
||||
Headers map[string][]string `json:"headers"` // 预设的请求头键值对
|
||||
}
|
||||
|
||||
// HeaderSetMetadata 是HeaderSet的轻量级版本, 用于在列表中显示, 不包含具体的Headers数据。
|
||||
type HeaderSetMetadata struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
|
||||
// registry 是一个私有变量, 作为所有Header预设的“数据库”。
|
||||
// 在这里添加、修改或删除预设。
|
||||
var registry = []HeaderSet{
|
||||
{
|
||||
ID: "real_ip_cloudflare",
|
||||
Name: "Cloudflare 真实IP",
|
||||
Description: "添加从Cloudflare获取真实客户端IP所需的请求头。适用于通过Cloudflare代理的流量。",
|
||||
Target: "upstream", // 此预设仅适用于上游请求头
|
||||
Headers: map[string][]string{
|
||||
"X-Forwarded-For": {"{http.request.header.CF-Connecting-IP}"},
|
||||
"X-Forwarded-Proto": {"{http.request.header.CF-Visitor}"},
|
||||
"X-Real-IP": {"{http.request.header.CF-Connecting-IP}"},
|
||||
},
|
||||
},
|
||||
// 真实IP-直接
|
||||
{
|
||||
ID: "real_ip_direct",
|
||||
Name: "真实IP (直接)",
|
||||
Description: "当Caddy直接暴露在互联网上时, 使用此预设获取客户端真实IP。",
|
||||
Target: "upstream",
|
||||
Headers: map[string][]string{
|
||||
"X-Forwarded-For": {"{http.request.remote.host}"},
|
||||
"X-Forwarded-Proto": {"{http.request.scheme}"},
|
||||
"X-Real-IP": {"{http.request.remote.host}"},
|
||||
},
|
||||
},
|
||||
// 真实IP-中间层
|
||||
{
|
||||
ID: "real_ip_intermediate",
|
||||
Name: "真实IP (中间层)",
|
||||
Description: "当Caddy位于反向代理或负载均衡器之后时, 使用此预设获取客户端真实IP。",
|
||||
Target: "upstream",
|
||||
Headers: map[string][]string{
|
||||
"X-Forwarded-For": {"{http.request.header.X-Forwarded-For}"},
|
||||
"X-Forwarded-Proto": {"{http.request.header.X-Forwarded-Proto}"},
|
||||
"X-Real-IP": {"{http.request.header.X-Real-IP}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "common_security_headers",
|
||||
Name: "通用安全响应头",
|
||||
Description: "添加一系列推荐的HTTP安全头以增强站点安全性 (HSTS, X-Frame-Options等)。",
|
||||
Target: "global", // 此预设适用于全局响应头
|
||||
Headers: map[string][]string{
|
||||
"Strict-Transport-Security": {"max-age=31536000; includeSubDomains; preload"},
|
||||
"X-Frame-Options": {"SAMEORIGIN"},
|
||||
"X-Content-Type-Options": {"nosniff"},
|
||||
"Referrer-Policy": {"strict-origin-when-cross-origin"},
|
||||
"Permissions-Policy": {"geolocation=(), microphone=()"},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "cors_allow_all",
|
||||
Name: "CORS (允许所有来源)",
|
||||
Description: "添加允许所有来源跨域请求的响应头。警告: 生产环境请谨慎使用。",
|
||||
Target: "global",
|
||||
Headers: map[string][]string{
|
||||
"Access-Control-Allow-Origin": {"*"},
|
||||
"Access-Control-Allow-Methods": {"GET, POST, PUT, DELETE, OPTIONS"},
|
||||
"Access-Control-Allow-Headers": {"Content-Type, Authorization, X-Requested-With"},
|
||||
"Access-Control-Allow-Credentials": {"true"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// GetHeaderSetMetadataList 返回所有可用预设的元数据列表。
|
||||
// 这个函数是线程安全的, 因为它只读取全局只读变量 registry。
|
||||
func GetHeaderSetMetadataList() []HeaderSetMetadata {
|
||||
metadata := make([]HeaderSetMetadata, len(registry))
|
||||
for i, set := range registry {
|
||||
metadata[i] = HeaderSetMetadata{
|
||||
ID: set.ID,
|
||||
Name: set.Name,
|
||||
Description: set.Description,
|
||||
Target: set.Target,
|
||||
}
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
|
||||
// GetHeaderSetByID 通过其唯一ID查找并返回一个完整的预设。
|
||||
// 返回找到的预设和一个布尔值, 表示是否找到。
|
||||
func GetHeaderSetByID(id string) (*HeaderSet, bool) {
|
||||
for _, set := range registry {
|
||||
if set.ID == id {
|
||||
// 返回该结构体的副本指针, 避免外部修改全局变量
|
||||
foundSet := set
|
||||
return &foundSet, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue