0b4a1b4594
- Updated OSSStorage validation errors to use internationalized messages. - Changed error messages in provider.go for unsupported file storage types. - Refactored tag_service.go to replace hardcoded error messages with internationalized versions. - Updated ticket_service.go to use internationalized error messages for various validation checks. - Refactored ticket_tag_service.go to use internationalized error messages for tag validation. - Changed ticket_view_service.go to use internationalized error messages for view validation. - Updated tool_catalog_service.go to use internationalized error messages for tool code validation. - Refactored user_service.go to replace error messages with internationalized versions. - Updated ws_service.go to use internationalized error messages for WebSocket handling. - Refactored wxwork_kf_inbound_service.go to use internationalized error messages for message handling. - Updated wxwork_kf_outbound_service.go to use internationalized error messages for outbound message handling. - Refactored wxwork_login_service.go to use internationalized error messages for login handling. - Updated login.go in wxwork package to use internationalized error messages for login state and ticket validation.
257 lines
6.8 KiB
Go
257 lines
6.8 KiB
Go
package wxwork
|
|
|
|
import (
|
|
"agent-desk/internal/pkg/dto/response"
|
|
"agent-desk/internal/pkg/errorsx"
|
|
"agent-desk/internal/pkg/i18nx"
|
|
"crypto/hmac"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/silenceper/wechat/v2/work/oauth"
|
|
)
|
|
|
|
const (
|
|
StateTTL = 5 * time.Minute
|
|
LoginTicketTTL = 1 * time.Minute
|
|
defaultLoginNextPath = "/dashboard"
|
|
)
|
|
|
|
var (
|
|
errStateInvalid = i18nx.Errorf("error.e0110")
|
|
loginTicketStore sync.Map
|
|
)
|
|
|
|
type statePayload struct {
|
|
Next string `json:"next"`
|
|
Nonce string `json:"nonce"`
|
|
ExpiredAt int64 `json:"expiredAt"`
|
|
}
|
|
|
|
type loginTicket struct {
|
|
Response *response.LoginResponse
|
|
ExpiredAt time.Time
|
|
}
|
|
|
|
func BuildLoginURL(state string) (string, error) {
|
|
if !Enabled() {
|
|
return "", i18nx.Errorf("error.e0109")
|
|
}
|
|
if strings.TrimSpace(wxCfg.OAuthRedirect) == "" {
|
|
return "", i18nx.Errorf("error.e0107")
|
|
}
|
|
if strings.TrimSpace(wxCfg.AgentID) == "" {
|
|
return "", i18nx.Errorf("error.e0093")
|
|
}
|
|
return fmt.Sprintf(
|
|
"https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_privateinfo&agentid=%s&state=%s#wechat_redirect",
|
|
url.QueryEscape(strings.TrimSpace(wxCfg.CorpID)),
|
|
url.QueryEscape(strings.TrimSpace(wxCfg.OAuthRedirect)),
|
|
url.QueryEscape(strings.TrimSpace(wxCfg.AgentID)),
|
|
url.QueryEscape(strings.TrimSpace(state)),
|
|
), nil
|
|
}
|
|
|
|
func BuildQRCodeLoginURL(state string) (string, error) {
|
|
if !Enabled() {
|
|
return "", i18nx.Errorf("error.e0109")
|
|
}
|
|
if strings.TrimSpace(wxCfg.OAuthRedirect) == "" {
|
|
return "", i18nx.Errorf("error.e0107")
|
|
}
|
|
if strings.TrimSpace(wxCfg.AgentID) == "" {
|
|
return "", i18nx.Errorf("error.e0093")
|
|
}
|
|
return fmt.Sprintf(
|
|
"https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=%s&agentid=%s&redirect_uri=%s&state=%s",
|
|
url.QueryEscape(strings.TrimSpace(wxCfg.CorpID)),
|
|
url.QueryEscape(strings.TrimSpace(wxCfg.AgentID)),
|
|
url.QueryEscape(strings.TrimSpace(wxCfg.OAuthRedirect)),
|
|
url.QueryEscape(strings.TrimSpace(state)),
|
|
), nil
|
|
}
|
|
|
|
func CreateState(next string) (string, error) {
|
|
secret := strings.TrimSpace(StateSecret())
|
|
if secret == "" {
|
|
return "", errorsx.BusinessError(1, "企业微信登录密钥未配置")
|
|
}
|
|
payload := statePayload{
|
|
Next: sanitizeNextPath(next),
|
|
ExpiredAt: time.Now().Add(StateTTL).Unix(),
|
|
}
|
|
nonce, err := randomToken("ws_")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
payload.Nonce = nonce
|
|
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
encoded := base64.RawURLEncoding.EncodeToString(body)
|
|
return encoded + "." + signState(encoded, secret), nil
|
|
}
|
|
|
|
func ParseState(state string) (string, error) {
|
|
secret := strings.TrimSpace(StateSecret())
|
|
if secret == "" {
|
|
return "", errStateInvalid
|
|
}
|
|
parts := strings.Split(strings.TrimSpace(state), ".")
|
|
if len(parts) != 2 {
|
|
return "", errStateInvalid
|
|
}
|
|
if !hmac.Equal([]byte(parts[1]), []byte(signState(parts[0], secret))) {
|
|
return "", errStateInvalid
|
|
}
|
|
body, err := base64.RawURLEncoding.DecodeString(parts[0])
|
|
if err != nil {
|
|
return "", errStateInvalid
|
|
}
|
|
payload := statePayload{}
|
|
if err = json.Unmarshal(body, &payload); err != nil {
|
|
return "", errStateInvalid
|
|
}
|
|
if payload.ExpiredAt <= time.Now().Unix() {
|
|
return "", errStateInvalid
|
|
}
|
|
return sanitizeNextPath(payload.Next), nil
|
|
}
|
|
|
|
func IssueLoginTicket(loginResp *response.LoginResponse) (string, error) {
|
|
if loginResp == nil {
|
|
return "", i18nx.Errorf("error.e0272")
|
|
}
|
|
ticket, err := randomToken("wlt_")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
cleanupExpiredLoginTickets()
|
|
loginTicketStore.Store(ticket, loginTicket{
|
|
Response: loginResp,
|
|
ExpiredAt: time.Now().Add(LoginTicketTTL),
|
|
})
|
|
return ticket, nil
|
|
}
|
|
|
|
func ConsumeLoginTicket(ticket string) (*response.LoginResponse, error) {
|
|
ticket = strings.TrimSpace(ticket)
|
|
if ticket == "" {
|
|
return nil, errorsx.InvalidParamI18n("error.e0072")
|
|
}
|
|
value, ok := loginTicketStore.LoadAndDelete(ticket)
|
|
if !ok {
|
|
return nil, errorsx.UnauthorizedI18n("error.e0271")
|
|
}
|
|
record, ok := value.(loginTicket)
|
|
if !ok || record.Response == nil || time.Now().After(record.ExpiredAt) {
|
|
return nil, errorsx.UnauthorizedI18n("error.e0271")
|
|
}
|
|
return record.Response, nil
|
|
}
|
|
|
|
func signState(content, secret string) string {
|
|
mac := hmac.New(sha256.New, []byte(secret))
|
|
_, _ = mac.Write([]byte(content))
|
|
return hex.EncodeToString(mac.Sum(nil))
|
|
}
|
|
|
|
func cleanupExpiredLoginTickets() {
|
|
now := time.Now()
|
|
loginTicketStore.Range(func(key, value any) bool {
|
|
record, ok := value.(loginTicket)
|
|
if !ok || now.After(record.ExpiredAt) {
|
|
loginTicketStore.Delete(key)
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
func sanitizeNextPath(next string) string {
|
|
next = strings.TrimSpace(next)
|
|
if next == "" || !strings.HasPrefix(next, "/") || strings.HasPrefix(next, "//") {
|
|
return defaultLoginNextPath
|
|
}
|
|
return next
|
|
}
|
|
|
|
func randomToken(prefix string) (string, error) {
|
|
buf := make([]byte, 24)
|
|
if _, err := rand.Read(buf); err != nil {
|
|
return "", err
|
|
}
|
|
return prefix + hex.EncodeToString(buf), nil
|
|
}
|
|
|
|
func GetUserDetail(code string) (*LoginUser, error) {
|
|
if !Enabled() {
|
|
return nil, i18nx.Errorf("error.e0109")
|
|
}
|
|
code = strings.TrimSpace(code)
|
|
if code == "" {
|
|
return nil, i18nx.Errorf("error.e0202")
|
|
}
|
|
|
|
oauthClient := w.GetOauth()
|
|
userInfo, err := oauthClient.GetUserInfo(code)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if strings.TrimSpace(userInfo.UserID) == "" {
|
|
return nil, i18nx.Errorf("error.e0198")
|
|
}
|
|
|
|
ret := &LoginUser{
|
|
CorpID: wxCfg.CorpID,
|
|
UserID: strings.TrimSpace(userInfo.UserID),
|
|
OpenID: strings.TrimSpace(userInfo.OpenID),
|
|
ExternalUserID: strings.TrimSpace(userInfo.ExternalUserID),
|
|
UserTicket: strings.TrimSpace(userInfo.UserTicket),
|
|
UserInfo: userInfo,
|
|
}
|
|
|
|
if ret.UserTicket != "" {
|
|
if detail, detailErr := oauthClient.GetUserDetail(&oauth.GetUserDetailRequest{UserTicket: ret.UserTicket}); detailErr == nil {
|
|
ret.UserDetail = detail
|
|
ret.Avatar = strings.TrimSpace(detail.Avatar)
|
|
ret.Mobile = strings.TrimSpace(detail.Mobile)
|
|
ret.Email = strings.TrimSpace(detail.Email)
|
|
ret.BizMail = strings.TrimSpace(detail.BizMail)
|
|
}
|
|
}
|
|
|
|
if profile, profileErr := w.GetAddressList().UserGet(ret.UserID); profileErr == nil {
|
|
ret.UserProfile = profile
|
|
if strings.TrimSpace(profile.Name) != "" {
|
|
ret.Name = strings.TrimSpace(profile.Name)
|
|
}
|
|
if strings.TrimSpace(profile.Avatar) != "" {
|
|
ret.Avatar = strings.TrimSpace(profile.Avatar)
|
|
}
|
|
if ret.Mobile == "" && strings.TrimSpace(profile.Mobile) != "" {
|
|
ret.Mobile = strings.TrimSpace(profile.Mobile)
|
|
}
|
|
if ret.Email == "" && strings.TrimSpace(profile.Email) != "" {
|
|
ret.Email = strings.TrimSpace(profile.Email)
|
|
}
|
|
if ret.BizMail == "" && strings.TrimSpace(profile.BizMail) != "" {
|
|
ret.BizMail = strings.TrimSpace(profile.BizMail)
|
|
}
|
|
}
|
|
|
|
if ret.Name == "" {
|
|
ret.Name = ret.UserID
|
|
}
|
|
return ret, nil
|
|
}
|