diff --git a/models/models.go b/models/models.go
index 11f445830..0f4679b4f 100755
--- a/models/models.go
+++ b/models/models.go
@@ -136,6 +136,7 @@ func init() {
new(AiModelManage),
new(OfficialTag),
new(OfficialTagRepos),
+ new(WechatBindLog),
)
tablesStatistic = append(tablesStatistic,
diff --git a/models/user.go b/models/user.go
index b362472e8..f7857248b 100755
--- a/models/user.go
+++ b/models/user.go
@@ -177,6 +177,10 @@ type User struct {
//BlockChain
PublicKey string `xorm:"INDEX"`
PrivateKey string `xorm:"INDEX"`
+
+ //Wechat
+ WechatOpenId string `xorm:"INDEX"`
+ WechatBindUnix timeutil.TimeStamp
}
// SearchOrganizationsOptions options to filter organizations
@@ -185,6 +189,11 @@ type SearchOrganizationsOptions struct {
All bool
}
+// GenerateRandomAvatar generates a random avatar for user.
+func (u *User) IsBindWechat() bool {
+ return u.WechatOpenId != ""
+}
+
// ColorFormat writes a colored string to identify this struct
func (u *User) ColorFormat(s fmt.State) {
log.ColorFprintf(s, "%d:%s",
diff --git a/models/wechat_bind.go b/models/wechat_bind.go
new file mode 100644
index 000000000..b100221f2
--- /dev/null
+++ b/models/wechat_bind.go
@@ -0,0 +1,98 @@
+package models
+
+import (
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/timeutil"
+ "time"
+)
+
+type WechatBindAction int
+
+const (
+ WECHAT_BIND WechatBindAction = iota + 1
+ WECHAT_UNBIND
+)
+
+type WechatBindLog struct {
+ ID int64 `xorm:"pk autoincr"`
+ UserID int64 `xorm:"INDEX"`
+ WechatOpenId string `xorm:"INDEX"`
+ Action int
+ CreateTime time.Time `xorm:"INDEX created"`
+}
+
+func BindWechatOpenId(userId int64, wechatOpenId string) error {
+ sess := x.NewSession()
+ defer sess.Close()
+ if err := sess.Begin(); err != nil {
+ return err
+ }
+
+ param := &User{WechatOpenId: wechatOpenId, WechatBindUnix: timeutil.TimeStampNow()}
+ n, err := sess.Where("ID = ?", userId).Update(param)
+ if err != nil {
+ log.Error("update wechat_open_id failed,e=%v", err)
+ if e := sess.Rollback(); e != nil {
+ log.Error("BindWechatOpenId: sess.Rollback: %v", e)
+ }
+ return err
+ }
+ if n == 0 {
+ log.Error("update wechat_open_id failed,user not exist,userId=%d", userId)
+ if e := sess.Rollback(); e != nil {
+ log.Error("BindWechatOpenId: sess.Rollback: %v", e)
+ }
+ return nil
+ }
+
+ logParam := &WechatBindLog{
+ UserID: userId,
+ WechatOpenId: wechatOpenId,
+ Action: int(WECHAT_BIND),
+ }
+ sess.Insert(logParam)
+ return sess.Commit()
+}
+
+func GetUserWechatOpenId(userId int64) string {
+ param := &User{}
+ x.Cols("wechat_open_id").Where("ID =?", userId).Get(param)
+ return param.WechatOpenId
+}
+
+func GetUserByWechatOpenId(wechatOpenId string) *User {
+ user := &User{}
+ x.Where("wechat_open_id = ?", wechatOpenId).Get(user)
+ return user
+}
+
+func UnbindWechatOpenId(userId int64, oldWechatOpenID string) error {
+ sess := x.NewSession()
+ defer sess.Close()
+ if err := sess.Begin(); err != nil {
+ return err
+ }
+
+ n, err := x.Table(new(User)).Where("ID = ? AND wechat_open_id =?", userId, oldWechatOpenID).Update(map[string]interface{}{"wechat_open_id": "", "wechat_bind_unix": nil})
+ if err != nil {
+ log.Error("update wechat_open_id failed,e=%v", err)
+ if e := sess.Rollback(); e != nil {
+ log.Error("UnbindWechatOpenId: sess.Rollback: %v", e)
+ }
+ return err
+ }
+ if n == 0 {
+ log.Error("update wechat_open_id failed,user not exist,userId=%d", userId)
+ if e := sess.Rollback(); e != nil {
+ log.Error("UnbindWechatOpenId: sess.Rollback: %v", e)
+ }
+ return nil
+ }
+ logParam := &WechatBindLog{
+ UserID: userId,
+ WechatOpenId: oldWechatOpenID,
+ Action: int(WECHAT_UNBIND),
+ }
+ sess.Insert(logParam)
+ return sess.Commit()
+}
diff --git a/modules/auth/wechat/access_token.go b/modules/auth/wechat/access_token.go
new file mode 100644
index 000000000..0a63bc2de
--- /dev/null
+++ b/modules/auth/wechat/access_token.go
@@ -0,0 +1,67 @@
+package wechat
+
+import (
+ "code.gitea.io/gitea/modules/redis/redis_client"
+ "code.gitea.io/gitea/modules/redis/redis_key"
+ "code.gitea.io/gitea/modules/redis/redis_lock"
+ "time"
+)
+
+const EMPTY_REDIS_VAL = "Nil"
+
+var accessTokenLock = redis_lock.NewDistributeLock()
+
+func GetWechatAccessToken() string {
+ token, _ := redis_client.Get(redis_key.WechatAccessTokenKey())
+ if token != "" {
+ if token == EMPTY_REDIS_VAL {
+ return ""
+ }
+ live, _ := redis_client.TTL(redis_key.WechatAccessTokenKey())
+ //refresh wechat access token when expire time less than 5 minutes
+ if live > 0 && live < 300 {
+ refreshAccessToken()
+ }
+ return token
+ }
+ return refreshAndGetAccessToken()
+}
+
+func refreshAccessToken() {
+ if ok := accessTokenLock.Lock(redis_key.AccessTokenLockKey(), 3*time.Second); ok {
+ defer accessTokenLock.UnLock(redis_key.AccessTokenLockKey())
+ callAccessTokenAndUpdateCache()
+ }
+}
+
+func refreshAndGetAccessToken() string {
+ if ok := accessTokenLock.LockWithWait(redis_key.AccessTokenLockKey(), 3*time.Second, 3*time.Second); ok {
+ defer accessTokenLock.UnLock(redis_key.AccessTokenLockKey())
+ token, _ := redis_client.Get(redis_key.WechatAccessTokenKey())
+ if token != "" {
+ if token == EMPTY_REDIS_VAL {
+ return ""
+ }
+ return token
+ }
+ return callAccessTokenAndUpdateCache()
+ }
+ return ""
+
+}
+
+func callAccessTokenAndUpdateCache() string {
+ r := callAccessToken()
+
+ var token string
+ if r != nil {
+ token = r.Access_token
+ }
+
+ if token == "" {
+ redis_client.Setex(redis_key.WechatAccessTokenKey(), EMPTY_REDIS_VAL, 10*time.Second)
+ return ""
+ }
+ redis_client.Setex(redis_key.WechatAccessTokenKey(), token, time.Duration(r.Expires_in)*time.Second)
+ return token
+}
diff --git a/modules/auth/wechat/bind.go b/modules/auth/wechat/bind.go
new file mode 100644
index 000000000..7b4bffc02
--- /dev/null
+++ b/modules/auth/wechat/bind.go
@@ -0,0 +1,71 @@
+package wechat
+
+import (
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/log"
+ "fmt"
+)
+
+type QRCode4BindCache struct {
+ UserId int64
+ Status int
+}
+
+const (
+ BIND_STATUS_UNBIND = 0
+ BIND_STATUS_SCANNED = 1
+ BIND_STATUS_BOUND = 2
+ BIND_STATUS_EXPIRED = 9
+)
+
+const (
+ BIND_REPLY_SUCCESS = "扫码成功,您可以使用OpenI启智社区算力环境。"
+ BIND_REPLY_WECHAT_ACCOUNT_USED = "认证失败,您的微信号已绑定其他启智账号"
+ BIND_REPLY_OPENI_ACCOUNT_USED = "认证失败,您待认证的启智账号已绑定其他微信号"
+ BIND_REPLY_FAILED_DEFAULT = "微信认证失败"
+)
+
+type WechatBindError struct {
+ Reply string
+}
+
+func NewWechatBindError(reply string) WechatBindError {
+ return WechatBindError{Reply: reply}
+}
+
+func (err WechatBindError) Error() string {
+ return fmt.Sprint("wechat bind error,reply=%s", err.Reply)
+}
+
+func BindWechat(userId int64, wechatOpenId string) error {
+ if !IsWechatAccountAvailable(userId, wechatOpenId) {
+ log.Error("bind wechat failed, because user use wrong wechat account to bind,userId=%d wechatOpenId=%s", userId, wechatOpenId)
+ return NewWechatBindError(BIND_REPLY_WECHAT_ACCOUNT_USED)
+ }
+ if !IsUserAvailableForWechatBind(userId, wechatOpenId) {
+ log.Error("openI account has been used,userId=%d wechatOpenId=%s", userId, wechatOpenId)
+ return NewWechatBindError(BIND_REPLY_OPENI_ACCOUNT_USED)
+ }
+ return models.BindWechatOpenId(userId, wechatOpenId)
+}
+
+func UnbindWechat(userId int64, oldWechatOpenId string) error {
+ return models.UnbindWechatOpenId(userId, oldWechatOpenId)
+}
+
+//IsUserAvailableForWechatBind if user has bound wechat and the bound openId is not the given wechatOpenId,return false
+//otherwise,return true
+func IsUserAvailableForWechatBind(userId int64, wechatOpenId string) bool {
+ currentOpenId := models.GetUserWechatOpenId(userId)
+ return currentOpenId == "" || currentOpenId == wechatOpenId
+}
+
+//IsWechatAccountAvailable if wechat account used by another account,return false
+//if wechat account not used or used by the given user,return true
+func IsWechatAccountAvailable(userId int64, wechatOpenId string) bool {
+ user := models.GetUserByWechatOpenId(wechatOpenId)
+ if user != nil && user.WechatOpenId != "" && user.ID != userId {
+ return false
+ }
+ return true
+}
diff --git a/modules/auth/wechat/call.go b/modules/auth/wechat/call.go
new file mode 100644
index 000000000..f93535c6b
--- /dev/null
+++ b/modules/auth/wechat/call.go
@@ -0,0 +1,5 @@
+package wechat
+
+type WechatCall interface {
+ call()
+}
diff --git a/modules/auth/wechat/client.go b/modules/auth/wechat/client.go
new file mode 100644
index 000000000..6734977a1
--- /dev/null
+++ b/modules/auth/wechat/client.go
@@ -0,0 +1,126 @@
+package wechat
+
+import (
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "encoding/json"
+ "fmt"
+ "github.com/go-resty/resty/v2"
+ "strconv"
+ "time"
+)
+
+var (
+ client *resty.Client
+)
+
+const (
+ GRANT_TYPE = "client_credential"
+ ACCESS_TOKEN_PATH = "/cgi-bin/token"
+ QR_CODE_Path = "/cgi-bin/qrcode/create"
+ ACTION_QR_STR_SCENE = "QR_STR_SCENE"
+
+ ERR_CODE_ACCESSTOKEN_EXPIRE = 42001
+ ERR_CODE_ACCESSTOKEN_INVALID = 40001
+)
+
+type AccessTokenResponse struct {
+ Access_token string
+ Expires_in int
+}
+
+type QRCodeResponse struct {
+ Ticket string `json:"ticket"`
+ Expire_Seconds int `json:"expire_seconds"`
+ Url string `json:"url"`
+}
+
+type QRCodeRequest struct {
+ Action_name string `json:"action_name"`
+ Action_info ActionInfo `json:"action_info"`
+ Expire_seconds int `json:"expire_seconds"`
+}
+
+type ActionInfo struct {
+ Scene Scene `json:"scene"`
+}
+
+type Scene struct {
+ Scene_str string `json:"scene_str"`
+}
+
+type ErrorResponse struct {
+ Errcode int
+ Errmsg string
+}
+
+func getWechatRestyClient() *resty.Client {
+ if client == nil {
+ client = resty.New()
+ client.SetTimeout(time.Duration(setting.WechatApiTimeoutSeconds) * time.Second)
+ }
+ return client
+}
+
+// api doc:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html
+func callAccessToken() *AccessTokenResponse {
+ client := getWechatRestyClient()
+
+ var result AccessTokenResponse
+ _, err := client.R().
+ SetQueryParam("grant_type", GRANT_TYPE).
+ SetQueryParam("appid", setting.WechatAppId).
+ SetQueryParam("secret", setting.WechatAppSecret).
+ SetResult(&result).
+ Get(setting.WechatApiHost + ACCESS_TOKEN_PATH)
+ if err != nil {
+ log.Error("get wechat access token failed,e=%v", err)
+ return nil
+ }
+ return &result
+}
+
+//callQRCodeCreate call the wechat api to create qr-code,
+// api doc: https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html
+func callQRCodeCreate(sceneStr string) (*QRCodeResponse, bool) {
+ client := getWechatRestyClient()
+
+ body := &QRCodeRequest{
+ Action_name: ACTION_QR_STR_SCENE,
+ Action_info: ActionInfo{Scene: Scene{Scene_str: sceneStr}},
+ Expire_seconds: setting.WechatQRCodeExpireSeconds,
+ }
+ bodyJson, _ := json.Marshal(body)
+ var result QRCodeResponse
+ r, err := client.R().
+ SetHeader("Content-Type", "application/json").
+ SetQueryParam("access_token", GetWechatAccessToken()).
+ SetBody(bodyJson).
+ SetResult(&result).
+ Post(setting.WechatApiHost + QR_CODE_Path)
+ if err != nil {
+ log.Error("create QR code failed,e=%v", err)
+ return nil, false
+ }
+ errCode := getErrorCodeFromResponse(r)
+ if errCode == ERR_CODE_ACCESSTOKEN_EXPIRE || errCode == ERR_CODE_ACCESSTOKEN_INVALID {
+ return nil, true
+ }
+ if result.Url == "" {
+ return nil, false
+ }
+ log.Info("%v", r)
+ return &result, false
+}
+
+func getErrorCodeFromResponse(r *resty.Response) int {
+ a := r.Body()
+ resultMap := make(map[string]interface{}, 0)
+ json.Unmarshal(a, &resultMap)
+ code := resultMap["errcode"]
+ if code == nil {
+ return -1
+ }
+ c, _ := strconv.Atoi(fmt.Sprint(code))
+ return c
+}
diff --git a/modules/auth/wechat/event_handle.go b/modules/auth/wechat/event_handle.go
new file mode 100644
index 000000000..b40ab3101
--- /dev/null
+++ b/modules/auth/wechat/event_handle.go
@@ -0,0 +1,76 @@
+package wechat
+
+import (
+ "code.gitea.io/gitea/modules/redis/redis_client"
+ "code.gitea.io/gitea/modules/redis/redis_key"
+ "encoding/json"
+ "encoding/xml"
+ "strings"
+ "time"
+)
+
+//
+//
+//
+// 123456789
+//
+//
+//
+//
+//
+type WechatEvent struct {
+ ToUserName string
+ FromUserName string
+ CreateTime int64
+ MsgType string
+ Event string
+ EventKey string
+ Ticket string
+}
+
+type EventReply struct {
+ XMLName xml.Name `xml:"xml"`
+ ToUserName string
+ FromUserName string
+ CreateTime int64
+ MsgType string
+ Content string
+}
+
+const (
+ WECHAT_EVENT_SUBSCRIBE = "subscribe"
+ WECHAT_EVENT_SCAN = "SCAN"
+)
+
+const (
+ WECHAT_MSG_TYPE_TEXT = "text"
+)
+
+func HandleSubscribeEvent(we WechatEvent) string {
+ eventKey := we.EventKey
+ if eventKey == "" {
+ return ""
+ }
+ sceneStr := strings.TrimPrefix(eventKey, "qrscene_")
+ key := redis_key.WechatBindingUserIdKey(sceneStr)
+ val, _ := redis_client.Get(key)
+ if val == "" {
+ return ""
+ }
+ qrCache := new(QRCode4BindCache)
+ json.Unmarshal([]byte(val), qrCache)
+ if qrCache.Status == BIND_STATUS_UNBIND {
+ err := BindWechat(qrCache.UserId, we.FromUserName)
+ if err != nil {
+ if err, ok := err.(WechatBindError); ok {
+ return err.Reply
+ }
+ return BIND_REPLY_FAILED_DEFAULT
+ }
+ qrCache.Status = BIND_STATUS_BOUND
+ jsonStr, _ := json.Marshal(qrCache)
+ redis_client.Setex(redis_key.WechatBindingUserIdKey(sceneStr), string(jsonStr), 60*time.Second)
+ }
+
+ return BIND_REPLY_SUCCESS
+}
diff --git a/modules/auth/wechat/qr_code.go b/modules/auth/wechat/qr_code.go
new file mode 100644
index 000000000..9d2f6ca04
--- /dev/null
+++ b/modules/auth/wechat/qr_code.go
@@ -0,0 +1,13 @@
+package wechat
+
+import "code.gitea.io/gitea/modules/log"
+
+func GetWechatQRCode4Bind(sceneStr string) *QRCodeResponse {
+ result, retryFlag := callQRCodeCreate(sceneStr)
+ if retryFlag {
+ log.Info("retry wechat qr-code calling,sceneStr=%s", sceneStr)
+ refreshAccessToken()
+ result, _ = callQRCodeCreate(sceneStr)
+ }
+ return result
+}
diff --git a/modules/context/auth.go b/modules/context/auth.go
index dba2c3269..287823dea 100755
--- a/modules/context/auth.go
+++ b/modules/context/auth.go
@@ -23,12 +23,14 @@ import (
// ToggleOptions contains required or check options
type ToggleOptions struct {
- SignInRequired bool
- SignOutRequired bool
- AdminRequired bool
- DisableCSRF bool
- BasicAuthRequired bool
- OperationRequired bool
+ SignInRequired bool
+ SignOutRequired bool
+ AdminRequired bool
+ DisableCSRF bool
+ BasicAuthRequired bool
+ OperationRequired bool
+ WechatAuthRequired bool
+ WechatAuthRequiredForAPI bool
}
// Toggle returns toggle options as middleware
@@ -135,6 +137,36 @@ func Toggle(options *ToggleOptions) macaron.Handler {
}
}
+ if setting.WechatAuthSwitch && options.WechatAuthRequired {
+ if !ctx.IsSigned {
+ ctx.SetCookie("redirect_to", setting.AppSubURL+ctx.Req.URL.RequestURI(), 0, setting.AppSubURL)
+ ctx.Redirect(setting.AppSubURL + "/user/login")
+ return
+ }
+ if ctx.User.WechatOpenId == "" {
+ ctx.SetCookie("redirect_to", setting.AppSubURL+ctx.Req.URL.RequestURI(), 0, setting.AppSubURL)
+ ctx.Redirect(setting.AppSubURL + "/authentication/wechat/bind")
+ }
+ }
+
+ if setting.WechatAuthSwitch && options.WechatAuthRequiredForAPI {
+ if !ctx.IsSigned {
+ ctx.SetCookie("redirect_to", setting.AppSubURL+ctx.Req.URL.RequestURI(), 0, setting.AppSubURL)
+ ctx.Redirect(setting.AppSubURL + "/user/login")
+ return
+ }
+ if ctx.User.WechatOpenId == "" {
+ redirectUrl := ctx.Query("redirect_to")
+ if redirectUrl == "" {
+ redirectUrl = ctx.Req.URL.RequestURI()
+ }
+ ctx.SetCookie("redirect_to", setting.AppSubURL+redirectUrl, 0, setting.AppSubURL)
+ ctx.JSON(200, map[string]string{
+ "WechatRedirectUrl": setting.AppSubURL + "/authentication/wechat/bind",
+ })
+ }
+ }
+
// Redirect to log in page if auto-signin info is provided and has not signed in.
if !options.SignOutRequired && !ctx.IsSigned && !auth.IsAPIPath(ctx.Req.URL.Path) &&
len(ctx.GetCookie(setting.CookieUserName)) > 0 {
diff --git a/modules/redis/redis_client/client.go b/modules/redis/redis_client/client.go
new file mode 100644
index 000000000..437aecdae
--- /dev/null
+++ b/modules/redis/redis_client/client.go
@@ -0,0 +1,87 @@
+package redis_client
+
+import (
+ "code.gitea.io/gitea/modules/labelmsg"
+ "fmt"
+ "github.com/gomodule/redigo/redis"
+ "math"
+ "strconv"
+ "time"
+)
+
+func Setex(key, value string, timeout time.Duration) (bool, error) {
+ redisClient := labelmsg.Get()
+ defer redisClient.Close()
+
+ seconds := int(math.Floor(timeout.Seconds()))
+ reply, err := redisClient.Do("SETEX", key, seconds, value)
+ if err != nil {
+ return false, err
+ }
+ if reply != "OK" {
+ return false, nil
+ }
+ return true, nil
+
+}
+
+func Setnx(key, value string, timeout time.Duration) (bool, error) {
+ redisClient := labelmsg.Get()
+ defer redisClient.Close()
+
+ seconds := int(math.Floor(timeout.Seconds()))
+ reply, err := redisClient.Do("SET", key, value, "NX", "EX", seconds)
+ if err != nil {
+ return false, err
+ }
+ if reply != "OK" {
+ return false, nil
+ }
+ return true, nil
+
+}
+
+func Get(key string) (string, error) {
+ redisClient := labelmsg.Get()
+ defer redisClient.Close()
+
+ reply, err := redisClient.Do("GET", key)
+ if err != nil {
+ return "", err
+ }
+ if reply == nil {
+ return "", err
+ }
+ s, _ := redis.String(reply, nil)
+ return s, nil
+
+}
+
+func Del(key string) (int, error) {
+ redisClient := labelmsg.Get()
+ defer redisClient.Close()
+
+ reply, err := redisClient.Do("DEL", key)
+ if err != nil {
+ return 0, err
+ }
+ if reply == nil {
+ return 0, err
+ }
+ s, _ := redis.Int(reply, nil)
+ return s, nil
+
+}
+
+func TTL(key string) (int, error) {
+ redisClient := labelmsg.Get()
+ defer redisClient.Close()
+
+ reply, err := redisClient.Do("TTL", key)
+ if err != nil {
+ return 0, err
+ }
+ n, _ := strconv.Atoi(fmt.Sprint(reply))
+ return n, nil
+
+}
diff --git a/modules/redis/redis_key/key_base.go b/modules/redis/redis_key/key_base.go
new file mode 100644
index 000000000..0efc6ed38
--- /dev/null
+++ b/modules/redis/redis_key/key_base.go
@@ -0,0 +1,16 @@
+package redis_key
+
+import "strings"
+
+const KEY_SEPARATE = ":"
+
+func KeyJoin(keys ...string) string {
+ var build strings.Builder
+ for _, v := range keys {
+ build.WriteString(v)
+ build.WriteString(KEY_SEPARATE)
+ }
+ s := build.String()
+ s = strings.TrimSuffix(s, KEY_SEPARATE)
+ return s
+}
diff --git a/modules/redis/redis_key/wechat_redis_key.go b/modules/redis/redis_key/wechat_redis_key.go
new file mode 100644
index 000000000..1858576fd
--- /dev/null
+++ b/modules/redis/redis_key/wechat_redis_key.go
@@ -0,0 +1,14 @@
+package redis_key
+
+const PREFIX = "wechat"
+
+func WechatBindingUserIdKey(sceneStr string) string {
+ return KeyJoin(PREFIX, sceneStr, "scene_userId")
+}
+
+func WechatAccessTokenKey() string {
+ return KeyJoin(PREFIX, "access_token")
+}
+func AccessTokenLockKey() string {
+ return KeyJoin(PREFIX, "access_token_lock")
+}
diff --git a/modules/redis/redis_lock/lock.go b/modules/redis/redis_lock/lock.go
new file mode 100644
index 000000000..0faed3237
--- /dev/null
+++ b/modules/redis/redis_lock/lock.go
@@ -0,0 +1,40 @@
+package redis_lock
+
+import (
+ "code.gitea.io/gitea/modules/redis/redis_client"
+ "time"
+)
+
+type DistributeLock struct {
+}
+
+func NewDistributeLock() *DistributeLock {
+ return &DistributeLock{}
+}
+
+func (lock *DistributeLock) Lock(lockKey string, expireTime time.Duration) bool {
+ isOk, _ := redis_client.Setnx(lockKey, "", expireTime)
+ return isOk
+}
+
+func (lock *DistributeLock) LockWithWait(lockKey string, expireTime time.Duration, waitTime time.Duration) bool {
+ start := time.Now().Unix() * 1000
+ duration := waitTime.Milliseconds()
+ for {
+ isOk, _ := redis_client.Setnx(lockKey, "", expireTime)
+ if isOk {
+ return true
+ }
+ if time.Now().Unix()*1000-start > duration {
+ return false
+ }
+ time.Sleep(50 * time.Millisecond)
+ }
+
+ return false
+}
+
+func (lock *DistributeLock) UnLock(lockKey string) error {
+ _, err := redis_client.Del(lockKey)
+ return err
+}
diff --git a/modules/setting/setting.go b/modules/setting/setting.go
index 4e063fe05..2a29dd700 100755
--- a/modules/setting/setting.go
+++ b/modules/setting/setting.go
@@ -470,7 +470,7 @@ var (
BenchmarkTypes string
BenchmarkGpuTypes string
BenchmarkResourceSpecs string
- BenchmarkMaxDuration int64
+ BenchmarkMaxDuration int64
//snn4imagenet config
IsSnn4imagenetEnabled bool
@@ -531,6 +531,14 @@ var (
ElkTimeFormat string
PROJECT_LIMIT_PAGES []string
+ //wechat config
+ WechatApiHost string
+ WechatApiTimeoutSeconds int
+ WechatAppId string
+ WechatAppSecret string
+ WechatQRCodeExpireSeconds int
+ WechatAuthSwitch bool
+
//nginx proxy
PROXYURL string
RadarMap = struct {
@@ -1345,6 +1353,14 @@ func NewContext() {
ElkTimeFormat = sec.Key("ELKTIMEFORMAT").MustString("date_time")
PROJECT_LIMIT_PAGES = strings.Split(sec.Key("project_limit_pages").MustString(""), ",")
+ sec = Cfg.Section("wechat")
+ WechatApiHost = sec.Key("HOST").MustString("https://api.weixin.qq.com")
+ WechatApiTimeoutSeconds = sec.Key("TIMEOUT_SECONDS").MustInt(3)
+ WechatAppId = sec.Key("APP_ID").MustString("wxba77b915a305a57d")
+ WechatAppSecret = sec.Key("APP_SECRET").MustString("e48e13f315adc32749ddc7057585f198")
+ WechatQRCodeExpireSeconds = sec.Key("QR_CODE_EXPIRE_SECONDS").MustInt(120)
+ WechatAuthSwitch = sec.Key("AUTH_SWITCH").MustBool(true)
+
SetRadarMapConfig()
sec = Cfg.Section("warn_mail")
diff --git a/package-lock.json b/package-lock.json
index 7b706207b..f5a941869 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7787,6 +7787,11 @@
}
}
},
+ "js-cookie": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz",
+ "integrity": "sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw=="
+ },
"js-file-download": {
"version": "0.4.12",
"resolved": "https://registry.npmjs.org/js-file-download/-/js-file-download-0.4.12.tgz",
@@ -11147,6 +11152,11 @@
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
"integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc="
},
+ "qrcodejs2": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/qrcodejs2/-/qrcodejs2-0.0.2.tgz",
+ "integrity": "sha1-Rlr+Xjnxn6zsuTLBH3oYYQkUauE="
+ },
"qs": {
"version": "6.9.4",
"resolved": "https://registry.npm.taobao.org/qs/download/qs-6.9.4.tgz",
diff --git a/package.json b/package.json
index e5f829bf1..0f93faaea 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"jquery": "3.5.1",
"jquery-datetimepicker": "2.5.21",
"jquery.are-you-sure": "1.9.0",
+ "js-cookie": "3.0.1",
"less-loader": "6.1.0",
"mini-css-extract-plugin": "0.9.0",
"monaco-editor": "0.20.0",
@@ -41,6 +42,7 @@
"postcss-loader": "3.0.0",
"postcss-preset-env": "6.7.0",
"postcss-safe-parser": "4.0.2",
+ "qrcodejs2": "0.0.2",
"qs": "6.9.4",
"remixicon": "2.5.0",
"spark-md5": "3.0.1",
diff --git a/public/img/qrcode_reload.png b/public/img/qrcode_reload.png
new file mode 100644
index 000000000..81ff160b1
Binary files /dev/null and b/public/img/qrcode_reload.png differ
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index c8dbc3a34..766a99af0 100755
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -59,6 +59,7 @@
package v1
import (
+ "code.gitea.io/gitea/routers/authentication"
"net/http"
"strings"
@@ -997,6 +998,12 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Group("/topics", func() {
m.Get("/search", repo.TopicSearch)
})
+ m.Group("/from_wechat", func() {
+ m.Get("/event", authentication.ValidEventSource)
+ m.Post("/event", authentication.AcceptWechatEvent)
+ m.Get("/prd/event", authentication.ValidEventSource)
+ m.Post("/prd/event", authentication.AcceptWechatEvent)
+ })
}, securityHeaders(), context.APIContexter(), sudo())
}
diff --git a/routers/authentication/wechat.go b/routers/authentication/wechat.go
new file mode 100644
index 000000000..72871afb3
--- /dev/null
+++ b/routers/authentication/wechat.go
@@ -0,0 +1,126 @@
+package authentication
+
+import (
+ "code.gitea.io/gitea/modules/auth/wechat"
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/redis/redis_client"
+ "code.gitea.io/gitea/modules/redis/redis_key"
+ "code.gitea.io/gitea/modules/setting"
+ "encoding/json"
+ "errors"
+ gouuid "github.com/satori/go.uuid"
+ "time"
+)
+
+const tplBindPage base.TplName = "repo/wx_autorize"
+
+type QRCodeResponse struct {
+ Url string
+ Ticket string
+ SceneStr string
+ ExpireSeconds int
+}
+
+// GetQRCode4Bind get QR code for wechat binding
+func GetQRCode4Bind(ctx *context.Context) {
+ userId := ctx.User.ID
+
+ r, err := createQRCode4Bind(userId)
+ if err != nil {
+ ctx.JSON(200, map[string]interface{}{
+ "code": "9999",
+ "msg": "Get QR code failed",
+ })
+ return
+ }
+
+ ctx.JSON(200, map[string]interface{}{
+ "code": "00",
+ "msg": "success",
+ "data": r,
+ })
+}
+
+// GetBindStatus the web page will poll the service to get bind status
+func GetBindStatus(ctx *context.Context) {
+ sceneStr := ctx.Query("sceneStr")
+ val, _ := redis_client.Get(redis_key.WechatBindingUserIdKey(sceneStr))
+ if val == "" {
+ ctx.JSON(200, map[string]interface{}{
+ "code": "00",
+ "msg": "QR code expired",
+ "data": map[string]interface{}{
+ "status": wechat.BIND_STATUS_EXPIRED,
+ },
+ })
+ return
+ }
+ qrCache := new(wechat.QRCode4BindCache)
+ json.Unmarshal([]byte(val), qrCache)
+ ctx.JSON(200, map[string]interface{}{
+ "code": "00",
+ "msg": "success",
+ "data": map[string]interface{}{
+ "status": qrCache.Status,
+ },
+ })
+}
+
+// UnbindWechat
+func UnbindWechat(ctx *context.Context) {
+ if ctx.User.WechatOpenId != "" {
+ wechat.UnbindWechat(ctx.User.ID, ctx.User.WechatOpenId)
+ }
+
+ ctx.JSON(200, map[string]interface{}{
+ "code": "00",
+ "msg": "success",
+ })
+}
+
+// GetBindPage
+func GetBindPage(ctx *context.Context) {
+ userId := ctx.User.ID
+ r, _ := createQRCode4Bind(userId)
+ if r != nil {
+ ctx.Data["qrcode"] = r
+ }
+ redirectUrl := ctx.Query("redirect_to")
+ if redirectUrl != "" {
+ ctx.SetCookie("redirect_to", setting.AppSubURL+redirectUrl, 0, setting.AppSubURL)
+ }
+ ctx.HTML(200, tplBindPage)
+}
+
+func createQRCode4Bind(userId int64) (*QRCodeResponse, error) {
+ log.Info("start to create qr-code for binding.userId=%d", userId)
+ sceneStr := gouuid.NewV4().String()
+ r := wechat.GetWechatQRCode4Bind(sceneStr)
+ if r == nil {
+ return nil, errors.New("createQRCode4Bind failed")
+ }
+
+ jsonStr, _ := json.Marshal(&wechat.QRCode4BindCache{
+ UserId: userId,
+ Status: wechat.BIND_STATUS_UNBIND,
+ })
+ isOk, err := redis_client.Setex(redis_key.WechatBindingUserIdKey(sceneStr), string(jsonStr), time.Duration(setting.WechatQRCodeExpireSeconds)*time.Second)
+ if err != nil {
+ log.Error("createQRCode4Bind failed.e=%+v", err)
+ return nil, err
+ }
+ if !isOk {
+ log.Error("createQRCode4Bind failed.redis reply is not ok")
+ return nil, errors.New("reply is not ok when set WechatBindingUserIdKey")
+ }
+
+ result := &QRCodeResponse{
+ Url: r.Url,
+ Ticket: r.Ticket,
+ SceneStr: sceneStr,
+ ExpireSeconds: setting.WechatQRCodeExpireSeconds,
+ }
+ return result, nil
+}
diff --git a/routers/authentication/wechat_event.go b/routers/authentication/wechat_event.go
new file mode 100644
index 000000000..9b1cebec6
--- /dev/null
+++ b/routers/authentication/wechat_event.go
@@ -0,0 +1,47 @@
+package authentication
+
+import (
+ "code.gitea.io/gitea/modules/auth/wechat"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/log"
+ "encoding/xml"
+ "io/ioutil"
+ "time"
+)
+
+// AcceptWechatEvent
+// https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html
+// https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Passive_user_reply_message.html
+func AcceptWechatEvent(ctx *context.Context) {
+ b, _ := ioutil.ReadAll(ctx.Req.Request.Body)
+ we := wechat.WechatEvent{}
+ xml.Unmarshal(b, &we)
+
+ log.Info("accept wechat event= %+v", we)
+ var replyStr string
+ switch we.Event {
+ case wechat.WECHAT_EVENT_SUBSCRIBE, wechat.WECHAT_EVENT_SCAN:
+ replyStr = wechat.HandleSubscribeEvent(we)
+ break
+ }
+
+ if replyStr == "" {
+ log.Info("reply str is empty")
+ return
+ }
+ reply := &wechat.EventReply{
+ ToUserName: we.FromUserName,
+ FromUserName: we.ToUserName,
+ CreateTime: time.Now().Unix(),
+ MsgType: wechat.WECHAT_MSG_TYPE_TEXT,
+ Content: replyStr,
+ }
+ ctx.XML(200, reply)
+}
+
+// ValidEventSource
+func ValidEventSource(ctx *context.Context) {
+ echostr := ctx.Query("echostr")
+ ctx.Write([]byte(echostr))
+ return
+}
diff --git a/routers/routes/routes.go b/routers/routes/routes.go
index e9c7d3fd1..21c5e2e79 100755
--- a/routers/routes/routes.go
+++ b/routers/routes/routes.go
@@ -6,6 +6,7 @@ package routes
import (
"bytes"
+ "code.gitea.io/gitea/routers/authentication"
"encoding/gob"
"net/http"
"path"
@@ -274,6 +275,8 @@ func RegisterRoutes(m *macaron.Macaron) {
ignSignInAndCsrf := context.Toggle(&context.ToggleOptions{DisableCSRF: true})
reqSignOut := context.Toggle(&context.ToggleOptions{SignOutRequired: true})
reqBasicAuth := context.Toggle(&context.ToggleOptions{BasicAuthRequired: true, DisableCSRF: true})
+ reqWechatBind := context.Toggle(&context.ToggleOptions{WechatAuthRequired: true})
+ reqWechatBindForApi := context.Toggle(&context.ToggleOptions{WechatAuthRequiredForAPI: true})
bindIgnErr := binding.BindIgnErr
validation.AddBindingRules()
@@ -392,6 +395,13 @@ func RegisterRoutes(m *macaron.Macaron) {
}, ignSignInAndCsrf, reqSignIn)
m.Post("/login/oauth/access_token", bindIgnErr(auth.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth)
+ m.Group("/authentication/wechat", func() {
+ m.Get("/qrCode4Bind", authentication.GetQRCode4Bind)
+ m.Get("/bindStatus", authentication.GetBindStatus)
+ m.Post("/unbind", authentication.UnbindWechat)
+ m.Get("/bind", authentication.GetBindPage)
+ }, reqSignIn)
+
m.Group("/user/settings", func() {
m.Get("", userSetting.Profile)
m.Post("", bindIgnErr(auth.UpdateProfileForm{}), userSetting.ProfilePost)
@@ -983,17 +993,17 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Get("", reqRepoCloudBrainReader, repo.CloudBrainShow)
})
m.Group("/:jobid", func() {
- m.Get("/debug", cloudbrain.AdminOrJobCreaterRight, repo.CloudBrainDebug)
+ m.Get("/debug", reqWechatBind,cloudbrain.AdminOrJobCreaterRight, repo.CloudBrainDebug)
m.Post("/commit_image", cloudbrain.AdminOrJobCreaterRight, bindIgnErr(auth.CommitImageCloudBrainForm{}), repo.CloudBrainCommitImage)
m.Post("/stop", cloudbrain.AdminOrOwnerOrJobCreaterRight, repo.CloudBrainStop)
m.Post("/del", cloudbrain.AdminOrOwnerOrJobCreaterRight, repo.CloudBrainDel)
- m.Post("/restart", cloudbrain.AdminOrJobCreaterRight, repo.CloudBrainRestart)
+ m.Post("/restart", reqWechatBindForApi, cloudbrain.AdminOrJobCreaterRight, repo.CloudBrainRestart)
m.Get("/rate", reqRepoCloudBrainReader, repo.GetRate)
m.Get("/models", reqRepoCloudBrainReader, repo.CloudBrainShowModels)
m.Get("/download_model", cloudbrain.AdminOrJobCreaterRight, repo.CloudBrainDownloadModel)
})
- m.Get("/create", reqRepoCloudBrainWriter, repo.CloudBrainNew)
- m.Post("/create", reqRepoCloudBrainWriter, bindIgnErr(auth.CreateCloudBrainForm{}), repo.CloudBrainCreate)
+ m.Get("/create", reqWechatBind, reqRepoCloudBrainWriter, repo.CloudBrainNew)
+ m.Post("/create", reqWechatBind, reqRepoCloudBrainWriter, bindIgnErr(auth.CreateCloudBrainForm{}), repo.CloudBrainCreate)
m.Group("/benchmark", func() {
m.Get("", reqRepoCloudBrainReader, repo.CloudBrainBenchmarkIndex)
@@ -1005,8 +1015,8 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Post("/del", cloudbrain.AdminOrOwnerOrJobCreaterRight, repo.BenchmarkDel)
m.Get("/rate", reqRepoCloudBrainReader, repo.GetRate)
})
- m.Get("/create", reqRepoCloudBrainWriter, repo.CloudBrainBenchmarkNew)
- m.Post("/create", reqRepoCloudBrainWriter, bindIgnErr(auth.CreateCloudBrainForm{}), repo.CloudBrainBenchmarkCreate)
+ m.Get("/create", reqWechatBind, reqRepoCloudBrainWriter, repo.CloudBrainBenchmarkNew)
+ m.Post("/create", reqWechatBind, reqRepoCloudBrainWriter, bindIgnErr(auth.CreateCloudBrainForm{}), repo.CloudBrainBenchmarkCreate)
m.Get("/get_child_types", repo.GetChildTypes)
})
}, context.RepoRef())
@@ -1068,8 +1078,8 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Get("/create_version", cloudbrain.AdminOrJobCreaterRight, repo.TrainJobNewVersion)
m.Post("/create_version", cloudbrain.AdminOrJobCreaterRight, bindIgnErr(auth.CreateModelArtsTrainJobForm{}), repo.TrainJobCreateVersion)
})
- m.Get("/create", reqRepoCloudBrainWriter, repo.TrainJobNew)
- m.Post("/create", reqRepoCloudBrainWriter, bindIgnErr(auth.CreateModelArtsTrainJobForm{}), repo.TrainJobCreate)
+ m.Get("/create", reqWechatBind, reqRepoCloudBrainWriter, repo.TrainJobNew)
+ m.Post("/create", reqWechatBind, reqRepoCloudBrainWriter, bindIgnErr(auth.CreateModelArtsTrainJobForm{}), repo.TrainJobCreate)
m.Get("/para-config-list", reqRepoCloudBrainReader, repo.TrainJobGetConfigList)
})
@@ -1081,8 +1091,8 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Get("/result_download", cloudbrain.AdminOrJobCreaterRight, repo.ResultDownload)
m.Get("/downloadall", repo.DownloadMultiResultFile)
})
- m.Get("/create", reqRepoCloudBrainWriter, repo.InferenceJobNew)
- m.Post("/create", reqRepoCloudBrainWriter, bindIgnErr(auth.CreateModelArtsInferenceJobForm{}), repo.InferenceJobCreate)
+ m.Get("/create", reqWechatBind, reqRepoCloudBrainWriter, repo.InferenceJobNew)
+ m.Post("/create", reqWechatBind, reqRepoCloudBrainWriter, bindIgnErr(auth.CreateModelArtsInferenceJobForm{}), repo.InferenceJobCreate)
})
}, context.RepoRef())
diff --git a/templates/repo/debugjob/index.tmpl b/templates/repo/debugjob/index.tmpl
index c5a1b9ee6..b624349cd 100755
--- a/templates/repo/debugjob/index.tmpl
+++ b/templates/repo/debugjob/index.tmpl
@@ -328,7 +328,7 @@
{{$.i18n.Tr "repo.debug"}}
{{else}}
-
+
{{$.i18n.Tr "repo.debug_again"}}
{{end}}
@@ -481,6 +481,7 @@
// 调试和评分新开窗口
const {AppSubUrl, StaticUrlPrefix, csrf} = window.config;
let url={{.RepoLink}}
+ let redirect_to = {{$.Link}}
let getParam=getQueryVariable('debugListType')
let dropdownValue = ['all','',false].includes(getParam)? '全部' : getParam
diff --git a/templates/repo/modelarts/trainjob/show.tmpl b/templates/repo/modelarts/trainjob/show.tmpl
index 7280076f6..667b98a52 100755
--- a/templates/repo/modelarts/trainjob/show.tmpl
+++ b/templates/repo/modelarts/trainjob/show.tmpl
@@ -562,16 +562,13 @@ td, th {
$('input[name="JobId"]').val(obj.JobID)
$('input[name="VersionName"]').val(obj.VersionName).addClass('model_disabled')
$('.ui.dimmer').css({"background-color":"rgb(136, 136, 136,0.7)"})
- createModelName()
-
-
+ createModelName()
},
onHide:function(){
document.getElementById("formId").reset();
$('.ui.dimmer').css({"background-color":""})
$('.ui.error.message').text()
$('.ui.error.message').css('display','none')
-
}
})
.modal('show')
diff --git a/templates/repo/wx_autorize.tmpl b/templates/repo/wx_autorize.tmpl
new file mode 100644
index 000000000..52b9f29bc
--- /dev/null
+++ b/templates/repo/wx_autorize.tmpl
@@ -0,0 +1,15 @@
+
+{{template "base/head" .}}
+
+
+ {{template "repo/header" .}}
+ {{template "base/alert" .}}
+
+
+
+
+
+
+
+{{template "base/footer" .}}
+
diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl
index 54c6695ec..d42415493 100644
--- a/templates/user/settings/profile.tmpl
+++ b/templates/user/settings/profile.tmpl
@@ -1,6 +1,7 @@
{{template "base/head" .}}
{{template "user/settings/navbar" .}}
+
{{template "base/alert" .}}
+
+ {{if not .SignedUser.IsBindWechat}}
+
+ {{else}}
+
+
+
+ |
+ 绑定账号信息
+ |
+
+
+ 绑定时间
+ |
+
+ 操作
+ |
+
+
+
+ 微信
+ |
+
+
+ {{TimeSinceUnix1 .SignedUser.WechatBindUnix}}
+ |
+
+ |
+
+
+
+ {{end}}
+
+
{{template "base/footer" .}}
+
diff --git a/web_src/js/components/WxAutorize.vue b/web_src/js/components/WxAutorize.vue
new file mode 100644
index 000000000..9643a5608
--- /dev/null
+++ b/web_src/js/components/WxAutorize.vue
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
+
![]()
+
+

+
+
微信扫码关注公众号即可完成绑定
+
+
+
+
+
+
+
+
+
+
diff --git a/web_src/js/features/cloudrbanin.js b/web_src/js/features/cloudrbanin.js
index f54212a0f..5fdc16564 100644
--- a/web_src/js/features/cloudrbanin.js
+++ b/web_src/js/features/cloudrbanin.js
@@ -195,16 +195,19 @@ export default async function initCloudrain() {
const jobName = this.dataset.jobname
getModelInfo(repoPath,modelName,versionName,jobName)
})
- function debugAgain(JobID,debugUrl){
+ function debugAgain(JobID,debugUrl,redirect_to){
if($('#' + JobID+ '-text').text()==="RUNNING"){
window.open(debugUrl+'debug')
}else{
$.ajax({
type:"POST",
- url:debugUrl+'restart',
+ url:debugUrl+'restart?redirect_to='+redirect_to,
data:$('#debugAgainForm-'+JobID).serialize(),
success:function(res){
- if(res.result_code==="0"){
+ if(res['WechatRedirectUrl']){
+ window.location.href=res['WechatRedirectUrl']
+ }
+ else if(res.result_code==="0"){
if(res.job_id!==JobID){
location.reload()
}else{
@@ -220,7 +223,6 @@ export default async function initCloudrain() {
},
error :function(res){
console.log(res)
-
}
})
}
@@ -228,7 +230,8 @@ export default async function initCloudrain() {
$('.ui.basic.ai_debug').click(function() {
const jobID = this.dataset.jobid
const repoPath = this.dataset.repopath
- debugAgain(jobID,repoPath)
+ const redirect_to = this.dataset.linkpath
+ debugAgain(jobID,repoPath,redirect_to)
})
}
diff --git a/web_src/js/index.js b/web_src/js/index.js
index 1abf3ba38..a1456d179 100755
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -10,6 +10,7 @@ import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import axios from 'axios';
import qs from 'qs';
+import Cookies from 'js-cookie'
import 'jquery.are-you-sure';
import './vendor/semanticdropdown.js';
import {svg} from './utils.js';
@@ -40,11 +41,13 @@ import EditTopics from './components/EditTopics.vue';
import DataAnalysis from './components/DataAnalysis.vue'
import Contributors from './components/Contributors.vue'
import Model from './components/Model.vue';
+import WxAutorize from './components/WxAutorize.vue'
import initCloudrain from './features/cloudrbanin.js'
Vue.use(ElementUI);
Vue.prototype.$axios = axios;
+Vue.prototype.$Cookies = Cookies;
Vue.prototype.qs = qs;
const {AppSubUrl, StaticUrlPrefix, csrf} = window.config;
@@ -2921,6 +2924,7 @@ $(document).ready(async () => {
initVueImages();
initVueModel();
initVueDataAnalysis();
+ initVueWxAutorize();
initTeamSettings();
initCtrlEnterSubmit();
initNavbarContentToggle();
@@ -3735,7 +3739,16 @@ function initObsUploader() {
template: ''
});
}
-
+function initVueWxAutorize() {
+ const el = document.getElementById('WxAutorize');
+ if (!el) {
+ return;
+ }
+ new Vue({
+ el:el,
+ render: h => h(WxAutorize)
+ });
+}
window.timeAddManual = function () {
$('.mini.modal')