diff --git a/.gitignore b/.gitignore index e63681f..4682fe7 100644 --- a/.gitignore +++ b/.gitignore @@ -271,4 +271,6 @@ Sessionx.vim tags # Persistent undo [._]*.un~ - +oa.db +static +OneAuth diff --git a/api/api.go b/api/api.go new file mode 100644 index 0000000..5888df8 --- /dev/null +++ b/api/api.go @@ -0,0 +1,22 @@ +package api + +import ( + "OneAuth/api/app" + "OneAuth/api/user" + "OneAuth/api/wx" + "github.com/veypi/OneBD" + "github.com/veypi/OneBD/core" +) + +func Router(r OneBD.Router) { + r.SetNotFoundFunc(func(m core.Meta) { + m.Write([]byte("{\"status\": 0}")) + }) + r.SetInternalErrorFunc(func(m core.Meta) { + m.Write([]byte("{\"status\": 0}")) + }) + user.Router(r.SubRouter("/auth/user")) + wx.Router(r.SubRouter("wx")) + app.Router(r.SubRouter("app")) + //message.Router(r.SubRouter("/message")) +} diff --git a/api/app/app.go b/api/app/app.go new file mode 100644 index 0000000..0c009b6 --- /dev/null +++ b/api/app/app.go @@ -0,0 +1,39 @@ +package app + +import ( + "OneAuth/cfg" + "OneAuth/libs/base" + "OneAuth/libs/oerr" + "OneAuth/models" + "github.com/veypi/OneBD" + "github.com/veypi/OneBD/rfc" +) + +func Router(r OneBD.Router) { + r.Set("/:id", appHandlerP, rfc.MethodGet) +} + +var appHandlerP = OneBD.NewHandlerPool(func() OneBD.Handler { + h := &appHandler{} + h.Ignore(rfc.MethodGet) + return h +}) + +type appHandler struct { + base.ApiHandler + query *models.App +} + +func (h *appHandler) Get() (interface{}, error) { + id := h.Meta().Params("id") + if id == "" { + return nil, oerr.ApiArgsMissing + } + h.query = &models.App{} + h.query.UUID = id + err := cfg.DB().Where(h.query).Preload("Wx").First(h.query).Error + if err != nil { + return nil, err + } + return h.query, nil +} diff --git a/api/role/auth.go b/api/role/auth.go new file mode 100644 index 0000000..7d3996f --- /dev/null +++ b/api/role/auth.go @@ -0,0 +1,22 @@ +package role + +import ( + "OneAuth/cfg" + "OneAuth/libs/base" + "OneAuth/models" + "github.com/veypi/OneBD" + "github.com/veypi/OneBD/core" +) + +var authP = OneBD.NewHandlerPool(func() core.Handler { + return &authHandler{} +}) + +type authHandler struct { + base.ApiHandler +} + +func (h *authHandler) Get() (interface{}, error) { + l := make([]*models.Auth, 0, 10) + return &l, cfg.DB().Find(&l).Error +} diff --git a/api/role/role.go b/api/role/role.go new file mode 100644 index 0000000..1bbefd7 --- /dev/null +++ b/api/role/role.go @@ -0,0 +1,138 @@ +package role + +import ( + "OneAuth/cfg" + "OneAuth/libs/base" + "OneAuth/libs/oerr" + "OneAuth/models" + "errors" + "github.com/veypi/OneBD" + "gorm.io/gorm" +) + +var roleP = OneBD.NewHandlerPool(func() OneBD.Handler { + return &roleHandler{} +}) + +type roleHandler struct { + base.ApiHandler +} + +func (h *roleHandler) Get() (interface{}, error) { + id := h.Meta().ParamsInt("id") + aid := h.Meta().ParamsInt("aid") + act := h.Meta().Params("action") + if id > 0 { + role := &models.Role{} + role.ID = uint(id) + err := cfg.DB().Preload("Auths").Preload("Users").First(role).Error + if err != nil { + return nil, err + } + if aid <= 0 { + return role, nil + } + if !h.CheckAuth("admin", "").CanDoAny() { + return nil, oerr.NoAuth + } + at := &models.RoleAuth{} + at.RoleID = role.ID + at.AuthID = uint(aid) + defer models.SyncGlobalRoles() + if act == "bind" { + err = cfg.DB().Where(at).FirstOrCreate(at).Error + } else if act == "unbind" { + err = cfg.DB().Where(at).Delete(at).Error + } + return role, nil + } + roles := make([]*models.Role, 0, 10) + err := cfg.DB().Preload("Auths").Preload("Users").Find(&roles).Error + return roles, err +} + +func (h *roleHandler) Post() (interface{}, error) { + if !h.CheckAuth("role", "").CanCreate() { + return nil, oerr.NoAuth + } + role := &models.Role{} + err := h.Meta().ReadJson(role) + if err != nil { + return nil, err + } + role.ID = 0 + if role.Category == 0 || role.Name == "" { + return nil, oerr.ApiArgsMissing + } + defer models.SyncGlobalRoles() + return role, cfg.DB().Where(role).FirstOrCreate(role).Error +} + +func (h *roleHandler) Patch() (interface{}, error) { + if !h.CheckAuth("role", "").CanUpdate() { + return nil, oerr.NoAuth + } + query := &struct { + Name *string `json:"name"` + // 角色标签 + Tag *string `json:"tag" gorm:"default:''"` + IsUnique *bool `json:"is_unique" gorm:"default:false"` + }{} + err := h.Meta().ReadJson(query) + if err != nil { + return nil, err + } + rid := h.Meta().ParamsInt("id") + if rid <= 0 { + return nil, oerr.ApiArgsError + } + role := &models.Role{} + role.ID = uint(rid) + err = cfg.DB().Preload("Users").Where(role).First(role).Error + if err != nil { + return nil, err + } + return nil, cfg.DB().Transaction(func(tx *gorm.DB) error { + var err error + if query.Tag != nil && *query.Tag != role.Tag { + err = tx.Model(role).Update("tag", *query.Tag).Error + if err != nil { + return err + } + } + if query.Name != nil && *query.Name != role.Name { + err = tx.Model(role).Update("name", *query.Name).Error + if err != nil { + return err + } + } + if query.IsUnique != nil && *query.IsUnique != role.IsUnique { + if *query.IsUnique && len(role.Users) > 1 { + return errors.New("该角色绑定用户已超过1个,请解绑后在修改") + } + err = tx.Table("roles").Where("id = ?", role.ID).Update("is_unique", *query.IsUnique).Error + if err != nil { + return err + } + } + return err + }) +} + +func (h *roleHandler) Delete() (interface{}, error) { + if !h.CheckAuth("role").CanDelete() { + return nil, oerr.NoAuth + } + rid := h.Meta().ParamsInt("id") + if rid <= 2 { + return nil, oerr.NoAuth + } + role := &models.Role{} + role.ID = uint(rid) + err := cfg.DB().Where(role).First(role).Error + if err != nil { + return nil, err + } + defer models.SyncGlobalRoles() + return nil, cfg.DB().Delete(role).Error +} diff --git a/api/role/router.go b/api/role/router.go new file mode 100644 index 0000000..712bf03 --- /dev/null +++ b/api/role/router.go @@ -0,0 +1,13 @@ +package role + +import ( + "github.com/veypi/OneBD" + "github.com/veypi/OneBD/rfc" +) + +func Router(r OneBD.Router) { + r.Set("/role/", roleP, rfc.MethodGet, rfc.MethodPost) + r.Set("/role/:id", roleP, rfc.MethodGet, rfc.MethodDelete, rfc.MethodPatch) + r.Set("/role/:id/:action/:aid", roleP, rfc.MethodGet) + r.Set("/auth/", authP, rfc.MethodGet) +} diff --git a/api/user/user.go b/api/user/user.go new file mode 100644 index 0000000..51ebe37 --- /dev/null +++ b/api/user/user.go @@ -0,0 +1,242 @@ +package user + +import ( + "OneAuth/cfg" + "OneAuth/libs/base" + "OneAuth/libs/oerr" + "OneAuth/models" + //"OneAuth/ws" + "encoding/base64" + "fmt" + "github.com/veypi/OneBD" + "github.com/veypi/OneBD/rfc" + "github.com/veypi/utils/log" + "gorm.io/gorm" + "math/rand" + "strconv" + "time" +) + +func Router(r OneBD.Router) { + pool := OneBD.NewHandlerPool(func() OneBD.Handler { + h := &handler{} + h.Ignore(rfc.MethodHead) + return h + }) + r.Set("/", pool, rfc.MethodGet, rfc.MethodPost) // list + r.Set("/:user_id", pool, rfc.MethodGet, rfc.MethodPatch, rfc.MethodHead, rfc.MethodDelete) + r.Set("/:user_id/role/", userRoleP, rfc.MethodPost) + r.Set("/:user_id/role/:role_id", userRoleP, rfc.MethodDelete) + //r.WS("/ws", func(m OneBD.Meta) (conn OneBD.WebsocketConn, err error) { + //return ws.User.Upgrade(m.ResponseWriter(), m.Request()) + //}) +} + +type handler struct { + base.ApiHandler + User *models.User +} + +// Get get user data +func (h *handler) Get() (interface{}, error) { + username := h.Meta().Query("username") + if username != "" { + users := make([]*models.User, 0, 10) + err := cfg.DB().Preload("Scores").Preload("Roles.Auths").Where("username LIKE ? OR nickname LIKE ?", "%"+username+"%", "%"+username+"%").Find(&users).Error + if err != nil { + return nil, err + } + return users, nil + } + userID := h.Meta().ParamsInt("user_id") + if userID != 0 { + user := &models.User{} + user.ID = uint(userID) + return user, cfg.DB().Where(user).Preload("Scores").Preload("Roles.Auths").Preload("Favorites").First(user).Error + } else { + users := make([]models.User, 10) + skip, err := strconv.Atoi(h.Meta().Query("skip")) + if err != nil || skip < 0 { + skip = 0 + } + if err := cfg.DB().Preload("Scores").Preload("Roles.Auths").Offset(skip).Find(&users).Error; err != nil { + return nil, err + } + return users, nil + } +} + +// Post register user +func (h *handler) Post() (interface{}, error) { + if !h.CheckAuth("user").CanCreate() { + return nil, oerr.NoAuth + } + var userdata = struct { + Username string `json:"username"` + Password string `json:"password"` + Nickname string `json:"nickname"` + Phone string `json:"phone"` + Email string `json:"email"` + Domain string `json:"domain"` + Title string `json:"title"` + Position string `json:"position"` + }{} + if err := h.Meta().ReadJson(&userdata); err != nil { + return nil, err + } + pass, err := base64.StdEncoding.DecodeString(userdata.Password) + if err != nil { + return nil, err + } + + if len(pass) > 32 || len(pass) < 6 { + return nil, oerr.PassError + } + + r := rand.New(rand.NewSource(time.Now().UnixNano())) + h.User = new(models.User) + h.User.Icon = fmt.Sprintf("/media/icon/default/%04d.jpg", r.Intn(230)) + h.User.Nickname = userdata.Nickname + h.User.Phone = userdata.Phone + h.User.Username = userdata.Username + h.User.Email = userdata.Email + h.User.Position = userdata.Position + if err := h.User.UpdateAuth(string(pass)); err != nil { + log.HandlerErrs(err) + return nil, oerr.ResourceCreatedFailed + } + tx := cfg.DB().Begin() + if err := tx.Create(&h.User).Error; err != nil { + tx.Rollback() + return nil, oerr.ResourceDuplicated + } + tx.Commit() + return h.User, nil +} + +// Patch update user data +func (h *handler) Patch() (interface{}, error) { + uid := h.Meta().Params("user_id") + opts := struct { + Password string `json:"password"` + Nickname string `json:"nickname"` + Phone string `json:"phone" gorm:"type:varchar(20);unique;default:null" json:",omitempty"` + Email string `json:"email" gorm:"type:varchar(50);unique;default:null" json:",omitempty"` + Status string `json:"status"` + Position string `json:"position"` + }{} + if err := h.Meta().ReadJson(&opts); err != nil { + return nil, err + } + target := models.User{} + if tempID, err := strconv.Atoi(uid); err != nil || tempID <= 0 { + return nil, oerr.ApiArgsError.Attach(err) + } else { + target.ID = uint(tempID) + } + tx := cfg.DB().Begin() + if err := cfg.DB().Where(&target).First(&target).Error; err != nil { + return nil, err + } + if target.ID != h.Payload.ID && !h.CheckAuth("admin").CanDoAny() { + return nil, oerr.NoAuth + } + if len(opts.Password) >= 6 { + if err := target.UpdateAuth(opts.Password); err != nil { + log.HandlerErrs(err) + return nil, oerr.ApiArgsError.AttachStr(err.Error()) + } + } + if opts.Nickname != "" { + target.Nickname = opts.Nickname + } + if opts.Position != "" { + target.Position = opts.Position + } + if opts.Phone != "" { + target.Phone = opts.Phone + } + if opts.Email != "" { + target.Email = opts.Email + } + if opts.Status != "" { + target.Status = opts.Status + } + if err := tx.Updates(&target).Error; err != nil { + tx.Rollback() + return nil, err + } + tx.Commit() + return nil, nil +} + +// Delete delete user +func (h *handler) Delete() (interface{}, error) { + // TODO:: + return nil, nil +} + +// Head user login +func (h *handler) Head() (interface{}, error) { + uid := h.Meta().Params("user_id") + pass, err := base64.StdEncoding.DecodeString(h.Meta().Query("password")) + if err != nil { + return nil, oerr.ApiArgsError.Attach(err) + } + password := string(pass) + if len(uid) == 0 || len(password) == 0 { + return nil, oerr.ApiArgsError + } + h.User = new(models.User) + uidType := h.Meta().Query("uid_type") + switch uidType { + case "username": + h.User.Username = uid + case "phone": + h.User.Phone = uid + case "email": + h.User.Email = uid + default: + h.User.Username = uid + } + if err := cfg.DB().Preload("Roles").Where(h.User).First(h.User).Error; err != nil { + if err.Error() == gorm.ErrRecordNotFound.Error() { + // admin 登录自动注册 + if h.User.Username == "admin" { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + h.User.Icon = fmt.Sprintf("/media/icon/default/%04d.jpg", r.Intn(230)) + err = h.User.UpdateAuth(password) + if err != nil { + return nil, err + } + role := &models.Role{} + role.ID = 1 + h.User.Roles = []*models.Role{role} + err = cfg.DB().Create(h.User).Error + if err != nil { + return nil, err + } + } else { + return nil, oerr.AccountNotExist + } + } else { + log.HandlerErrs(err) + return nil, err + } + } + isAuth, err := h.User.CheckLogin(password) + if err != nil || !isAuth { + return nil, oerr.PassError.Attach(err) + } + if h.User.Status == "disabled" { + return nil, oerr.DisableLogin + } + token, err := h.User.GetToken(cfg.CFG.Key) + if err != nil { + log.HandlerErrs(err) + return nil, oerr.Unknown.Attach(err) + } + h.Meta().SetHeader("auth_token", token) + log.Info().Msg(h.User.Username + " login") + return nil, nil +} diff --git a/api/user/user_role.go b/api/user/user_role.go new file mode 100644 index 0000000..05f86d7 --- /dev/null +++ b/api/user/user_role.go @@ -0,0 +1,86 @@ +package user + +import ( + "OneAuth/cfg" + "OneAuth/libs/base" + "OneAuth/libs/oerr" + "OneAuth/models" + "errors" + "github.com/veypi/OneBD" + "gorm.io/gorm" +) + +var userRoleP = OneBD.NewHandlerPool(func() OneBD.Handler { + return &userRoleHandler{} +}) + +type userRoleHandler struct { + base.ApiHandler +} + +func (h *userRoleHandler) Post() (interface{}, error) { + if !h.CheckAuth("role").CanCreate() { + return nil, oerr.NoAuth + } + uid := h.Meta().ParamsInt("user_id") + if uid <= 0 { + return nil, oerr.ApiArgsMissing + } + query := &models.Role{} + err := h.Meta().ReadJson(query) + if err != nil { + return nil, err + } + if query.ID != 0 { + err = cfg.DB().First(query, query.ID).Error + } else if query.Name != "" { + err = cfg.DB().Where(map[string]interface{}{ + "name": query.Name, + "category": query.Category, + "tag": query.Tag, + }).First(query).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + err = cfg.DB().Create(query).Error + } + } else { + return nil, oerr.ApiArgsMissing + } + if err != nil { + return nil, err + } + + if query.IsUnique { + } + link := &models.UserRole{} + link.UserID = uint(uid) + link.RoleID = query.ID + err = cfg.DB().Transaction(func(tx *gorm.DB) (err error) { + if query.IsUnique { + err = tx.Where("role_id = ?", query.ID).Delete(models.UserRole{}).Error + if err != nil { + return err + } + } + return tx.Where(link).FirstOrCreate(link).Error + }) + return link, err +} + +func (h *userRoleHandler) Delete() (interface{}, error) { + if !h.CheckAuth("role").CanDelete() { + return nil, oerr.NoAuth + } + uid := h.Meta().ParamsInt("user_id") + id := h.Meta().ParamsInt("role_id") + if uid <= 0 || id <= 0 { + return nil, oerr.ApiArgsMissing + } + link := &models.UserRole{} + link.UserID = uint(uid) + link.RoleID = uint(id) + err := cfg.DB().Where(link).First(link).Error + if err != nil { + return nil, err + } + return nil, cfg.DB().Delete(link).Error +} diff --git a/api/wx/login.go b/api/wx/login.go new file mode 100644 index 0000000..5bffe13 --- /dev/null +++ b/api/wx/login.go @@ -0,0 +1,160 @@ +package wx + +import ( + "OneAuth/cfg" + "OneAuth/libs/tools" + "OneAuth/models" + "errors" + "fmt" + "github.com/veypi/OneBD" + "github.com/veypi/OneBD/rfc" + "github.com/veypi/utils" + "github.com/veypi/utils/log" + "net/url" + "strings" + "time" +) + +var tokens = map[uint]string{ + 1: "", +} + +func login(m OneBD.Meta) { + var loc = "" + defer func() { + m.Header().Set("Location", loc) + log.Warn().Msg(loc) + m.WriteHeader(rfc.StatusPermanentRedirect) + }() + app := &models.App{ + UUID: m.Params("id"), + } + err := cfg.DB().Preload("Wx").Where(app).First(app).Error + loc = fmt.Sprintf("/#/wx?uuid=%s&msg=", app.UUID) + if err != nil { + loc += err.Error() + return + } + if app.Wx == nil { + loc += "微信登录未绑定" + return + } + if tokens[app.Wx.ID] == "" { + tokens[app.Wx.ID], err = requestCorpToken(app.Wx.CorpID, app.Wx.CorpSecret) + if err != nil { + log.Warn().Msg("get corp token failed: " + err.Error()) + loc += err.Error() + return + } + } + user, err := getUserID(tokens[app.Wx.ID], m.Query("code")) + if err != nil { + if strings.Contains(err.Error(), "access_token expired") { + tokens[app.Wx.ID], err = requestCorpToken(app.Wx.CorpID, app.Wx.CorpSecret) + if err != nil { + log.Warn().Msg("refresh corp token failed: " + err.Error()) + loc += err.Error() + return + } + user, err = getUserID(tokens[app.Wx.ID], m.Query("code")) + if err != nil { + log.Warn().Msg("get user token failed: " + err.Error()) + loc += err.Error() + return + } + } else { + log.Warn().Msg("get user token failed: " + err.Error()) + loc += err.Error() + return + } + } + info, err := getUserInfo(tokens[app.Wx.ID], user) + if err != nil { + log.Warn().Msg("get user info failed: " + err.Error()) + loc += err.Error() + return + } + log.Warn().Msgf("\ncode= %s\nstate= %s\nu = %s\n%v", + m.Query("code"), m.Query("state"), user, info) + pass, err := utils.AesEncrypt(fmt.Sprintf("%s.%d", user, time.Now().Unix()), []byte(app.UUID)) + if err != nil { + loc += err.Error() + return + } + log.Warn().Msgf("pass: %s", pass) + v := url.Values{} + v.Add("wid", pass) + u, err := url.Parse(app.Host) + u.RawQuery = v.Encode() + if err != nil { + loc += err.Error() + return + } + loc = u.String() +} + +func requestCorpToken(corpid, corpsecret string) (string, error) { + addr := "https://qyapi.weixin.qq.com/cgi-bin/gettoken" + query := map[string]string{ + "corpid": corpid, + "corpsecret": corpsecret, + } + res := &struct { + Errmsg string `json:"errmsg"` + Errcode *uint `json:"errcode"` + AccessToken string `json:"access_token"` + }{} + err := tools.Query(addr, query, res) + if err != nil { + return "", errors.New("request token response json parse err :" + err.Error()) + } + if res.Errcode != nil && *res.Errcode == 0 { + return res.AccessToken, nil + } else { + //返回错误信息 + err = errors.New(fmt.Sprintf("%d:%s", res.Errcode, res.Errmsg)) + return "", err + } +} + +func getUserID(token, code string) (string, error) { + addr := "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo" + res := &struct { + Errmsg string `json:"errmsg"` + Errcode *uint `json:"errcode"` + UserId string `json:"UserId"` + DeviceId string `json:"device_id"` + }{} + query := map[string]string{ + "access_token": token, + "code": code, + } + err := tools.Query(addr, query, res) + if err != nil { + return "", err + } + if res.Errcode != nil && *res.Errcode == 0 { + return res.UserId, nil + } + return "", errors.New(fmt.Sprintf("%d:%s", res.Errcode, res.Errmsg)) +} + +func getUserInfo(token, id string) (interface{}, error) { + addr := "https://qyapi.weixin.qq.com/cgi-bin/user/get" + res := map[string]interface{}{} + query := map[string]string{ + "access_token": token, + "userid": id, + } + err := tools.Query(addr, query, &res) + if err != nil { + return "", err + } + errcode := int(res["errcode"].(float64)) + errmsg := res["errmsg"].(string) + if errcode == 0 { + log.Warn().Msgf("%v", res) + return res, nil + } + return "", errors.New(fmt.Sprintf("%d:%s", errcode, errmsg)) +} diff --git a/api/wx/router.go b/api/wx/router.go new file mode 100644 index 0000000..5ea19b2 --- /dev/null +++ b/api/wx/router.go @@ -0,0 +1,10 @@ +package wx + +import ( + "github.com/veypi/OneBD" + "github.com/veypi/OneBD/rfc" +) + +func Router(r OneBD.Router) { + r.Set("/login/:id", login, rfc.MethodGet) +} diff --git a/cfg/cfg.go b/cfg/cfg.go new file mode 100644 index 0000000..d7a2d6c --- /dev/null +++ b/cfg/cfg.go @@ -0,0 +1,77 @@ +package cfg + +import ( + "fmt" + "github.com/veypi/utils/cmd" + "gorm.io/driver/mysql" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +var Path = cmd.GetCfgPath("OneAuth", "oa") + +var CFG = &struct { + AdminUser string + Host string + LoggerPath string + LoggerLevel string + Key string + TimeFormat string + Debug bool + EXEDir string + DB struct { + Type string + Addr string + User string + Pass string + DB string + } +}{ + AdminUser: "admin", + Host: "0.0.0.0:19528", + LoggerPath: "", + LoggerLevel: "debug", + TimeFormat: "2006/01/02 15:04:05", + Debug: true, + EXEDir: "./", + DB: struct { + Type string + Addr string + User string + Pass string + DB string + }{ + Type: "sqlite", + //Addr: "127.0.0.1:3306", + Addr: "oa.db", + User: "root", + Pass: "123456", + DB: "one_auth", + }, +} + +var ( + db *gorm.DB +) + +func DB() *gorm.DB { + if db == nil { + ConnectDB() + } + return db +} +func ConnectDB() *gorm.DB { + var err error + conn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local", CFG.DB.User, CFG.DB.Pass, CFG.DB.Addr, CFG.DB.DB) + if CFG.DB.Type == "sqlite" { + conn = CFG.DB.Addr + db, err = gorm.Open(sqlite.Open(conn), &gorm.Config{}) + } else { + db, err = gorm.Open(mysql.Open(conn), &gorm.Config{}) + } + + if err != nil { + panic(err) + } + return db +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..685f4c2 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module OneAuth + +go 1.16 + +require ( + github.com/json-iterator/go v1.1.10 + github.com/urfave/cli/v2 v2.2.0 + github.com/veypi/OneBD v0.4.1 + github.com/veypi/utils v0.2.2 + gorm.io/driver/mysql v1.0.5 + gorm.io/driver/sqlite v1.1.4 + gorm.io/gorm v1.21.3 +) +replace github.com/veypi/OneBD v0.4.1 => ../OceanCurrent/OneBD diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3e1b2e1 --- /dev/null +++ b/go.sum @@ -0,0 +1,77 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E= +github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/kardianos/service v1.1.0 h1:QV2SiEeWK42P0aEmGcsAgjApw/lRxkwopvT+Gu6t1/0= +github.com/kardianos/service v1.1.0/go.mod h1:RrJI2xn5vve/r32U5suTbeaSGoMU6GbNPoj36CVYcHc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ= +github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.17.2 h1:RMRHFw2+wF7LO0QqtELQwo8hqSmqISyCJeFeAAuWcRo= +github.com/rs/zerolog v1.17.2/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4= +github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/veypi/OneBD v0.4.1 h1:IkuV2tqay3fyX+FsM9XrKiKS9N7pymUJnrscGx2T0mc= +github.com/veypi/OneBD v0.4.1/go.mod h1:9IoMOBzwIGyv6IZGF7ZnTYwTcHltLKicDgcwha66G0U= +github.com/veypi/utils v0.1.5/go.mod h1:oKcwTDfvE1qtuhJuCcDcfvGquv9bHdFaCGA42onVMC4= +github.com/veypi/utils v0.2.2 h1:BRxu0mYJJpuubPjmIIrRVr0XEq9NMp//KUCrVTkFums= +github.com/veypi/utils v0.2.2/go.mod h1:rAkC6Fbk5cBa3u+8pyCpsVcnXw74EhEQJGmPND9FvRg= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gorm.io/driver/mysql v1.0.5 h1:WAAmvLK2rG0tCOqrf5XcLi2QUwugd4rcVJ/W3aoon9o= +gorm.io/driver/mysql v1.0.5/go.mod h1:N1OIhHAIhx5SunkMGqWbGFVeh4yTNWKmMo1GOAsohLI= +gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM= +gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw= +gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.21.3 h1:qDFi55ZOsjZTwk5eN+uhAmHi8GysJ/qCTichM/yO7ME= +gorm.io/gorm v1.21.3/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= diff --git a/libs/auth/auth.go b/libs/auth/auth.go new file mode 100644 index 0000000..bed7ada --- /dev/null +++ b/libs/auth/auth.go @@ -0,0 +1,43 @@ +package auth + +import ( + "OneAuth/cfg" + "OneAuth/libs/oerr" + "OneAuth/models" + "github.com/veypi/OneBD" + "github.com/veypi/OneBD/rfc" +) + +type Auth struct { + Payload *models.PayLoad + ignoreMethod map[rfc.Method]bool +} + +func (a *Auth) Init(m OneBD.Meta) error { + if a.ignoreMethod != nil && a.ignoreMethod[m.Method()] { + return nil + } + a.Payload = new(models.PayLoad) + token := m.GetHeader("auth_token") + if token == "" { + return oerr.NotLogin + } + ok, err := models.ParseToken(token, cfg.CFG.Key, a.Payload) + if ok { + return nil + } + return oerr.NotLogin.Attach(err) +} + +func (a *Auth) Ignore(methods ...rfc.Method) { + if a.ignoreMethod == nil { + a.ignoreMethod = make(map[rfc.Method]bool) + } + for _, m := range methods { + a.ignoreMethod[m] = true + } +} + +func (a *Auth) CheckAuth(name string, tags ...string) models.AuthLevel { + return a.Payload.CheckAuth(name, tags...) +} diff --git a/libs/base/api_handler.go b/libs/base/api_handler.go new file mode 100644 index 0000000..7cf34f6 --- /dev/null +++ b/libs/base/api_handler.go @@ -0,0 +1,72 @@ +package base + +import ( + "OneAuth/libs/auth" + "OneAuth/libs/oerr" + "OneAuth/libs/tools" + "github.com/json-iterator/go" + "github.com/veypi/OneBD" + "github.com/veypi/OneBD/rfc" + "github.com/veypi/utils/log" + "strconv" + "sync" + "time" +) + +var json = jsoniter.ConfigFastest + +type ApiHandler struct { + OneBD.BaseHandler + auth.Auth +} + +func (h *ApiHandler) Init(m OneBD.Meta) error { + return tools.MultiIniter(m, &h.BaseHandler, &h.Auth) +} + +func (h *ApiHandler) OnResponse(data interface{}) { + if h.Meta().Method() == rfc.MethodHead { + h.Meta().SetHeader("status", "1") + return + } + p, err := json.Marshal(map[string]interface{}{"status": 1, "content": data}) + if err != nil { + log.Warn().Err(err).Msg("encode json data error") + return + } + h.Meta().Write(p) +} + +func (h *ApiHandler) OnError(err error) { + log.WithNoCaller.Warn().Err(err).Msg(h.Meta().RequestPath()) + msg := err.Error() + if h.Meta().Method() == rfc.MethodHead { + h.Meta().SetHeader("status", "0") + h.Meta().SetHeader("code", strconv.Itoa(int(oerr.OfType(msg)))) + h.Meta().SetHeader("err", msg) + } else { + p, _ := json.Marshal(map[string]interface{}{"status": 0, "code": oerr.OfType(msg), "err": msg}) + h.Meta().Write(p) + } +} + +var ioNumLimit = make(map[string]time.Time) +var limitLocker = sync.RWMutex{} + +func (h *ApiHandler) SetAccessDelta(d time.Duration) error { + // 尽量对写操作加频率限制 + now := time.Now() + limitLocker.Lock() + label := h.Meta().RemoteAddr() + h.Meta().RequestPath() + last, ok := ioNumLimit[label] + defer func() { + ioNumLimit[label] = now + limitLocker.Unlock() + }() + if !ok { + return nil + } else if now.Sub(last) >= d { + return nil + } + return oerr.AccessTooFast +} diff --git a/libs/oerr/oerr.go b/libs/oerr/oerr.go new file mode 100644 index 0000000..d5f20c7 --- /dev/null +++ b/libs/oerr/oerr.go @@ -0,0 +1,210 @@ +package oerr + +import ( + "errors" + "gorm.io/gorm" + "strconv" +) + +// 错误描述 + +type Code uint + +/* + +5位10进制码表示错误, 00000 etc. + +0 代表未知,或不必定义的有通用意义的错误 + +## 第1位 错误类型 + - 1 : 系统级错误 比如 内存申请失败, 系统调用失败,文件打开失败等等 + - 2 : 数据库错误 + - 3 : 保留 + - 4 : 权限错误 + - 5 : 配置错误 + - 6 : 参数错误 + - 7 : 时序(控制)错误 + +## 第2位 2级错误类型 + +## 第3,4位 具体错误编号 + +## 第5位 错误严重程度 + - 0 : 无任何影响错误,简单重试可以解决 + - 1 : 无影响错误,重试不可解决 + - 2 : 有影响用户体验或系统性能错误, 重试可解决 + - 3 : 有影响用户体验或系统性能错误, 重试不可解决 + - 4 : 有影响组件功能的错误, 重试可解决 + - 5 : 有影响组件功能的错误, 重试不可解决 + - 6 : 有影响服务运行的错误, 重启可解决 + - 7 : 有影响服务运行的错误,重启不可解决 + - 8 : 有影响系统运行的错误 + - 9 : 本不可能发生的错误,例如被人攻击导致数据异常产生的逻辑错误 + +*/ +const ( + Unknown Code = 0 +) +const ( + // 2 数据库错误 + // -1 系统错误 + // -2 数据读写错误 + DBErr Code = 20001 + ResourceCreatedFailed Code = 22012 + ResourceDuplicated Code = 22021 + ResourceNotExist Code = 22031 +) + +const ( +// 3 +) + +const ( + // 4 权限类型错误 + // 1: 登录权限 + // 2: 资源操作权限 + NotLogin Code = 41001 + LoginExpired Code = 41011 + PassError Code = 41021 + DisableLogin Code = 41031 + AccountNotExist Code = 41041 + NoAuth Code = 42011 +) + +// 6 参数类型错误 +/* + -1: 协议参数 + -2: 接口参数 + -3: 函数参数 + -4: 数据依赖错误 +*/ +const ( + MethodNotSupport Code = 61111 + MethodNotAllowed Code = 61121 + + ApiArgsError Code = 62001 + ApiArgsMissing Code = 62011 + TableArgsMissing Code = 62021 + TableArgsErr Code = 62031 + + FuncArgsError Code = 63001 + UrlPatternNotSupport Code = 63117 + UrlDefinedDuplicate Code = 63127 + UrlParamDuplicate Code = 63137 + + DataError Code = 64009 +) + +// 7 : 时序(控制)错误 +/* + -1: 访问控制 +*/ + +const ( + AccessErr Code = 71001 + AccessTooFast Code = 71010 +) + +var codeMap = map[Code]string{ + Unknown: "unknown error", + DBErr: "db error", + ResourceCreatedFailed: "resource created failed", + ResourceDuplicated: "resource duplicated", + ResourceNotExist: "Resource not exist", + MethodNotSupport: "this http method is not supported", + MethodNotAllowed: "this http method is not allowed", + ApiArgsError: "base args error", + ApiArgsMissing: "missing args", + TableArgsMissing: "missing data", + TableArgsErr: "invalid table data", + FuncArgsError: "func args error", + UrlPatternNotSupport: "this router's url pattern is not supported.", + UrlDefinedDuplicate: "this router's url has been defined", + UrlParamDuplicate: "this param defined in router's url duplicated", + DataError: "data error", + NotLogin: "not login", + LoginExpired: "login expired", + DisableLogin: "disabled to login", + PassError: "password/account error", + AccountNotExist: "account not exist", + NoAuth: "no auth to access", + AccessErr: "access error", + AccessTooFast: "access too fast", +} + +func (c Code) Error() string { + return strconv.Itoa(int(c)) + ":" + c.String() +} + +func (c Code) String() string { + s, ok := codeMap[c] + if ok && len(s) > 0 { + return s + } + return codeMap[Unknown] +} + +// 附加错误详细原因 +func (c Code) Attach(errs ...error) (e error) { + e = c + for _, err := range errs { + if err != nil { + e = &wrapErr{msg: e.Error() + "\n" + err.Error(), err: e} + } + } + return e +} + +func (c Code) AttachStr(errs ...string) (e error) { + e = c + for _, m := range errs { + if m != "" { + e = &wrapErr{ + msg: e.Error() + "\n" + m, + err: e, + } + } + } + return e +} + +func OfType(errMsg string) Code { + s := "" + if gorm.ErrRecordNotFound.Error() == errMsg { + return ResourceNotExist + } + for _, v := range errMsg { + if v == ':' { + break + } + s += string(v) + } + c, _ := strconv.Atoi(s) + return Code(c) +} + +type wrapErr struct { + msg string + err error +} + +func (w *wrapErr) Error() string { + return w.msg +} + +func (w *wrapErr) UnWrap() error { + return w.err +} + +func CheckMultiErr(errs ...error) error { + msg := "" + for _, e := range errs { + if e != nil { + msg += e.Error() + "\n" + } + } + if msg != "" { + return errors.New(msg) + } + return nil +} diff --git a/libs/tools/tools.go b/libs/tools/tools.go new file mode 100644 index 0000000..146dd9c --- /dev/null +++ b/libs/tools/tools.go @@ -0,0 +1,44 @@ +package tools + +import ( + "encoding/json" + "github.com/veypi/OneBD" + "net/http" + "net/url" +) + +type Initer interface { + Init(OneBD.Meta) error +} + +func MultiIniter(m OneBD.Meta, is ...Initer) (err error) { + for _, i := range is { + err = i.Init(m) + if err != nil { + return + } + } + return +} + +func Query(addr string, query map[string]string, res interface{}) error { + u, err := url.Parse(addr) + if err != nil { + return err + } + paras := &url.Values{} + //设置请求参数 + for k, v := range query { + paras.Set(k, v) + } + u.RawQuery = paras.Encode() + resp, err := http.Get(u.String()) + //关闭资源 + if resp != nil && resp.Body != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + return json.NewDecoder(resp.Body).Decode(res) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..ec2cd06 --- /dev/null +++ b/main.go @@ -0,0 +1,79 @@ +package main + +import ( + "OneAuth/cfg" + "OneAuth/sub" + "github.com/urfave/cli/v2" + "github.com/veypi/utils/cmd" + "github.com/veypi/utils/log" + "os" + "path/filepath" +) + +const Version = "v0.1.0" + +func main() { + cmd.LoadCfg(cfg.Path, cfg.CFG) + app := cli.NewApp() + app.Name = "OneAuth" + app.Usage = "one auth" + app.Version = Version + app.Flags = []cli.Flag{ + &cli.BoolFlag{ + Name: "debug", + Aliases: []string{"d"}, + Value: cfg.CFG.Debug, + Destination: &cfg.CFG.Debug, + }, + &cli.StringFlag{ + Name: "log_level,log", + Value: cfg.CFG.LoggerLevel, + Destination: &cfg.CFG.LoggerLevel, + }, + &cli.StringFlag{ + Name: "log_path", + Value: cfg.CFG.LoggerPath, + Destination: &cfg.CFG.LoggerPath, + }, + &cli.StringFlag{ + Name: "key", + Value: cfg.CFG.Key, + Destination: &cfg.CFG.Key, + }, + &cli.StringFlag{ + Name: "exe_dir", + Value: cfg.CFG.EXEDir, + Destination: &cfg.CFG.EXEDir, + }, + &cli.StringFlag{ + Name: "host", + Value: cfg.CFG.Host, + Destination: &cfg.CFG.Host, + }, + } + app.Commands = []*cli.Command{ + &sub.Web, + } + srv, err := cmd.NewSrv(app, sub.RunWeb, cfg.CFG, cfg.Path) + if err != nil { + log.Warn().Msg(err.Error()) + return + } + srv.SetExecMax(1) + srv.SetStopFunc(func() { + }) + app.Before = func(c *cli.Context) error { + var err error + cfg.CFG.EXEDir, err = filepath.Abs(cfg.CFG.EXEDir) + if err != nil { + return err + } + if cfg.CFG.Debug { + cfg.CFG.LoggerLevel = "debug" + } + cfg.ConnectDB() + return nil + } + _ = app.Run(os.Args) + +} diff --git a/models/app.go b/models/app.go new file mode 100644 index 0000000..76392e3 --- /dev/null +++ b/models/app.go @@ -0,0 +1,22 @@ +package models + +type App struct { + BaseModel + Name string `json:"name"` + UUID string `json:"uuid"` + Host string `json:"host"` + WxID string `json:"wx_id" gorm:""` + Wx *Wechat `json:"wx" gorm:"association_foreignkey:ID"` +} + +type Wechat struct { + BaseModel + // 网页授权登录用 + WxID string `json:"wx_id"` + AgentID string `json:"agent_id"` + Url string `json:"url"` + + // 获取access_token用 + CorpID string `json:"corp_id"` + CorpSecret string `json:"corp_secret"` +} diff --git a/models/init.go b/models/init.go new file mode 100644 index 0000000..275edd7 --- /dev/null +++ b/models/init.go @@ -0,0 +1,102 @@ +package models + +import ( + "OneAuth/cfg" + "bytes" + "database/sql/driver" + "errors" + "fmt" + "time" +) + +type JSON []byte + +func (j JSON) Value() (driver.Value, error) { + if j.IsNull() { + return nil, nil + } + return string(j), nil +} +func (j *JSON) Scan(value interface{}) error { + if value == nil { + *j = nil + return nil + } + s, ok := value.([]byte) + if !ok { + return errors.New("invalid scan source") + } + *j = append((*j)[0:0], s...) + return nil +} +func (j JSON) MarshalJSON() ([]byte, error) { + if j == nil { + return []byte("null"), nil + } + return j, nil +} +func (j *JSON) UnmarshalJSON(data []byte) error { + if j == nil { + return errors.New("null point exception") + } + *j = append((*j)[0:0], data...) + return nil +} +func (j JSON) IsNull() bool { + return len(j) == 0 || string(j) == "null" +} +func (j JSON) Equals(j1 JSON) bool { + return bytes.Equal(j, j1) +} + +// JSONTime custom json time +type JSONTime struct { + time.Time +} + +func Now() *JSONTime { + return &JSONTime{time.Now()} +} + +// MarshalJSON 实现它的json序列化方法 +func (jt JSONTime) MarshalJSON() ([]byte, error) { + var stamp = fmt.Sprintf("\"%s\"", jt.Format(cfg.CFG.TimeFormat)) + return []byte(stamp), nil +} + +// UnmarshalJSON 反序列化方法 +func (jt *JSONTime) UnmarshalJSON(data []byte) (err error) { + now, err := time.ParseInLocation(`"`+cfg.CFG.TimeFormat+`"`, string(data), time.Local) + *jt = JSONTime{now} + return +} + +// Value insert timestamp into mysql need this function. +func (jt JSONTime) Value() (driver.Value, error) { + var zeroTime time.Time + if jt.Time.UnixNano() == zeroTime.UnixNano() { + return nil, nil + } + return jt.Time, nil +} + +// Scan value of time.Time +func (jt *JSONTime) Scan(v interface{}) error { + value, ok := v.(time.Time) + if ok { + *jt = JSONTime{Time: value} + return nil + } + return fmt.Errorf("can not convert %v to timestamp", v) +} + +func (jt *JSONTime) SetTime(t time.Time) { + jt.Time = t +} + +type BaseModel struct { + ID uint `json:"id" gorm:"primary_key"` + CreatedAt JSONTime `json:"created_at"` + UpdatedAt JSONTime `json:"updated_at"` + DeletedAt *JSONTime `json:"deleted_at" sql:"index"` +} diff --git a/models/role.go b/models/role.go new file mode 100644 index 0000000..ed68db9 --- /dev/null +++ b/models/role.go @@ -0,0 +1,104 @@ +package models + +import ( + "OneAuth/cfg" + "github.com/veypi/utils/log" +) + +var GlobalRoles = make(map[uint]*Role) + +func SyncGlobalRoles() { + roles := make([]*Role, 0, 10) + err := cfg.DB().Preload("Auths").Find(&roles).Error + if err != nil { + log.Warn().Msgf("sync global roles error: %s", err.Error()) + return + } + for _, r := range roles { + GlobalRoles[r.ID] = r + } +} + +type UserRole struct { + BaseModel + UserID uint `json:"user_id"` + RoleID uint `json:"role_id"` +} + +type RoleAuth struct { + BaseModel + RoleID uint `json:"role_id"` + AuthID uint `json:"auth_id"` +} + +type Role struct { + BaseModel + Name string `json:"name"` + // 角色类型 + // 0: 系统角色 1: 用户角色 + Category uint `json:"category" gorm:"default:0"` + // 角色标签 + Tag string `json:"tag" gorm:"default:''"` + Users []*User `json:"users" gorm:"many2many:user_role;"` + // 具体权限 + Auths []*Auth `json:"auths" gorm:"many2many:role_auth;"` + IsUnique bool `json:"is_unique" gorm:"default:false"` +} + +func (r Role) CheckAuth(name string, tags ...string) AuthLevel { + res := AuthNone + tag := "" + if len(tags) > 0 { + tag = tags[0] + } + for _, a := range r.Auths { + if a.Name == "admin" && a.Tag == "" || (a.Name == "admin" && a.Tag == tag) || (a.Name == name && a.Tag == tag) { + if a.Level > res { + res = a.Level + } + } + } + return res +} + +type AuthLevel uint + +const ( + AuthNone AuthLevel = 0 + AuthRead AuthLevel = 1 + AuthCreate AuthLevel = 2 + AuthUpdate AuthLevel = 3 + AuthDelete AuthLevel = 4 +) + +func (a AuthLevel) CanRead() bool { + return a >= AuthRead +} + +func (a AuthLevel) CanCreate() bool { + return a >= AuthCreate +} + +func (a AuthLevel) CanUpdate() bool { + return a >= AuthUpdate +} + +func (a AuthLevel) CanDelete() bool { + return a >= AuthDelete +} + +func (a AuthLevel) CanDoAny() bool { + return a >= AuthDelete +} + +// 资源权限 + +type Auth struct { + BaseModel + Name string `json:"name"` + AppID uint `json:"app_id"` + // 权限标签 + Tag string `json:"tag"` + // 权限等级 0 相当于没有 1 读权限 2 创建权限 3 修改权限 4 删除权限 + Level AuthLevel `json:"level"` +} diff --git a/models/user.go b/models/user.go new file mode 100644 index 0000000..c1a1419 --- /dev/null +++ b/models/user.go @@ -0,0 +1,159 @@ +package models + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "errors" + "github.com/veypi/utils" + "github.com/veypi/utils/log" + "strings" + "time" +) + +// User db user model +type User struct { + BaseModel + Username string `json:"username" gorm:"type:varchar(100);unique;not null"` + Nickname string `json:"nickname" gorm:"type:varchar(100)" json:",omitempty"` + Phone string `json:"phone" gorm:"type:varchar(20);unique;default:null" json:",omitempty"` + Email string `json:"email" gorm:"type:varchar(50);unique;default:null" json:",omitempty"` + CheckCode string `gorm:"type:varchar(64);not null" json:"-"` + RealCode string `gorm:"type:varchar(32);not null" json:"-"` + Position string `json:"position"` + // disabled 禁用 + Status string `json:"status"` + + Icon string `json:"icon"` + Roles []*Role `json:"roles" gorm:"many2many:user_role;"` +} + +// TODO:: roles 是否会造成token过大 ? +type PayLoad struct { + ID uint `json:"id"` + Username string `json:"username"` + Nickname string `json:"nickname"` + Icon string `json:"icon"` + Iat int64 `json:"iat"` //token time + Exp int64 `json:"exp"` + Roles []uint `json:"roles"` +} + +func (p *PayLoad) CheckAuth(name string, tags ...string) AuthLevel { + res := AuthNone + if p == nil || p.Roles == nil { + return res + } + for _, id := range p.Roles { + r := GlobalRoles[id] + if r == nil { + log.Warn().Msgf("not found role id: %d", id) + continue + } + t := r.CheckAuth(name, tags...) + if t > res { + res = t + } + } + return res +} + +func (u *User) String() string { + return u.Username + ":" + u.Nickname +} + +func (u *User) CheckAuth(name string, tags ...string) AuthLevel { + res := AuthNone + if u == nil || u.Roles == nil { + return res + } + for _, t := range u.Roles { + r := GlobalRoles[t.ID] + if r == nil { + log.Warn().Msgf("not found role id: %d", t.ID) + continue + } + t := r.CheckAuth(name, tags...) + if t > res { + res = t + } + } + return res +} + +func (u *User) GetToken(key string) (string, error) { + header := map[string]string{ + "typ": "JWT", + "alg": "HS256", + } + //header := "{\"typ\": \"JWT\", \"alg\": \"HS256\"}" + now := time.Now().Unix() + payload := PayLoad{ + ID: u.ID, + Username: u.Username, + Nickname: u.Nickname, + Icon: u.Icon, + Iat: now, + Exp: now + 60*60*24, + } + for _, r := range u.Roles { + payload.Roles = append(payload.Roles, r.ID) + } + a, err := json.Marshal(header) + if err != nil { + return "", err + } + b, err := json.Marshal(payload) + if err != nil { + return "", err + } + A := base64.StdEncoding.EncodeToString(a) + B := base64.StdEncoding.EncodeToString(b) + hmacCipher := hmac.New(sha256.New, []byte(key)) + hmacCipher.Write([]byte(A + "." + B)) + C := hmacCipher.Sum(nil) + return A + "." + B + "." + base64.StdEncoding.EncodeToString(C), nil +} + +var ( + InvalidToken = errors.New("invalid token") + ExpiredToken = errors.New("expired token") +) + +func ParseToken(token string, key string, payload *PayLoad) (bool, error) { + var A, B, C string + if seqs := strings.Split(token, "."); len(seqs) == 3 { + A, B, C = seqs[0], seqs[1], seqs[2] + } else { + return false, InvalidToken + } + hmacCipher := hmac.New(sha256.New, []byte(key)) + hmacCipher.Write([]byte(A + "." + B)) + tempC := hmacCipher.Sum(nil) + if !hmac.Equal([]byte(C), []byte(base64.StdEncoding.EncodeToString(tempC))) { + return false, nil + } + tempPayload, err := base64.StdEncoding.DecodeString(B) + if err != nil { + return false, err + } + if err := json.Unmarshal(tempPayload, payload); err != nil { + return false, err + } + if time.Now().Unix() > payload.Exp { + return false, ExpiredToken + } + return true, nil +} + +func (u *User) UpdateAuth(ps string) (err error) { + u.RealCode = utils.RandSeq(32) + u.CheckCode, err = utils.AesEncrypt(u.RealCode, []byte(ps)) + return err +} + +func (u *User) CheckLogin(ps string) (bool, error) { + temp, err := utils.AesDecrypt(u.CheckCode, []byte(ps)) + return temp == u.RealCode, err +} diff --git a/oaf/.eslintignore b/oaf/.eslintignore new file mode 100644 index 0000000..539ad84 --- /dev/null +++ b/oaf/.eslintignore @@ -0,0 +1 @@ +src/libs/wwLogin.js diff --git a/oaf/.eslintrc.js b/oaf/.eslintrc.js index b5422a6..8244700 100644 --- a/oaf/.eslintrc.js +++ b/oaf/.eslintrc.js @@ -12,6 +12,10 @@ module.exports = { ecmaVersion: 2020 }, rules: { + 'object-curly-spacing': 0, + 'space-before-function-paren': 0, + '@typescript-eslint/camelcase': 0, + '@typescript-eslint/no-empty-function': 0, 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' } diff --git a/oaf/package.json b/oaf/package.json index 8bc2d92..8693115 100644 --- a/oaf/package.json +++ b/oaf/package.json @@ -8,7 +8,9 @@ "lint": "vue-cli-service lint" }, "dependencies": { + "axios": "^0.21.1", "core-js": "^3.6.5", + "js-base64": "^3.6.0", "vue": "^2.6.11", "vue-class-component": "^7.2.3", "vue-property-decorator": "^9.1.2", diff --git a/oaf/src/App.vue b/oaf/src/App.vue index 15e3daf..6f867e8 100644 --- a/oaf/src/App.vue +++ b/oaf/src/App.vue @@ -6,56 +6,33 @@ dark >
- - -
- - - - Latest Release - mdi-open-in-new + + 统一认证 - + diff --git a/oaf/src/api/ajax.ts b/oaf/src/api/ajax.ts new file mode 100644 index 0000000..8ba2779 --- /dev/null +++ b/oaf/src/api/ajax.ts @@ -0,0 +1,65 @@ +import axios from 'axios' +import store from '@/store' + +function baseRequests(url: string, method: any = 'GET', query: any, data: any, success: any, fail?: Function) { + return axios({ + url: url, + params: query, + data: data, + method: method, + headers: { + auth_token: localStorage.auth_token + } + }) + .then((res: any) => { + if ('auth_token' in res.headers) { + localStorage.auth_token = res.headers.auth_token + } + if (method === 'HEAD') { + success(res.headers) + } else { + success(res.data) + } + }) + .catch((e: any) => { + if (e.response && e.response.status === 401) { + console.log(e) + store.dispatch('handleLogOut') + return + } + console.log(e) + if (e.response && e.response.status === 500) { + return + } + if (typeof fail === 'function') { + fail(e.response) + } else if (e.response && e.response.status === 400) { + console.log(400) + } else { + console.log(e.request) + } + }) +} + +const ajax = { + get(url: '', data = {}, success = {}, fail?: Function) { + return baseRequests(url, 'GET', data, {}, success, fail) + }, + head(url: '', data = {}, success = {}, fail?: Function) { + return baseRequests(url, 'HEAD', data, {}, success, fail) + }, + delete(url: '', data = {}, success = {}, fail?: Function) { + return baseRequests(url, 'DELETE', data, {}, success, fail) + }, + post(url: '', data = {}, success = {}, fail?: Function) { + return baseRequests(url, 'POST', {}, data, success, fail) + }, + put(url: '', data = {}, success = {}, fail?: Function) { + return baseRequests(url, 'PUT', {}, data, success, fail) + }, + patch(url: '', data = {}, success = {}, fail?: Function) { + return baseRequests(url, 'PATCH', {}, data, success, fail) + } +} + +export default ajax diff --git a/oaf/src/api/index.ts b/oaf/src/api/index.ts new file mode 100644 index 0000000..94cb810 --- /dev/null +++ b/oaf/src/api/index.ts @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2019 light + * + * Distributed under terms of the MIT license. + */ + +import {Base64} from 'js-base64' +import ajax from './ajax' +import store from '@/store' + +const Code = { + 42011: '无操作权限', + 22031: '资源不存在 或 您无权操作该资源' +} + +class Interface { + private readonly method: Function + private readonly api: string + private readonly data: any + + constructor(method: Function, api: string, data?: any) { + this.method = method + this.api = api + this.data = data + } + + Start(success: Function, fail?: Function) { + const newFail = function (data: any) { + if (data && data.code === 40001) { + // no login + store.dispatch('handleLogOut') + return + } + if (data && data.code > 0 && Code[data.code]) { + // Message.warning({message: Code[data.code] || data.err, offset: 100}) + } + if (fail) { + fail(data) + } + } + + const newSuccess = function (data: any) { + if (Number(data.status) === 1) { + if (success) { + success(data.content) + } + } else { + newFail(data) + if (data.code === 41001) { + store.dispatch('handleLogOut') + // bus.$emit('log_out') + } + } + } + this.method(this.api, this.data, newSuccess, newFail) + } +} + +const message = { + count() { + return new Interface(ajax.get, '/api/message/', { + count: true, + status: 'UnRead' + }) + }, + get_content(id: number) { + return new Interface(ajax.get, '/api/message/' + Number(id)) + }, + list(status: string) { + return new Interface(ajax.get, '/api/message/', {status}) + }, + update(id: number, status: string) { + return new Interface(ajax.patch, '/api/message/' + Number(id), {status}) + } +} + +const role = { + local: '/api/role/', + get(id: number) { + return new Interface(ajax.get, this.local + id) + }, + list() { + return new Interface(ajax.get, this.local) + }, + update(id: number, props: any) { + return new Interface(ajax.patch, this.local + id, props) + }, + create(props: any) { + return new Interface(ajax.post, this.local, props) + }, + del(id: number) { + return new Interface(ajax.delete, this.local + id) + }, + bind(id: number, aid: number) { + return new Interface(ajax.get, this.local + id + '/bind/' + aid) + }, + unbind(id: number, aid: number) { + return new Interface(ajax.get, this.local + id + '/unbind/' + aid) + } +} + +const app = { + local: '/api/app/', + get(id: string) { + return new Interface(ajax.get, this.local + id) + } +} + +const api = { + role: role, + app: app, + admin: { + auths() { + return new Interface(ajax.get, '/api/auth/') + }, + user: { + create(props: any) { + const p = Object.assign({}, props) + p.password = Base64.encode(props.password) + return new Interface(ajax.post, '/api/user/', p) + }, + update(user_id: number, props: any) { + return new Interface(ajax.patch, '/api/user/' + user_id, props) + }, + enable(user_id: number) { + return new Interface(ajax.patch, '/api/user/' + user_id, { + status: 'ok' + }) + }, + disable(user_id: number) { + return new Interface(ajax.patch, '/api/user/' + user_id, { + status: 'disabled' + }) + }, + attach_role(user_id: number, props: any) { + return new Interface(ajax.post, '/api/user/' + user_id + '/role/', props) + }, + detach_role(user_id: number, id: any) { + return new Interface(ajax.delete, '/api/user/' + user_id + '/role/' + id) + }, + reset_pass(user_id: number, password: string) { + return new Interface(ajax.patch, '/api/user/' + user_id, {password}) + } + } + }, + auth: { + event() { + return { + local: '/api/user/event/', + list() { + return new Interface(ajax.get, this.local) + }, + create(title: string, tag: string, start_date: any, end_date: any) { + return new Interface(ajax.post, this.local, {title, tag, start_date, end_date}) + }, + del(id: number) { + return new Interface(ajax.delete, this.local + id) + } + } + }, + favorite(name: string, tag: string, ok: boolean) { + if (ok) { + return new Interface(ajax.post, '/api/user/favorite', {name, tag}) + } + return new Interface(ajax.delete, '/api/user/favorite', {name, tag}) + }, + get(id: number) { + return new Interface(ajax.get, '/api/user/' + id) + }, + search(username: string) { + return new Interface(ajax.get, '/api/user/', { + username + }) + }, + login(username: string, password: string) { + return new Interface(ajax.head, '/api/user/' + username, { + password: Base64.encode(password) + }) + }, + // @title 职位 + // @domain 部门 + register(username: string, password: string, domain?: string, title?: string) { + return new Interface(ajax.post, '/api/user/', { + username: username, + password: Base64.encode(password), + domain: domain, + title: title + }) + } + }, + message: message +} + +const Api = { + install(Vue: any) { + Vue.prototype.api = api + } +} +export {Api} +export default api diff --git a/oaf/src/components/HelloWorld.vue b/oaf/src/components/HelloWorld.vue deleted file mode 100644 index ff173ff..0000000 --- a/oaf/src/components/HelloWorld.vue +++ /dev/null @@ -1,153 +0,0 @@ - - - diff --git a/oaf/src/components/WxLogin.vue b/oaf/src/components/WxLogin.vue new file mode 100644 index 0000000..d2cfc67 --- /dev/null +++ b/oaf/src/components/WxLogin.vue @@ -0,0 +1,42 @@ + + + diff --git a/oaf/src/components/demo.vue b/oaf/src/components/demo.vue new file mode 100644 index 0000000..a106312 --- /dev/null +++ b/oaf/src/components/demo.vue @@ -0,0 +1,20 @@ + + + diff --git a/oaf/src/libs/wwLogin.js b/oaf/src/libs/wwLogin.js new file mode 100644 index 0000000..e62111b --- /dev/null +++ b/oaf/src/libs/wwLogin.js @@ -0,0 +1,2 @@ +!function(a,b,c){function d(c){var d=b.createElement("iframe"),e="https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid="+c.appid+"&agentid="+c.agentid+"&redirect_uri="+c.redirect_uri+"&state="+c.state+"&login_type=jssdk";e+=c.style?"&style="+c.style:"",e+=c.href?"&href="+c.href:"",d.src=e,d.frameBorder="0",d.allowTransparency="true",d.scrolling="no",d.width="300px",d.height="400px";var f=b.getElementById(c.id);f.innerHTML="",f.appendChild(d),d.onload=function(){d.contentWindow.postMessage&&a.addEventListener&&(a.addEventListener("message",function(b){ +b.data&&b.origin.indexOf("work.weixin.qq.com")>-1&&(a.location.href=b.data)}),d.contentWindow.postMessage("ask_usePostMessage","*"))}}a.WwLogin=d}(window,document); \ No newline at end of file diff --git a/oaf/src/main.ts b/oaf/src/main.ts index 4fb129d..36a87a3 100644 --- a/oaf/src/main.ts +++ b/oaf/src/main.ts @@ -3,6 +3,9 @@ import App from './App.vue' import router from './router' import store from './store' import vuetify from './plugins/vuetify' +import {Api} from '@/api' + +Vue.use(Api) Vue.config.productionTip = false diff --git a/oaf/src/plugins/vuetify.ts b/oaf/src/plugins/vuetify.ts index 496ec37..68aa754 100644 --- a/oaf/src/plugins/vuetify.ts +++ b/oaf/src/plugins/vuetify.ts @@ -3,5 +3,22 @@ import Vuetify from 'vuetify/lib/framework' Vue.use(Vuetify) +const light = { + primary: '#2196f3', + secondary: '#00bcd4', + accent: '#3f51b5', + error: '#f44336', + warning: '#ff5722', + info: '#ff9800', + success: '#4caf50', + reset: '#684bff' +} + export default new Vuetify({ + theme: { + dark: false, + themes: { + light: light + } + } }) diff --git a/oaf/src/router/index.ts b/oaf/src/router/index.ts index 7d2f012..83f5ef8 100644 --- a/oaf/src/router/index.ts +++ b/oaf/src/router/index.ts @@ -1,5 +1,5 @@ import Vue from 'vue' -import VueRouter, { RouteConfig } from 'vue-router' +import VueRouter, {RouteConfig} from 'vue-router' import Home from '../views/Home.vue' Vue.use(VueRouter) @@ -17,6 +17,11 @@ const routes: Array = [ // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') + }, + { + path: '/wx', + name: 'wx', + component: () => import('../views/wx.vue') } ] diff --git a/oaf/src/views/Home.vue b/oaf/src/views/Home.vue index b0964a2..e15109c 100644 --- a/oaf/src/views/Home.vue +++ b/oaf/src/views/Home.vue @@ -1,18 +1,25 @@ + - diff --git a/oaf/src/views/wx.vue b/oaf/src/views/wx.vue new file mode 100644 index 0000000..b42e6c0 --- /dev/null +++ b/oaf/src/views/wx.vue @@ -0,0 +1,66 @@ + + + diff --git a/oaf/vue.config.js b/oaf/vue.config.js index 2ae460b..a25a4f3 100644 --- a/oaf/vue.config.js +++ b/oaf/vue.config.js @@ -1,5 +1,28 @@ module.exports = { transpileDependencies: [ 'vuetify' - ] + ], + configureWebpack: { + output: { + filename: '[name].[hash].js' + } + }, + outputDir: '../sub/static', + devServer: { + host: '0.0.0.0', + port: 19528, + disableHostCheck: true, + proxy: { + '^/api': { + target: 'http://127.0.0.1:4001', + ws: true, + changeOrigin: true + }, + '^/media': { + target: 'http://127.0.0.1:4001', + ws: true, + changeOrigin: true + } + } + } } diff --git a/oaf/yarn.lock b/oaf/yarn.lock index c6b7ce3..21b5362 100644 --- a/oaf/yarn.lock +++ b/oaf/yarn.lock @@ -1937,6 +1937,13 @@ aws4@^1.8.0: resolved "https://registry.npm.taobao.org/aws4/download/aws4-1.11.0.tgz?cache=0&sync_timestamp=1604101421225&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Faws4%2Fdownload%2Faws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha1-1h9G2DslGSUOJ4Ta9bCUeai0HFk= +axios@^0.21.1: + version "0.21.1" + resolved "https://registry.npm.taobao.org/axios/download/axios-0.21.1.tgz?cache=0&sync_timestamp=1608609419814&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Faxios%2Fdownload%2Faxios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" + integrity sha1-IlY0gZYvTWvemnbVFu8OXTwJsrg= + dependencies: + follow-redirects "^1.10.0" + babel-code-frame@^6.22.0: version "6.26.0" resolved "https://registry.npm.taobao.org/babel-code-frame/download/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" @@ -4187,7 +4194,7 @@ flush-write-stream@^1.0.0: inherits "^2.0.3" readable-stream "^2.3.6" -follow-redirects@^1.0.0: +follow-redirects@^1.0.0, follow-redirects@^1.10.0: version "1.13.3" resolved "https://registry.npm.taobao.org/follow-redirects/download/follow-redirects-1.13.3.tgz?cache=0&sync_timestamp=1614436958094&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ffollow-redirects%2Fdownload%2Ffollow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267" integrity sha1-5VmK1QF0wbxOhyMB6CrCzZf5Amc= @@ -5323,6 +5330,11 @@ jest-worker@^25.4.0: merge-stream "^2.0.0" supports-color "^7.0.0" +js-base64@^3.6.0: + version "3.6.0" + resolved "https://registry.npm.taobao.org/js-base64/download/js-base64-3.6.0.tgz?cache=0&sync_timestamp=1604448740467&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fjs-base64%2Fdownload%2Fjs-base64-3.6.0.tgz#773e1de628f4f298d65a7e9842c50244751f5756" + integrity sha1-dz4d5ij08pjWWn6YQsUCRHUfV1Y= + js-message@1.0.7: version "1.0.7" resolved "https://registry.npm.taobao.org/js-message/download/js-message-1.0.7.tgz?cache=0&sync_timestamp=1605135348213&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fjs-message%2Fdownload%2Fjs-message-1.0.7.tgz#fbddd053c7a47021871bb8b2c95397cc17c20e47" diff --git a/sub/static/index.html b/sub/static/index.html new file mode 100644 index 0000000..9831347 --- /dev/null +++ b/sub/static/index.html @@ -0,0 +1 @@ +oaf
\ No newline at end of file diff --git a/sub/web.go b/sub/web.go new file mode 100644 index 0000000..a19cda1 --- /dev/null +++ b/sub/web.go @@ -0,0 +1,86 @@ +package sub + +import ( + "OneAuth/api" + "OneAuth/cfg" + "OneAuth/models" + "embed" + "github.com/urfave/cli/v2" + "github.com/veypi/OneBD" + "github.com/veypi/OneBD/core" + "github.com/veypi/OneBD/rfc" + "github.com/veypi/utils/log" + "net/http" + "os" +) + +//go:embed static +var staticFiles embed.FS + +//go:embed static/index.html +var indexFile []byte + +var Web = cli.Command{ + Name: "web", + Usage: "", + Description: "oa 核心http服务", + Action: RunWeb, + Flags: []cli.Flag{}, +} + +func RunWeb(c *cli.Context) error { + _ = runSyncDB(c) + ll := log.InfoLevel + if l, err := log.ParseLevel(cfg.CFG.LoggerLevel); err == nil { + ll = l + } + app := OneBD.New(&OneBD.Config{ + Host: cfg.CFG.Host, + LoggerPath: cfg.CFG.LoggerPath, + LoggerLevel: ll, + }) + app.Router().EmbedFile("/", indexFile) + app.Router().EmbedDir("/", staticFiles, "static/") + + // TODO media 文件需要检验权限 + //app.Router().SubRouter("/media/").Static("/", cfg.CFG.EXEDir+"/media") + + app.Router().SetNotFoundFunc(func(m core.Meta) { + f, err := os.Open(cfg.CFG.EXEDir + "/static/index.html") + if err != nil { + m.WriteHeader(rfc.StatusNotFound) + return + } + defer f.Close() + info, err := f.Stat() + if err != nil { + m.WriteHeader(rfc.StatusNotFound) + return + } + if info.IsDir() { + // TODO:: dir list + m.WriteHeader(rfc.StatusNotFound) + return + } + http.ServeContent(m, m.Request(), info.Name(), info.ModTime(), f) + }) + + api.Router(app.Router().SubRouter("api")) + + log.Info().Msg("\nRouting Table\n" + app.Router().String()) + return app.Run() +} + +func runSyncDB(*cli.Context) error { + db := cfg.DB() + log.HandlerErrs( + db.SetupJoinTable(&models.User{}, "Roles", &models.UserRole{}), + db.SetupJoinTable(&models.Role{}, "Users", &models.UserRole{}), + db.SetupJoinTable(&models.Role{}, "Auths", &models.RoleAuth{}), + db.AutoMigrate(&models.User{}, &models.Role{}, &models.Auth{}), + ) + log.HandlerErrs( + db.AutoMigrate(&models.App{}, &models.Wechat{}), + ) + return nil +}