3a5a15fe77
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.
454 lines
14 KiB
Go
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"`
|
|
}
|