package service import ( "basic.com/valib/bhomeclient.git" "basic.com/valib/bhomedbapi.git" "basic.com/valib/c_bhomebus.git/proto/source/bhome_msg" "basic.com/valib/licence.git" "basic.com/valib/logger.git" "basic.com/valib/version.git" "context" "crypto/md5" "encoding/base64" "encoding/json" "errors" "fmt" "github.com/skip2/go-qrcode" "io" "io/ioutil" "net/http" "net/url" "os" "path" "runtime" "strings" "sync" "time" "vamicro/config" "vamicro/extend/util" "vamicro/version-control/models" "vamicro/version-control/response" "vamicro/version-control/utils" ) const ( uriVersion string = "/data/api-u/upgrade/findUpgradeVersion" uriDownload string = "/data/api-p/download" uriAuth string = "/data/api-s/authorization" uriMobile string = "/data/api-s/fromQrcode" ) type ( Payload struct { Arch string `json:"arch"` Programs []*models.Program `json:"programs"` Version string `json:"version"` Intro string `json:"intro"` PatchUrl string `json:"patchUrl"` } UpdateInfo struct { response.ResponseHead Data []Payload `json:"data"` } VersionInfo struct { Build string `json:"build"` Commit string `json:"commit"` Name string `json:"name"` Version string `json:"version"` } UpdateNotice struct { NoticeUser map[string]int HaveNewVersion int64 //是否有新版本,1有,0没有 PkgDownloaded int64 //是否下载完成,1有,0没有 LastNoticeTime int64 NewVersionProgram []*models.Program NoticeStatus bool } ) var ( updateNotice UpdateNotice updateNoticeLock sync.Mutex LastDownFile []string LastDownLock sync.RWMutex ) func Init() { backUpPath := GetBackupPath() if _, err := os.Stat(backUpPath); os.IsNotExist(err) { _ = os.MkdirAll(backUpPath, 0777) } preDownPath := GetPreDownPath() if _, err := os.Stat(preDownPath); os.IsNotExist(err) { _ = os.MkdirAll(preDownPath, 0777) } updateNotice.NoticeUser = make(map[string]int) LastDownFile = make([]string, 0) } func OnlineUpgrade() ([]*models.Program, error) { programMap := make(map[string]*models.Program) LastDownLock.Lock() defer LastDownLock.Unlock() //查找预下载升级包 preDowns := GetLastDownFile() logger.Info("系统开始升级!") for _, preDown := range preDowns { logger.Info(preDowns) ps, err := UpgradeViaZip(preDown) if err != nil { return []*models.Program{}, err } for _, p := range ps { programMap[p.Name] = p } } logger.Info("系统升级完成!!") if len(programMap) > 0 { //使用预下载升级完成 programs := make([]*models.Program, 0, len(programMap)) for _, v := range programMap { programs = append(programs, v) } return programs, nil } info, err := getUpdateInfo() if nil != err { return []*models.Program{}, errors.New("系统没有更新可用") } logger.Info("系统升级开始!") //循环下载,解压,覆盖升级包 for _, payload := range info.Data { ps, err := HandlerPatchPkg(payload) if err != nil { return []*models.Program{}, err } for _, p := range ps { programMap[p.Name] = p } } logger.Info("系统升级完成!!") if len(programMap) > 0 { // programs := make([]*models.Program, 0, len(programMap)) for _, v := range programMap { programs = append(programs, v) } return programs, nil } return []*models.Program{}, err } //循环下载,解压,覆盖升级包 func HandlerPatchPkg(payload Payload) ([]*models.Program, error) { if payload.PatchUrl == "" { return []*models.Program{}, errors.New("No dist file get") } //u, err := url.Parse("http://" + util.GetShopUrl() + uriDownload) u, err := url.Parse(payload.PatchUrl) if err != nil { logger.Error("parse url failed, url:", payload.PatchUrl, ", err:", err.Error()) return []*models.Program{}, err } upgradePath := GetPreDownPath() upfile := upgradePath + "/" + GetMd5(payload.Version) + ".tgz" //tmpFile, err := ioutil.TempFile("", "dist-*.zip") tmpFile, err := os.OpenFile(upfile, os.O_CREATE|os.O_WRONLY, 0666) if nil != err { logger.Error("OnlineUpgrade create temp file failed, err:", err.Error()) return []*models.Program{}, err } fmt.Println("OnlineUpgrade tmpFile.Name: ", tmpFile.Name()) defer func() { tmpFile.Close() backupPath := GetBackupPath() //文件从upgrade目录移动到backup目录 os.Rename(tmpFile.Name(), backupPath+"/"+tmpFile.Name()) //os.Remove(tmpFile.Name()) }() //query := u.Query() //query.Set("filename", info.Data.Archive) //u.RawQuery = query.Encode() resp, err := http.Get(u.String()) if err != nil { logger.Error("OnlineUpgrade parse url failed, url:", u.String(), ", err:", err.Error()) return []*models.Program{}, err } defer resp.Body.Close() if resp.StatusCode != 200 { logger.Error("OnlineUpgrade incorrect status, url:", u.String(), ", status:", resp.StatusCode) return []*models.Program{}, errors.New("Status code not 200") } _, err = io.Copy(tmpFile, resp.Body) if err != nil { logger.Error("OnlineUpgrade save upgrade file failed, url:", u.String(), ", err:", err.Error()) return []*models.Program{}, err } return UpgradeViaZip(tmpFile.Name()) } //循环下载升级包 func DownloadPatchPkg(payload Payload) (string, error) { if payload.PatchUrl == "" { return "", errors.New("no dist file get") } //u, err := url.Parse("http://" + util.GetShopUrl() + uriDownload) u, err := url.Parse(payload.PatchUrl) if err != nil { logger.Error("parse url failed, url:", payload.PatchUrl, ", err:", err.Error()) return "", err } upgradePath := GetPreDownPath() upfile := upgradePath + "/" + GetMd5(payload.Version) + ".tgz" _, err = os.Stat(upfile) if nil == err { //已下载过 logger.Info("PreDownUpgrade predown upgrade file have down:", upfile) return upfile, nil } tmpFile, err := os.OpenFile(upfile, os.O_CREATE|os.O_WRONLY, 0666) if nil != err { logger.Error("OnlineUpgrade create temp file failed, err:", err.Error()) return "", err } fmt.Println("OnlineUpgrade tmpFile.Name: ", tmpFile.Name()) defer func() { tmpFile.Close() }() resp, err := http.Get(u.String()) if err != nil { logger.Error("OnlineUpgrade parse url failed, url:", u.String(), ", err:", err.Error()) return "", err } defer resp.Body.Close() if resp.StatusCode != 200 { logger.Error("OnlineUpgrade incorrect status, url:", u.String(), ", status:", resp.StatusCode) return "", errors.New("status code not 200") } _, err = io.Copy(tmpFile, resp.Body) if err != nil { logger.Error("PreDownUpgrade parse url failed, url:", u.String(), ", err:", err.Error()) tmpFile.Close() os.Remove(upfile) return "", err } return upfile, nil } //检查是否需要更新版本 func CheckVersion() (string, string, string) { //获取服务端所有程序最新版本信息。 info, err := getUpdateInfo() if nil != err { return "", "", "" } if len(info.Data) <= 0 { logger.Error("checkVersion no programs get") return "", "", "" } //获取需要更新版本的程序列表 //programs := needUpgrade(info.Data.Programs) last := len(info.Data) - 1 return info.Data[last].PatchUrl, info.Data[last].Version, info.Data[last].Intro } func GetCurVersion() string { curEnv, err := GetRunVersionEnv() if err != nil { return "" } return curEnv } //获取服务端最新程序及其版本 func getUpdateInfo() (*UpdateInfo, error) { //const PrtSize = 32 << uintptr(^uintptr(0)>>63) //wordSize := strconv.Itoa(PrtSize) u, err := url.Parse("http://" + util.GetShopUrl() + uriVersion) if err != nil { logger.Error("parse url failed, url:", "http://"+util.GetShopUrl()+uriVersion, ", err:", err.Error()) return nil, err } query := u.Query() //query.Set("os", runtime.GOOS) query.Set("arch", runtime.GOARCH) query.Set("versionNum", GetCurVersion()) //query.Set("wordSize", wordSize) u.RawQuery = query.Encode() resp, err := http.Get(u.String()) if err != nil { logger.Error("checkVersion parse url failed, url:", u.String(), ", err:", err.Error()) return nil, err } if resp.StatusCode != 200 { logger.Error("checkVersion incorrect status, url:", u.String(), ", status:", resp.StatusCode) return nil, errors.New("Status code not 200") } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { logger.Error("checkVersion read body failed, url:", u.String(), ", err:", err.Error()) return nil, err } logger.Info("checkVersion dump body:", string(body)) var info UpdateInfo err = json.Unmarshal(body, &info) if err != nil { logger.Error("checkVersion json.Unmarshal failed, url:", u.String(), ", err:", err.Error()) return nil, err } //b, err := json.Marshal(&info) //if err != nil { // logger.Error("checkVersion json.Marshal failed, url:", u.String(), ", err:", err.Error()) //} else { // logger.Info("checkVersion get response, url:", u.String(), ", response:", string(b)) //} return &info, nil } //需要升级的程序 func needUpgrade(programs []*models.Program) []*models.Program { //获取当前运行版本目录 //dir := utils.GetExePath() dir := util.GetVamicroPath() newPrograms := []*models.Program{} for _, p := range programs { exe := path.Join(dir, p.Name) if !utils.PathExists(exe) { newPrograms = append(newPrograms, p) continue } lVersion, err := dynamicGetVersion(dir, exe) if err != nil { logger.Error("exe:", exe, "get local version failed, err:", err.Error()) continue } //版本名称转换为版本号 rVersion, err := vaversion.VersionName2VaVersion(p.Version) if err != nil { logger.Error("exe:", exe, "get remote version failed:", p.Version, ", err:", err.Error()) continue } c, err := lVersion.Compare(rVersion) if err != nil { logger.Error("exe:", exe, "version compare failed, remote:", p.Version, ", err:", err.Error()) continue } if c > 0 { logger.Error("exe:", exe, "local version greater, local:", lVersion, "remote:", p.Version) continue } if c < 0 { newPrograms = append(newPrograms, p) } } return newPrograms } func NoticeTick(c context.Context) { // tick := time.Tick(1 * time.Second) tick := time.Tick(24 * time.Hour) for { select { case <-c.Done(): logger.Info("proc close, self update exit") return case <-tick: //logger.Info("NoticeTick !!!") updateNoticeLock.Lock() for key, _ := range updateNotice.NoticeUser { if 0 < updateNotice.NoticeUser[key] { updateNotice.NoticeUser[key]-- } } updateNoticeLock.Unlock() } } } //升级配置处理 func SelfUpdateStart(c context.Context, ms *bhomeclient.MicroNode) { //三十秒检查一次配置并更新 // tick := time.Tick(30 * time.Second) tick := time.Tick(24 * time.Hour) var setting models.SysSetting for { select { case <-c.Done(): logger.Info("proc close, self update exit") return case <-tick: settings, err := setting.GetAllSetting() if nil != err { logger.Error("fetch setting failed") continue } for _, set := range settings { data, _ := json.Marshal(set) var nodes []bhome_msg.BHAddress nodes = append(nodes, bhome_msg.BHAddress{}) go ms.PublishNetTimeout(nodes, SysUpdateConfigTopic, data, 10) switch set.Name { case "sys_auto_clean": if "1" == set.Value { err := os.RemoveAll(GetBackupPath()) if nil != err { logger.Error("clean update package failed:", err.Error()) } _ = os.MkdirAll(GetBackupPath(), 0777) } case "sys_update_notice": { // 判断有新版本, 不再重复检测 if updateNotice.HaveNewVersion == 1 { continue } // 设备更新提醒 updateNoticeLock.Lock() //获取需要更新版本的程序列表 _, version, _ := CheckVersion() curVersion := GetCurVersion() IsLastUpdate := true if version != curVersion { // 设置更新提醒, 重新下载升级包 updateNotice.HaveNewVersion = 1 updateNotice.PkgDownloaded = 0 IsLastUpdate = true } else { IsLastUpdate = false } if !IsLastUpdate { //如果没有更新,设置用户延迟时间为0 for key, _ := range updateNotice.NoticeUser { if 0 > updateNotice.NoticeUser[key] { updateNotice.NoticeUser[key] = 0 } } } if "1" == set.Value { updateNotice.NoticeStatus = true } else { updateNotice.NoticeStatus = false } updateNoticeLock.Unlock() } case "sys_auto_update": { if "1" == set.Value && updateNotice.HaveNewVersion > 0 && updateNotice.PkgDownloaded == 0 { err := PreDownUpdateFile() if nil != err { logger.Error("pre download update file failed:", err.Error()) } else { updateNotice.PkgDownloaded = 1 } } } } } } } } //检查是否距上次比较有更新 func IsLastUpdate(programs []*models.Program, NewVersionProgram []*models.Program) bool { for _, program := range programs { for _, program2 := range NewVersionProgram { if program.Name == program2.Name { if program.Version != program2.Version { return true } } } } return true } //获取更新提醒 func GetUpdateNotice() UpdateNotice { return updateNotice } //延迟提醒 func DelayNotice(uid string, second int) UpdateNotice { updateNoticeLock.Lock() updateNotice.NoticeUser[uid] = second updateNoticeLock.Unlock() return updateNotice } //获取代码备份位置 func GetBackupPath() string { dir := util.GetVamicroPath() + "/backup" _, err := os.Stat(dir) if nil != err { if os.IsNotExist(err) { os.Mkdir(dir, 0744) } else { return "./backup" } } return dir } //获取解压后的补丁包位置 func GetPatchPath() string { dir := util.GetVamicroPath() + "/patch" _, err := os.Stat(dir) if nil != err { if os.IsNotExist(err) { os.Mkdir(dir, 0744) } else { return "./patch" } } return dir } //获取预下载升级包目录 func GetPreDownPath() string { dir := util.GetVamicroPath() + "/upgrade" _, err := os.Stat(dir) if nil != err { if os.IsNotExist(err) { os.Mkdir(dir, 0744) } else { return "./upgrade" } } return dir } //获取上传升级包目录 func GetPreUploadPath() string { dir := util.GetVamicroPath() + "/upgrade_manual" _, err := os.Stat(dir) if nil != err { if os.IsNotExist(err) { os.Mkdir(dir, 0744) } else { return "./upgrade_manual" } } return dir } func GetMd5(in string) string { data := md5.Sum([]byte(in)) return fmt.Sprintf("%x", data) } //预下载升级文件 func PreDownUpdateFile() error { info, err := getUpdateInfo() if nil != err { return err } LastDownLock.Lock() defer LastDownLock.Unlock() LastDownFile = make([]string, 0) //循环下载升级包 for _, payload := range info.Data { logger.Info("正在下载版本补丁【" + payload.Version + "】") upfile, err := DownloadPatchPkg(payload) logger.Info("下载版本补丁完成【" + payload.Version + "】") if err != nil { return err } LastDownFile = append(LastDownFile, upfile) } return nil } //获取预下载升级包 func GetLastDownFile() []string { var res = make([]string, 0) if len(LastDownFile) > 0 { for _, file := range LastDownFile { _, err := os.Stat(file) if nil == err { res = append(res, file) } } } return res } //回滚版本 func Rollback(version string) error { dir := util.GetVamicroPath() + "/" + version _, err := os.Stat(dir) if nil != err { return err } SetRunVersionEnv(version) versionEnv = version return nil } type RegUserInfo struct { UserType string `json:"userType"` //个人:personal 公司: company PhoneNum string `json:"phoneNum"` //手机号码 Name string `json:"name"` //姓名或公司名称 ProvinceId string `json:"provinceId"` //省 CityId string `json:"cityId"` //市 CountyId string `json:"countyId"` //县 Email string `json:"email"` //邮箱 } //获取授权 func Authorization(code string, isManual bool) (authinfo util.AuthorizationInfo, err error) { sn := util.GetVamicroPath() + "/sn.txt" authorization := util.GetVamicroPath() + "/auth.txt" activateCode := "" if len(code) == 29 && code[5:6] == "-" { //25位激活码激活 activateCode = code } if "" != code && len(code) != 29 && code[5:6] != "-" { authinfo, err := util.GetAuthorizationInfo(code) if nil == err { activateCode = authinfo.ActivateCode logger.Debug("code found:" + code) if isManual { //手动操作更新授权文件,则立即返回,不用再次访问商城 ioutil.WriteFile(sn, []byte(authinfo.ActivateCode), os.ModePerm) ioutil.WriteFile(authorization, []byte(code), os.ModePerm) return authinfo, nil } else { //否则以商城为准 defer func() { if nil != err { //ioutil.WriteFile(sn, []byte(authinfo.ActivateCode), os.ModePerm) //ioutil.WriteFile(authorization, []byte(code), os.ModePerm) } }() } } else { return authinfo, errors.New("非法的授权!!") } } devId := config.Server.AnalyServerId machineCode := licence.GetMachineCode() authinfo, authcode, err := postAuthReq(util.GetSn(), activateCode, devId, machineCode, "") if nil != err { logger.Error("Authorization err:", err.Error(), authcode) //ioutil.WriteFile(authorization, []byte(authcode), os.ModePerm) //bug:局域网会把授权清除 return authinfo, err } if authinfo.MachineCode != machineCode { logger.Error("GetAuthorization machineCode not match, local:", machineCode, " remote:", authinfo.MachineCode) return authinfo, errors.New("授权不匹配") } ioutil.WriteFile(sn, []byte(authinfo.ActivateCode), os.ModePerm) ioutil.WriteFile(authorization, []byte(authcode), os.ModePerm) return authinfo, nil } func postAuthReq(sn string, activateCode string, deviceId string, machineCode string, oldDeviceId string) (authinfo util.AuthorizationInfo, authcode string, err error) { u, err := url.Parse("http://" + util.GetShopUrl() + uriAuth) if err != nil { logger.Error("parse url failed, url:", "http://"+util.GetShopUrl()+uriAuth, ", err:", err.Error()) return authinfo, "", err } query := u.Query() query.Set("sn", sn) query.Set("activateCode", activateCode) query.Set("deviceId", deviceId) query.Set("machineCode", machineCode) query.Set("oldDeviceId", oldDeviceId) query.Set("deviceType", config.Server.DeviceType) query.Set("deviceMode", config.Server.DeviceModel) query.Set("vGpu", util.GetVGpu()) var sysInitApi bhomedbapi.SysInitApi b, rInfo := sysInitApi.GetRegInfo() if b { rbd, e := json.Marshal(rInfo) if e == nil { var sysRI RegUserInfo if e = json.Unmarshal(rbd, &sysRI); e == nil { query.Set("userType", sysRI.UserType) query.Set("name", sysRI.Name) query.Set("phoneNum", sysRI.PhoneNum) query.Set("provinceId", sysRI.ProvinceId) query.Set("cityId", sysRI.CityId) query.Set("countyId", sysRI.CountyId) query.Set("email", sysRI.Email) } else { logger.Error("json.Unmarshal sysRI e:", e) } } else { logger.Error("json.Marshal rInfo e:", e) } } u.RawQuery = query.Encode() resp, err := http.Get(u.String()) if err != nil { logger.Error("GetAuthorization parse url failed, url:", u.String(), ", err:", err.Error()) return authinfo, "", err } if resp.StatusCode != 200 { logger.Error("GetAuthorization incorrect status, url:", u.String(), ", status:", resp.StatusCode) return authinfo, "", errors.New("Status code not 200") } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { logger.Error("GetAuthorization read body failed, url:", u.String(), ", err:", err.Error()) return authinfo, "", err } logger.Info("postAuthReq body:", string(body)) var info map[string]interface{} err = json.Unmarshal(body, &info) if err != nil { logger.Error("GetAuthorization json.Unmarshal failed, url:", u.String(), ", err:", err.Error()) return authinfo, "", err } authinfo, err = util.GetAuthorizationInfo(info["data"].(string)) if nil == err { logger.Error("err:", err) return authinfo, info["data"].(string), nil } if 500 == int(info["code"].(float64)) { logger.Error("ret 500") return authinfo, "", errors.New(info["msg"].(string)) } if true != info["success"].(bool) { logger.Error("not success") return authinfo, "", errors.New(info["msg"].(string)) } return authinfo, info["data"].(string), nil } func GenQRCode() ([]byte, string, error) { sn := util.GetSn() var authInfo util.AuthorizationInfo authInfo.Sn = sn authInfo.DevId = config.Server.AnalyServerId authInfo.MachineCode = licence.GetMachineCode() authInfo.DeviceType = config.Server.DeviceType authInfo.DeviceMode = config.Server.DeviceModel authInfo.VGpu = util.GetVGpu() var sysInitApi bhomedbapi.SysInitApi if bsi, rInfo := sysInitApi.GetRegInfo(); bsi { rbd, e := json.Marshal(rInfo) if e == nil { var sysRI RegUserInfo if e = json.Unmarshal(rbd, &sysRI); e == nil { authInfo.UserType = sysRI.UserType authInfo.Name = sysRI.Name authInfo.PhoneNum = sysRI.PhoneNum authInfo.ProvinceId = sysRI.ProvinceId authInfo.CityId = sysRI.CityId authInfo.CountyId = sysRI.CountyId authInfo.Email = sysRI.Email } else { logger.Error("json.Unmarshal sysRI e:", e) } } else { logger.Error("json.Marshal rInfo e:", e) } } hackQ, _ := HackAuthorizationInfo(authInfo) url := "http://" + util.GetShopUrl() + uriMobile + "?q=" + hackQ logger.Info("qrcode len:", len(url), " content:", url) // 生成二维码 q, err := qrcode.New(url, qrcode.Highest) if err != nil { return nil, url, err } png, err1 := q.PNG(350) return png, url, err1 } //刷新授权到其他进程 func AuthorizationUpdate(c context.Context, ms *bhomeclient.MicroNode) { //三十秒检查一次配置并更新 // tick := time.Tick(30 * time.Second) tick := time.Tick(24 * time.Hour) var authInfo util.AuthorizationInfo var err error for { select { case <-c.Done(): logger.Info("proc close, self update exit") return case <-tick: author := util.GetAuthorization() sn := util.GetSn() authInfo, err = Authorization(sn, false) if nil != err && !strings.Contains(err.Error(), "成功") { logger.Error("GetAuthorization error:", err.Error()) if "" != author { authInfo, err = util.GetAuthorizationInfo(author) } } logger.Debug("authInfo:", authInfo) data, _ := json.Marshal(authInfo) var nodes []bhome_msg.BHAddress nodes = append(nodes, bhome_msg.BHAddress{}) go ms.PublishNetTimeout(nodes, AuthorizationUpdateTopic, data, 10) } } } func HackAuthorizationInfo(authorizationInfo util.AuthorizationInfo) (string, error) { b, err := json.Marshal(authorizationInfo) if nil != err { return "", err } logger.Debug("authorInfo", authorizationInfo) info, err := util.RsaEncrypt(b) if nil != err { logger.Error("HackAuthorizationInfo utils.RsaEncrypt failed, err:", err.Error()) return "", err } return base64.StdEncoding.EncodeToString(info), nil } func GetQ() string { sn := util.GetSn() var authinfo util.AuthorizationInfo authinfo.Sn = sn authinfo.DevId = config.Server.AnalyServerId authinfo.MachineCode = licence.GetMachineCode() authinfo.VGpu = util.GetVGpu() q, err := HackAuthorizationInfo(authinfo) if nil == err { return q } return err.Error() } func CancelAuthorization(passwd string, q string) (error, string) { uApi := bhomedbapi.UserApi{} ok, _ := uApi.Login("basic", passwd) if !ok { return errors.New("密码不正确!"), "" } //获取请求码里的devId qInfo, err := util.GetAuthorizationInfo(q) if nil != err { return errors.New("请求码格式不正确"), "" } curAuthInfo := util.GetAuthorization() //获取当前授权码 authInfo, err := util.GetAuthorizationInfo(curAuthInfo) if nil != err { return errors.New("当前服务器授权信息不完整"), "" } authInfo.OldDeviceId = authInfo.DevId authInfo.DevId = qInfo.DevId authInfo.VGpu = util.GetVGpu() sn := util.GetVamicroPath() + "/sn.txt" authorization := util.GetVamicroPath() + "/auth.txt" authCode, err := HackAuthorizationInfo(authInfo) if nil != err { return errors.New("产品密钥导出失败"), "" } ioutil.WriteFile(sn, []byte(""), os.ModePerm) time.Sleep(100 * time.Microsecond) ioutil.WriteFile(authorization, []byte(""), os.ModePerm) _, _, err = postAuthReq(authInfo.Sn, authInfo.ActivateCode, qInfo.DevId, licence.GetMachineCode(), authInfo.OldDeviceId) if nil != err { return err, "" } return nil, authCode }