Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
262 lines
7.2 KiB
Go
262 lines
7.2 KiB
Go
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 将登录/注册返回写入 cookie。
|
||
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, "introduction", user.Introduction, secure)
|
||
setCookie(c, "avatar", user.Avatar, secure)
|
||
setCookie(c, "email", user.Email, secure)
|
||
setCookie(c, "inviteCode", user.InviteCode, 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, "showCardId", strconv.FormatInt(user.ShowCardID, 10), secure)
|
||
setCookie(c, "showCardName", user.ShowCardName, secure)
|
||
setCookie(c, "userType", strconv.Itoa(user.Type), secure)
|
||
isReg := "0"
|
||
if user.IsRegister || user.Type == 1 {
|
||
isReg = "1"
|
||
}
|
||
setCookie(c, "isRegister", isReg, secure)
|
||
if sid := useragent.SIDFromRequest(c.Request); sid != "" {
|
||
setCookie(c, "sid", sid, secure)
|
||
}
|
||
}
|
||
|
||
func setCookie(c *gin.Context, name, value string, secure bool) {
|
||
// Cookie Value 仅允许 ASCII;中文昵称等需 URL 编码。
|
||
// 使用 PathEscape(空格→%20),与前端 encodeURIComponent 一致;
|
||
// 避免 QueryEscape 的空格→+,否则 JS decodeURIComponent 会留下字面量 "+"。
|
||
http.SetCookie(c.Writer, &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),
|
||
})
|
||
}
|
||
|
||
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) {
|
||
for _, name := range authCookieNames() {
|
||
http.SetCookie(c.Writer, &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 ""
|
||
}
|