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(OfficialTag), | |||
| new(OfficialTagRepos), | |||
| new(WechatBindLog), | |||
| ) | |||
| tablesStatistic = append(tablesStatistic, | |||
| @@ -177,6 +177,10 @@ type User struct { | |||
| //BlockChain | |||
| PublicKey string `xorm:"INDEX"` | |||
| PrivateKey string `xorm:"INDEX"` | |||
| 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", | |||
| @@ -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 | |||
| 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 { | |||
| @@ -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 | |||
| 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") | |||
| @@ -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", | |||
| @@ -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", | |||
| @@ -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()) | |||
| } | |||
| @@ -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 ( | |||
| "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()) | |||
| @@ -328,7 +328,7 @@ | |||
| {{$.i18n.Tr "repo.debug"}} | |||
| </a> | |||
| {{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"}} | |||
| </a> | |||
| {{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 | |||
| @@ -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') | |||
| @@ -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" .}} | |||
| <div class="user settings profile"> | |||
| {{template "user/settings/navbar" .}} | |||
| <div class="alert" style="top: 0;"></div> | |||
| <div class="ui container"> | |||
| {{template "base/alert" .}} | |||
| <h4 class="ui top attached header"> | |||
| @@ -102,6 +103,77 @@ | |||
| </div> | |||
| </form> | |||
| </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> | |||
| {{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 | |||
| 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) | |||
| }) | |||
| } | |||
| @@ -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: '<ObsUploader />' | |||
| }); | |||
| } | |||
| function initVueWxAutorize() { | |||
| const el = document.getElementById('WxAutorize'); | |||
| if (!el) { | |||
| return; | |||
| } | |||
| new Vue({ | |||
| el:el, | |||
| render: h => h(WxAutorize) | |||
| }); | |||
| } | |||
| window.timeAddManual = function () { | |||
| $('.mini.modal') | |||