Reviewed-on: https://git.openi.org.cn/OpenI/aiforge/pulls/1565 Reviewed-by: lewis <747342561@qq.com>tags/v1.22.2.2^2
| @@ -136,6 +136,7 @@ func init() { | |||||
| new(AiModelManage), | new(AiModelManage), | ||||
| new(OfficialTag), | new(OfficialTag), | ||||
| new(OfficialTagRepos), | new(OfficialTagRepos), | ||||
| new(WechatBindLog), | |||||
| ) | ) | ||||
| tablesStatistic = append(tablesStatistic, | tablesStatistic = append(tablesStatistic, | ||||
| @@ -177,6 +177,10 @@ type User struct { | |||||
| //BlockChain | //BlockChain | ||||
| PublicKey string `xorm:"INDEX"` | PublicKey string `xorm:"INDEX"` | ||||
| PrivateKey string `xorm:"INDEX"` | PrivateKey string `xorm:"INDEX"` | ||||
| WechatOpenId string `xorm:"INDEX"` | |||||
| WechatBindUnix timeutil.TimeStamp | |||||
| } | } | ||||
| // SearchOrganizationsOptions options to filter organizations | // SearchOrganizationsOptions options to filter organizations | ||||
| @@ -185,6 +189,11 @@ type SearchOrganizationsOptions struct { | |||||
| All bool | 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 | // ColorFormat writes a colored string to identify this struct | ||||
| func (u *User) ColorFormat(s fmt.State) { | func (u *User) ColorFormat(s fmt.State) { | ||||
| log.ColorFprintf(s, "%d:%s", | log.ColorFprintf(s, "%d:%s", | ||||
| @@ -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() | |||||
| } | |||||
| @@ -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 | |||||
| } | |||||
| @@ -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 | |||||
| } | |||||
| @@ -0,0 +1,5 @@ | |||||
| package wechat | |||||
| type WechatCall interface { | |||||
| call() | |||||
| } | |||||
| @@ -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 | |||||
| } | |||||
| @@ -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" | |||||
| ) | |||||
| //<xml> | |||||
| // <ToUserName><![CDATA[toUser]]></ToUserName> | |||||
| // <FromUserName><![CDATA[FromUser]]></FromUserName> | |||||
| // <CreateTime>123456789</CreateTime> | |||||
| // <MsgType><![CDATA[event]]></MsgType> | |||||
| // <Event><![CDATA[SCAN]]></Event> | |||||
| // <EventKey><![CDATA[SCENE_VALUE]]></EventKey> | |||||
| // <Ticket><![CDATA[TICKET]]></Ticket> | |||||
| //</xml> | |||||
| 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 | |||||
| } | |||||
| @@ -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 | |||||
| } | |||||
| @@ -23,12 +23,14 @@ import ( | |||||
| // ToggleOptions contains required or check options | // ToggleOptions contains required or check options | ||||
| type ToggleOptions struct { | 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 | // 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. | // 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) && | if !options.SignOutRequired && !ctx.IsSigned && !auth.IsAPIPath(ctx.Req.URL.Path) && | ||||
| len(ctx.GetCookie(setting.CookieUserName)) > 0 { | len(ctx.GetCookie(setting.CookieUserName)) > 0 { | ||||
| @@ -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 | |||||
| } | |||||
| @@ -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 | |||||
| } | |||||
| @@ -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") | |||||
| } | |||||
| @@ -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 | |||||
| } | |||||
| @@ -470,7 +470,7 @@ var ( | |||||
| BenchmarkTypes string | BenchmarkTypes string | ||||
| BenchmarkGpuTypes string | BenchmarkGpuTypes string | ||||
| BenchmarkResourceSpecs string | BenchmarkResourceSpecs string | ||||
| BenchmarkMaxDuration int64 | |||||
| BenchmarkMaxDuration int64 | |||||
| //snn4imagenet config | //snn4imagenet config | ||||
| IsSnn4imagenetEnabled bool | IsSnn4imagenetEnabled bool | ||||
| @@ -531,6 +531,14 @@ var ( | |||||
| ElkTimeFormat string | ElkTimeFormat string | ||||
| PROJECT_LIMIT_PAGES []string | PROJECT_LIMIT_PAGES []string | ||||
| //wechat config | |||||
| WechatApiHost string | |||||
| WechatApiTimeoutSeconds int | |||||
| WechatAppId string | |||||
| WechatAppSecret string | |||||
| WechatQRCodeExpireSeconds int | |||||
| WechatAuthSwitch bool | |||||
| //nginx proxy | //nginx proxy | ||||
| PROXYURL string | PROXYURL string | ||||
| RadarMap = struct { | RadarMap = struct { | ||||
| @@ -1345,6 +1353,14 @@ func NewContext() { | |||||
| ElkTimeFormat = sec.Key("ELKTIMEFORMAT").MustString("date_time") | ElkTimeFormat = sec.Key("ELKTIMEFORMAT").MustString("date_time") | ||||
| PROJECT_LIMIT_PAGES = strings.Split(sec.Key("project_limit_pages").MustString(""), ",") | 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() | SetRadarMapConfig() | ||||
| sec = Cfg.Section("warn_mail") | sec = Cfg.Section("warn_mail") | ||||
| @@ -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": { | "js-file-download": { | ||||
| "version": "0.4.12", | "version": "0.4.12", | ||||
| "resolved": "https://registry.npmjs.org/js-file-download/-/js-file-download-0.4.12.tgz", | "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", | "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", | ||||
| "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" | "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" | ||||
| }, | }, | ||||
| "qrcodejs2": { | |||||
| "version": "0.0.2", | |||||
| "resolved": "https://registry.npmjs.org/qrcodejs2/-/qrcodejs2-0.0.2.tgz", | |||||
| "integrity": "sha1-Rlr+Xjnxn6zsuTLBH3oYYQkUauE=" | |||||
| }, | |||||
| "qs": { | "qs": { | ||||
| "version": "6.9.4", | "version": "6.9.4", | ||||
| "resolved": "https://registry.npm.taobao.org/qs/download/qs-6.9.4.tgz", | "resolved": "https://registry.npm.taobao.org/qs/download/qs-6.9.4.tgz", | ||||
| @@ -33,6 +33,7 @@ | |||||
| "jquery": "3.5.1", | "jquery": "3.5.1", | ||||
| "jquery-datetimepicker": "2.5.21", | "jquery-datetimepicker": "2.5.21", | ||||
| "jquery.are-you-sure": "1.9.0", | "jquery.are-you-sure": "1.9.0", | ||||
| "js-cookie": "3.0.1", | |||||
| "less-loader": "6.1.0", | "less-loader": "6.1.0", | ||||
| "mini-css-extract-plugin": "0.9.0", | "mini-css-extract-plugin": "0.9.0", | ||||
| "monaco-editor": "0.20.0", | "monaco-editor": "0.20.0", | ||||
| @@ -41,6 +42,7 @@ | |||||
| "postcss-loader": "3.0.0", | "postcss-loader": "3.0.0", | ||||
| "postcss-preset-env": "6.7.0", | "postcss-preset-env": "6.7.0", | ||||
| "postcss-safe-parser": "4.0.2", | "postcss-safe-parser": "4.0.2", | ||||
| "qrcodejs2": "0.0.2", | |||||
| "qs": "6.9.4", | "qs": "6.9.4", | ||||
| "remixicon": "2.5.0", | "remixicon": "2.5.0", | ||||
| "spark-md5": "3.0.1", | "spark-md5": "3.0.1", | ||||
| @@ -59,6 +59,7 @@ | |||||
| package v1 | package v1 | ||||
| import ( | import ( | ||||
| "code.gitea.io/gitea/routers/authentication" | |||||
| "net/http" | "net/http" | ||||
| "strings" | "strings" | ||||
| @@ -997,6 +998,12 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
| m.Group("/topics", func() { | m.Group("/topics", func() { | ||||
| m.Get("/search", repo.TopicSearch) | 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()) | }, securityHeaders(), context.APIContexter(), sudo()) | ||||
| } | } | ||||
| @@ -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 | |||||
| } | |||||
| @@ -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 | |||||
| } | |||||
| @@ -6,6 +6,7 @@ package routes | |||||
| import ( | import ( | ||||
| "bytes" | "bytes" | ||||
| "code.gitea.io/gitea/routers/authentication" | |||||
| "encoding/gob" | "encoding/gob" | ||||
| "net/http" | "net/http" | ||||
| "path" | "path" | ||||
| @@ -274,6 +275,8 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
| ignSignInAndCsrf := context.Toggle(&context.ToggleOptions{DisableCSRF: true}) | ignSignInAndCsrf := context.Toggle(&context.ToggleOptions{DisableCSRF: true}) | ||||
| reqSignOut := context.Toggle(&context.ToggleOptions{SignOutRequired: true}) | reqSignOut := context.Toggle(&context.ToggleOptions{SignOutRequired: true}) | ||||
| reqBasicAuth := context.Toggle(&context.ToggleOptions{BasicAuthRequired: true, DisableCSRF: 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 | bindIgnErr := binding.BindIgnErr | ||||
| validation.AddBindingRules() | validation.AddBindingRules() | ||||
| @@ -392,6 +395,13 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
| }, ignSignInAndCsrf, reqSignIn) | }, ignSignInAndCsrf, reqSignIn) | ||||
| m.Post("/login/oauth/access_token", bindIgnErr(auth.AccessTokenForm{}), ignSignInAndCsrf, user.AccessTokenOAuth) | 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.Group("/user/settings", func() { | ||||
| m.Get("", userSetting.Profile) | m.Get("", userSetting.Profile) | ||||
| m.Post("", bindIgnErr(auth.UpdateProfileForm{}), userSetting.ProfilePost) | m.Post("", bindIgnErr(auth.UpdateProfileForm{}), userSetting.ProfilePost) | ||||
| @@ -983,17 +993,17 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
| m.Get("", reqRepoCloudBrainReader, repo.CloudBrainShow) | m.Get("", reqRepoCloudBrainReader, repo.CloudBrainShow) | ||||
| }) | }) | ||||
| m.Group("/:jobid", func() { | 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("/commit_image", cloudbrain.AdminOrJobCreaterRight, bindIgnErr(auth.CommitImageCloudBrainForm{}), repo.CloudBrainCommitImage) | ||||
| m.Post("/stop", cloudbrain.AdminOrOwnerOrJobCreaterRight, repo.CloudBrainStop) | m.Post("/stop", cloudbrain.AdminOrOwnerOrJobCreaterRight, repo.CloudBrainStop) | ||||
| m.Post("/del", cloudbrain.AdminOrOwnerOrJobCreaterRight, repo.CloudBrainDel) | 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("/rate", reqRepoCloudBrainReader, repo.GetRate) | ||||
| m.Get("/models", reqRepoCloudBrainReader, repo.CloudBrainShowModels) | m.Get("/models", reqRepoCloudBrainReader, repo.CloudBrainShowModels) | ||||
| m.Get("/download_model", cloudbrain.AdminOrJobCreaterRight, repo.CloudBrainDownloadModel) | 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.Group("/benchmark", func() { | ||||
| m.Get("", reqRepoCloudBrainReader, repo.CloudBrainBenchmarkIndex) | m.Get("", reqRepoCloudBrainReader, repo.CloudBrainBenchmarkIndex) | ||||
| @@ -1005,8 +1015,8 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
| m.Post("/del", cloudbrain.AdminOrOwnerOrJobCreaterRight, repo.BenchmarkDel) | m.Post("/del", cloudbrain.AdminOrOwnerOrJobCreaterRight, repo.BenchmarkDel) | ||||
| m.Get("/rate", reqRepoCloudBrainReader, repo.GetRate) | 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) | m.Get("/get_child_types", repo.GetChildTypes) | ||||
| }) | }) | ||||
| }, context.RepoRef()) | }, context.RepoRef()) | ||||
| @@ -1068,8 +1078,8 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
| m.Get("/create_version", cloudbrain.AdminOrJobCreaterRight, repo.TrainJobNewVersion) | m.Get("/create_version", cloudbrain.AdminOrJobCreaterRight, repo.TrainJobNewVersion) | ||||
| m.Post("/create_version", cloudbrain.AdminOrJobCreaterRight, bindIgnErr(auth.CreateModelArtsTrainJobForm{}), repo.TrainJobCreateVersion) | 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) | 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("/result_download", cloudbrain.AdminOrJobCreaterRight, repo.ResultDownload) | ||||
| m.Get("/downloadall", repo.DownloadMultiResultFile) | 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()) | }, context.RepoRef()) | ||||
| @@ -328,7 +328,7 @@ | |||||
| {{$.i18n.Tr "repo.debug"}} | {{$.i18n.Tr "repo.debug"}} | ||||
| </a> | </a> | ||||
| {{else}} | {{else}} | ||||
| <a id="ai-debug-{{.JobID}}" class='ui basic ai_debug {{if eq .Status "CREATING" "STOPPING" "WAITING" "STARTING"}} disabled {{else}}blue {{end}}button' data-jobid="{{.JobID}}" data-repopath='{{$.RepoLink}}{{if eq .ComputeResource "CPU/GPU"}}/cloudbrain{{else}}/modelarts/notebook{{end}}/{{.JobID}}/'> | |||||
| <a id="ai-debug-{{.JobID}}" class='ui basic ai_debug {{if eq .Status "CREATING" "STOPPING" "WAITING" "STARTING"}} disabled {{else}}blue {{end}}button' data-jobid="{{.JobID}}" data-repopath='{{$.RepoLink}}{{if eq .ComputeResource "CPU/GPU"}}/cloudbrain{{else}}/modelarts/notebook{{end}}/{{.JobID}}/' data-linkpath='{{$.Link}}'> | |||||
| {{$.i18n.Tr "repo.debug_again"}} | {{$.i18n.Tr "repo.debug_again"}} | ||||
| </a> | </a> | ||||
| {{end}} | {{end}} | ||||
| @@ -481,6 +481,7 @@ | |||||
| // 调试和评分新开窗口 | // 调试和评分新开窗口 | ||||
| const {AppSubUrl, StaticUrlPrefix, csrf} = window.config; | const {AppSubUrl, StaticUrlPrefix, csrf} = window.config; | ||||
| let url={{.RepoLink}} | let url={{.RepoLink}} | ||||
| let redirect_to = {{$.Link}} | |||||
| let getParam=getQueryVariable('debugListType') | let getParam=getQueryVariable('debugListType') | ||||
| let dropdownValue = ['all','',false].includes(getParam)? '全部' : getParam | let dropdownValue = ['all','',false].includes(getParam)? '全部' : getParam | ||||
| @@ -562,16 +562,13 @@ td, th { | |||||
| $('input[name="JobId"]').val(obj.JobID) | $('input[name="JobId"]').val(obj.JobID) | ||||
| $('input[name="VersionName"]').val(obj.VersionName).addClass('model_disabled') | $('input[name="VersionName"]').val(obj.VersionName).addClass('model_disabled') | ||||
| $('.ui.dimmer').css({"background-color":"rgb(136, 136, 136,0.7)"}) | $('.ui.dimmer').css({"background-color":"rgb(136, 136, 136,0.7)"}) | ||||
| createModelName() | |||||
| createModelName() | |||||
| }, | }, | ||||
| onHide:function(){ | onHide:function(){ | ||||
| document.getElementById("formId").reset(); | document.getElementById("formId").reset(); | ||||
| $('.ui.dimmer').css({"background-color":""}) | $('.ui.dimmer').css({"background-color":""}) | ||||
| $('.ui.error.message').text() | $('.ui.error.message').text() | ||||
| $('.ui.error.message').css('display','none') | $('.ui.error.message').css('display','none') | ||||
| } | } | ||||
| }) | }) | ||||
| .modal('show') | .modal('show') | ||||
| @@ -0,0 +1,15 @@ | |||||
| <!-- 头部导航栏 --> | |||||
| {{template "base/head" .}} | |||||
| <div class="alert" style="top: 0;"></div> | |||||
| <div class="repository release dataset-list view"> | |||||
| {{template "repo/header" .}} | |||||
| {{template "base/alert" .}} | |||||
| <input id="WxAutorize-qrcode" type="hidden" value="{{.qrcode.Ticket}}"> | |||||
| <div id="WxAutorize"> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| {{template "base/footer" .}} | |||||
| @@ -1,6 +1,7 @@ | |||||
| {{template "base/head" .}} | {{template "base/head" .}} | ||||
| <div class="user settings profile"> | <div class="user settings profile"> | ||||
| {{template "user/settings/navbar" .}} | {{template "user/settings/navbar" .}} | ||||
| <div class="alert" style="top: 0;"></div> | |||||
| <div class="ui container"> | <div class="ui container"> | ||||
| {{template "base/alert" .}} | {{template "base/alert" .}} | ||||
| <h4 class="ui top attached header"> | <h4 class="ui top attached header"> | ||||
| @@ -102,6 +103,77 @@ | |||||
| </div> | </div> | ||||
| </form> | </form> | ||||
| </div> | </div> | ||||
| <h4 class="ui top attached header"> | |||||
| 微信绑定<!-- {{.i18n.Tr "settings.avatar"}} --> | |||||
| </h4> | |||||
| {{if not .SignedUser.IsBindWechat}} | |||||
| <div class="ui attached segment"> | |||||
| <a href="/authentication/wechat/bind?redirect_to=/user/settings" class="ui green button">绑定微信</a> | |||||
| </div> | |||||
| {{else}} | |||||
| <div class="ui attached segment"> | |||||
| <table class="ui celled striped table provider titleless"> | |||||
| <thead> | |||||
| <th class="center aligned"> | |||||
| 绑定账号信息 | |||||
| </th> | |||||
| <!-- <th class="center aligned"> | |||||
| 详情 | |||||
| </th> --> | |||||
| <th class="center aligned"> | |||||
| 绑定时间 | |||||
| </th> | |||||
| <th class="center aligned"> | |||||
| 操作 | |||||
| </th> | |||||
| </thead> | |||||
| <tbody> | |||||
| <td class="center aligned"> | |||||
| 微信 | |||||
| </td> | |||||
| <!-- <td class="center aligned"> | |||||
| <img class="ui avatar image" width="24" height="24" src="{{.SignedUser.RelAvatarLink}}">{{.SignedUser.Name}} | |||||
| </td> --> | |||||
| <td class="center aligned"> | |||||
| {{TimeSinceUnix1 .SignedUser.WechatBindUnix}} | |||||
| <td class="center aligned"> | |||||
| <div> | |||||
| <a class="ui inverted orange button " onclick="showcreate(this)" href="javascript: void(0)">解除绑定</a> | |||||
| </div> | |||||
| </td> | |||||
| </tbody> | |||||
| </table> | |||||
| </div> | |||||
| {{end}} | |||||
| </div> | |||||
| <div class="ui mini modal wx-unbind"> | |||||
| <div class="header">确定要解绑微信?</div> | |||||
| <div class="content"> | |||||
| <p>解绑后将无法使用云脑计算资源</p> | |||||
| </div> | |||||
| <div class="actions"> | |||||
| <div class="ui blank cancel button">取消</div> | |||||
| <div class="ui green approve button">确定</div> | |||||
| </div> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| {{template "base/footer" .}} | {{template "base/footer" .}} | ||||
| <script> | |||||
| function showcreate(obj){ | |||||
| const {csrf} = window.config | |||||
| $('.ui.modal.wx-unbind') | |||||
| .modal({ | |||||
| onShow:function(){ | |||||
| }, | |||||
| onHide:function(){ | |||||
| }, | |||||
| onApprove:function($element){ | |||||
| $.post('/authentication/wechat/unbind',{_csrf:csrf},(data)=>{ | |||||
| $('.alert').html('解绑成功!').removeClass('alert-danger').addClass('alert-success').show().delay(1500).fadeOut(); | |||||
| window.location.href = '/user/settings' | |||||
| }) | |||||
| } | |||||
| }) | |||||
| .modal('show') | |||||
| } | |||||
| </script> | |||||
| @@ -0,0 +1,150 @@ | |||||
| <template> | |||||
| <div> | |||||
| <div class="ui container"> | |||||
| <div class="ui placeholder segment bgtask-none"> | |||||
| <div class="bgtask-content-header"> | |||||
| <h2 class="wx-title">微信扫码认证</h2> | |||||
| <p class="wx-desc-top">请绑定微信,然后再使用启智算力环境</p> | |||||
| </div> | |||||
| <div class="wx-login"> | |||||
| <div class="qrcode" > | |||||
| <img class="wx-qrcode" :src="wx_qrcodeImg" v-show="!!wx_qrcodeImg"> | |||||
| <span class="el-icon-loading" v-show="wxLoading"></span> | |||||
| <img class="wx-qrcode-reload" v-if="qrCodeReload" @click="getWxQrcode(true)" src="https://files.wondercv.com/auth/qrcode_reload.png"> | |||||
| </div> | |||||
| <div class="wx-desc-bottom" style="color:#919191">微信扫码关注公众号即可完成绑定</div> | |||||
| </div> | |||||
| <div class="user-agreement"> | |||||
| <i class="ri-information-line orange"></i>绑定微信代表已阅读并接受<a>OpenI启智社区AI协作平台使用协议</a> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </template> | |||||
| <script> | |||||
| const {_AppSubUrl, _StaticUrlPrefix, csrf} = window.config; | |||||
| export default { | |||||
| components: { | |||||
| }, | |||||
| data() { | |||||
| return { | |||||
| wx_qrcodeImg:'', | |||||
| wxLoading:false, | |||||
| isLogin:false, | |||||
| SceneStr:'', | |||||
| status:'', | |||||
| qrCodeReload:false | |||||
| }; | |||||
| }, | |||||
| methods: { | |||||
| getWxQrcode(reloadFlag) { | |||||
| if(reloadFlag){ | |||||
| this.qrCodeReload=false | |||||
| } | |||||
| this.wxLoading = true | |||||
| this.$axios.get('/authentication/wechat/qrCode4Bind').then((res)=>{ | |||||
| let ticket = res.data.data.Ticket | |||||
| this.SceneStr = res.data.data.SceneStr | |||||
| this.wx_qrcodeImg = 'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket='+ticket | |||||
| this.wxLoading = false | |||||
| this.WxqrCheck() | |||||
| }) | |||||
| }, | |||||
| WxqrCheck(){ | |||||
| this.$axios.get(`/authentication/wechat/bindStatus?sceneStr=${this.SceneStr}`).then((res)=>{ | |||||
| this.status = res.data.data.status | |||||
| this.isLogin = true | |||||
| }) | |||||
| } | |||||
| }, | |||||
| watch:{ | |||||
| isLogin: function () { | |||||
| let times = setInterval(async () => { | |||||
| if (this.status===0) { | |||||
| this.WxqrCheck() | |||||
| } else if (this.status === 9) { | |||||
| this.qrCodeReload=true | |||||
| } else if (this.status === 2) { | |||||
| //用户登录成功后清除定时器 | |||||
| clearInterval(times) | |||||
| $('.alert').html('绑定成功!').removeClass('alert-danger').addClass('alert-success').show().delay(1500).fadeOut(); | |||||
| window.location.href = this.$Cookies.get('redirect_to') | |||||
| } | |||||
| }, 1000) | |||||
| }, | |||||
| }, | |||||
| mounted() { | |||||
| this.getWxQrcode(false) | |||||
| // let QrcodeData = $("#WxAutorize-qrcode").val() | |||||
| // this.wx_qrcodeImg = 'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket='+QrcodeData | |||||
| }, | |||||
| created() { | |||||
| } | |||||
| }; | |||||
| </script> | |||||
| <style scoped> | |||||
| .el-icon-loading{ | |||||
| position: absolute; | |||||
| font-size: 32px; | |||||
| color: #bcbcbc; | |||||
| top: calc(50% - 16px); | |||||
| left: calc(50% - 16px); | |||||
| animation: rotating 2s linear infinite; | |||||
| } | |||||
| .wx-title{ | |||||
| font-family: SourceHanSansSC-medium; | |||||
| font-size: 24px; | |||||
| color: rgba(16, 16, 16, 100); | |||||
| } | |||||
| .wx-desc-top{ | |||||
| font-size: 14px; | |||||
| color: rgba(16, 16, 16, 100); | |||||
| font-family: SourceHanSansSC-light; | |||||
| } | |||||
| .qrcode{ | |||||
| width: 200px; | |||||
| height: 200px; | |||||
| line-height: 180px; | |||||
| text-align: center; | |||||
| position: relative; | |||||
| } | |||||
| .wx-login{ | |||||
| display: flex; | |||||
| flex-direction: column; | |||||
| align-items: center; | |||||
| margin-top: 24px; | |||||
| } | |||||
| .wx-qrcode{ | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| } | |||||
| .wx-qrcode-reload{ | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| left: 0px; | |||||
| top: 0px; | |||||
| position: absolute; | |||||
| cursor: pointer; | |||||
| } | |||||
| .wx-desc-bottom{ | |||||
| color: rgba(145, 145, 145, 100); | |||||
| font-size: 14px; | |||||
| font-family: SourceHanSansSC-regular; | |||||
| } | |||||
| .user-agreement{ | |||||
| display: flex; | |||||
| align-items: center; | |||||
| justify-content: center; | |||||
| margin-top: 51px; | |||||
| color: rgba(16, 16, 16, 100); | |||||
| font-size: 14px; | |||||
| font-family: SourceHanSansSC-regular; | |||||
| } | |||||
| .orange{ | |||||
| color: #f2711c; | |||||
| } | |||||
| </style> | |||||
| @@ -195,16 +195,19 @@ export default async function initCloudrain() { | |||||
| const jobName = this.dataset.jobname | const jobName = this.dataset.jobname | ||||
| getModelInfo(repoPath,modelName,versionName,jobName) | getModelInfo(repoPath,modelName,versionName,jobName) | ||||
| }) | }) | ||||
| function debugAgain(JobID,debugUrl){ | |||||
| function debugAgain(JobID,debugUrl,redirect_to){ | |||||
| if($('#' + JobID+ '-text').text()==="RUNNING"){ | if($('#' + JobID+ '-text').text()==="RUNNING"){ | ||||
| window.open(debugUrl+'debug') | window.open(debugUrl+'debug') | ||||
| }else{ | }else{ | ||||
| $.ajax({ | $.ajax({ | ||||
| type:"POST", | type:"POST", | ||||
| url:debugUrl+'restart', | |||||
| url:debugUrl+'restart?redirect_to='+redirect_to, | |||||
| data:$('#debugAgainForm-'+JobID).serialize(), | data:$('#debugAgainForm-'+JobID).serialize(), | ||||
| success:function(res){ | 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){ | if(res.job_id!==JobID){ | ||||
| location.reload() | location.reload() | ||||
| }else{ | }else{ | ||||
| @@ -220,7 +223,6 @@ export default async function initCloudrain() { | |||||
| }, | }, | ||||
| error :function(res){ | error :function(res){ | ||||
| console.log(res) | console.log(res) | ||||
| } | } | ||||
| }) | }) | ||||
| } | } | ||||
| @@ -228,7 +230,8 @@ export default async function initCloudrain() { | |||||
| $('.ui.basic.ai_debug').click(function() { | $('.ui.basic.ai_debug').click(function() { | ||||
| const jobID = this.dataset.jobid | const jobID = this.dataset.jobid | ||||
| const repoPath = this.dataset.repopath | const repoPath = this.dataset.repopath | ||||
| debugAgain(jobID,repoPath) | |||||
| const redirect_to = this.dataset.linkpath | |||||
| debugAgain(jobID,repoPath,redirect_to) | |||||
| }) | }) | ||||
| } | } | ||||
| @@ -10,6 +10,7 @@ import ElementUI from 'element-ui'; | |||||
| import 'element-ui/lib/theme-chalk/index.css'; | import 'element-ui/lib/theme-chalk/index.css'; | ||||
| import axios from 'axios'; | import axios from 'axios'; | ||||
| import qs from 'qs'; | import qs from 'qs'; | ||||
| import Cookies from 'js-cookie' | |||||
| import 'jquery.are-you-sure'; | import 'jquery.are-you-sure'; | ||||
| import './vendor/semanticdropdown.js'; | import './vendor/semanticdropdown.js'; | ||||
| import {svg} from './utils.js'; | import {svg} from './utils.js'; | ||||
| @@ -40,11 +41,13 @@ import EditTopics from './components/EditTopics.vue'; | |||||
| import DataAnalysis from './components/DataAnalysis.vue' | import DataAnalysis from './components/DataAnalysis.vue' | ||||
| import Contributors from './components/Contributors.vue' | import Contributors from './components/Contributors.vue' | ||||
| import Model from './components/Model.vue'; | import Model from './components/Model.vue'; | ||||
| import WxAutorize from './components/WxAutorize.vue' | |||||
| import initCloudrain from './features/cloudrbanin.js' | import initCloudrain from './features/cloudrbanin.js' | ||||
| Vue.use(ElementUI); | Vue.use(ElementUI); | ||||
| Vue.prototype.$axios = axios; | Vue.prototype.$axios = axios; | ||||
| Vue.prototype.$Cookies = Cookies; | |||||
| Vue.prototype.qs = qs; | Vue.prototype.qs = qs; | ||||
| const {AppSubUrl, StaticUrlPrefix, csrf} = window.config; | const {AppSubUrl, StaticUrlPrefix, csrf} = window.config; | ||||
| @@ -2921,6 +2924,7 @@ $(document).ready(async () => { | |||||
| initVueImages(); | initVueImages(); | ||||
| initVueModel(); | initVueModel(); | ||||
| initVueDataAnalysis(); | initVueDataAnalysis(); | ||||
| initVueWxAutorize(); | |||||
| initTeamSettings(); | initTeamSettings(); | ||||
| initCtrlEnterSubmit(); | initCtrlEnterSubmit(); | ||||
| initNavbarContentToggle(); | initNavbarContentToggle(); | ||||
| @@ -3735,7 +3739,16 @@ function initObsUploader() { | |||||
| template: '<ObsUploader />' | template: '<ObsUploader />' | ||||
| }); | }); | ||||
| } | } | ||||
| function initVueWxAutorize() { | |||||
| const el = document.getElementById('WxAutorize'); | |||||
| if (!el) { | |||||
| return; | |||||
| } | |||||
| new Vue({ | |||||
| el:el, | |||||
| render: h => h(WxAutorize) | |||||
| }); | |||||
| } | |||||
| window.timeAddManual = function () { | window.timeAddManual = function () { | ||||
| $('.mini.modal') | $('.mini.modal') | ||||