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" .}}

@@ -102,6 +103,77 @@

+

+ 微信绑定 +

+ {{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')