Files
Artur Iusupov 3a5a15fe77 fix(site): require explicit email verification setting
Replace OptionalBool with an explicit require_email_verification value for the login settings save request while keeping legacy read defaults intact.

Use positive RequireEmailVerification naming through the registration flow and inline the site setting mapping.

Add validation, save-path, and registration coverage for the explicit email verification setting.
2026-07-10 10:19:39 +08:00

454 lines
14 KiB
Go

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package schema
import (
"context"
"encoding/json"
"github.com/apache/answer/internal/base/handler"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/base/translator"
"github.com/apache/answer/pkg/day"
"github.com/segmentfault/pacman/errors"
"github.com/apache/answer/internal/base/constant"
"github.com/apache/answer/internal/base/validator"
"github.com/apache/answer/internal/entity"
"github.com/apache/answer/pkg/checker"
"github.com/apache/answer/pkg/converter"
"github.com/jinzhu/copier"
)
// UserVerifyEmailReq user verify email request
type UserVerifyEmailReq struct {
// code
Code string `validate:"required,gt=0,lte=500" form:"code"`
// content
Content string `json:"-"`
}
// UserLoginResp get user response
type UserLoginResp struct {
// user id
ID string `json:"id"`
// create time
CreatedAt int64 `json:"created_at"`
// last login date
LastLoginDate int64 `json:"last_login_date"`
// username
Username string `json:"username"`
// email
EMail string `json:"e_mail"`
// mail status(1 pass 2 to be verified)
MailStatus int `json:"mail_status"`
// notice status(1 on 2off)
NoticeStatus int `json:"notice_status"`
// follow count
FollowCount int `json:"follow_count"`
// answer count
AnswerCount int `json:"answer_count"`
// question count
QuestionCount int `json:"question_count"`
// rank
Rank int `json:"rank"`
// authority group
AuthorityGroup int `json:"authority_group"`
// display name
DisplayName string `json:"display_name"`
// avatar
Avatar string `json:"avatar"`
// mobile
Mobile string `json:"mobile"`
// bio markdown
Bio string `json:"bio"`
// bio html
BioHTML string `json:"bio_html"`
// website
Website string `json:"website"`
// location
Location string `json:"location"`
// language
Language string `json:"language"`
// Color scheme
ColorScheme string `json:"color_scheme"`
// access token
AccessToken string `json:"access_token"`
// role id
RoleID int `json:"role_id"`
// user status
Status string `json:"status"`
// user have password
HavePassword bool `json:"have_password"`
// visit token
VisitToken string `json:"visit_token"`
// suspended until timestamp
SuspendedUntil int64 `json:"suspended_until"`
}
func (r *UserLoginResp) ConvertFromUserEntity(userInfo *entity.User) {
_ = copier.Copy(r, userInfo)
r.CreatedAt = userInfo.CreatedAt.Unix()
r.LastLoginDate = userInfo.LastLoginDate.Unix()
r.Status = constant.ConvertUserStatus(userInfo.Status, userInfo.MailStatus)
r.HavePassword = len(userInfo.Pass) > 0
if !userInfo.SuspendedUntil.IsZero() {
r.SuspendedUntil = userInfo.SuspendedUntil.Unix()
}
}
type GetCurrentLoginUserInfoResp struct {
*UserLoginResp
Avatar *AvatarInfo `json:"avatar"`
}
func (r *GetCurrentLoginUserInfoResp) ConvertFromUserEntity(userInfo *entity.User) {
_ = copier.Copy(r, userInfo)
r.CreatedAt = userInfo.CreatedAt.Unix()
r.LastLoginDate = userInfo.LastLoginDate.Unix()
r.Status = constant.ConvertUserStatus(userInfo.Status, userInfo.MailStatus)
if len(r.ColorScheme) == 0 {
r.ColorScheme = constant.ColorSchemeDefault
}
if !userInfo.SuspendedUntil.IsZero() {
r.SuspendedUntil = userInfo.SuspendedUntil.Unix()
}
}
// GetOtherUserInfoByUsernameResp get user response
type GetOtherUserInfoByUsernameResp struct {
// user id
ID string `json:"id"`
// create time
CreatedAt int64 `json:"created_at"`
// last login date
LastLoginDate int64 `json:"last_login_date"`
// username
Username string `json:"username"`
// email
// follow count
FollowCount int `json:"follow_count"`
// answer count
AnswerCount int `json:"answer_count"`
// question count
QuestionCount int `json:"question_count"`
// rank
Rank int `json:"rank"`
// display name
DisplayName string `json:"display_name"`
// avatar
Avatar string `json:"avatar"`
// mobile
Mobile string `json:"mobile"`
// bio markdown
Bio string `json:"bio"`
// bio html
BioHTML string `json:"bio_html"`
// website
Website string `json:"website"`
// location
Location string `json:"location"`
Status string `json:"status"`
StatusMsg string `json:"status_msg,omitempty"`
// suspended until timestamp
SuspendedUntil int64 `json:"suspended_until"`
}
func (r *GetOtherUserInfoByUsernameResp) ConvertFromUserEntity(userInfo *entity.User) {
_ = copier.Copy(r, userInfo)
r.CreatedAt = userInfo.CreatedAt.Unix()
r.LastLoginDate = userInfo.LastLoginDate.Unix()
r.Status = constant.ConvertUserStatus(userInfo.Status, userInfo.MailStatus)
if !userInfo.SuspendedUntil.IsZero() {
r.SuspendedUntil = userInfo.SuspendedUntil.Unix()
}
r.StatusMsg = ""
}
func (r *GetOtherUserInfoByUsernameResp) ConvertFromUserEntityWithLang(ctx context.Context, userInfo *entity.User) {
_ = copier.Copy(r, userInfo)
r.CreatedAt = userInfo.CreatedAt.Unix()
r.LastLoginDate = userInfo.LastLoginDate.Unix()
r.Status = constant.ConvertUserStatus(userInfo.Status, userInfo.MailStatus)
lang := handler.GetLangByCtx(ctx)
if userInfo.MailStatus == entity.EmailStatusToBeVerified {
r.StatusMsg = translator.Tr(lang, reason.UserStatusInactive)
}
switch userInfo.Status {
case entity.UserStatusSuspended:
if userInfo.SuspendedUntil.IsZero() || userInfo.SuspendedUntil.Year() >= 2099 {
r.StatusMsg = translator.Tr(lang, reason.UserStatusSuspendedForever)
} else {
r.SuspendedUntil = userInfo.SuspendedUntil.Unix()
trans := translator.GlobalTrans.Tr(lang, "ui.dates.long_date_with_time")
suspendedUntilFormatted := day.Format(userInfo.SuspendedUntil.Unix(), trans, "UTC")
r.StatusMsg = translator.TrWithData(lang, reason.UserStatusSuspendedUntil, map[string]any{
"SuspendedUntil": suspendedUntilFormatted,
})
}
case entity.UserStatusDeleted:
r.StatusMsg = translator.Tr(lang, reason.UserStatusDeleted)
}
}
// UserEmailLoginReq user email login request
type UserEmailLoginReq struct {
Email string `validate:"required,email,gt=0,lte=500" json:"e_mail"`
Pass string `validate:"required,gte=8,lte=32" json:"pass"`
CaptchaID string `json:"captcha_id"`
CaptchaCode string `json:"captcha_code"`
}
// UserRegisterReq user register request
type UserRegisterReq struct {
Name string `validate:"required,gte=2,lte=30" json:"name"`
Email string `validate:"required,email,gt=0,lte=500" json:"e_mail" `
Pass string `validate:"required,gte=8,lte=32" json:"pass"`
CaptchaID string `json:"captcha_id"`
CaptchaCode string `json:"captcha_code"`
IP string `json:"-" `
RequireEmailVerification bool `json:"-"`
}
func (u *UserRegisterReq) Check() (errFields []*validator.FormErrorField, err error) {
if err = checker.CheckPassword(u.Pass); err != nil {
errFields = append(errFields, &validator.FormErrorField{
ErrorField: "pass",
ErrorMsg: err.Error(),
})
return errFields, err
}
return nil, nil
}
type UserModifyPasswordReq struct {
OldPass string `validate:"omitempty,gte=8,lte=32" json:"old_pass"`
Pass string `validate:"required,gte=8,lte=32" json:"pass"`
CaptchaID string `json:"captcha_id"`
CaptchaCode string `json:"captcha_code"`
UserID string `json:"-"`
AccessToken string `json:"-"`
}
func (u *UserModifyPasswordReq) Check() (errFields []*validator.FormErrorField, err error) {
if err = checker.CheckPassword(u.Pass); err != nil {
errFields = append(errFields, &validator.FormErrorField{
ErrorField: "pass",
ErrorMsg: err.Error(),
})
return errFields, err
}
return nil, nil
}
type UpdateInfoRequest struct {
DisplayName string `validate:"omitempty,gte=2,lte=30" json:"display_name"`
Username string `validate:"omitempty,gte=2,lte=30" json:"username"`
Avatar AvatarInfo `json:"avatar"`
Bio string `validate:"omitempty,gt=0,lte=4096" json:"bio"`
BioHTML string `json:"-"`
Website string `validate:"omitempty,gt=0,lte=500" json:"website"`
Location string `validate:"omitempty,gt=0,lte=100" json:"location"`
UserID string `json:"-"`
IsAdmin bool `json:"-"`
}
type AvatarInfo struct {
Type string `validate:"omitempty,gt=0,lte=100" json:"type"`
Gravatar string `validate:"omitempty,gt=0,lte=200" json:"gravatar"`
Custom string `validate:"omitempty,gt=0,lte=200" json:"custom"`
}
func (a *AvatarInfo) ToJsonString() string {
data, _ := json.Marshal(a)
return string(data)
}
func (a *AvatarInfo) GetURL() string {
switch a.Type {
case constant.AvatarTypeGravatar:
return a.Gravatar
case constant.AvatarTypeCustom:
return a.Custom
default:
return ""
}
}
func CustomAvatar(url string) *AvatarInfo {
return &AvatarInfo{
Type: constant.AvatarTypeCustom,
Custom: url,
}
}
func (req *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, err error) {
req.BioHTML = converter.Markdown2BasicHTML(req.Bio)
if len(req.Website) > 0 && !checker.IsURL(req.Website) {
return append(errFields, &validator.FormErrorField{
ErrorField: "website",
ErrorMsg: reason.InvalidURLError,
}), errors.BadRequest(reason.InvalidURLError)
}
return nil, nil
}
// UpdateUserInterfaceRequest update user interface request
type UpdateUserInterfaceRequest struct {
// language
Language string `validate:"required,gt=1,lte=100" json:"language"`
// Color scheme
ColorScheme string `validate:"required,gt=1,lte=100" json:"color_scheme"`
// user id
UserId string `json:"-"`
}
func (req *UpdateUserInterfaceRequest) Check() (errFields []*validator.FormErrorField, err error) {
if !translator.CheckLanguageIsValid(req.Language) {
return nil, errors.BadRequest(reason.LangNotFound)
}
if req.ColorScheme != constant.ColorSchemeDefault &&
req.ColorScheme != constant.ColorSchemeLight &&
req.ColorScheme != constant.ColorSchemeDark &&
req.ColorScheme != constant.ColorSchemeSystem {
req.ColorScheme = constant.ColorSchemeDefault
}
return nil, nil
}
type UserRetrievePassWordRequest struct {
Email string `validate:"required,email,gt=0,lte=500" json:"e_mail"`
CaptchaID string `json:"captcha_id"`
CaptchaCode string `json:"captcha_code"`
}
type UserRePassWordRequest struct {
Code string `validate:"required,gt=0,lte=100" json:"code"`
Pass string `validate:"required,gt=0,lte=32" json:"pass"`
Content string `json:"-"`
}
func (u *UserRePassWordRequest) Check() (errFields []*validator.FormErrorField, err error) {
if err = checker.CheckPassword(u.Pass); err != nil {
errFields = append(errFields, &validator.FormErrorField{
ErrorField: "pass",
ErrorMsg: err.Error(),
})
return errFields, err
}
return nil, nil
}
type ActionRecordReq struct {
Action string `validate:"required,oneof=email password edit_userinfo question answer comment edit invitation_answer search report delete vote" form:"action"`
IP string `json:"-"`
UserID string `json:"-"`
}
type ActionRecordResp struct {
CaptchaID string `json:"captcha_id"`
CaptchaImg string `json:"captcha_img"`
Verify bool `json:"verify"`
}
type UserBasicInfo struct {
ID string `json:"id"`
Username string `json:"username"`
Rank int `json:"rank"`
DisplayName string `json:"display_name"`
Avatar string `json:"avatar"`
Website string `json:"website"`
Location string `json:"location"`
Language string `json:"language"`
Status string `json:"status"`
SuspendedUntil int64 `json:"suspended_until"`
}
type GetOtherUserInfoByUsernameReq struct {
Username string `validate:"required,gt=0,lte=500" form:"username"`
UserID string `json:"-"`
IsAdmin bool `json:"-"`
}
type GetOtherUserInfoResp struct {
Info *GetOtherUserInfoByUsernameResp `json:"info"`
}
type UserChangeEmailSendCodeReq struct {
UserVerifyEmailSendReq
Email string `validate:"required,email,gt=0,lte=500" json:"e_mail"`
Pass string `validate:"omitempty,gte=8,lte=32" json:"pass"`
UserID string `json:"-"`
}
type UserChangeEmailVerifyReq struct {
Code string `validate:"required,gt=0,lte=500" json:"code"`
Content string `json:"-"`
}
type UserVerifyEmailSendReq struct {
CaptchaID string `json:"captcha_id"`
CaptchaCode string `json:"captcha_code"`
}
// UserRankingResp user ranking response
type UserRankingResp struct {
UsersWithTheMostReputation []*UserRankingSimpleInfo `json:"users_with_the_most_reputation"`
UsersWithTheMostVote []*UserRankingSimpleInfo `json:"users_with_the_most_vote"`
Staffs []*UserRankingSimpleInfo `json:"staffs"`
}
// UserRankingSimpleInfo user ranking simple info
type UserRankingSimpleInfo struct {
// username
Username string `json:"username"`
// rank
Rank int `json:"rank"`
// vote
VoteCount int `json:"vote_count"`
// display name
DisplayName string `json:"display_name"`
// avatar
Avatar string `json:"avatar"`
}
// UserUnsubscribeNotificationReq user unsubscribe email notification request
type UserUnsubscribeNotificationReq struct {
Code string `validate:"required,gt=0,lte=500" json:"code"`
Content string `json:"-"`
}
// GetUserStaffReq get user staff request
type GetUserStaffReq struct {
Username string `validate:"omitempty,gt=0,lte=500" form:"username"`
PageSize int `validate:"omitempty,min=1" form:"page_size"`
}
// GetUserStaffResp get user staff response
type GetUserStaffResp struct {
// username
Username string `json:"username"`
// display name
DisplayName string `json:"display_name"`
// avatar
Avatar string `json:"avatar"`
}