feat: simplify user login

v3
veypi 6 months ago
parent 545f49c0f3
commit 7e7e6ed506

@ -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
}

@ -0,0 +1,138 @@
//
// post.go
// Copyright (C) 2025 veypi <i@veypi.com>
// 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
}
}

@ -0,0 +1,55 @@
//
// get.go
// Copyright (C) 2025 veypi <i@veypi.com>
// 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
}

@ -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)
}

@ -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
}

@ -0,0 +1,108 @@
//
// create.go
// Copyright (C) 2025 veypi <i@veypi.com>
// 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
}

@ -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)
)

@ -0,0 +1,101 @@
//
// login.go
// Copyright (C) 2025 veypi <i@veypi.com>
// 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)
}

@ -0,0 +1,40 @@
//
// crud.go
// Copyright (C) 2025 veypi <i@veypi.com>
// 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
}

@ -0,0 +1,27 @@
//
// del.go
// Copyright (C) 2025 veypi <i@veypi.com>
// 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
}

@ -0,0 +1,71 @@
//
// get.go
// Copyright (C) 2025 veypi <i@veypi.com>
// 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
}

@ -0,0 +1,12 @@
//
// init.go
// Copyright (C) 2025 veypi <i@veypi.com>
// 2025-05-06 15:12
// Distributed under terms of the MIT license.
//
package role
import "github.com/veypi/OneBD/rest"
var Router = rest.NewRouter()

@ -0,0 +1,42 @@
//
// crud.go
// Copyright (C) 2025 veypi <i@veypi.com>
// 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
}

@ -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
}

@ -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
}

@ -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"`
}

@ -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"`
}

@ -5,6 +5,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>登录与注册</title>
<meta name="description" content="用户登录与注册页面" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
<style>
* {
@ -260,6 +261,12 @@
.social-container a:hover {
background-color: #f2f2f2;
}
.error-message {
color: red;
font-size: 12px;
margin-top: 8px;
}
</style>
</head>
@ -279,9 +286,10 @@
</div>
<span>或使用您的用户名进行注册</span>
<input type="text" placeholder="用户名" !value="signUpForm.username"
@input='signUpForm.username=$event.target.value' />
@input="signUpForm.username = $event.target.value" />
<input type="password" placeholder="密码" !value="signUpForm.password"
@input='signUpForm.password=$event.target.value' />
@input="signUpForm.password = $event.target.value" />
<div class="error-message">{{ signUpError }}</div>
<button>注册</button>
</form>
</div>
@ -295,9 +303,9 @@
</div>
<span>或使用您的账户</span>
<input type="text" placeholder="用户名" !value="signInForm.username"
@input='signInForm.username = $event.target.value' />
@input="signInForm.username = $event.target.value" />
<input type="password" placeholder="密码" !value="signInForm.password"
@input='signInForm.password = $event.target.value' />
@input="signInForm.password = $event.target.value" />
<a href="#">忘记密码?</a>
<button>登录</button>
</form>
@ -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 || '登录失败,请检查您的凭据。');
}
};

Loading…
Cancel
Save