feat: 添加用户注册功能,包括表单验证和路由注册
This commit is contained in:
@@ -1,13 +1,19 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"quyun/v2/app/errorx"
|
"quyun/v2/app/errorx"
|
||||||
"quyun/v2/app/http/web/dto"
|
"quyun/v2/app/http/web/dto"
|
||||||
"quyun/v2/app/services"
|
"quyun/v2/app/services"
|
||||||
|
"quyun/v2/database/models"
|
||||||
"quyun/v2/pkg/consts"
|
"quyun/v2/pkg/consts"
|
||||||
"quyun/v2/providers/jwt"
|
"quyun/v2/providers/jwt"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @provider
|
// @provider
|
||||||
@@ -15,6 +21,8 @@ type auth struct {
|
|||||||
jwt *jwt.JWT
|
jwt *jwt.JWT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var reUsername = regexp.MustCompile(`^[a-zA-Z0-9_]{3,32}$`)
|
||||||
|
|
||||||
// Login 用户登录(平台侧,非超级管理员)。
|
// Login 用户登录(平台侧,非超级管理员)。
|
||||||
//
|
//
|
||||||
// @Summary 用户登录
|
// @Summary 用户登录
|
||||||
@@ -44,6 +52,64 @@ func (ctl *auth) login(ctx fiber.Ctx, form *dto.LoginForm) (*dto.LoginResponse,
|
|||||||
return &dto.LoginResponse{Token: token}, nil
|
return &dto.LoginResponse{Token: token}, 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 刷新登录凭证。
|
// Token 刷新登录凭证。
|
||||||
//
|
//
|
||||||
// @Summary 刷新 Token
|
// @Summary 刷新 Token
|
||||||
|
|||||||
@@ -9,6 +9,18 @@ type LoginForm struct {
|
|||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterForm 平台侧用户注册表单(用于创建用户并获取 JWT 访问凭证)。
|
||||||
|
type RegisterForm struct {
|
||||||
|
// Username 用户名;需全局唯一(users.username);建议仅允许字母/数字/下划线,且长度在合理范围内。
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
// Password 明文密码;后端会在创建用户时自动加密(bcrypt)。
|
||||||
|
Password string `json:"password,omitempty"`
|
||||||
|
// ConfirmPassword 确认密码;必须与 Password 一致,避免误输入导致无法登录。
|
||||||
|
ConfirmPassword string `json:"confirmPassword,omitempty"`
|
||||||
|
// VerifyCode 验证码(预留字段);当前版本仅透传/占位,不做后端校验。
|
||||||
|
VerifyCode string `json:"verifyCode,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// LoginResponse 登录响应。
|
// LoginResponse 登录响应。
|
||||||
type LoginResponse struct {
|
type LoginResponse struct {
|
||||||
// Token JWT 访问令牌;前端应以 `Authorization: Bearer <token>` 方式携带。
|
// Token JWT 访问令牌;前端应以 `Authorization: Bearer <token>` 方式携带。
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ func (r *Routes) Register(router fiber.Router) {
|
|||||||
r.auth.login,
|
r.auth.login,
|
||||||
Body[dto.LoginForm]("form"),
|
Body[dto.LoginForm]("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
|
// Register routes for controller: me
|
||||||
r.log.Debugf("Registering route: Get /v1/me -> me.me")
|
r.log.Debugf("Registering route: Get /v1/me -> me.me")
|
||||||
router.Get("/v1/me"[len(r.Path()):], DataFunc0(
|
router.Get("/v1/me"[len(r.Path()):], DataFunc0(
|
||||||
|
|||||||
@@ -10,17 +10,24 @@ import (
|
|||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func shouldSkipUserJWTAuth(path string) bool {
|
func shouldSkipUserJWTAuth(path string, method string) bool {
|
||||||
// 登录接口无需鉴权。
|
// 仅对明确的公开接口放行,避免误伤其它路径。
|
||||||
if strings.Contains(path, "/v1/auth/login") {
|
if method != fiber.MethodPost {
|
||||||
return true
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
p := strings.TrimSuffix(path, "/")
|
||||||
|
switch p {
|
||||||
|
case "/v1/auth/login", "/v1/auth/register":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserAuth 为平台通用(非租户域)接口提供 JWT 校验,并写入 claims 到 ctx locals。
|
// UserAuth 为平台通用(非租户域)接口提供 JWT 校验,并写入 claims 到 ctx locals。
|
||||||
func (f *Middlewares) UserAuth(c fiber.Ctx) error {
|
func (f *Middlewares) UserAuth(c fiber.Ctx) error {
|
||||||
if shouldSkipUserJWTAuth(c.Path()) {
|
if shouldSkipUserJWTAuth(c.Path(), c.Method()) {
|
||||||
f.log.Debug("middlewares.user.auth.skipped")
|
f.log.Debug("middlewares.user.auth.skipped")
|
||||||
return c.Next()
|
return c.Next()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3885,6 +3885,39 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v1/auth/register": {
|
||||||
|
"post": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Web"
|
||||||
|
],
|
||||||
|
"summary": "用户注册",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "form",
|
||||||
|
"name": "form",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.RegisterForm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "成功",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/quyun_v2_app_http_web_dto.LoginResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v1/auth/token": {
|
"/v1/auth/token": {
|
||||||
"get": {
|
"get": {
|
||||||
"consumes": [
|
"consumes": [
|
||||||
@@ -4906,6 +4939,27 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dto.RegisterForm": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"confirmPassword": {
|
||||||
|
"description": "ConfirmPassword 确认密码;必须与 Password 一致,避免误输入导致无法登录。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"description": "Password 明文密码;后端会在创建用户时自动加密(bcrypt)。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"description": "Username 用户名;需全局唯一(users.username);建议仅允许字母/数字/下划线,且长度在合理范围内。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"verifyCode": {
|
||||||
|
"description": "VerifyCode 验证码(预留字段);当前版本仅透传/占位,不做后端校验。",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.SuperContentItem": {
|
"dto.SuperContentItem": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -3879,6 +3879,39 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v1/auth/register": {
|
||||||
|
"post": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Web"
|
||||||
|
],
|
||||||
|
"summary": "用户注册",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "form",
|
||||||
|
"name": "form",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.RegisterForm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "成功",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/quyun_v2_app_http_web_dto.LoginResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v1/auth/token": {
|
"/v1/auth/token": {
|
||||||
"get": {
|
"get": {
|
||||||
"consumes": [
|
"consumes": [
|
||||||
@@ -4900,6 +4933,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dto.RegisterForm": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"confirmPassword": {
|
||||||
|
"description": "ConfirmPassword 确认密码;必须与 Password 一致,避免误输入导致无法登录。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"description": "Password 明文密码;后端会在创建用户时自动加密(bcrypt)。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"description": "Username 用户名;需全局唯一(users.username);建议仅允许字母/数字/下划线,且长度在合理范围内。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"verifyCode": {
|
||||||
|
"description": "VerifyCode 验证码(预留字段);当前版本仅透传/占位,不做后端校验。",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.SuperContentItem": {
|
"dto.SuperContentItem": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -685,6 +685,21 @@ definitions:
|
|||||||
description: Order is the created or existing order record (may be nil for
|
description: Order is the created or existing order record (may be nil for
|
||||||
owner/free-path without order).
|
owner/free-path without order).
|
||||||
type: object
|
type: object
|
||||||
|
dto.RegisterForm:
|
||||||
|
properties:
|
||||||
|
confirmPassword:
|
||||||
|
description: ConfirmPassword 确认密码;必须与 Password 一致,避免误输入导致无法登录。
|
||||||
|
type: string
|
||||||
|
password:
|
||||||
|
description: Password 明文密码;后端会在创建用户时自动加密(bcrypt)。
|
||||||
|
type: string
|
||||||
|
username:
|
||||||
|
description: Username 用户名;需全局唯一(users.username);建议仅允许字母/数字/下划线,且长度在合理范围内。
|
||||||
|
type: string
|
||||||
|
verifyCode:
|
||||||
|
description: VerifyCode 验证码(预留字段);当前版本仅透传/占位,不做后端校验。
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
dto.SuperContentItem:
|
dto.SuperContentItem:
|
||||||
properties:
|
properties:
|
||||||
content:
|
content:
|
||||||
@@ -4208,6 +4223,27 @@ paths:
|
|||||||
summary: 用户登录
|
summary: 用户登录
|
||||||
tags:
|
tags:
|
||||||
- Web
|
- Web
|
||||||
|
/v1/auth/register:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
parameters:
|
||||||
|
- description: form
|
||||||
|
in: body
|
||||||
|
name: form
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.RegisterForm'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: 成功
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/quyun_v2_app_http_web_dto.LoginResponse'
|
||||||
|
summary: 用户注册
|
||||||
|
tags:
|
||||||
|
- Web
|
||||||
/v1/auth/token:
|
/v1/auth/token:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
|||||||
Reference in New Issue
Block a user