package handler import ( "context" "crypto/subtle" "errors" "net/http" "os" "strings" "time" "github.com/gin-gonic/gin" apierrors "github.com/user-management-system/internal/pkg/errors" "github.com/user-management-system/internal/service" ) // newBackgroundCtx 创建用于后台 goroutine 的带超时独立 context(与请求 context 无关) func newBackgroundCtx(timeoutSec int) (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), time.Duration(timeoutSec)*time.Second) } // ActivateEmailRequest 邮箱激活请求 type ActivateEmailRequest struct { Token string `json:"token" binding:"required"` } // AuthHandler handles authentication requests type AuthHandler struct { authService *service.AuthService } // NewAuthHandler creates a new AuthHandler func NewAuthHandler(authService *service.AuthService) *AuthHandler { return &AuthHandler{authService: authService} } // Register 用户注册 // @Summary 用户注册 // @Description 用户注册新账号,支持用户名+密码或手机号注册 // @Tags 认证 // @Accept json // @Produce json // @Param request body service.RegisterRequest true "注册请求" // @Success 201 {object} Response{data=service.UserInfo} "注册成功" // @Failure 400 {object} Response{code=int,message=string} "请求参数错误" // @Failure 409 {object} Response{code=int,message=string} "用户已存在" // @Router /api/v1/auth/register [post] func (h *AuthHandler) Register(c *gin.Context) { var req struct { Username string `json:"username" binding:"required"` Email string `json:"email"` Phone string `json:"phone"` Password string `json:"password" binding:"required"` Nickname string `json:"nickname"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } registerReq := &service.RegisterRequest{ Username: req.Username, Email: req.Email, Phone: req.Phone, Password: req.Password, Nickname: req.Nickname, } userInfo, err := h.authService.Register(c.Request.Context(), registerReq) if err != nil { handleError(c, err) return } c.JSON(http.StatusCreated, gin.H{ "code": 0, "message": "success", "data": userInfo, }) } // Login 用户登录 // @Summary 用户登录 // @Description 用户使用账号密码登录,支持多种认证方式(用户名/邮箱/手机号) // @Tags 认证 // @Accept json // @Produce json // @Param request body service.LoginRequest true "登录请求" // @Success 200 {object} Response{data=service.LoginResponse} "登录成功" // @Failure 400 {object} Response{code=int,message=string} "请求参数错误" // @Failure 401 {object} Response{code=int,message=string} "认证失败" // @Failure 429 {object} Response{code=int,message=string} "登录尝试过多" // @Router /api/v1/auth/login [post] func (h *AuthHandler) Login(c *gin.Context) { var req struct { Account string `json:"account"` Username string `json:"username"` Email string `json:"email"` Phone string `json:"phone"` Password string `json:"password"` DeviceID string `json:"device_id"` DeviceName string `json:"device_name"` DeviceBrowser string `json:"device_browser"` DeviceOS string `json:"device_os"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } loginReq := &service.LoginRequest{ Account: req.Account, Username: req.Username, Email: req.Email, Phone: req.Phone, Password: req.Password, DeviceID: req.DeviceID, DeviceName: req.DeviceName, DeviceBrowser: req.DeviceBrowser, DeviceOS: req.DeviceOS, } clientIP := c.ClientIP() resp, err := h.authService.Login(c.Request.Context(), loginReq, clientIP) if err != nil { handleError(c, err) return } c.JSON(http.StatusOK, gin.H{ "code": 0, "message": "success", "data": resp, }) } // VerifyTOTPAfterPasswordLogin 完成密码登录后的TOTP验证 // @Summary TOTP验证(密码登录后) // @Description 当登录返回requires_totp=true时,使用此接口完成TOTP验证 // @Tags 认证 // @Accept json // @Produce json // @Param request body TOTPVerifyRequest true "TOTP验证请求" // @Success 200 {object} Response{data=service.LoginResponse} "验证成功" // @Failure 400 {object} Response "请求参数错误" // @Failure 401 {object} Response "TOTP验证失败" // @Router /api/v1/auth/login/totp-verify [post] func (h *AuthHandler) VerifyTOTPAfterPasswordLogin(c *gin.Context) { var req struct { UserID int64 `json:"user_id" binding:"required"` Code string `json:"code" binding:"required"` DeviceID string `json:"device_id"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()}) return } resp, err := h.authService.VerifyTOTPAfterPasswordLogin(c.Request.Context(), req.UserID, req.Code, req.DeviceID) if err != nil { handleError(c, err) return } c.JSON(http.StatusOK, gin.H{ "code": 0, "message": "success", "data": resp, }) } // Logout 用户登出 // @Summary 用户登出 // @Description 使当前 access_token 和 refresh_token 失效 // @Tags 认证 // @Accept json // @Produce json // @Security BearerAuth // @Param request body service.LogoutRequest false "登出请求(token可从header获取)" // @Success 200 {object} Response{code=int,message=string} "登出成功" // @Router /api/v1/auth/logout [post] func (h *AuthHandler) Logout(c *gin.Context) { var req struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` } // 允许 body 为空(仅凭 Authorization header 里的 access_token 注销也可以) _ = c.ShouldBindJSON(&req) // 如果 body 里没有 access_token,则从 Authorization header 中取 if req.AccessToken == "" { if bearer := c.GetHeader("Authorization"); len(bearer) > 7 { req.AccessToken = bearer[7:] // 去掉 "Bearer " } } username, _ := c.Get("username") usernameStr, _ := username.(string) logoutReq := &service.LogoutRequest{ AccessToken: req.AccessToken, RefreshToken: req.RefreshToken, } _ = h.authService.Logout(c.Request.Context(), usernameStr, logoutReq) c.JSON(http.StatusOK, gin.H{"message": "logged out"}) } // RefreshToken 刷新访问令牌 // @Summary 刷新访问令牌 // @Description 使用 refresh_token 获取新的 access_token // @Tags 认证 // @Accept json // @Produce json // @Param request body RefreshTokenRequest true "刷新令牌请求" // @Success 200 {object} Response{data=service.LoginResponse} "刷新成功" // @Failure 400 {object} Response{code=int,message=string} "请求参数错误" // @Failure 401 {object} Response{code=int,message=string} "refresh_token无效或已过期" // @Router /api/v1/auth/refresh-token [post] func (h *AuthHandler) RefreshToken(c *gin.Context) { var req struct { RefreshToken string `json:"refresh_token" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } resp, err := h.authService.RefreshToken(c.Request.Context(), req.RefreshToken) if err != nil { handleError(c, err) return } c.JSON(http.StatusOK, gin.H{ "code": 0, "message": "success", "data": resp, }) } // GetUserInfo 获取当前用户信息 // @Summary 获取当前用户信息 // @Description 获取已登录用户的详细信息 // @Tags 认证 // @Produce json // @Security BearerAuth // @Success 200 {object} Response{data=service.UserInfo} "用户信息" // @Failure 401 {object} Response{code=int,message=string} "未认证" // @Router /api/v1/auth/userinfo [get] func (h *AuthHandler) GetUserInfo(c *gin.Context) { userID, ok := getUserIDFromContext(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) return } userInfo, err := h.authService.GetUserInfo(c.Request.Context(), userID) if err != nil { handleError(c, err) return } c.JSON(http.StatusOK, gin.H{ "code": 0, "message": "success", "data": userInfo, }) } // GetCSRFToken 获取CSRF令牌 // @Summary 获取CSRF令牌 // @Description 由于系统使用JWT Bearer Token认证,不存在CSRF风险,返回空token // @Tags 认证 // @Produce json // @Success 200 {object} map "CSRF token(为空)" // @Router /api/v1/auth/csrf-token [get] func (h *AuthHandler) GetCSRFToken(c *gin.Context) { // 系统使用 JWT Bearer Token 认证,Bearer Token 不会被浏览器自动携带(非 cookie) // 因此不存在传统意义上的 CSRF 风险,此端点返回空 token 作为兼容响应 c.JSON(http.StatusOK, gin.H{ "csrf_token": "", "note": "JWT Bearer Token authentication; CSRF protection not required", }) } // GetAuthCapabilities 获取认证能力 // @Summary 获取系统认证能力 // @Description 返回系统支持的认证方式和配置(如是否需要邮件激活、是否支持OAuth等) // @Tags 认证 // @Produce json // @Success 200 {object} Response{data=service.AuthCapabilities} "认证能力配置" // @Router /api/v1/auth/capabilities [get] func (h *AuthHandler) GetAuthCapabilities(c *gin.Context) { ctx := c.Request.Context() caps := h.authService.GetAuthCapabilities(ctx) c.JSON(http.StatusOK, gin.H{ "code": 0, "message": "success", "data": caps, }) } // OAuthLogin OAuth登录初始化 // @Summary OAuth登录初始化 // @Description 发起OAuth登录流程(当前未配置) // @Tags OAuth // @Produce json // @Param provider path string true "OAuth提供商(如 github, google)" // @Success 200 {object} Response "OAuth未配置" // @Router /api/v1/auth/oauth/{provider} [get] func (h *AuthHandler) OAuthLogin(c *gin.Context) { provider := c.Param("provider") c.JSON(http.StatusOK, gin.H{"code": 0, "message": "OAuth not configured", "data": gin.H{"provider": provider}}) } // OAuthCallback OAuth回调 // @Summary OAuth回调处理 // @Description 处理OAuth provider回调(当前未配置) // @Tags OAuth // @Produce json // @Param provider path string true "OAuth提供商" // @Success 200 {object} Response "OAuth未配置" // @Router /api/v1/auth/oauth/{provider}/callback [get] func (h *AuthHandler) OAuthCallback(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0, "message": "OAuth not configured"}) } // OAuthExchange OAuth令牌交换 // @Summary OAuth令牌交换 // @Description 使用OAuth code交换access_token(当前未配置) // @Tags OAuth // @Accept json // @Produce json // @Param provider path string true "OAuth提供商" // @Success 200 {object} Response "OAuth未配置" // @Router /api/v1/auth/oauth/{provider}/exchange [post] func (h *AuthHandler) OAuthExchange(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0, "message": "OAuth not configured"}) } // GetEnabledOAuthProviders 获取已启用的OAuth提供商 // @Summary 获取OAuth提供商列表 // @Description 返回系统已配置并启用的OAuth提供商列表 // @Tags OAuth // @Produce json // @Success 200 {object} Response{data=map} "提供商列表" // @Router /api/v1/auth/oauth/providers [get] func (h *AuthHandler) GetEnabledOAuthProviders(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success", "data": gin.H{"providers": []string{}}}) } // ActivateEmail 激活邮箱 // @Summary 激活用户邮箱 // @Description 使用邮箱激活token激活用户账号 // @Tags 邮箱认证 // @Accept json // @Produce json // @Param request body ActivateEmailRequest true "激活请求" // @Success 200 {object} Response "激活成功" // @Failure 400 {object} Response "token缺失" // @Failure 401 {object} Response "token无效或已过期" // @Router /api/v1/auth/activate-email [post] func (h *AuthHandler) ActivateEmail(c *gin.Context) { var req ActivateEmailRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "token is required"}) return } if err := h.authService.ActivateEmail(c.Request.Context(), req.Token); err != nil { handleError(c, err) return } c.JSON(http.StatusOK, gin.H{"code": 0, "message": "email activated successfully"}) } // ResendActivationEmail 重发激活邮件 // @Summary 重发激活邮件 // @Description 重新发送账号激活邮件(防枚举:无论邮箱是否注册都返回成功) // @Tags 邮箱认证 // @Accept json // @Produce json // @Param request body ResendActivationRequest true "邮箱地址" // @Success 200 {object} Response "激活邮件已发送(如果邮箱已注册)" // @Failure 400 {object} Response "邮箱格式错误" // @Router /api/v1/auth/resend-activation-email [post] func (h *AuthHandler) ResendActivationEmail(c *gin.Context) { var req struct { Email string `json:"email" binding:"required,email"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()}) return } if err := h.authService.ResendActivationEmail(c.Request.Context(), req.Email); err != nil { handleError(c, err) return } // 防枚举:无论邮箱是否存在,统一返回成功 c.JSON(http.StatusOK, gin.H{"code": 0, "message": "activation email sent if address is registered"}) } // SendEmailCode 发送邮箱验证码 // @Summary 发送邮箱验证码 // @Description 发送邮箱登录验证码(防枚举:无论邮箱是否注册都返回成功) // @Tags 邮箱认证 // @Accept json // @Produce json // @Param request body SendEmailCodeRequest true "邮箱地址" // @Success 200 {object} Response "验证码已发送" // @Failure 400 {object} Response "邮箱格式错误" // @Router /api/v1/auth/send-email-code [post] func (h *AuthHandler) SendEmailCode(c *gin.Context) { var req struct { Email string `json:"email" binding:"required,email"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()}) return } // SendEmailLoginCode 内部会忽略未注册邮箱(防枚举),始终返回 ok if err := h.authService.SendEmailLoginCode(c.Request.Context(), req.Email); err != nil { handleError(c, err) return } c.JSON(http.StatusOK, gin.H{"code": 0, "message": "验证码已发送"}) } // LoginByEmailCode 使用邮箱验证码登录 // @Summary 邮箱验证码登录 // @Description 使用邮箱和验证码完成登录 // @Tags 邮箱认证 // @Accept json // @Produce json // @Param request body LoginByEmailCodeRequest true "登录请求" // @Success 200 {object} Response{data=service.LoginResponse} "登录成功" // @Failure 400 {object} Response "请求参数错误" // @Failure 401 {object} Response "验证码错误或已过期" // @Router /api/v1/auth/login-by-email-code [post] func (h *AuthHandler) LoginByEmailCode(c *gin.Context) { var req struct { Email string `json:"email" binding:"required,email"` Code string `json:"code" binding:"required"` DeviceID string `json:"device_id"` DeviceName string `json:"device_name"` DeviceBrowser string `json:"device_browser"` DeviceOS string `json:"device_os"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()}) return } clientIP := c.ClientIP() resp, err := h.authService.LoginByEmailCode(c.Request.Context(), req.Email, req.Code, clientIP) if err != nil { handleError(c, err) return } // 异步注册设备(不阻塞主流程) // 注意:必须用 context.WithTimeout(context.Background()) 而非 c.Request.Context() // gin 在 c.JSON 返回后会回收 context,goroutine 中引用会得到已取消的 context if req.DeviceID != "" && resp != nil && resp.User != nil { loginReq := &service.LoginRequest{ DeviceID: req.DeviceID, DeviceName: req.DeviceName, DeviceBrowser: req.DeviceBrowser, DeviceOS: req.DeviceOS, } userID := resp.User.ID go func() { devCtx, cancel := newBackgroundCtx(5) defer cancel() h.authService.BestEffortRegisterDevicePublic(devCtx, userID, loginReq) }() } c.JSON(http.StatusOK, gin.H{ "code": 0, "message": "success", "data": resp, }) } // BootstrapAdmin 引导初始化管理员 // @Summary 引导初始化管理员账号 // @Description 在系统未配置管理员时,创建第一个管理员账号(需要BOOTSTRAP_SECRET) // @Tags 系统初始化 // @Accept json // @Produce json // @Security BootstrapSecret // @Param X-Bootstrap-Secret header string true "引导密钥" // @Param request body BootstrapAdminRequest true "管理员信息" // @Success 201 {object} Response{data=service.UserInfo} "管理员创建成功" // @Failure 401 {object} Response "引导密钥无效" // @Failure 403 {object} Response "引导初始化未授权" // @Router /api/v1/auth/bootstrap-admin [post] func (h *AuthHandler) BootstrapAdmin(c *gin.Context) { // P0 修复:BootstrapAdmin 端点需要 bootstrap secret 验证 bootstrapSecret := os.Getenv("BOOTSTRAP_SECRET") if bootstrapSecret == "" { c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "引导初始化未授权"}) return } providedSecret := c.GetHeader("X-Bootstrap-Secret") if providedSecret == "" { c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "缺少引导密钥"}) return } // 使用恒定时间比较防止时序攻击 if subtle.ConstantTimeCompare([]byte(providedSecret), []byte(bootstrapSecret)) != 1 { c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "引导密钥无效"}) return } var req struct { Username string `json:"username" binding:"required"` Email string `json:"email" binding:"required"` Password string `json:"password" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()}) return } bootstrapReq := &service.BootstrapAdminRequest{ Username: req.Username, Email: req.Email, Password: req.Password, } clientIP := c.ClientIP() resp, err := h.authService.BootstrapAdmin(c.Request.Context(), bootstrapReq, clientIP) if err != nil { handleError(c, err) return } c.JSON(http.StatusCreated, gin.H{ "code": 0, "message": "success", "data": resp, }) } // SendEmailBindCode 发送邮箱绑定验证码 // @Summary 发送邮箱绑定验证码 // @Description 发送验证码到邮箱以绑定邮箱(当前未配置) // @Tags 邮箱绑定 // @Accept json // @Produce json // @Success 200 {object} Response "功能未配置" // @Router /api/v1/auth/email/bind/send [post] func (h *AuthHandler) SendEmailBindCode(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0, "message": "email bind not configured"}) } // BindEmail 绑定邮箱 // @Summary 绑定邮箱 // @Description 使用邮箱验证码绑定账号(当前未配置) // @Tags 邮箱绑定 // @Accept json // @Produce json // @Success 200 {object} Response "功能未配置" // @Router /api/v1/auth/email/bind [post] func (h *AuthHandler) BindEmail(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0, "message": "email bind not configured"}) } // UnbindEmail 解绑邮箱 // @Summary 解绑邮箱 // @Description 解绑账号关联的邮箱(当前未配置) // @Tags 邮箱绑定 // @Accept json // @Produce json // @Success 200 {object} Response "功能未配置" // @Router /api/v1/auth/email/unbind [post] func (h *AuthHandler) UnbindEmail(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0, "message": "email unbind not configured"}) } // SendPhoneBindCode 发送手机绑定验证码 // @Summary 发送手机绑定验证码 // @Description 发送验证码到手机以绑定手机号(当前未配置) // @Tags 手机绑定 // @Accept json // @Produce json // @Success 200 {object} Response "功能未配置" // @Router /api/v1/auth/phone/bind/send [post] func (h *AuthHandler) SendPhoneBindCode(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0, "message": "phone bind not configured"}) } // BindPhone 绑定手机号 // @Summary 绑定手机号 // @Description 使用手机验证码绑定账号(当前未配置) // @Tags 手机绑定 // @Accept json // @Produce json // @Success 200 {object} Response "功能未配置" // @Router /api/v1/auth/phone/bind [post] func (h *AuthHandler) BindPhone(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0, "message": "phone bind not configured"}) } // UnbindPhone 解绑手机号 // @Summary 解绑手机号 // @Description 解绑账号关联的手机号(当前未配置) // @Tags 手机绑定 // @Accept json // @Produce json // @Success 200 {object} Response "功能未配置" // @Router /api/v1/auth/phone/unbind [post] func (h *AuthHandler) UnbindPhone(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0, "message": "phone unbind not configured"}) } // GetSocialAccounts 获取社交账号列表 // @Summary 获取已绑定的社交账号列表 // @Description 获取当前用户绑定的第三方社交账号列表 // @Tags 社交账号 // @Produce json // @Security BearerAuth // @Success 200 {object} Response "社交账号列表" // @Router /api/v1/auth/social-accounts [get] func (h *AuthHandler) GetSocialAccounts(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success", "data": gin.H{"accounts": []interface{}{}}}) } // BindSocialAccount 绑定社交账号 // @Summary 绑定社交账号 // @Description 绑定第三方社交账号到当前用户(当前未配置) // @Tags 社交账号 // @Accept json // @Produce json // @Success 200 {object} Response "功能未配置" // @Router /api/v1/auth/social/bind [post] func (h *AuthHandler) BindSocialAccount(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0, "message": "social binding not configured"}) } // UnbindSocialAccount 解绑社交账号 // @Summary 解绑社交账号 // @Description 解绑当前用户关联的第三方社交账号(当前未配置) // @Tags 社交账号 // @Accept json // @Produce json // @Success 200 {object} Response "功能未配置" // @Router /api/v1/auth/social/unbind [post] func (h *AuthHandler) UnbindSocialAccount(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0, "message": "social unbinding not configured"}) } func (h *AuthHandler) SupportsEmailCodeLogin() bool { return h.authService.HasEmailCodeService() } func getUserIDFromContext(c *gin.Context) (int64, bool) { userID, exists := c.Get("user_id") if !exists { return 0, false } id, ok := userID.(int64) return id, ok } // handleError 将 error 转换为对应的 HTTP 响应。 // 优先识别 ApplicationError,其次通过关键词推断业务错误类型,兜底返回 500。 func handleError(c *gin.Context, err error) { if err == nil { return } // 优先尝试 ApplicationError(内置 HTTP 状态码) var appErr *apierrors.ApplicationError if errors.As(err, &appErr) { c.JSON(int(appErr.Code), gin.H{"code": appErr.Code, "message": appErr.Message}) return } // 对普通 errors.New 按关键词推断语义,但只返回通用错误信息给客户端 httpCode := classifyErrorMessage(err.Error()) c.JSON(httpCode, gin.H{"code": httpCode, "message": "服务器内部错误"}) } // classifyErrorMessage 通过错误信息关键词推断 HTTP 状态码,避免业务错误被 500 吞掉 func classifyErrorMessage(msg string) int { lower := strings.ToLower(msg) switch { case contains(lower, "not found", "不存在", "找不到"): return http.StatusNotFound case contains(lower, "already exists", "已存在", "已注册", "duplicate"): return http.StatusConflict case contains(lower, "unauthorized", "invalid token", "token", "令牌", "未认证"): return http.StatusUnauthorized case contains(lower, "forbidden", "permission", "权限", "禁止"): return http.StatusForbidden case contains(lower, "invalid", "required", "must", "cannot be empty", "不能为空", "格式", "参数", "密码不正确", "incorrect", "wrong", "too short", "too long", "已失效", "expired", "验证码不正确", "不能与"): return http.StatusBadRequest case contains(lower, "locked", "too many", "账号已被锁定", "rate limit"): return http.StatusTooManyRequests default: return http.StatusInternalServerError } } // contains 检查 s 是否包含 keywords 中的任意一个 func contains(s string, keywords ...string) bool { for _, kw := range keywords { if strings.Contains(s, kw) { return true } } return false }