2026-05-02 13:02:38 +08:00

297 lines
9.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package http
import (
"net"
"net/http"
"strings"
"time"
userv1 "hyapp.local/api/proto/user/v1"
"hyapp/pkg/appcode"
"hyapp/services/gateway-service/internal/auth"
)
// authTokenData 是 gateway 对外 auth API 的扁平响应格式。
type authTokenData struct {
AppCode string `json:"app_code"`
UserID string `json:"user_id,omitempty"`
DisplayUserID string `json:"display_user_id,omitempty"`
DefaultDisplayUserID string `json:"default_display_user_id,omitempty"`
DisplayUserIDKind string `json:"display_user_id_kind,omitempty"`
DisplayUserIDExpiresAtMs int64 `json:"display_user_id_expires_at_ms"`
SessionID string `json:"session_id"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresInSec int64 `json:"expires_in_sec"`
TokenType string `json:"token_type"`
IsNewUser *bool `json:"is_new_user,omitempty"`
ProfileCompleted bool `json:"profile_completed"`
OnboardingStatus string `json:"onboarding_status"`
}
// loginPassword 处理 display_user_id + 密码登录。
func (h *Handler) loginPassword(writer http.ResponseWriter, request *http.Request) {
var body struct {
DisplayUserID string `json:"display_user_id"`
Password string `json:"password"`
DeviceID string `json:"device_id"`
}
if !decode(writer, request, &body) {
return
}
if !h.allowPublicAuthRequest(writer, request, authRateInput{
operation: authOperationPassword,
deviceID: body.DeviceID,
displayUserID: body.DisplayUserID,
}) {
return
}
if h.userClient == nil {
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
return
}
resp, err := h.userClient.LoginPassword(request.Context(), &userv1.LoginPasswordRequest{
Meta: authRequestMeta(request, body.DeviceID),
DisplayUserId: body.DisplayUserID,
Password: body.Password,
})
if err != nil {
writeRPCError(writer, request, err)
return
}
writeOK(writer, request, authData(resp.GetToken(), nil))
}
// loginThirdParty 处理三方登录即注册。
func (h *Handler) loginThirdParty(writer http.ResponseWriter, request *http.Request) {
var body struct {
Provider string `json:"provider"`
Credential string `json:"credential"`
Username string `json:"username"`
Gender string `json:"gender"`
Country string `json:"country"`
InviteCode string `json:"invite_code"`
DeviceID string `json:"device_id"`
Device string `json:"device"`
OSVersion string `json:"os_version"`
Avatar string `json:"avatar"`
Birth string `json:"birth"`
AppVersion string `json:"app_version"`
BuildNumber string `json:"build_number"`
Source string `json:"source"`
InstallChannel string `json:"install_channel"`
Campaign string `json:"campaign"`
Platform string `json:"platform"`
Language string `json:"language"`
Timezone string `json:"timezone"`
}
if !decode(writer, request, &body) {
return
}
if !h.allowPublicAuthRequest(writer, request, authRateInput{
operation: authOperationThirdParty,
deviceID: body.DeviceID,
provider: body.Provider,
}) {
return
}
if h.userClient == nil {
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
return
}
resp, err := h.userClient.LoginThirdParty(request.Context(), &userv1.LoginThirdPartyRequest{
Meta: authRequestMeta(request, body.DeviceID),
Provider: body.Provider,
Credential: body.Credential,
Username: body.Username,
Gender: body.Gender,
Country: body.Country,
InviteCode: body.InviteCode,
DeviceId: body.DeviceID,
Device: body.Device,
OsVersion: body.OSVersion,
Avatar: body.Avatar,
Birth: body.Birth,
AppVersion: body.AppVersion,
BuildNumber: body.BuildNumber,
Source: body.Source,
InstallChannel: body.InstallChannel,
Campaign: body.Campaign,
Platform: body.Platform,
Language: body.Language,
Timezone: body.Timezone,
})
if err != nil {
writeRPCError(writer, request, err)
return
}
isNewUser := resp.GetIsNewUser()
writeOK(writer, request, authData(resp.GetToken(), &isNewUser))
}
// setPassword 处理已登录用户首次设置密码。
func (h *Handler) setPassword(writer http.ResponseWriter, request *http.Request) {
var body struct {
Password string `json:"password"`
}
if !decode(writer, request, &body) {
return
}
if h.userClient == nil {
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
return
}
resp, err := h.userClient.SetPassword(request.Context(), &userv1.SetPasswordRequest{
Meta: authRequestMeta(request, ""),
UserId: auth.UserIDFromContext(request.Context()),
Password: body.Password,
})
if err != nil {
writeRPCError(writer, request, err)
return
}
writeOK(writer, request, map[string]bool{"password_set": resp.GetPasswordSet()})
}
// refreshToken 处理 refresh token 轮换。
func (h *Handler) refreshToken(writer http.ResponseWriter, request *http.Request) {
var body struct {
RefreshToken string `json:"refresh_token"`
DeviceID string `json:"device_id"`
}
if !decode(writer, request, &body) {
return
}
if !h.allowPublicAuthRequest(writer, request, authRateInput{
operation: authOperationRefresh,
deviceID: body.DeviceID,
}) {
return
}
if h.userClient == nil {
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
return
}
resp, err := h.userClient.RefreshToken(request.Context(), &userv1.RefreshTokenRequest{
Meta: authRequestMeta(request, body.DeviceID),
RefreshToken: body.RefreshToken,
})
if err != nil {
writeRPCError(writer, request, err)
return
}
writeOK(writer, request, authData(resp.GetToken(), nil))
}
// logout 处理 refresh session 失效。
func (h *Handler) logout(writer http.ResponseWriter, request *http.Request) {
var body struct {
RefreshToken string `json:"refresh_token"`
SessionID string `json:"session_id"`
}
if !decode(writer, request, &body) {
return
}
if !h.allowPublicAuthRequest(writer, request, authRateInput{
operation: authOperationLogout,
sessionID: body.SessionID,
}) {
return
}
if h.userClient == nil {
writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error")
return
}
resp, err := h.userClient.Logout(request.Context(), &userv1.LogoutRequest{
Meta: authRequestMeta(request, ""),
SessionId: body.SessionID,
RefreshToken: body.RefreshToken,
})
if err != nil {
writeRPCError(writer, request, err)
return
}
writeOK(writer, request, map[string]bool{"revoked": resp.GetRevoked()})
}
// authRequestMeta 生成 user-service auth RPC 的统一追踪和审计元信息。
func authRequestMeta(request *http.Request, deviceID string) *userv1.RequestMeta {
return &userv1.RequestMeta{
RequestId: requestIDFromContext(request.Context()),
Caller: "gateway-service",
GatewayNodeId: "gateway-local",
SentAtMs: time.Now().UnixMilli(),
DeviceId: deviceID,
ClientIp: clientIP(request),
UserAgent: request.UserAgent(),
CountryByIp: countryByIP(request),
SessionId: auth.SessionIDFromContext(request.Context()),
AppCode: appcode.FromContext(request.Context()),
}
}
// authData 把 user-service 的 token response 转成外部 HTTP 扁平 data。
func authData(token *userv1.AuthToken, isNewUser *bool) authTokenData {
if token == nil {
return authTokenData{IsNewUser: isNewUser}
}
return authTokenData{
UserID: userIDString(token.GetUserId()),
AppCode: token.GetAppCode(),
DisplayUserID: token.GetDisplayUserId(),
DefaultDisplayUserID: token.GetDefaultDisplayUserId(),
DisplayUserIDKind: token.GetDisplayUserIdKind(),
DisplayUserIDExpiresAtMs: token.GetDisplayUserIdExpiresAtMs(),
SessionID: token.GetSessionId(),
AccessToken: token.GetAccessToken(),
RefreshToken: token.GetRefreshToken(),
ExpiresInSec: token.GetExpiresInSec(),
TokenType: token.GetTokenType(),
IsNewUser: isNewUser,
ProfileCompleted: token.GetProfileCompleted(),
OnboardingStatus: token.GetOnboardingStatus(),
}
}
// clientIP 提取 gateway 看到的客户端地址。
func clientIP(request *http.Request) string {
forwarded := strings.TrimSpace(request.Header.Get("X-Forwarded-For"))
if forwarded != "" {
parts := strings.Split(forwarded, ",")
return strings.TrimSpace(parts[0])
}
host, _, err := net.SplitHostPort(request.RemoteAddr)
if err != nil {
return request.RemoteAddr
}
return host
}
func countryByIP(request *http.Request) string {
// country_by_ip 不接受 JSON body只采信 gateway/边缘层写入的地域头。
for _, header := range []string{"CF-IPCountry", "CloudFront-Viewer-Country", "X-Vercel-IP-Country", "X-AppEngine-Country", "X-Country-Code"} {
country := strings.ToUpper(strings.TrimSpace(request.Header.Get(header)))
if country == "" || country == "XX" || country == "UNKNOWN" {
continue
}
if len(country) >= 2 && len(country) <= 3 {
return country
}
}
return ""
}