diff --git a/api/token/base.go b/api/token/base.go new file mode 100644 index 0000000..60b9be7 --- /dev/null +++ b/api/token/base.go @@ -0,0 +1,49 @@ +package token + +import ( + "oa/cfg" + "oa/models" + "time" + + "github.com/veypi/OneBD/rest" +) + +var _ = Router.Patch("/:token_id", tokenPatch) + +type patchOpts struct { + ID string `json:"id" parse:"path@token_id"` + ExpiredAt *time.Time `json:"expired_at" parse:"json"` + OverPerm *string `json:"over_perm" parse:"json"` +} + +func tokenPatch(x *rest.X) (any, error) { + opts := &patchOpts{} + err := x.Parse(opts) + if err != nil { + return nil, err + } + data := &models.Token{} + + err = cfg.DB().Where("id = ?", opts.ID).First(data).Error + if err != nil { + return nil, err + } + optsMap := make(map[string]interface{}) + if opts.ExpiredAt != nil { + optsMap["expired_at"] = opts.ExpiredAt + } + if opts.OverPerm != nil { + optsMap["over_perm"] = opts.OverPerm + } + err = cfg.DB().Model(data).Updates(optsMap).Error + + return data, err +} + +var _ = Router.Delete("/:token_id", tokenDelete) + +func tokenDelete(x *rest.X) (any, error) { + data := &models.Token{} + err := cfg.DB().Where("id = ?", x.Params.Get("token_id")).Delete(data).Error + return data, err +} diff --git a/api/token/create.go b/api/token/create.go new file mode 100644 index 0000000..19aa35b --- /dev/null +++ b/api/token/create.go @@ -0,0 +1,138 @@ +// +// post.go +// Copyright (C) 2025 veypi +// 2025-05-09 17:26 +// Distributed under terms of the MIT license. +// + +package token + +import ( + "net/http" + "oa/cfg" + "oa/libs/auth" + "oa/models" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/veypi/OneBD/rest" + "github.com/veypi/utils/logv" +) + +type postOpts struct { + // 两种获取token方式,一种用refreshtoken换取apptoken(应用登录),一种用密码加密code换refreshtoken (oa登录) + Refresh *string `json:"refresh" parse:"json"` + Typ *string `json:"typ" parse:"json"` + + AppID *string `json:"app_id" gorm:"index;type:varchar(32)" parse:"json"` + ExpiredAt *time.Time `json:"expired_at" parse:"json"` + OverPerm *string `json:"over_perm" parse:"json"` + Device *string `json:"device" parse:"json"` +} + +var _ = Router.Post("/", tokenPost) + +// for user login app +func tokenPost(x *rest.X) (any, error) { + opts := &postOpts{} + err := x.Parse(opts) + if err != nil { + return nil, err + } + aid := cfg.Config.ID + if opts.AppID != nil && *opts.AppID != "" { + aid = *opts.AppID + } + data := &models.Token{} + claim := &auth.Claims{} + claim.IssuedAt = jwt.NewNumericDate(time.Now()) + claim.Issuer = cfg.Config.ID + if opts.Refresh != nil { + typ := "app" + if opts.Typ != nil { + typ = *opts.Typ + } + // for other app redirect + refresh, err := auth.ParseJwt(*opts.Refresh) + if err != nil { + return nil, err + } + if refresh.ID == "" { + return nil, rest.ErrNotAuthorized + } + err = cfg.DB().Where("id = ?", refresh.ID).First(data).Error + if err != nil { + return nil, err + } + claim.AID = aid + claim.UID = refresh.UID + claim.Name = refresh.Name + claim.Icon = refresh.Icon + claim.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Minute * 10)) + if typ == "app" { + if refresh.AID == aid { + // refresh token + acList := make(auth.Access, 0, 10) + logv.AssertError(cfg.DB().Table("accesses a"). + Select("a.name, a.t_id, a.level"). + Joins("INNER JOIN user_roles ur ON ur.role_id = a.role_id AND ur.user_id = ? AND a.app_id = ?", refresh.UID, aid). + Scan(&acList).Error) + claim.Access = acList + if aid == cfg.Config.ID { + return auth.GenJwt(claim) + } + app := &models.App{} + err = cfg.DB().Where("id = ?", aid).First(app).Error + if err != nil { + return nil, err + } + return auth.GenJwtWithKey(claim, app.Key) + } else if refresh.ID == cfg.Config.ID { + // oa应用生成其他应用的refresh token + newToken := &models.Token{} + newToken.UserID = refresh.UID + newToken.AppID = aid + newToken.ExpiredAt = time.Now().Add(time.Hour * 24) + if opts.OverPerm != nil { + newToken.OverPerm = *opts.OverPerm + } + if opts.Device != nil { + newToken.Device = *opts.Device + } + newToken.Ip = x.GetRemoteIp() + logv.AssertError(cfg.DB().Create(newToken).Error) + // gen other app token + claim.ID = newToken.ID + claim.ExpiresAt = jwt.NewNumericDate(newToken.ExpiredAt) + return auth.GenJwt(claim) + } else { + return nil, rest.ErrNotPermitted + } + } else if typ == "ufs" { + claim.AID = refresh.AID + claim.UID = refresh.UID + claim.Name = refresh.Name + claim.Icon = refresh.Icon + claim.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Minute * 10)) + claim.Access = auth.Access{ + {Name: "fs", TID: "/", Level: auth.Do}, + } + token := logv.AssertFuncErr(auth.GenJwt(claim)) + + cookie := &http.Cookie{ + Name: "fstoken", // Cookie 的名称 + Value: token, // Cookie 的值 + Path: "/fs/u/", // Cookie 的路径,通常是根路径 + MaxAge: 600, // Cookie 的最大年龄,单位是秒 + HttpOnly: true, // 是否仅限 HTTP(S) 访问 + Secure: false, // 是否通过安全连接传输 Cookie + } + http.SetCookie(x, cookie) + return token, nil + } else { + return nil, rest.ErrArgInvalid + } + } else { + return nil, rest.ErrArgInvalid + } +} diff --git a/api/token/get.go b/api/token/get.go new file mode 100644 index 0000000..548c969 --- /dev/null +++ b/api/token/get.go @@ -0,0 +1,55 @@ +// +// get.go +// Copyright (C) 2025 veypi +// 2025-05-09 17:24 +// Distributed under terms of the MIT license. +// + +package token + +import ( + "oa/cfg" + "oa/models" + + "github.com/veypi/OneBD/rest" +) + +type getOpts struct { + ID string `json:"id" parse:"path@token_id"` +} + +var _ = Router.Get("/:token_id", tokenGet) + +func tokenGet(x *rest.X) (any, error) { + opts := &getOpts{} + err := x.Parse(opts) + if err != nil { + return nil, err + } + data := &models.Token{} + err = cfg.DB().Where("id = ?", opts.ID).First(data).Error + return data, err +} + +type listOpts struct { + Limit int `json:"limit"` + UserID string `json:"user_id" gorm:"index;type:varchar(32)" parse:"query"` + AppID string `json:"app_id" gorm:"index;type:varchar(32)" parse:"query"` +} + +var _ = Router.Get("/", tokenList) + +func tokenList(x *rest.X) (any, error) { + opts := &listOpts{} + err := x.Parse(opts) + if err != nil { + return nil, err + } + data := make([]*models.Token, 0, 10) + + query := cfg.DB() + query = query.Where("user_id = ?", opts.UserID) + query = query.Where("app_id = ?", opts.AppID) + err = query.Limit(opts.Limit).Find(&data).Error + return data, err +} diff --git a/api/token/init.go b/api/token/init.go index 9455006..e2e49c2 100644 --- a/api/token/init.go +++ b/api/token/init.go @@ -3,14 +3,12 @@ // 2024-09-24 22:37:12 // Distributed under terms of the MIT license. // -// Auto generated by OneBD. DO NOT EDIT +// package token import ( "github.com/veypi/OneBD/rest" ) + var Router = rest.NewRouter() -func init() { - useToken(Router) -} diff --git a/api/token/token.go b/api/token/token.go deleted file mode 100644 index 23af3c9..0000000 --- a/api/token/token.go +++ /dev/null @@ -1,258 +0,0 @@ -package token - -import ( - "encoding/hex" - "encoding/json" - "net/http" - "oa/cfg" - "oa/errs" - "oa/libs/auth" - M "oa/models" - "time" - - "github.com/golang-jwt/jwt/v5" - "github.com/veypi/OneBD/rest" - "github.com/veypi/utils" - "github.com/veypi/utils/logv" -) - -func useToken(r rest.Router) { - r.Post("/salt", tokenSalt) - r.Post("/", tokenPost) - r.Get("/:token_id", tokenGet) - r.Patch("/:token_id", tokenPatch) - r.Delete("/:token_id", tokenDelete) - r.Get("/", tokenList) -} -func tokenSalt(x *rest.X) (any, error) { - opts := &M.TokenSalt{} - err := x.Parse(opts) - logv.Warn().Msg(opts.Username) - if err != nil { - return nil, err - } - data := &M.User{} - query := "username = ?" - if opts.Typ == nil { - } else if *opts.Typ == "email" { - query = "email = ?" - } else if *opts.Typ == "phone" { - query = "phone = ?" - } - - err = cfg.DB().Where(query, opts.Username).First(data).Error - return map[string]string{"salt": data.Salt, "id": data.ID}, err -} - -// for user login app -func tokenPost(x *rest.X) (any, error) { - opts := &M.TokenPost{} - err := x.Parse(opts) - if err != nil { - return nil, err - } - aid := cfg.Config.ID - if opts.AppID != nil && *opts.AppID != "" { - aid = *opts.AppID - } - data := &M.Token{} - claim := &auth.Claims{} - claim.IssuedAt = jwt.NewNumericDate(time.Now()) - claim.Issuer = cfg.Config.ID - if opts.Refresh != nil { - typ := "app" - if opts.Typ != nil { - typ = *opts.Typ - } - // for other app redirect - refresh, err := auth.ParseJwt(*opts.Refresh) - if err != nil { - return nil, err - } - if refresh.ID == "" { - return nil, errs.AuthInvalid - } - err = cfg.DB().Where("id = ?", refresh.ID).First(data).Error - if err != nil { - return nil, err - } - claim.AID = aid - claim.UID = refresh.UID - claim.Name = refresh.Name - claim.Icon = refresh.Icon - claim.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Minute * 10)) - if typ == "app" { - if refresh.AID == aid { - // refresh token - acList := make(auth.Access, 0, 10) - logv.AssertError(cfg.DB().Table("accesses a"). - Select("a.name, a.t_id, a.level"). - Joins("INNER JOIN user_roles ur ON ur.role_id = a.role_id AND ur.user_id = ? AND a.app_id = ?", refresh.UID, aid). - Scan(&acList).Error) - claim.Access = acList - if aid == cfg.Config.ID { - return auth.GenJwt(claim) - } - app := &M.App{} - err = cfg.DB().Where("id = ?", aid).First(app).Error - if err != nil { - return nil, err - } - return auth.GenJwtWithKey(claim, app.Key) - } else if aid != cfg.Config.ID { - // 只能生成其他应用的refresh token - newToken := &M.Token{} - newToken.UserID = refresh.UID - newToken.AppID = aid - newToken.ExpiredAt = time.Now().Add(time.Hour * 24) - if opts.OverPerm != nil { - newToken.OverPerm = *opts.OverPerm - } - if opts.Device != nil { - newToken.Device = *opts.Device - } - newToken.Ip = x.GetRemoteIp() - logv.AssertError(cfg.DB().Create(newToken).Error) - // gen other app token - claim.ID = newToken.ID - claim.ExpiresAt = jwt.NewNumericDate(newToken.ExpiredAt) - return auth.GenJwt(claim) - } else { - // 其他应用获取访问oa的token - claim.Access = make(auth.Access, 0, 10) - if data.OverPerm != "" { - err = json.Unmarshal([]byte(data.OverPerm), &claim.Access) - if err != nil { - return nil, err - } - } - return auth.GenJwt(claim) - } - } else if typ == "ufs" { - claim.AID = refresh.AID - claim.UID = refresh.UID - claim.Name = refresh.Name - claim.Icon = refresh.Icon - claim.ExpiresAt = jwt.NewNumericDate(time.Now().Add(time.Minute * 10)) - claim.Access = auth.Access{ - {Name: "fs", TID: "/", Level: auth.Do}, - } - token := logv.AssertFuncErr(auth.GenJwt(claim)) - - cookie := &http.Cookie{ - Name: "fstoken", // Cookie 的名称 - Value: token, // Cookie 的值 - Path: "/fs/u/", // Cookie 的路径,通常是根路径 - MaxAge: 600, // Cookie 的最大年龄,单位是秒 - HttpOnly: true, // 是否仅限 HTTP(S) 访问 - Secure: false, // 是否通过安全连接传输 Cookie - } - http.SetCookie(x, cookie) - - return token, nil - } else { - return nil, errs.ArgsInvalid - } - } else if opts.Code != nil && aid == cfg.Config.ID && opts.Salt != nil && opts.UserID != nil { - // for oa login - user := &M.User{} - err = cfg.DB().Where("id = ?", opts.UserID).Find(user).Error - if err != nil { - return nil, err - } - logv.Info().Str("user", user.ID).Msg("login") - code := *opts.Code - salt := logv.AssertFuncErr(hex.DecodeString(*opts.Salt)) - key := logv.AssertFuncErr(hex.DecodeString(user.Code)) - de, err := utils.AesDecrypt([]byte(code), key, salt) - if err != nil || de != user.ID { - return nil, errs.AuthFailed - } - data.UserID = *opts.UserID - data.AppID = aid - data.ExpiredAt = time.Now().Add(time.Hour * 72) - if opts.OverPerm != nil { - data.OverPerm = *opts.OverPerm - } - if opts.Device != nil { - data.Device = *opts.Device - } - data.Ip = x.GetRemoteIp() - logv.AssertError(cfg.DB().Create(data).Error) - claim.ID = data.ID - claim.AID = aid - claim.UID = user.ID - claim.Name = user.Username - claim.Icon = user.Icon - claim.ExpiresAt = jwt.NewNumericDate(data.ExpiredAt) - if user.Nickname != "" { - claim.Name = user.Nickname - } - return auth.GenJwt(claim) - } else { - return nil, errs.ArgsInvalid - } -} - -func tokenGet(x *rest.X) (any, error) { - opts := &M.TokenGet{} - err := x.Parse(opts) - if err != nil { - return nil, err - } - data := &M.Token{} - - err = cfg.DB().Where("id = ?", opts.ID).First(data).Error - - return data, err -} -func tokenPatch(x *rest.X) (any, error) { - opts := &M.TokenPatch{} - err := x.Parse(opts) - if err != nil { - return nil, err - } - data := &M.Token{} - - err = cfg.DB().Where("id = ?", opts.ID).First(data).Error - if err != nil { - return nil, err - } - optsMap := make(map[string]interface{}) - if opts.ExpiredAt != nil { - optsMap["expired_at"] = opts.ExpiredAt - } - if opts.OverPerm != nil { - optsMap["over_perm"] = opts.OverPerm - } - err = cfg.DB().Model(data).Updates(optsMap).Error - - return data, err -} -func tokenDelete(x *rest.X) (any, error) { - opts := &M.TokenDelete{} - err := x.Parse(opts) - if err != nil { - return nil, err - } - data := &M.Token{} - - err = cfg.DB().Where("id = ?", opts.ID).Delete(data).Error - - return data, err -} -func tokenList(x *rest.X) (any, error) { - opts := &M.TokenList{} - err := x.Parse(opts) - if err != nil { - return nil, err - } - data := make([]*M.Token, 0, 10) - - query := cfg.DB() - query = query.Where("user_id = ?", opts.UserID) - query = query.Where("app_id = ?", opts.AppID) - err = query.Limit(opts.Limit).Find(&data).Error - - return data, err -} diff --git a/api/user/create.go b/api/user/create.go new file mode 100644 index 0000000..066158e --- /dev/null +++ b/api/user/create.go @@ -0,0 +1,108 @@ +// +// create.go +// Copyright (C) 2025 veypi +// 2025-05-06 15:05 +// Distributed under terms of the MIT license. +// + +package user + +import ( + "encoding/base64" + "fmt" + "oa/cfg" + M "oa/models" + "strings" + "time" + + "math/rand" + + "github.com/google/uuid" + "github.com/veypi/OneBD/rest" + "github.com/veypi/utils" + "gorm.io/gorm" +) + +var _ = Router.Post("/", userPost) + +type postOpts struct { + Username string `json:"username" gorm:"varchar(100);unique;default:not null" parse:"json"` + Code string `json:"code" gorm:"varchar(128)" parse:"json"` + Nickname *string `json:"nickname" parse:"json"` + Icon *string `json:"icon" parse:"json"` + Email *string `json:"email" gorm:"varchar(20);unique;default:null" parse:"json"` + Phone *string `json:"phone" gorm:"varchar(50);unique;default:null" parse:"json"` +} + +func userPost(x *rest.X) (any, error) { + opts := &postOpts{} + err := x.Parse(opts) + if err != nil { + return nil, err + } + data := &M.User{} + + data.ID = strings.ReplaceAll(uuid.New().String(), "-", "") + data.Username = opts.Username + data.Code = opts.Code + data.Salt = utils.RandSeq(16) + if len(data.Username) < 2 { + return nil, rest.ErrArgInvalid.WithArgs("username length") + } + code, err := base64.URLEncoding.DecodeString(opts.Code) + if err != nil || len(code) < 8 { + return nil, rest.ErrArgInvalid.WithArgs("code") + } + code = utils.PKCS7Padding(code, 32) + data.Code, err = utils.AesEncrypt([]byte(data.ID), code, []byte(data.Salt)) + if err != nil { + return nil, rest.ErrArgInvalid.WithArgs("code") + } + ncode, err := utils.AesDecrypt([]byte(data.Code), code, []byte(data.Salt)) + if err != nil || ncode != data.ID { + return nil, rest.ErrInternalServer.AppendString("code decrypt failed") + } + if opts.Nickname != nil { + data.Nickname = *opts.Nickname + } + if opts.Icon != nil { + data.Icon = *opts.Icon + } else { + data.Icon = fmt.Sprintf("https://public.veypi.com/img/avatar/%04d.jpg", rand.New(rand.NewSource(time.Now().UnixNano())).Intn(220)) + } + if opts.Email != nil { + data.Email = *opts.Email + } + if opts.Phone != nil { + data.Phone = *opts.Phone + } + data.Status = 1 + err = cfg.DB().Transaction(func(tx *gorm.DB) error { + err := tx.Create(data).Error + if err != nil { + return err + } + app := &M.App{} + err = tx.Where("id = ?", cfg.Config.ID).First(app).Error + if err != nil { + return err + } + status := "ok" + switch app.Typ { + case "private": + return rest.ErrNotPermitted.WithArgs("not enable register") + case "apply": + status = "applying" + case "public": + } + if app.Typ != "public" { + } + + return tx.Create(&M.AppUser{ + UserID: data.ID, + AppID: cfg.Config.ID, + Status: status, + }).Error + }) + return data, err +} diff --git a/api/user/init.go b/api/user/init.go index 330b966..b9ebb03 100644 --- a/api/user/init.go +++ b/api/user/init.go @@ -8,7 +8,13 @@ package user import ( + "oa/api/user/role" + "github.com/veypi/OneBD/rest" ) var Router = rest.NewRouter() + +var ( + _ = Router.Extend("/:user_id/user_role", role.Router) +) diff --git a/api/user/login.go b/api/user/login.go new file mode 100644 index 0000000..b9f8fa3 --- /dev/null +++ b/api/user/login.go @@ -0,0 +1,101 @@ +// +// login.go +// Copyright (C) 2025 veypi +// 2025-05-12 17:35 +// Distributed under terms of the MIT license. +// + +package user + +import ( + "encoding/base64" + "oa/cfg" + "oa/libs/auth" + "oa/models" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/veypi/OneBD/rest" + "github.com/veypi/utils" + "github.com/veypi/utils/logv" +) + +var _ = Router.Post("/login", userLogin) + +type loginOpts struct { + UserName string `json:"username" parse:"json"` + Code string `json:"code" parse:"json"` + Phone *string `json:"phone" parse:"json"` + Email *string `json:"email" parse:"json"` + Type *string `json:"type" parse:"json"` + AppID *string `json:"app_id" parse:"json"` + Device *string `json:"device" parse:"json"` +} + +func userLogin(x *rest.X) (any, error) { + // Implement login logic here + // For example, validate user credentials and return a token + opts := &loginOpts{} + err := x.Parse(opts) + if err != nil { + return nil, err + } + user := &models.User{} + query := cfg.DB() + typ := "" + if opts.Type != nil { + typ = *opts.Type + } + switch typ { + case "phone": + query = query.Where("phone = ?", opts.Phone) + case "email": + query = query.Where("email = ?", opts.Email) + default: + query = query.Where("username = ?", opts.UserName) + } + err = query.First(user).Error + if err != nil { + return nil, err + } + logv.Info().Str("user", user.ID).Msg("login") + code, err := base64.URLEncoding.DecodeString(opts.Code) + if err != nil { + return nil, rest.ErrArgInvalid.WithArgs("code") + } + logv.Warn().Msgf("code: %s", code) + + ncode, err := utils.AesDecrypt([]byte(user.Code), utils.PKCS7Padding(code, 32), []byte(user.Salt)) + logv.Warn().Msgf("id: %s\n%s", ncode, user.ID) + if err != nil || string(ncode) != user.ID { + return nil, rest.ErrNotAuthorized + } + aid := cfg.Config.ID + if opts.AppID != nil && *opts.AppID != "" { + aid = *opts.AppID + } + + data := &models.Token{} + + data.UserID = user.ID + data.AppID = aid + data.ExpiredAt = time.Now().Add(time.Hour * 72) + if opts.Device != nil { + data.Device = *opts.Device + } + data.Ip = x.GetRemoteIp() + logv.AssertError(cfg.DB().Create(data).Error) + claim := &auth.Claims{} + claim.IssuedAt = jwt.NewNumericDate(time.Now()) + claim.Issuer = cfg.Config.ID + claim.ID = data.ID + claim.AID = aid + claim.UID = user.ID + claim.Name = user.Username + claim.Icon = user.Icon + claim.ExpiresAt = jwt.NewNumericDate(data.ExpiredAt) + if user.Nickname != "" { + claim.Name = user.Nickname + } + return auth.GenJwt(claim) +} diff --git a/api/user/role/create.go b/api/user/role/create.go new file mode 100644 index 0000000..29ac9c3 --- /dev/null +++ b/api/user/role/create.go @@ -0,0 +1,40 @@ +// +// crud.go +// Copyright (C) 2025 veypi +// 2025-05-06 15:12 +// Distributed under terms of the MIT license. +// + +package role + +import ( + "github.com/veypi/OneBD/rest" + "oa/cfg" + M "oa/models" +) + +var _ = Router.Post("/", userRolePost) + +type postOpts struct { + UserID string `json:"user_id" parse:"path"` + RoleID string `json:"role_id" parse:"json"` + AppID string `json:"app_id" parse:"json"` + Status string `json:"status" parse:"json"` +} + +func userRolePost(x *rest.X) (any, error) { + opts := &postOpts{} + err := x.Parse(opts) + if err != nil { + return nil, err + } + data := &M.UserRole{} + + data.UserID = opts.UserID + data.RoleID = opts.RoleID + data.AppID = opts.AppID + data.Status = opts.Status + err = cfg.DB().Create(data).Error + + return data, err +} diff --git a/api/user/role/del.go b/api/user/role/del.go new file mode 100644 index 0000000..a4c395b --- /dev/null +++ b/api/user/role/del.go @@ -0,0 +1,27 @@ +// +// del.go +// Copyright (C) 2025 veypi +// 2025-05-06 15:24 +// Distributed under terms of the MIT license. +// + +package role + +import ( + "oa/cfg" + "oa/models" + + "github.com/veypi/OneBD/rest" +) + +var _ = Router.Delete("/:id", userRoleDelete) + +func userRoleDelete(x *rest.X) (any, error) { + id := x.Params.Get("id") + if id == "" { + return nil, rest.ErrArgInvalid.WithArgs("id") + } + data := &models.UserRole{} + err := cfg.DB().Where("id = ?", id).Delete(data).Error + return data, err +} diff --git a/api/user/role/get.go b/api/user/role/get.go new file mode 100644 index 0000000..ae5029b --- /dev/null +++ b/api/user/role/get.go @@ -0,0 +1,71 @@ +// +// get.go +// Copyright (C) 2025 veypi +// 2025-05-06 15:21 +// Distributed under terms of the MIT license. +// + +package role + +import ( + "oa/cfg" + M "oa/models" + + "github.com/veypi/OneBD/rest" +) + +var _ = Router.Get("/:id", ` + get user role +`, userRoleGet) + +func userRoleGet(x *rest.X) (any, error) { + data := &M.UserRole{} + err := cfg.DB().Where("id = ?", x.Params.Get("id")).First(data).Error + return data, err +} + +var _ = Router.Get("/", ` + list user roles +`, userRoleList) + +type listOpts struct { + UserID *string `json:"user_id" parse:"path"` + RoleID *string `json:"role_id" parse:"query"` + AppID *string `json:"app_id" parse:"query"` + Status *string `json:"status" parse:"query"` +} + +func userRoleList(x *rest.X) (any, error) { + opts := &listOpts{} + err := x.Parse(opts) + if err != nil { + return nil, err + } + data := make([]*struct { + M.UserRole + Username string `json:"username"` + Nickname string `json:"nickname"` + Icon string `json:"icon"` + RoleName string `json:"role_name"` + }, 0, 10) + // data := make([]*M.UserRole, 0, 10) + + query := cfg.DB().Debug().Table("user_roles").Select("user_roles.*,users.username,users.nickname,users.icon,roles.name as role_name"). + Joins("JOIN users ON users.id = user_roles.user_id"). + Joins("JOIN roles ON roles.id = user_roles.role_id") + if opts.UserID != nil && *opts.UserID != "-" { + query = query.Where("user_id LIKE ?", opts.UserID) + } + if opts.RoleID != nil { + query = query.Where("role_id LIKE ?", opts.RoleID) + } + if opts.AppID != nil { + query = query.Where("app_id LIKE ?", opts.AppID) + } + if opts.Status != nil { + query = query.Where("status LIKE ?", opts.Status) + } + err = query.Scan(&data).Error + + return data, err +} diff --git a/api/user/role/init.go b/api/user/role/init.go new file mode 100644 index 0000000..f5c2d5d --- /dev/null +++ b/api/user/role/init.go @@ -0,0 +1,12 @@ +// +// init.go +// Copyright (C) 2025 veypi +// 2025-05-06 15:12 +// Distributed under terms of the MIT license. +// + +package role + +import "github.com/veypi/OneBD/rest" + +var Router = rest.NewRouter() diff --git a/api/user/role/patch.go b/api/user/role/patch.go new file mode 100644 index 0000000..f1f6ac9 --- /dev/null +++ b/api/user/role/patch.go @@ -0,0 +1,42 @@ +// +// crud.go +// Copyright (C) 2025 veypi +// 2025-05-06 15:12 +// Distributed under terms of the MIT license. +// + +package role + +import ( + "github.com/veypi/OneBD/rest" + "oa/cfg" + M "oa/models" +) + +type patchOpts struct { + ID string `json:"id" parse:"path"` + Status *string `json:"status" parse:"json"` +} + +var _ = Router.Patch("/:id", userRolePatch) + +func userRolePatch(x *rest.X) (any, error) { + opts := &patchOpts{} + err := x.Parse(opts) + if err != nil { + return nil, err + } + data := &M.UserRole{} + + err = cfg.DB().Where("id = ?", opts.ID).First(data).Error + if err != nil { + return nil, err + } + optsMap := make(map[string]interface{}) + if opts.Status != nil { + optsMap["status"] = opts.Status + } + err = cfg.DB().Model(data).Updates(optsMap).Error + + return data, err +} diff --git a/api/user/user.go b/api/user/user.go index 140a560..2d33921 100644 --- a/api/user/user.go +++ b/api/user/user.go @@ -1,39 +1,29 @@ package user import ( - "fmt" - "math/rand" "oa/cfg" - "oa/errs" "oa/libs/auth" M "oa/models" - "strings" - "time" - "github.com/google/uuid" "github.com/veypi/OneBD/rest" - "gorm.io/gorm" ) var _ = Router.Delete("/:user_id", auth.Check("user", "user_id", auth.DoDelete), userDelete) func userDelete(x *rest.X) (any, error) { - opts := &M.UserDelete{} - err := x.Parse(opts) - if err != nil { - return nil, err - } data := &M.User{} - - err = cfg.DB().Where("id = ?", opts.ID).Delete(data).Error - + err := cfg.DB().Where("id = ?", x.Params.Get("user_id")).Delete(data).Error return data, err } var _ = Router.Get("/:user_id", auth.Check("user", "user_id", auth.DoRead), userGet) +type getOpts struct { + ID string `json:"id" parse:"path@user_id"` +} + func userGet(x *rest.X) (any, error) { - opts := &M.UserGet{} + opts := &getOpts{} err := x.Parse(opts) if err != nil { return nil, err @@ -47,8 +37,16 @@ func userGet(x *rest.X) (any, error) { var _ = Router.Get("/", auth.Check("user", "", auth.DoRead), userList) +type listOpts struct { + Username *string `json:"username" parse:"query"` + Nickname *string `json:"nickname" parse:"query"` + Email *string `json:"email" parse:"query"` + Phone *string `json:"phone" parse:"query"` + Status *uint `json:"status" parse:"query"` +} + func userList(x *rest.X) (any, error) { - opts := &M.UserList{} + opts := &listOpts{} err := x.Parse(opts) if err != nil { return nil, err @@ -78,8 +76,18 @@ func userList(x *rest.X) (any, error) { var _ = Router.Patch("/:user_id", auth.Check("user", "user_id", auth.DoUpdate), userPatch) +type patchOpts struct { + ID string `json:"id" parse:"path@user_id"` + Username *string `json:"username" parse:"json"` + Nickname *string `json:"nickname" parse:"json"` + Icon *string `json:"icon" parse:"json"` + Email *string `json:"email" parse:"json"` + Phone *string `json:"phone" parse:"json"` + Status *uint `json:"status" parse:"json"` +} + func userPatch(x *rest.X) (any, error) { - opts := &M.UserPatch{} + opts := &patchOpts{} err := x.Parse(opts) if err != nil { return nil, err @@ -113,65 +121,3 @@ func userPatch(x *rest.X) (any, error) { return data, err } - -var _ = Router.Post("/", userPost) - -func userPost(x *rest.X) (any, error) { - opts := &M.UserPost{} - err := x.Parse(opts) - if err != nil { - return nil, err - } - data := &M.User{} - - data.ID = strings.ReplaceAll(uuid.New().String(), "-", "") - data.Username = opts.Username - data.Salt = opts.Salt - data.Code = opts.Code - if data.Username == "" || len(data.Salt) != 32 || len(data.Code) != 64 { - return nil, errs.ArgsInvalid.WithStr("username/salt/code length") - } - if opts.Nickname != nil { - data.Nickname = *opts.Nickname - } - if opts.Icon != nil { - data.Icon = *opts.Icon - } else { - data.Icon = fmt.Sprintf("https://public.veypi.com/img/avatar/%04d.jpg", rand.New(rand.NewSource(time.Now().UnixNano())).Intn(220)) - } - if opts.Email != nil { - data.Email = *opts.Email - } - if opts.Phone != nil { - data.Phone = *opts.Phone - } - data.Status = 1 - err = cfg.DB().Transaction(func(tx *gorm.DB) error { - err := tx.Create(data).Error - if err != nil { - return err - } - app := &M.App{} - err = tx.Where("id = ?", cfg.Config.ID).First(app).Error - if err != nil { - return err - } - status := "ok" - switch app.Typ { - case "private": - return errs.AuthNoPerm.WithStr("not enable register") - case "apply": - status = "applying" - case "public": - } - if app.Typ != "public" { - } - - return tx.Create(&M.AppUser{ - UserID: data.ID, - AppID: cfg.Config.ID, - Status: status, - }).Error - }) - return data, err -} diff --git a/api/user/user_role.go b/api/user/user_role.go deleted file mode 100644 index 2e60826..0000000 --- a/api/user/user_role.go +++ /dev/null @@ -1,111 +0,0 @@ -package user - -import ( - "github.com/veypi/OneBD/rest" - "oa/cfg" - M "oa/models" -) - -func init() { - r := Router.SubRouter(":user_id/user_role") - r.Delete("/:user_role_id", userRoleDelete) - r.Get("/:user_role_id", userRoleGet) - r.Get("/", userRoleList) - r.Patch("/:user_role_id", userRolePatch) - r.Post("/", userRolePost) -} - -func userRoleList(x *rest.X) (any, error) { - opts := &M.UserRoleList{} - err := x.Parse(opts) - if err != nil { - return nil, err - } - data := make([]*struct { - M.UserRole - Username string `json:"username"` - Nickname string `json:"nickname"` - Icon string `json:"icon"` - RoleName string `json:"role_name"` - }, 0, 10) - // data := make([]*M.UserRole, 0, 10) - - query := cfg.DB().Debug().Table("user_roles").Select("user_roles.*,users.username,users.nickname,users.icon,roles.name as role_name"). - Joins("JOIN users ON users.id = user_roles.user_id"). - Joins("JOIN roles ON roles.id = user_roles.role_id") - if opts.UserID != nil && *opts.UserID != "-" { - query = query.Where("user_id LIKE ?", opts.UserID) - } - if opts.RoleID != nil { - query = query.Where("role_id LIKE ?", opts.RoleID) - } - if opts.AppID != nil { - query = query.Where("app_id LIKE ?", opts.AppID) - } - if opts.Status != nil { - query = query.Where("status LIKE ?", opts.Status) - } - err = query.Scan(&data).Error - - return data, err -} -func userRolePost(x *rest.X) (any, error) { - opts := &M.UserRolePost{} - err := x.Parse(opts) - if err != nil { - return nil, err - } - data := &M.UserRole{} - - data.UserID = opts.UserID - data.RoleID = opts.RoleID - data.AppID = opts.AppID - data.Status = opts.Status - err = cfg.DB().Create(data).Error - - return data, err -} -func userRoleGet(x *rest.X) (any, error) { - opts := &M.UserRoleGet{} - err := x.Parse(opts) - if err != nil { - return nil, err - } - data := &M.UserRole{} - - err = cfg.DB().Where("id = ?", opts.ID).First(data).Error - - return data, err -} -func userRolePatch(x *rest.X) (any, error) { - opts := &M.UserRolePatch{} - err := x.Parse(opts) - if err != nil { - return nil, err - } - data := &M.UserRole{} - - err = cfg.DB().Where("id = ?", opts.ID).First(data).Error - if err != nil { - return nil, err - } - optsMap := make(map[string]interface{}) - if opts.Status != nil { - optsMap["status"] = opts.Status - } - err = cfg.DB().Model(data).Updates(optsMap).Error - - return data, err -} -func userRoleDelete(x *rest.X) (any, error) { - opts := &M.UserRoleDelete{} - err := x.Parse(opts) - if err != nil { - return nil, err - } - data := &M.UserRole{} - - err = cfg.DB().Where("id = ?", opts.ID).Delete(data).Error - - return data, err -} diff --git a/models/token.gen.go b/models/token.gen.go deleted file mode 100644 index 94c4c87..0000000 --- a/models/token.gen.go +++ /dev/null @@ -1,44 +0,0 @@ -package models - -import "time" - -type TokenSalt struct { - Username string `json:"username" parse:"json"` - Typ *string `json:"typ" parse:"json"` -} - -type TokenPost struct { - // 两种获取token方式,一种用refreshtoken换取apptoken(应用登录),一种用密码加密code换refreshtoken (oa登录) - Refresh *string `json:"refresh" parse:"json"` - Typ *string `json:"typ" parse:"json"` - - // 登录方随机生成的salt,非用户salt - UserID *string `json:"user_id" gorm:"index;type:varchar(32)" parse:"json"` - Salt *string `json:"salt" parse:"json"` - Code *string `json:"code" parse:"json"` - - AppID *string `json:"app_id" gorm:"index;type:varchar(32)" parse:"json"` - ExpiredAt *time.Time `json:"expired_at" parse:"json"` - OverPerm *string `json:"over_perm" parse:"json"` - Device *string `json:"device" parse:"json"` -} - -type TokenGet struct { - ID string `json:"id" gorm:"primaryKey;type:varchar(32)" parse:"path@token_id"` -} - -type TokenPatch struct { - ID string `json:"id" gorm:"primaryKey;type:varchar(32)" parse:"path@token_id"` - ExpiredAt *time.Time `json:"expired_at" parse:"json"` - OverPerm *string `json:"over_perm" parse:"json"` -} - -type TokenDelete struct { - ID string `json:"id" gorm:"primaryKey;type:varchar(32)" parse:"path@token_id"` -} - -type TokenList struct { - Limit int `json:"limit"` - UserID string `json:"user_id" gorm:"index;type:varchar(32)" parse:"query"` - AppID string `json:"app_id" gorm:"index;type:varchar(32)" parse:"query"` -} diff --git a/models/user.gen.go b/models/user.gen.go deleted file mode 100644 index 4193329..0000000 --- a/models/user.gen.go +++ /dev/null @@ -1,66 +0,0 @@ -package models - -import () - -type UserGet struct { - ID string `json:"id" gorm:"primaryKey;type:varchar(32)" parse:"path@user_id"` -} - -type UserPatch struct { - ID string `json:"id" gorm:"primaryKey;type:varchar(32)" parse:"path@user_id"` - Username *string `json:"username" gorm:"varchar(100);unique;default:not null" parse:"json"` - Nickname *string `json:"nickname" parse:"json"` - Icon *string `json:"icon" parse:"json"` - Email *string `json:"email" gorm:"varchar(20);unique;default:null" parse:"json"` - Phone *string `json:"phone" gorm:"varchar(50);unique;default:null" parse:"json"` - Status *uint `json:"status" parse:"json"` -} - -type UserDelete struct { - ID string `json:"id" gorm:"primaryKey;type:varchar(32)" parse:"path@user_id"` -} - -type UserPost struct { - Username string `json:"username" gorm:"varchar(100);unique;default:not null" parse:"json"` - Nickname *string `json:"nickname" parse:"json"` - Icon *string `json:"icon" parse:"json"` - Email *string `json:"email" gorm:"varchar(20);unique;default:null" parse:"json"` - Phone *string `json:"phone" gorm:"varchar(50);unique;default:null" parse:"json"` - Salt string `json:"salt" gorm:"varchar(32)" parse:"json"` - Code string `json:"code" gorm:"varchar(128)" parse:"json"` -} - -type UserList struct { - Username *string `json:"username" gorm:"type:varchar(100);unique;default:not null" parse:"query"` - Nickname *string `json:"nickname" gorm:"type:varchar(100)" parse:"query"` - Email *string `json:"email" gorm:"unique;type:varchar(50);default:null" parse:"query"` - Phone *string `json:"phone" gorm:"type:varchar(30);unique;default:null" parse:"query"` - Status *uint `json:"status" parse:"query"` -} - -type UserRoleGet struct { - ID string `json:"id" gorm:"primaryKey;type:varchar(32)" parse:"path@user_role_id"` -} - -type UserRolePatch struct { - ID string `json:"id" gorm:"primaryKey;type:varchar(32)" parse:"path@user_role_id"` - Status *string `json:"status" parse:"json"` -} - -type UserRoleDelete struct { - ID string `json:"id" gorm:"primaryKey;type:varchar(32)" parse:"path@user_role_id"` -} - -type UserRoleList struct { - UserID *string `json:"user_id" parse:"path"` - RoleID *string `json:"role_id" parse:"query"` - AppID *string `json:"app_id" parse:"query"` - Status *string `json:"status" parse:"query"` -} - -type UserRolePost struct { - UserID string `json:"user_id" parse:"path"` - RoleID string `json:"role_id" parse:"json"` - AppID string `json:"app_id" parse:"json"` - Status string `json:"status" parse:"json"` -} diff --git a/ui/page/login.html b/ui/page/login.html index bd08f01..44c85ca 100644 --- a/ui/page/login.html +++ b/ui/page/login.html @@ -5,6 +5,7 @@ 登录与注册 + @@ -279,9 +286,10 @@ 或使用您的用户名进行注册 + @input="signUpForm.username = $event.target.value" /> + @input="signUpForm.password = $event.target.value" /> +
{{ signUpError }}
@@ -295,9 +303,9 @@ 或使用您的账户 + @input="signInForm.username = $event.target.value" /> + @input="signInForm.password = $event.target.value" /> 忘记密码? @@ -325,7 +333,13 @@ signUpForm = {username: '', password: ''}; signInForm = {username: '', password: ''}; bubbles = []; - errorMessage = ''; + signUpError = ''; + + // 验证密码是否符合要求 + const validatePassword = (password) => { + const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[_]).{9,}$/; + return regex.test(password); + }; // 切换到注册页面 switchToSignUp = () => { @@ -353,21 +367,28 @@ // 处理注册表单提交 handleSignUp = async (e) => { e.preventDefault(); + signUpError = ''; + if (!validatePassword(signUpForm.password) && false) { + signUpError = '密码必须大于8位,且包含大小写字母、下划线和数字。'; + return; + } + if (signUpForm.username.length < 2) { + signUpError = '用户名必须大于2位。'; + return; + } + signUpError = ''; try { - const salt = CryptoJS.lib.WordArray.random(128 / 8).toString(); - const key = deriveKey(signUpForm.password, salt); const response = await api.Post('/api/user', { username: signUpForm.username, - salt: salt, - code: key.toString(CryptoJS.enc.Hex) + code: btoa(signUpForm.password), }); if (response) { alert('注册成功!'); switchToSignIn(); } } catch (error) { - errorMessage = error.message || '注册失败,请重试。'; - console.error(errorMessage); + signUpError = error.message || '注册失败,请重试。'; + console.error(signUpError); } }; @@ -375,28 +396,16 @@ handleSignIn = async (e) => { e.preventDefault(); try { - const tokenSaltResponse = await api.Post('/api/token/salt', {username: signInForm.username}); - const {id, salt} = tokenSaltResponse; - const key = deriveKey(signInForm.password, salt); - const iv = CryptoJS.lib.WordArray.random(128 / 8); - const encryptedId = CryptoJS.AES.encrypt(id, key, { - iv: iv, - mode: CryptoJS.mode.CBC, - padding: CryptoJS.pad.Pkcs7 - }).toString(); - - const loginResponse = await api.Post('/api/token', { - user_id: id, - code: encryptedId, - salt: iv.toString() + const loginResponse = await api.Post('/api/user/login', { + username: signInForm.username, + code: btoa(signInForm.password), }); localStorage.setItem('refresh', loginResponse) if (loginResponse) { window.location.href = redirect } } catch (error) { - errorMessage = error.message || '登录失败,请检查您的凭据。'; - alert(errorMessage); + alert(error.message || '登录失败,请检查您的凭据。'); } };