reset backend
This commit is contained in:
@@ -1,326 +0,0 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"quyun/v2/app/errorx"
|
||||
"quyun/v2/app/http/web/dto"
|
||||
"quyun/v2/app/services"
|
||||
"quyun/v2/database/models"
|
||||
"quyun/v2/pkg/consts"
|
||||
"quyun/v2/providers/jwt"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// @provider
|
||||
type auth struct {
|
||||
jwt *jwt.JWT
|
||||
}
|
||||
|
||||
var (
|
||||
reUsername = regexp.MustCompile(`^[a-zA-Z0-9_]{3,32}$`)
|
||||
rePhone = regexp.MustCompile(`^[0-9]{6,20}$`)
|
||||
)
|
||||
|
||||
type passwordResetState struct {
|
||||
code string
|
||||
codeExpire time.Time
|
||||
lastSentAt time.Time
|
||||
resetToken string
|
||||
tokenExpire time.Time
|
||||
}
|
||||
|
||||
var passwordResetStore = struct {
|
||||
mu sync.Mutex
|
||||
phoneToItem map[string]*passwordResetState
|
||||
tokenToPhone map[string]string
|
||||
}{
|
||||
phoneToItem: make(map[string]*passwordResetState),
|
||||
tokenToPhone: make(map[string]string),
|
||||
}
|
||||
|
||||
const (
|
||||
passwordResetCodeTTL = 5 * time.Minute
|
||||
passwordResetTokenTTL = 10 * time.Minute
|
||||
passwordResetSendGap = 60 * time.Second
|
||||
)
|
||||
|
||||
// Login 用户登录(平台侧,非超级管理员)。
|
||||
//
|
||||
// @Summary 用户登录
|
||||
// @Tags Web
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param form body dto.LoginForm true "form"
|
||||
// @Success 200 {object} dto.LoginResponse "成功"
|
||||
// @Router /v1/auth/login [post]
|
||||
// @Bind form body
|
||||
func (ctl *auth) login(ctx fiber.Ctx, form *dto.LoginForm) (*dto.LoginResponse, error) {
|
||||
m, err := services.User.FindByUsername(ctx, form.Username)
|
||||
if err != nil {
|
||||
return nil, errorx.Wrap(err).WithMsg("用户名或密码错误")
|
||||
}
|
||||
if ok := m.ComparePassword(ctx, form.Password); !ok {
|
||||
return nil, errorx.Wrap(errorx.ErrInvalidCredentials).WithMsg("用户名或密码错误")
|
||||
}
|
||||
|
||||
token, err := ctl.jwt.CreateToken(ctl.jwt.CreateClaims(jwt.BaseClaims{
|
||||
UserID: m.ID,
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, errorx.Wrap(err).WithMsg("登录凭证生成失败")
|
||||
}
|
||||
|
||||
return &dto.LoginResponse{Token: token}, nil
|
||||
}
|
||||
|
||||
// PasswordResetSendSMS 找回密码:发送短信验证码(预留:当前返回验证码用于前端弹窗展示)。
|
||||
//
|
||||
// @Summary 找回密码-发送短信验证码
|
||||
// @Tags Web
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param form body dto.PasswordResetSendSMSForm true "form"
|
||||
// @Success 200 {object} dto.PasswordResetSendSMSResponse "成功"
|
||||
// @Router /v1/auth/password/reset/sms [post]
|
||||
// @Bind form body
|
||||
func (ctl *auth) passwordResetSendSMS(ctx fiber.Ctx, form *dto.PasswordResetSendSMSForm) (*dto.PasswordResetSendSMSResponse, error) {
|
||||
phone := strings.TrimSpace(form.Phone)
|
||||
if phone == "" {
|
||||
return nil, errorx.ErrMissingParameter.WithMsg("请输入手机号")
|
||||
}
|
||||
if !rePhone.MatchString(phone) {
|
||||
return nil, errorx.ErrInvalidParameter.WithMsg("手机号格式不正确")
|
||||
}
|
||||
|
||||
passwordResetStore.mu.Lock()
|
||||
defer passwordResetStore.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
item := passwordResetStore.phoneToItem[phone]
|
||||
if item == nil {
|
||||
item = &passwordResetState{}
|
||||
passwordResetStore.phoneToItem[phone] = item
|
||||
}
|
||||
|
||||
if !item.lastSentAt.IsZero() {
|
||||
elapsed := now.Sub(item.lastSentAt)
|
||||
if elapsed < passwordResetSendGap {
|
||||
remain := int((passwordResetSendGap - elapsed).Seconds())
|
||||
if remain < 1 {
|
||||
remain = 1
|
||||
}
|
||||
return nil, errorx.ErrTooManyRequests.WithMsgf("操作太频繁,请 %d 秒后再试", remain)
|
||||
}
|
||||
}
|
||||
|
||||
n, err := rand.Int(rand.Reader, big.NewInt(1000000))
|
||||
if err != nil {
|
||||
return nil, errorx.Wrap(err).WithMsg("验证码生成失败,请稍后再试")
|
||||
}
|
||||
code := fmt.Sprintf("%06d", n.Int64())
|
||||
|
||||
item.code = code
|
||||
item.codeExpire = now.Add(passwordResetCodeTTL)
|
||||
item.lastSentAt = now
|
||||
item.resetToken = ""
|
||||
item.tokenExpire = time.Time{}
|
||||
|
||||
return &dto.PasswordResetSendSMSResponse{
|
||||
NextSendSeconds: int(passwordResetSendGap.Seconds()),
|
||||
Code: code,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PasswordResetVerify 找回密码:校验短信验证码。
|
||||
//
|
||||
// @Summary 找回密码-校验验证码
|
||||
// @Tags Web
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param form body dto.PasswordResetVerifyForm true "form"
|
||||
// @Success 200 {object} dto.PasswordResetVerifyResponse "成功"
|
||||
// @Router /v1/auth/password/reset/verify [post]
|
||||
// @Bind form body
|
||||
func (ctl *auth) passwordResetVerify(ctx fiber.Ctx, form *dto.PasswordResetVerifyForm) (*dto.PasswordResetVerifyResponse, error) {
|
||||
phone := strings.TrimSpace(form.Phone)
|
||||
if phone == "" {
|
||||
return nil, errorx.ErrMissingParameter.WithMsg("请输入手机号")
|
||||
}
|
||||
if !rePhone.MatchString(phone) {
|
||||
return nil, errorx.ErrInvalidParameter.WithMsg("手机号格式不正确")
|
||||
}
|
||||
code := strings.TrimSpace(form.Code)
|
||||
if code == "" {
|
||||
return nil, errorx.ErrMissingParameter.WithMsg("请输入验证码")
|
||||
}
|
||||
|
||||
passwordResetStore.mu.Lock()
|
||||
defer passwordResetStore.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
item := passwordResetStore.phoneToItem[phone]
|
||||
if item == nil || item.code == "" || item.codeExpire.IsZero() || now.After(item.codeExpire) {
|
||||
return nil, errorx.ErrPreconditionFailed.WithMsg("验证码已过期,请重新获取")
|
||||
}
|
||||
if item.code != code {
|
||||
return nil, errorx.ErrInvalidParameter.WithMsg("验证码错误,请重新输入")
|
||||
}
|
||||
|
||||
// 创建一次性重置令牌,并清理验证码,避免复用。
|
||||
resetToken := uuid.NewString()
|
||||
item.resetToken = resetToken
|
||||
item.tokenExpire = now.Add(passwordResetTokenTTL)
|
||||
item.code = ""
|
||||
item.codeExpire = time.Time{}
|
||||
|
||||
passwordResetStore.tokenToPhone[resetToken] = phone
|
||||
|
||||
return &dto.PasswordResetVerifyResponse{ResetToken: resetToken}, nil
|
||||
}
|
||||
|
||||
// PasswordReset 找回密码:重置密码。
|
||||
//
|
||||
// @Summary 找回密码-重置密码
|
||||
// @Tags Web
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param form body dto.PasswordResetForm true "form"
|
||||
// @Success 200 {object} dto.PasswordResetResponse "成功"
|
||||
// @Router /v1/auth/password/reset [post]
|
||||
// @Bind form body
|
||||
func (ctl *auth) passwordReset(ctx fiber.Ctx, form *dto.PasswordResetForm) (*dto.PasswordResetResponse, error) {
|
||||
resetToken := strings.TrimSpace(form.ResetToken)
|
||||
if resetToken == "" {
|
||||
return nil, errorx.ErrMissingParameter.WithMsg("请先完成验证码校验")
|
||||
}
|
||||
if form.Password == "" || form.ConfirmPassword == "" {
|
||||
return nil, errorx.ErrMissingParameter.WithMsg("请输入新密码并确认")
|
||||
}
|
||||
if len(form.Password) < 8 {
|
||||
return nil, errorx.ErrParameterTooShort.WithMsg("密码至少 8 位,请设置更安全的密码")
|
||||
}
|
||||
if form.Password != form.ConfirmPassword {
|
||||
return nil, errorx.ErrInvalidParameter.WithMsg("两次输入的密码不一致,请重新确认")
|
||||
}
|
||||
|
||||
passwordResetStore.mu.Lock()
|
||||
phone, ok := passwordResetStore.tokenToPhone[resetToken]
|
||||
item := passwordResetStore.phoneToItem[phone]
|
||||
now := time.Now()
|
||||
if !ok || phone == "" || item == nil || item.resetToken != resetToken || item.tokenExpire.IsZero() || now.After(item.tokenExpire) {
|
||||
passwordResetStore.mu.Unlock()
|
||||
return nil, errorx.ErrTokenInvalid.WithMsg("重置会话已失效,请重新获取验证码")
|
||||
}
|
||||
// 令牌一次性使用
|
||||
delete(passwordResetStore.tokenToPhone, resetToken)
|
||||
item.resetToken = ""
|
||||
item.tokenExpire = time.Time{}
|
||||
passwordResetStore.mu.Unlock()
|
||||
|
||||
// 当前版本将手机号视为用户名。
|
||||
if _, err := services.User.FindByUsername(ctx, phone); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errorx.ErrRecordNotFound.WithMsg("该手机号尚未注册")
|
||||
}
|
||||
return nil, errorx.Wrap(err).WithMsg("用户信息校验失败,请稍后再试")
|
||||
}
|
||||
|
||||
if err := services.User.ResetPasswordByUsername(ctx, phone, form.Password); err != nil {
|
||||
return nil, errorx.Wrap(err).WithMsg("重置密码失败,请稍后再试")
|
||||
}
|
||||
|
||||
return &dto.PasswordResetResponse{Ok: true}, nil
|
||||
}
|
||||
|
||||
// Register 用户注册(平台侧,非超级管理员)。
|
||||
//
|
||||
// @Summary 用户注册
|
||||
// @Tags Web
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param form body dto.RegisterForm true "form"
|
||||
// @Success 200 {object} dto.LoginResponse "成功"
|
||||
// @Router /v1/auth/register [post]
|
||||
// @Bind form body
|
||||
func (ctl *auth) register(ctx fiber.Ctx, form *dto.RegisterForm) (*dto.LoginResponse, error) {
|
||||
username := strings.TrimSpace(form.Username)
|
||||
if username == "" {
|
||||
return nil, errorx.ErrMissingParameter.WithMsg("请输入用户名")
|
||||
}
|
||||
if !reUsername.MatchString(username) {
|
||||
return nil, errorx.ErrInvalidParameter.WithMsg("用户名需为 3-32 位字母/数字/下划线")
|
||||
}
|
||||
if form.Password == "" {
|
||||
return nil, errorx.ErrMissingParameter.WithMsg("请输入密码")
|
||||
}
|
||||
if len(form.Password) < 8 {
|
||||
return nil, errorx.ErrParameterTooShort.WithMsg("密码至少 8 位,请设置更安全的密码")
|
||||
}
|
||||
if form.Password != form.ConfirmPassword {
|
||||
return nil, errorx.ErrInvalidParameter.WithMsg("两次输入的密码不一致,请重新确认")
|
||||
}
|
||||
|
||||
// 先查询用户名是否已存在,避免直接插入导致不友好的数据库错误信息。
|
||||
_, err := services.User.FindByUsername(ctx, username)
|
||||
if err == nil {
|
||||
return nil, errorx.ErrRecordDuplicated.WithMsg("用户名已被占用,换一个试试")
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errorx.Wrap(err).WithMsg("用户信息校验失败,请稍后再试")
|
||||
}
|
||||
|
||||
m := &models.User{
|
||||
Username: username,
|
||||
Password: form.Password,
|
||||
Roles: []consts.Role{consts.RoleUser},
|
||||
Status: consts.UserStatusVerified,
|
||||
}
|
||||
if _, err := services.User.Create(ctx, m); err != nil {
|
||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
return nil, errorx.ErrRecordDuplicated.WithMsg("用户名已被占用,换一个试试")
|
||||
}
|
||||
return nil, errorx.Wrap(err).WithMsg("注册失败,请稍后再试")
|
||||
}
|
||||
|
||||
token, err := ctl.jwt.CreateToken(ctl.jwt.CreateClaims(jwt.BaseClaims{UserID: m.ID}))
|
||||
if err != nil {
|
||||
return nil, errorx.Wrap(err).WithMsg("登录凭证生成失败")
|
||||
}
|
||||
|
||||
return &dto.LoginResponse{Token: token}, nil
|
||||
}
|
||||
|
||||
// Token 刷新登录凭证。
|
||||
//
|
||||
// @Summary 刷新 Token
|
||||
// @Tags Web
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.LoginResponse "成功"
|
||||
// @Router /v1/auth/token [get]
|
||||
func (ctl *auth) token(ctx fiber.Ctx) (*dto.LoginResponse, error) {
|
||||
claims, ok := ctx.Locals(consts.CtxKeyClaims).(*jwt.Claims)
|
||||
if !ok || claims == nil || claims.UserID <= 0 {
|
||||
return nil, errorx.ErrTokenInvalid
|
||||
}
|
||||
|
||||
token, err := ctl.jwt.CreateToken(ctl.jwt.CreateClaims(jwt.BaseClaims{
|
||||
UserID: claims.UserID,
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, errorx.Wrap(err).WithMsg("登录凭证生成失败")
|
||||
}
|
||||
|
||||
return &dto.LoginResponse{Token: token}, nil
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"quyun/v2/app/errorx"
|
||||
"quyun/v2/app/http/web/dto"
|
||||
"quyun/v2/app/services"
|
||||
"quyun/v2/pkg/consts"
|
||||
"quyun/v2/providers/jwt"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// @provider
|
||||
type me struct{}
|
||||
|
||||
// Me 获取当前登录用户信息(脱敏)。
|
||||
//
|
||||
// @Summary 当前用户信息
|
||||
// @Tags Web
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.MeResponse "成功"
|
||||
// @Router /v1/me [get]
|
||||
func (ctl *me) me(ctx fiber.Ctx) (*dto.MeResponse, error) {
|
||||
claims, ok := ctx.Locals(consts.CtxKeyClaims).(*jwt.Claims)
|
||||
if !ok || claims == nil || claims.UserID <= 0 {
|
||||
return nil, errorx.ErrTokenInvalid
|
||||
}
|
||||
|
||||
m, err := services.User.FindByID(ctx, claims.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dto.MeResponse{
|
||||
ID: m.ID,
|
||||
Username: m.Username,
|
||||
Roles: m.Roles,
|
||||
Status: m.Status,
|
||||
StatusDescription: m.Status.Description(),
|
||||
CreatedAt: m.CreatedAt,
|
||||
UpdatedAt: m.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// MyTenants 获取当前用户可进入的租户列表。
|
||||
//
|
||||
// @Summary 我的租户列表
|
||||
// @Tags Web
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {array} dto.MyTenantItem "成功"
|
||||
// @Router /v1/me/tenants [get]
|
||||
func (ctl *me) myTenants(ctx fiber.Ctx) ([]*dto.MyTenantItem, error) {
|
||||
claims, ok := ctx.Locals(consts.CtxKeyClaims).(*jwt.Claims)
|
||||
if !ok || claims == nil || claims.UserID <= 0 {
|
||||
return nil, errorx.ErrTokenInvalid
|
||||
}
|
||||
|
||||
return services.Tenant.UserTenants(ctx, claims.UserID)
|
||||
}
|
||||
@@ -1,60 +1,9 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"quyun/v2/app/middlewares"
|
||||
"quyun/v2/providers/jwt"
|
||||
|
||||
"go.ipao.vip/atom"
|
||||
"go.ipao.vip/atom/container"
|
||||
"go.ipao.vip/atom/contracts"
|
||||
"go.ipao.vip/atom/opt"
|
||||
)
|
||||
|
||||
func Provide(opts ...opt.Option) error {
|
||||
if err := container.Container.Provide(func(
|
||||
jwt *jwt.JWT,
|
||||
) (*auth, error) {
|
||||
obj := &auth{
|
||||
jwt: jwt,
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.Container.Provide(func() (*me, error) {
|
||||
obj := &me{}
|
||||
|
||||
return obj, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.Container.Provide(func(
|
||||
auth *auth,
|
||||
me *me,
|
||||
middlewares *middlewares.Middlewares,
|
||||
tenantApply *tenantApply,
|
||||
) (contracts.HttpRoute, error) {
|
||||
obj := &Routes{
|
||||
auth: auth,
|
||||
me: me,
|
||||
middlewares: middlewares,
|
||||
tenantApply: tenantApply,
|
||||
}
|
||||
if err := obj.Prepare(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}, atom.GroupRoutes); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.Container.Provide(func() (*tenantApply, error) {
|
||||
obj := &tenantApply{}
|
||||
|
||||
return obj, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
// Code generated by atomctl. DO NOT EDIT.
|
||||
|
||||
// Package web provides HTTP route definitions and registration
|
||||
// for the quyun/v2 application.
|
||||
package web
|
||||
|
||||
import (
|
||||
"quyun/v2/app/http/web/dto"
|
||||
"quyun/v2/app/middlewares"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
_ "go.ipao.vip/atom"
|
||||
_ "go.ipao.vip/atom/contracts"
|
||||
. "go.ipao.vip/atom/fen"
|
||||
)
|
||||
|
||||
// Routes implements the HttpRoute contract and provides route registration
|
||||
// for all controllers in the web module.
|
||||
//
|
||||
// @provider contracts.HttpRoute atom.GroupRoutes
|
||||
type Routes struct {
|
||||
log *log.Entry `inject:"false"`
|
||||
middlewares *middlewares.Middlewares
|
||||
// Controller instances
|
||||
auth *auth
|
||||
me *me
|
||||
tenantApply *tenantApply
|
||||
}
|
||||
|
||||
// Prepare initializes the routes provider with logging configuration.
|
||||
func (r *Routes) Prepare() error {
|
||||
r.log = log.WithField("module", "routes.web")
|
||||
r.log.Info("Initializing routes module")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name returns the unique identifier for this routes provider.
|
||||
func (r *Routes) Name() string {
|
||||
return "web"
|
||||
}
|
||||
|
||||
// Register registers all HTTP routes with the provided fiber router.
|
||||
// Each route is registered with its corresponding controller action and parameter bindings.
|
||||
func (r *Routes) Register(router fiber.Router) {
|
||||
// Register routes for controller: auth
|
||||
r.log.Debugf("Registering route: Get /v1/auth/token -> auth.token")
|
||||
router.Get("/v1/auth/token"[len(r.Path()):], DataFunc0(
|
||||
r.auth.token,
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/auth/login -> auth.login")
|
||||
router.Post("/v1/auth/login"[len(r.Path()):], DataFunc1(
|
||||
r.auth.login,
|
||||
Body[dto.LoginForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/auth/password/reset -> auth.passwordReset")
|
||||
router.Post("/v1/auth/password/reset"[len(r.Path()):], DataFunc1(
|
||||
r.auth.passwordReset,
|
||||
Body[dto.PasswordResetForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/auth/password/reset/sms -> auth.passwordResetSendSMS")
|
||||
router.Post("/v1/auth/password/reset/sms"[len(r.Path()):], DataFunc1(
|
||||
r.auth.passwordResetSendSMS,
|
||||
Body[dto.PasswordResetSendSMSForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/auth/password/reset/verify -> auth.passwordResetVerify")
|
||||
router.Post("/v1/auth/password/reset/verify"[len(r.Path()):], DataFunc1(
|
||||
r.auth.passwordResetVerify,
|
||||
Body[dto.PasswordResetVerifyForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/auth/register -> auth.register")
|
||||
router.Post("/v1/auth/register"[len(r.Path()):], DataFunc1(
|
||||
r.auth.register,
|
||||
Body[dto.RegisterForm]("form"),
|
||||
))
|
||||
// Register routes for controller: me
|
||||
r.log.Debugf("Registering route: Get /v1/me -> me.me")
|
||||
router.Get("/v1/me"[len(r.Path()):], DataFunc0(
|
||||
r.me.me,
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/me/tenants -> me.myTenants")
|
||||
router.Get("/v1/me/tenants"[len(r.Path()):], DataFunc0(
|
||||
r.me.myTenants,
|
||||
))
|
||||
// Register routes for controller: tenantApply
|
||||
r.log.Debugf("Registering route: Get /v1/tenant/application -> tenantApply.application")
|
||||
router.Get("/v1/tenant/application"[len(r.Path()):], DataFunc0(
|
||||
r.tenantApply.application,
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/tenant/apply -> tenantApply.apply")
|
||||
router.Post("/v1/tenant/apply"[len(r.Path()):], DataFunc1(
|
||||
r.tenantApply.apply,
|
||||
Body[dto.TenantApplyForm]("form"),
|
||||
))
|
||||
|
||||
r.log.Info("Successfully registered all routes")
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package web
|
||||
|
||||
func (r *Routes) Path() string {
|
||||
return "/v1"
|
||||
}
|
||||
|
||||
func (r *Routes) Middlewares() []any {
|
||||
return []any{
|
||||
r.middlewares.UserAuth,
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"quyun/v2/app/errorx"
|
||||
"quyun/v2/app/http/web/dto"
|
||||
"quyun/v2/app/services"
|
||||
"quyun/v2/pkg/consts"
|
||||
"quyun/v2/providers/jwt"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// @provider
|
||||
type tenantApply struct{}
|
||||
|
||||
var reTenantCode = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]{2,63}$`)
|
||||
|
||||
// Application 获取当前用户的租户申请信息(申请创作者)。
|
||||
//
|
||||
// @Summary 获取租户申请信息(申请创作者)
|
||||
// @Tags Web
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.TenantApplicationResponse "成功"
|
||||
// @Router /v1/tenant/application [get]
|
||||
func (ctl *tenantApply) application(ctx fiber.Ctx) (*dto.TenantApplicationResponse, error) {
|
||||
claims, ok := ctx.Locals(consts.CtxKeyClaims).(*jwt.Claims)
|
||||
if !ok || claims == nil || claims.UserID <= 0 {
|
||||
return nil, errorx.ErrTokenInvalid
|
||||
}
|
||||
|
||||
m, err := services.Tenant.FindOwnedByUserID(ctx, claims.UserID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return &dto.TenantApplicationResponse{HasApplication: false}, nil
|
||||
}
|
||||
return nil, errorx.Wrap(err).WithMsg("查询申请信息失败,请稍后再试")
|
||||
}
|
||||
|
||||
return &dto.TenantApplicationResponse{
|
||||
HasApplication: true,
|
||||
TenantID: m.ID,
|
||||
TenantCode: m.Code,
|
||||
TenantName: m.Name,
|
||||
Status: m.Status,
|
||||
StatusDescription: m.Status.Description(),
|
||||
CreatedAt: m.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Apply 申请创作者(创建租户申请)。
|
||||
//
|
||||
// @Summary 提交租户申请(申请创作者)
|
||||
// @Tags Web
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param form body dto.TenantApplyForm true "form"
|
||||
// @Success 200 {object} dto.TenantApplicationResponse "成功"
|
||||
// @Router /v1/tenant/apply [post]
|
||||
// @Bind form body
|
||||
func (ctl *tenantApply) apply(ctx fiber.Ctx, form *dto.TenantApplyForm) (*dto.TenantApplicationResponse, error) {
|
||||
claims, ok := ctx.Locals(consts.CtxKeyClaims).(*jwt.Claims)
|
||||
if !ok || claims == nil || claims.UserID <= 0 {
|
||||
return nil, errorx.ErrTokenInvalid
|
||||
}
|
||||
|
||||
code := strings.ToLower(strings.TrimSpace(form.Code))
|
||||
if code == "" {
|
||||
return nil, errorx.ErrMissingParameter.WithMsg("请填写租户 ID")
|
||||
}
|
||||
if !reTenantCode.MatchString(code) {
|
||||
return nil, errorx.ErrInvalidParameter.WithMsg("租户 ID 需为 3-64 位小写字母/数字/下划线/短横线,且以字母或数字开头")
|
||||
}
|
||||
name := strings.TrimSpace(form.Name)
|
||||
if name == "" {
|
||||
return nil, errorx.ErrMissingParameter.WithMsg("请填写租户名称")
|
||||
}
|
||||
|
||||
// 一个用户仅可申请为一个租户创作者:若已存在 owned tenant,直接返回当前申请信息。
|
||||
existing, err := services.Tenant.FindOwnedByUserID(ctx, claims.UserID)
|
||||
if err == nil && existing != nil && existing.ID > 0 {
|
||||
return &dto.TenantApplicationResponse{
|
||||
HasApplication: true,
|
||||
TenantID: existing.ID,
|
||||
TenantCode: existing.Code,
|
||||
TenantName: existing.Name,
|
||||
Status: existing.Status,
|
||||
StatusDescription: existing.Status.Description(),
|
||||
CreatedAt: existing.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errorx.Wrap(err).WithMsg("申请校验失败,请稍后再试")
|
||||
}
|
||||
|
||||
tenant, err := services.Tenant.ApplyOwnedTenant(ctx, claims.UserID, code, name)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
return nil, errorx.ErrRecordDuplicated.WithMsg("租户 ID 已被占用,请换一个试试")
|
||||
}
|
||||
return nil, errorx.Wrap(err).WithMsg("提交申请失败,请稍后再试")
|
||||
}
|
||||
|
||||
return &dto.TenantApplicationResponse{
|
||||
HasApplication: true,
|
||||
TenantID: tenant.ID,
|
||||
TenantCode: tenant.Code,
|
||||
TenantName: tenant.Name,
|
||||
Status: tenant.Status,
|
||||
StatusDescription: tenant.Status.Description(),
|
||||
CreatedAt: tenant.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user