package authser import ( "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "strings" "time" "videoweb/app/service/upstream" "videoweb/pub/common/useragent" "github.com/gin-gonic/gin" ) const ( registerPath = "/api/app/auth/register" loginPath = "/api/app/auth/login" resetPath = "/api/app/auth/resetPassword" guestPath = "/api/app/auth/guest" getInfoPath = "/api/app/auth/getInfo" profilePath = "/api/app/auth/profile" uploadPath = "/api/app/upload/image" cookieMaxAge = 7 * 24 * 3600 ) // Register 调用上游注册接口。 func Register(c *gin.Context, req RegisterReq) (*APIEnvelope, *LoginResp, error) { var env APIEnvelope if _, err := upstream.PostJSON(c, registerPath, req, &env); err != nil { return nil, nil, err } if env.Code != 200 { return &env, nil, nil } user, err := decodeLoginData(env.Data) if err != nil { return &env, nil, err } return &env, user, nil } // ResetPassword 通过账号和邮箱校验后重置密码。 func ResetPassword(c *gin.Context, req ResetPasswordReq) (*APIEnvelope, error) { var env APIEnvelope if _, err := upstream.PostJSON(c, resetPath, req, &env); err != nil { return nil, err } return &env, nil } // Login 调用上游登录接口。 func Login(c *gin.Context, req LoginReq) (*APIEnvelope, *LoginResp, error) { var env APIEnvelope if _, err := upstream.PostJSON(c, loginPath, req, &env); err != nil { return nil, nil, err } if env.Code != 200 { return &env, nil, nil } user, err := decodeLoginData(env.Data) if err != nil { return &env, nil, err } return &env, user, nil } // GuestLogin 调用上游游客登录(按设备 ID 创建或找回游客账号)。 func GuestLogin(c *gin.Context, req GuestLoginReq) (*APIEnvelope, *LoginResp, error) { var env APIEnvelope if _, err := upstream.PostJSON(c, guestPath, req, &env); err != nil { return nil, nil, err } if env.Code != 200 { return &env, nil, nil } user, err := decodeLoginData(env.Data) if err != nil { return &env, nil, err } return &env, user, nil } // GetInfo 通过 token 拉取最新用户信息。 func GetInfo(c *gin.Context) (*APIEnvelope, *LoginResp, error) { var env APIEnvelope if err := upstream.GetJSON(c, getInfoPath, &env); err != nil { return nil, nil, err } if env.Code != 200 { return &env, nil, nil } user, err := decodeLoginData(env.Data) if err != nil { return &env, nil, err } return &env, user, nil } // UpdateProfile 修改资料(昵称 / 头像等)。 func UpdateProfile(c *gin.Context, req UpdateProfileReq) (*APIEnvelope, error) { var env APIEnvelope if _, err := upstream.PostJSON(c, profilePath, req, &env); err != nil { return nil, err } return &env, nil } // UploadImage 上传图片;头像场景不加水印。 func UploadImage(c *gin.Context, fileName string, r io.Reader) (*ImageUploadResp, error) { var resp ImageUploadResp _, err := upstream.PostMultipart(c, uploadPath, map[string]string{ "watermark": "0", }, &upstream.MultipartFile{ FieldName: "file", FileName: fileName, Reader: r, }, &resp) if err != nil { return nil, err } return &resp, nil } func decodeLoginData(raw json.RawMessage) (*LoginResp, error) { if len(raw) == 0 || string(raw) == "null" || string(raw) == `""` { return nil, fmt.Errorf("empty login data") } var user LoginResp if err := json.Unmarshal(raw, &user); err != nil { return nil, err } return &user, nil } // SetAuthCookies 只持久化 SSR 与前端登录态实际依赖的字段。 // 个人简介、邮箱、邀请码、展示卡信息由 getInfo 即时提供,不再随每个页面响应重复写入; // sid 已存在于请求时也无需原样回写。ClearAuthCookies 仍保留这些历史键用于兼容清理。 func SetAuthCookies(c *gin.Context, user *LoginResp) { if c == nil || user == nil { return } secure := c.Request != nil && c.Request.TLS != nil if strings.TrimSpace(user.Token) != "" { setCookie(c, "token", user.Token, secure) } setCookie(c, "userId", strconv.FormatInt(user.UserID, 10), secure) setCookie(c, "accountNo", user.AccountNo, secure) setCookie(c, "nickName", user.NickName, secure) setCookie(c, "avatar", user.Avatar, secure) setCookie(c, "recharge", strconv.FormatInt(user.Recharge, 10), secure) setCookie(c, "vipType", strconv.Itoa(user.VipType), secure) setCookie(c, "currentVipExpire", user.CurrentVipExpire, secure) setCookie(c, "userType", strconv.Itoa(user.Type), secure) isReg := "0" if user.IsRegister || user.Type == 1 { isReg = "1" } setCookie(c, "isRegister", isReg, secure) // 后续 SSR 服务必须能立即读取本次刚建立/刷新的会话;同时记录为本请求的 // 权威结果,避免 HTML 渲染阶段再次调用 getInfo 并重复写整套 Cookie。 patchAuthCookiesToRequest(c, user) rememberRequestAuthUser(c, user) } func setCookie(c *gin.Context, name, value string, secure bool) { // Cookie Value 仅允许 ASCII;中文昵称等需 URL 编码。 // 使用 PathEscape(空格→%20),与前端 encodeURIComponent 一致; // 避免 QueryEscape 的空格→+,否则 JS decodeURIComponent 会留下字面量 "+"。 writeCookie(c, &http.Cookie{ Name: name, Value: url.PathEscape(value), Path: "/", MaxAge: cookieMaxAge, HttpOnly: false, Secure: secure, SameSite: http.SameSiteLaxMode, Expires: time.Now().Add(7 * 24 * time.Hour), }) } // writeCookie 对同一响应中同名、同 Path、同 Domain 的 Cookie 做 upsert,避免会话恢复的 // 「先清除、再重建」产生两套响应头,同时保留其他作用域下可能存在的同名 Cookie。 func writeCookie(c *gin.Context, cookie *http.Cookie) { if c == nil || c.Writer == nil || cookie == nil || cookie.Name == "" { return } header := c.Writer.Header() existing := header.Values("Set-Cookie") if len(existing) > 0 { header.Del("Set-Cookie") for _, raw := range existing { if sameCookieScope(raw, cookie) { continue } header.Add("Set-Cookie", raw) } } http.SetCookie(c.Writer, cookie) } func sameCookieScope(raw string, target *http.Cookie) bool { response := &http.Response{Header: http.Header{"Set-Cookie": []string{raw}}} for _, existing := range response.Cookies() { if existing.Name == target.Name && existing.Path == target.Path && strings.EqualFold(existing.Domain, target.Domain) { return true } } return false } func cookieValue(c *gin.Context, name string) string { ck, err := c.Request.Cookie(name) if err != nil || ck == nil { return "" } raw := strings.TrimSpace(ck.Value) if raw == "" { return "" } // QueryUnescape 同时兼容历史 QueryEscape(+)与 PathEscape(%20) if decoded, err := url.QueryUnescape(raw); err == nil { return strings.TrimSpace(decoded) } return raw } // ClearAuthCookies 清除登录相关 cookie。 func ClearAuthCookies(c *gin.Context) { forgetRequestAuthUser(c) for _, name := range authCookieNames() { writeCookie(c, &http.Cookie{ Name: name, Value: "", Path: "/", MaxAge: -1, HttpOnly: false, SameSite: http.SameSiteLaxMode, }) } } func authCookieNames() []string { return []string{ "token", "_token_", "sid", "_s_token_", "userId", "accountNo", "nickName", "introduction", "avatar", "email", "inviteCode", "recharge", "vipType", "currentVipExpire", "showCardId", "showCardName", "userType", "isRegister", } } // AuthFromCookies 从 cookie 读取登录态,供 SSR 头部 / 个人中心展示。 func AuthFromCookies(c *gin.Context) (loggedIn bool, userName, avatar, email string) { token := useragent.TokenFromRequest(c.Request) if token == "" { return false, "", "", "" } userName = cookieValue(c, "nickName") if userName == "" { userName = cookieValue(c, "accountNo") } avatar = usableAvatarURL(cookieValue(c, "avatar")) email = cookieValue(c, "email") return true, userName, avatar, email } // IsRegisteredFromCookies 是否正式注册账号(非游客)。 func IsRegisteredFromCookies(c *gin.Context) bool { if useragent.TokenFromRequest(c.Request) == "" { return false } if cookieValue(c, "isRegister") == "1" { return true } return cookieValue(c, "userType") == "1" } func usableAvatarURL(raw string) string { raw = strings.TrimSpace(raw) if raw == "" { return "" } if strings.HasPrefix(raw, "http://") || strings.HasPrefix(raw, "https://") || strings.HasPrefix(raw, "/static/") { return raw } // 无域名的相对路径暂不可用(解密拉取需要完整 URL) return "" }