// // create.go // Copyright (C) 2025 veypi // 2025-05-06 15:05 // Distributed under terms of the MIT license. // package user import ( "encoding/base64" "fmt" "math/rand" "strings" "time" "github.com/google/uuid" "github.com/veypi/vbase/api/sms" "github.com/veypi/vbase/cfg" "github.com/veypi/vbase/libs/utils" "github.com/veypi/vbase/models" "github.com/veypi/vigo" "gorm.io/gorm" ) var _ = Router.Post("/", "用户注册", vigo.SkipBefore, publicLimits, userPost) type postOpts struct { Username string `json:"username" gorm:"varchar(100);unique;default:not null" src:"json" desc:"用户名"` Code string `json:"code" gorm:"varchar(128)" src:"json" desc:"授权码/密码"` Phone string `json:"phone" gorm:"varchar(50);unique;default:null" src:"json" desc:"手机号"` VerifyCode string `json:"verify_code" src:"json" desc:"验证码"` Region string `json:"region" src:"json" desc:"区域"` Nickname *string `json:"nickname" src:"json" desc:"昵称"` Icon *string `json:"icon" src:"json" desc:"头像"` Email *string `json:"email" gorm:"varchar(20);unique;default:null" src:"json" desc:"邮箱"` } func userPost(x *vigo.X, opts *postOpts) (*models.User, error) { data := &models.User{} if cfg.Config.SMS.Enable { data.Phone = opts.Region + opts.Phone data.Region = opts.Region err := sms.VerifyCode(opts.Phone, opts.VerifyCode, opts.Region, "signup") if err != nil { return nil, vigo.ErrArgInvalid.WithArgs("verify code").WithString(err.Error()) } } data.Username = opts.Username data.Code = opts.Code data.Salt = utils.RandSeq(16) if len(data.Username) < 2 { return nil, vigo.ErrArgInvalid.WithArgs("username length") } code, err := base64.URLEncoding.DecodeString(opts.Code) if err != nil || len(code) < 8 { return nil, vigo.ErrArgInvalid.WithArgs("code") } code = utils.PKCS7Padding(code, 32) // We need ID for encryption, but it's not generated yet. // We can generate it manually here since vigo.Model doesn't auto-generate it before Create data.ID = strings.ReplaceAll(uuid.New().String(), "-", "") data.Code, err = utils.AesEncrypt([]byte(data.ID), code, []byte(data.Salt)) if err != nil { return nil, vigo.ErrArgInvalid.WithArgs("code") } ncode, err := utils.AesDecrypt([]byte(data.Code), code, []byte(data.Salt)) if err != nil || string(ncode) != data.ID { return nil, vigo.ErrInternalServer.WithString("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 } data.Status = 1 err = cfg.DB().Transaction(func(tx *gorm.DB) error { err := tx.Create(data).Error if err != nil { return err } app := &models.App{} err = tx.Where("id = ?", cfg.Config.ID).First(app).Error if err != nil { return err } status := "ok" switch app.Typ { case "private": return vigo.ErrNotPermitted.WithArgs("not enable register") case "apply": status = "applying" case "public": } if app.Typ != "public" { } return tx.Create(&models.AppUser{ UserID: data.ID, AppID: cfg.Config.ID, Status: status, }).Error }) return data, err }