8 Commits

16 changed files with 820 additions and 373 deletions

View File

@@ -2,7 +2,7 @@ name: Docker
on:
push:
branches: [ "main" ]
branches: ["v*"]
env:
REGISTRY: ghcr.io
@@ -10,14 +10,12 @@ env:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4

View File

@@ -40,6 +40,7 @@ http 调用 clients 的初始化函数
jsonb 类型转换问题,考虑一个高效的 any 到 struct 转换工具
慢速请求底层调用埋点监控
- redis
- gorm
- 三方接口
@@ -64,7 +65,8 @@ jsonb 类型转换问题,考虑一个高效的 any 到 struct 转换工具
### 节点分配与存储逻辑
提取:
提取
- 检查用户套餐与白名单
- 选中代理
- 找到当前可用端口最多的代理
@@ -76,6 +78,7 @@ jsonb 类型转换问题,考虑一个高效的 any 到 struct 转换工具
- 分别提交连接与配置请求
释放:
- 根据批次查出所有端口与相关节点
- 分别提交断开与关闭请求
- 释放端口

16
publish.ps1 Normal file
View File

@@ -0,0 +1,16 @@
if (-not $args) {
Write-Error "需要指定版本号"
exit 1
}
$confrim = Read-Host "构建版本为 [platform:$($args[0])],是否继续?(y/n)"
if ($confrim -ne "y") {
Write-Host "已取消构建"
exit 0
}
docker build -t 43.226.58.254:53000/lanhu/platform:latest .
docker build -t 43.226.58.254:53000/lanhu/platform:$($args[0]) .
docker push 43.226.58.254:53000/lanhu/platform:latest
docker push 43.226.58.254:53000/lanhu/platform:$($args[0])

156
web/auth/account.go Normal file
View File

@@ -0,0 +1,156 @@
package auth
import (
"context"
"errors"
"log/slog"
"platform/pkg/u"
"platform/web/core"
m "platform/web/models"
q "platform/web/queries"
s "platform/web/services"
"golang.org/x/crypto/bcrypt"
)
func authClient(clientId string, clientSecrets ...string) (*m.Client, error) {
// 获取客户端信息
client, err := q.Client.
Where(
q.Client.ClientID.Eq(clientId),
q.Client.Status.Eq(1)).
Take()
if err != nil {
return nil, err
}
// 检查客户端密钥
if client.Spec == m.ClientSpecWeb || client.Spec == m.ClientSpecAPI {
if len(clientSecrets) == 0 {
return nil, errors.New("客户端密钥错误")
}
clientSecret := clientSecrets[0]
if bcrypt.CompareHashAndPassword([]byte(client.ClientSecret), []byte(clientSecret)) != nil {
return nil, errors.New("客户端密钥错误")
}
}
// todo 查询客户端关联权限
// 组织授权信息(一次性请求)
return client, nil
}
func authUser(loginType PwdLoginType, username, password string) (user *m.User, err error) {
switch loginType {
case PwdLoginByPhone:
user, err = authUserBySms(q.Q, username, password)
if err != nil {
return nil, err
}
if user == nil {
user = &m.User{
Phone: username,
Username: u.P(username),
Status: m.UserStatusEnabled,
}
}
case PwdLoginByEmail:
user, err = authUserByEmail(q.Q, username, password)
if err != nil {
return nil, err
}
case PwdLoginByPassword:
user, err = authUserByPassword(q.Q, username, password)
if err != nil {
return nil, err
}
default:
return nil, ErrAuthorizeInvalidRequest
}
// 账户状态
if user.Status == m.UserStatusDisabled {
return nil, core.NewBizErr("账号已禁用")
}
return user, nil
}
func authUserBySms(tx *q.Query, username, code string) (*m.User, error) {
// 验证验证码
err := s.Verifier.VerifySms(context.Background(), username, code)
if err != nil {
return nil, core.NewBizErr("短信认证失败", err)
}
// 查找用户
return tx.User.Where(tx.User.Phone.Eq(username)).Take()
}
func authUserByEmail(tx *q.Query, username, code string) (*m.User, error) {
return nil, core.NewServErr("邮箱登录不可用")
}
func authUserByPassword(tx *q.Query, username, password string) (*m.User, error) {
user, err := tx.User.
Where(tx.User.Phone.Eq(username)).
Or(tx.User.Email.Eq(username)).
Or(tx.User.Username.Eq(username)).
Take()
if err != nil {
slog.Debug("查找用户失败", "error", err)
return nil, core.NewBizErr("用户不存在或密码错误")
}
// 验证密码
if user.Password == nil || *user.Password == "" {
slog.Debug("用户未设置密码", "username", username)
return nil, core.NewBizErr("用户不存在或密码错误")
}
if bcrypt.CompareHashAndPassword([]byte(*user.Password), []byte(password)) != nil {
slog.Debug("密码验证失败", "username", username)
return nil, core.NewBizErr("用户不存在或密码错误")
}
return user, nil
}
func authAdmin(loginType PwdLoginType, username, password string) (admin *m.Admin, err error) {
switch loginType {
case PwdLoginByPhone, PwdLoginByEmail:
return nil, core.NewServErr("不支持的登录方式:" + string(loginType))
case PwdLoginByPassword:
admin, err = authAdminByPassword(q.Q, username, password)
if err != nil {
return nil, err
}
default:
return nil, ErrAuthorizeInvalidRequest
}
// 账户状态
if admin.Status == m.AdminStatusDisabled {
return nil, core.NewBizErr("账号已禁用")
}
return admin, nil
}
func authAdminByPassword(tx *q.Query, username, password string) (*m.Admin, error) {
admin, err := tx.Admin.Where(tx.Admin.Username.Eq(username)).Take()
if err != nil {
return nil, core.NewBizErr("账号不存在或密码错误")
}
// 验证密码
if admin.Password == "" {
return nil, core.NewBizErr("账号不存在或密码错误")
}
if bcrypt.CompareHashAndPassword([]byte(admin.Password), []byte(password)) != nil {
return nil, core.NewBizErr("账号不存在或密码错误")
}
return admin, nil
}

View File

@@ -6,10 +6,8 @@ import (
"encoding/base64"
"encoding/json"
"errors"
"log/slog"
"platform/pkg/env"
"platform/pkg/u"
"platform/web/core"
g "platform/web/globals"
"platform/web/globals/orm"
m "platform/web/models"
@@ -22,67 +20,52 @@ import (
"gorm.io/gorm"
)
type GrantType string
// AuthorizeGet 授权端点
func AuthorizeGet(ctx *fiber.Ctx) error {
const (
GrantAuthorizationCode = GrantType("authorization_code") // 授权码模式
GrantClientCredentials = GrantType("client_credentials") // 客户端凭证模式
GrantRefreshToken = GrantType("refresh_token") // 刷新令牌模式
GrantPassword = GrantType("password") // 密码模式(私有扩展)
)
// 检查请求
req := new(AuthorizeGetReq)
if err := g.Validator.ParseQuery(ctx, req); err != nil {
return err
}
type PasswordGrantType string
// 检查客户端
client, err := authClient(req.ClientID)
if err != nil {
return err
}
const (
GrantPasswordSecret = PasswordGrantType("password") // 账号密码
GrantPasswordPhone = PasswordGrantType("phone_code") // 手机验证码
GrantPasswordEmail = PasswordGrantType("email_code") // 邮箱验证码
)
if client.RedirectURI == nil || *client.RedirectURI != req.RedirectURI {
return errors.New("客户端重定向URI错误")
}
type TokenReq struct {
GrantType GrantType `json:"grant_type" form:"grant_type"`
ClientID string `json:"client_id" form:"client_id"`
ClientSecret string `json:"client_secret" form:"client_secret"`
Scope string `json:"scope" form:"scope"`
GrantCodeData
GrantClientData
GrantRefreshData
GrantPasswordData
// todo 检查 scope
// 授权确认页面
return nil
}
type GrantCodeData struct {
Code string `json:"code" form:"code"`
RedirectURI string `json:"redirect_uri" form:"redirect_uri"`
CodeVerifier string `json:"code_verifier" form:"code_verifier"`
type AuthorizeGetReq struct {
ResponseType string `json:"response_type" validate:"eq=code"`
ClientID string `json:"client_id" validate:"required"`
RedirectURI string `json:"redirect_uri" validate:"required"`
Scope string `json:"scope"`
State string `json:"state"`
}
type GrantClientData struct {
func AuthorizePost(ctx *fiber.Ctx) error {
// todo 解析用户授权的范围
return nil
}
type GrantRefreshData struct {
RefreshToken string `json:"refresh_token" form:"refresh_token"`
}
type GrantPasswordData struct {
LoginType PasswordGrantType `json:"login_type" form:"login_type"`
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
Remember bool `json:"remember" form:"remember"`
}
type TokenResp struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
Scope string `json:"scope,omitempty"`
}
type TokenErrResp struct {
Error string `json:"error"`
Description string `json:"error_description,omitempty"`
type AuthorizePostReq struct {
Accept bool `json:"accept"`
Scope string `json:"scope"`
}
// Token 令牌端点
func Token(c *fiber.Ctx) error {
now := time.Now()
@@ -165,6 +148,75 @@ func Token(c *fiber.Ctx) error {
})
}
type TokenReq struct {
GrantType GrantType `json:"grant_type" form:"grant_type"`
ClientID string `json:"client_id" form:"client_id"`
ClientSecret string `json:"client_secret" form:"client_secret"`
Scope string `json:"scope" form:"scope"`
GrantCodeData
GrantClientData
GrantRefreshData
GrantPasswordData
}
type GrantCodeData struct {
Code string `json:"code" form:"code"`
RedirectURI string `json:"redirect_uri" form:"redirect_uri"`
CodeVerifier string `json:"code_verifier" form:"code_verifier"`
}
type GrantClientData struct {
}
type GrantRefreshData struct {
RefreshToken string `json:"refresh_token" form:"refresh_token"`
}
type GrantPasswordData struct {
LoginType PwdLoginType `json:"login_type" form:"login_type"`
LoginPool PwdLoginPool `json:"login_pool" form:"login_pool"`
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
Remember bool `json:"remember" form:"remember"`
}
type GrantType string
const (
GrantAuthorizationCode = GrantType("authorization_code") // 授权码模式
GrantClientCredentials = GrantType("client_credentials") // 客户端凭证模式
GrantRefreshToken = GrantType("refresh_token") // 刷新令牌模式
GrantPassword = GrantType("password") // 密码模式(私有扩展)
)
type PwdLoginType string
const (
PwdLoginByPassword = PwdLoginType("password") // 账号密码
PwdLoginByPhone = PwdLoginType("phone_code") // 手机验证码
PwdLoginByEmail = PwdLoginType("email_code") // 邮箱验证码
)
type PwdLoginPool string
const (
PwdLoginAsUser = PwdLoginPool("user") // 用户池
PwdLoginAsAdmin = PwdLoginPool("admin") // 管理员池
)
type TokenResp struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
Scope string `json:"scope,omitempty"`
}
type TokenErrResp struct {
Error string `json:"error"`
Description string `json:"error_description,omitempty"`
}
func authAuthorizationCode(c *fiber.Ctx, auth *AuthCtx, req *TokenReq, now time.Time) (*m.Session, error) {
// 检查 code 获取用户授权信息
@@ -226,7 +278,7 @@ func authAuthorizationCode(c *fiber.Ctx, auth *AuthCtx, req *TokenReq, now time.
session.RefreshTokenExpires = u.P(now.Add(time.Duration(env.SessionRefreshExpire) * time.Second))
}
err = SaveSession(session)
err = SaveSession(q.Q, session)
if err != nil {
return nil, err
}
@@ -249,7 +301,7 @@ func authClientCredential(c *fiber.Ctx, auth *AuthCtx, _ *TokenReq, now time.Tim
}
// 保存会话
err := SaveSession(session)
err := SaveSession(q.Q, session)
if err != nil {
return nil, err
}
@@ -261,71 +313,85 @@ func authPassword(c *fiber.Ctx, auth *AuthCtx, req *TokenReq, now time.Time) (*m
ip, _ := orm.ParseInet(c.IP()) // 可空字段,忽略异常
ua := u.X(c.Get(fiber.HeaderUserAgent))
// 分池认证
var err error
var user *m.User
err := q.Q.Transaction(func(tx *q.Query) (err error) {
switch req.LoginType {
case GrantPasswordPhone:
user, err = authUserBySms(tx, req.Username, req.Password)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
var admin *m.Admin
pool := req.LoginPool
if pool == "" {
pool = PwdLoginAsUser
}
if user == nil {
switch pool {
case PwdLoginAsUser:
user, err = authUser(req.LoginType, req.Username, req.Password)
if err != nil {
if req.LoginType != PwdLoginByPhone || !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
// 手机号首次登录的自动创建用户
user = &m.User{
Phone: req.Username,
Username: u.P(req.Username),
Status: m.UserStatusEnabled,
}
}
case GrantPasswordEmail:
user, err = authUserByEmail(tx, req.Username, req.Password)
if err != nil {
return err
}
case GrantPasswordSecret:
user, err = authUserByPassword(tx, req.Username, req.Password)
if err != nil {
return err
}
default:
return ErrAuthorizeInvalidRequest
}
// 账户状态
if user.Status == m.UserStatusDisabled {
slog.Debug("账户状态异常", "username", req.Username, "status", user.Status)
return core.NewBizErr("账号无法登录")
}
// 更新用户的登录时间
user.LastLogin = u.P(time.Now())
user.LastLoginIP = ip
user.LastLoginUA = ua
if err := tx.User.Save(user); err != nil {
return err
}
return nil
})
case PwdLoginAsAdmin:
admin, err = authAdmin(req.LoginType, req.Username, req.Password)
if err != nil {
return nil, err
}
// 更新管理员登录时间
admin.LastLogin = u.P(time.Now())
admin.LastLoginIP = ip
admin.LastLoginUA = ua
default:
return nil, ErrAuthorizeInvalidRequest
}
// 生成会话
session := &m.Session{
IP: ip,
UA: ua,
UserID: &user.ID,
ClientID: &auth.Client.ID,
Scopes: u.X(req.Scope),
AccessToken: uuid.NewString(),
AccessTokenExpires: now.Add(time.Duration(env.SessionAccessExpire) * time.Second),
}
if req.Remember {
session.RefreshToken = u.P(uuid.NewString())
session.RefreshTokenExpires = u.P(now.Add(time.Duration(env.SessionRefreshExpire) * time.Second))
}
err = SaveSession(session)
// 保存用户更新和会话
err = q.Q.Transaction(func(tx *q.Query) error {
if user != nil {
if err := tx.User.Save(user); err != nil {
return err
}
session.UserID = &user.ID
}
if admin != nil {
if err := tx.Admin.Save(admin); err != nil {
return err
}
session.AdminID = &admin.ID
}
if err := SaveSession(tx, session); err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
@@ -353,7 +419,7 @@ func authRefreshToken(_ *fiber.Ctx, _ *AuthCtx, req *TokenReq, now time.Time) (*
}
// 保存令牌
err = SaveSession(session)
err = SaveSession(q.Q, session)
if err != nil {
return nil, err
}
@@ -394,12 +460,117 @@ func sendError(c *fiber.Ctx, err error, description ...string) error {
return err
}
func Revoke() error {
// Revoke 令牌撤销端点
func Revoke(ctx *fiber.Ctx) error {
_, err := GetAuthCtx(ctx).PermitUser()
if err != nil {
// 用户未登录
return nil
}
// 解析请求参数
req := new(RevokeReq)
if err := ctx.BodyParser(req); err != nil {
return err
}
// 删除会话
err = RemoveSession(ctx.Context(), req.AccessToken, req.RefreshToken)
if err != nil {
return err
}
return nil
}
func Introspect() error {
return nil
type RevokeReq struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
// Introspect 令牌检查端点
func Introspect(ctx *fiber.Ctx) error {
authCtx := GetAuthCtx(ctx)
// 尝试验证用户权限
if _, err := authCtx.PermitUser(); err == nil {
return introspectUser(ctx, authCtx)
}
// 尝试验证管理员权限
if _, err := authCtx.PermitAdmin(); err == nil {
return introspectAdmin(ctx, authCtx)
}
return ErrAuthenticateForbidden
}
// introspectUser 获取并返回用户信息
func introspectUser(ctx *fiber.Ctx, authCtx *AuthCtx) error {
// 获取用户信息
profile, err := q.User.
Where(q.User.ID.Eq(authCtx.User.ID)).
Omit(q.User.DeletedAt).
Take()
if err != nil {
return err
}
// 检查用户是否设置了密码
hasPassword := false
if profile.Password != nil && *profile.Password != "" {
hasPassword = true
profile.Password = nil // 不返回密码
}
// 掩码敏感信息
if profile.Phone != "" {
profile.Phone = maskPhone(profile.Phone)
}
if profile.IDNo != nil && *profile.IDNo != "" {
profile.IDNo = u.P(maskIdNo(*profile.IDNo))
}
return ctx.JSON(struct {
m.User
HasPassword bool `json:"has_password"` // 是否设置了密码
}{*profile, hasPassword})
}
// introspectAdmin 获取并返回管理员信息
func introspectAdmin(ctx *fiber.Ctx, authCtx *AuthCtx) error {
// 获取管理员信息
profile, err := q.Admin.
Where(q.Admin.ID.Eq(authCtx.Admin.ID)).
Omit(q.Admin.DeletedAt).
Take()
if err != nil {
return err
}
// 不返回密码
profile.Password = ""
// 掩码敏感信息
if profile.Phone != nil && *profile.Phone != "" {
profile.Phone = u.P(maskPhone(*profile.Phone))
}
return ctx.JSON(profile)
}
func maskPhone(phone string) string {
if len(phone) < 11 {
return phone
}
return phone[:3] + "****" + phone[7:]
}
func maskIdNo(idNo string) string {
if len(idNo) < 18 {
return idNo
}
return idNo[:3] + "*********" + idNo[14:]
}
type CodeContext struct {

View File

@@ -6,15 +6,10 @@ import (
"errors"
"fmt"
"log/slog"
"platform/web/core"
m "platform/web/models"
q "platform/web/queries"
s "platform/web/services"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"golang.org/x/crypto/bcrypt"
)
func Authenticate() fiber.Handler {
@@ -123,67 +118,3 @@ func authBasic(_ context.Context, token string) (*AuthCtx, error) {
Scopes: []string{},
}, nil
}
func authClient(clientId, clientSecret string) (*m.Client, error) {
// 获取客户端信息
client, err := q.Client.
Where(
q.Client.ClientID.Eq(clientId),
q.Client.Status.Eq(1)).
Take()
if err != nil {
return nil, err
}
// 检查客户端密钥
if client.Spec == m.ClientSpecWeb || client.Spec == m.ClientSpecAPI {
if bcrypt.CompareHashAndPassword([]byte(client.ClientSecret), []byte(clientSecret)) != nil {
return nil, errors.New("客户端密钥错误")
}
}
// todo 查询客户端关联权限
// 组织授权信息(一次性请求)
return client, nil
}
func authUserBySms(tx *q.Query, username, code string) (*m.User, error) {
// 验证验证码
err := s.Verifier.VerifySms(context.Background(), username, code)
if err != nil {
return nil, core.NewBizErr("短信认证失败:%w", err)
}
// 查找用户
return tx.User.Where(tx.User.Phone.Eq(username)).Take()
}
func authUserByEmail(tx *q.Query, username, code string) (*m.User, error) {
return nil, core.NewServErr("邮箱登录不可用")
}
func authUserByPassword(tx *q.Query, username, password string) (*m.User, error) {
user, err := tx.User.
Where(tx.User.Phone.Eq(username)).
Or(tx.User.Email.Eq(username)).
Or(tx.User.Username.Eq(username)).
Take()
if err != nil {
slog.Debug("查找用户失败", "error", err)
return nil, core.NewBizErr("用户不存在或密码错误")
}
// 验证密码
if user.Password == nil || *user.Password == "" {
slog.Debug("用户未设置密码", "username", username)
return nil, core.NewBizErr("用户不存在或密码错误")
}
if bcrypt.CompareHashAndPassword([]byte(*user.Password), []byte(password)) != nil {
slog.Debug("密码验证失败", "username", username)
return nil, core.NewBizErr("用户不存在或密码错误")
}
return user, nil
}

View File

@@ -29,8 +29,8 @@ func FindSessionByRefresh(refreshToken string, now time.Time) (*m.Session, error
).First()
}
func SaveSession(session *m.Session) error {
return q.Session.Save(session)
func SaveSession(tx *q.Query, session *m.Session) error {
return tx.Session.Save(session)
}
func RemoveSession(ctx context.Context, accessToken string, refreshToken string) error {

View File

@@ -1,97 +0,0 @@
package handlers
import (
"platform/pkg/u"
auth2 "platform/web/auth"
m "platform/web/models"
q "platform/web/queries"
"github.com/gofiber/fiber/v2"
)
// region /revoke
type RevokeReq struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
func Revoke(c *fiber.Ctx) error {
_, err := auth2.GetAuthCtx(c).PermitUser()
if err != nil {
// 用户未登录
return nil
}
// 解析请求参数
req := new(RevokeReq)
if err := c.BodyParser(req); err != nil {
return err
}
// 删除会话
err = auth2.RemoveSession(c.Context(), req.AccessToken, req.RefreshToken)
if err != nil {
return err
}
return nil
}
// endregion
// region /profile
type IntrospectResp struct {
m.User
HasPassword bool `json:"has_password"` // 是否设置了密码
}
func Introspect(c *fiber.Ctx) error {
// 验证权限
authCtx, err := auth2.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
// 获取用户信息
profile, err := q.User.
Where(q.User.ID.Eq(authCtx.User.ID)).
Omit(q.User.DeletedAt).
Take()
if err != nil {
return err
}
// 检查用户是否设置了密码
hasPassword := false
if profile.Password != nil && *profile.Password != "" {
hasPassword = true
profile.Password = nil // 不返回密码
}
// 掩码敏感信息
if profile.Phone != "" {
profile.Phone = maskPhone(profile.Phone)
}
if profile.IDNo != nil && *profile.IDNo != "" {
profile.IDNo = u.P(maskIdNo(*profile.IDNo))
}
return c.JSON(IntrospectResp{*profile, hasPassword})
}
func maskPhone(phone string) string {
if len(phone) < 11 {
return phone
}
return phone[:3] + "****" + phone[7:]
}
func maskIdNo(idNo string) string {
if len(idNo) < 18 {
return idNo
}
return idNo[:3] + "*********" + idNo[14:]
}
// endregion

View File

@@ -55,3 +55,25 @@ type PageResourceBatchReq struct {
TimeStart *time.Time `json:"time_start"`
TimeEnd *time.Time `json:"time_end"`
}
// PageBatchByAdmin 分页查询所有提取记录
func PageBatchByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin()
if err != nil {
return err
}
req := new(struct{ core.PageReq })
if err = g.Validator.ParseBody(c, req); err != nil {
return err
}
list, total, err := q.LogsUserUsage.FindByPage(req.GetOffset(), req.GetLimit())
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
}

View File

@@ -3,20 +3,40 @@ package handlers
import (
"platform/web/auth"
"platform/web/core"
g "platform/web/globals"
q "platform/web/queries"
"time"
"github.com/gofiber/fiber/v2"
)
// region ListBill
// PageBillByAdmin 分页查询全部账单
func PageBillByAdmin(c *fiber.Ctx) error {
// 检查权限
_, err := auth.GetAuthCtx(c).PermitAdmin()
if err != nil {
return err
}
type ListBillReq struct {
core.PageReq
BillNo *string `json:"bill_no"`
Type *int `json:"type"`
CreateAfter *time.Time `json:"create_after"`
CreateBefore *time.Time `json:"create_before"`
// 解析请求参数
req := new(core.PageReq)
if err := g.Validator.ParseBody(c, req); err != nil {
return err
}
// 查询用户列表
list, total, err := q.Bill.FindByPage(req.GetOffset(), req.GetLimit())
if err != nil {
return err
}
// 返回结果
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
}
// ListBill 获取账单列表
@@ -79,4 +99,10 @@ func ListBill(c *fiber.Ctx) error {
})
}
// endregion
type ListBillReq struct {
core.PageReq
BillNo *string `json:"bill_no"`
Type *int `json:"type"`
CreateAfter *time.Time `json:"create_after"`
CreateBefore *time.Time `json:"create_before"`
}

View File

@@ -14,15 +14,36 @@ import (
"github.com/gofiber/fiber/v2"
)
// region ListChannels
// PageChannelsByAdmin 分页查询所有通道
func PageChannelsByAdmin(c *fiber.Ctx) error {
// 检查权限
_, err := auth.GetAuthCtx(c).PermitAdmin()
if err != nil {
return err
}
type ListChannelsReq struct {
core.PageReq
AuthType s.ChannelAuthType `json:"auth_type"`
ExpireAfter *time.Time `json:"expire_after"`
ExpireBefore *time.Time `json:"expire_before"`
// 解析请求参数
req := new(core.PageReq)
if err := g.Validator.ParseBody(c, req); err != nil {
return err
}
// 查询通道列表
list, total, err := q.Channel.FindByPage(req.GetOffset(), req.GetLimit())
if err != nil {
return err
}
// 返回结果
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
}
// 分页查询当前用户通道
func ListChannels(c *fiber.Ctx) error {
// 检查权限
authContext, err := auth.GetAuthCtx(c).PermitUser()
@@ -86,28 +107,14 @@ func ListChannels(c *fiber.Ctx) error {
})
}
// endregion
// region CreateChannel
type CreateChannelReq struct {
ResourceId int32 `json:"resource_id" validate:"required"`
AuthType s.ChannelAuthType `json:"auth_type" validate:"required"`
Protocol int `json:"protocol" validate:"required"`
Count int `json:"count" validate:"required"`
Prov *string `json:"prov"`
City *string `json:"city"`
Isp *int `json:"isp"`
}
type CreateChannelRespItem struct {
Proto int `json:"-"`
Host string `json:"host"`
Port uint16 `json:"port"`
Username *string `json:"username,omitempty"`
Password *string `json:"password,omitempty"`
type ListChannelsReq struct {
core.PageReq
AuthType s.ChannelAuthType `json:"auth_type"`
ExpireAfter *time.Time `json:"expire_after"`
ExpireBefore *time.Time `json:"expire_before"`
}
// 创建新通道
func CreateChannel(c *fiber.Ctx) error {
// 解析参数
@@ -154,16 +161,25 @@ func CreateChannel(c *fiber.Ctx) error {
return c.JSON(resp)
}
type CreateChannelResultType string
// endregion
// region RemoveChannels
type RemoveChannelsReq struct {
Batch string `json:"batch" validate:"required"`
type CreateChannelReq struct {
ResourceId int32 `json:"resource_id" validate:"required"`
AuthType s.ChannelAuthType `json:"auth_type" validate:"required"`
Protocol int `json:"protocol" validate:"required"`
Count int `json:"count" validate:"required"`
Prov *string `json:"prov"`
City *string `json:"city"`
Isp *int `json:"isp"`
}
type CreateChannelRespItem struct {
Proto int `json:"-"`
Host string `json:"host"`
Port uint16 `json:"port"`
Username *string `json:"username,omitempty"`
Password *string `json:"password,omitempty"`
}
// RemoveChannels 删除通道
func RemoveChannels(c *fiber.Ctx) error {
// 检查权限
_, err := auth.GetAuthCtx(c).PermitOfficialClient()
@@ -186,4 +202,6 @@ func RemoveChannels(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusOK)
}
// endregion
type RemoveChannelsReq struct {
Batch string `json:"batch" validate:"required"`
}

View File

@@ -15,8 +15,8 @@ import (
"github.com/gofiber/fiber/v2"
)
// ListResourceShort 分页短效套餐
func ListResourceShort(c *fiber.Ctx) error {
// PageResourceShort 分页查询当前用户短效套餐
func PageResourceShort(c *fiber.Ctx) error {
// 检查权限
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
@@ -24,7 +24,7 @@ func ListResourceShort(c *fiber.Ctx) error {
}
// 解析请求参数
req := new(ListResourceShortReq)
req := new(PageResourceShortReq)
if err := c.BodyParser(req); err != nil {
return err
}
@@ -86,7 +86,7 @@ func ListResourceShort(c *fiber.Ctx) error {
})
}
type ListResourceShortReq struct {
type PageResourceShortReq struct {
core.PageReq
ResourceNo *string `json:"resource_no"`
Active *bool `json:"active"`
@@ -97,8 +97,8 @@ type ListResourceShortReq struct {
ExpireBefore *time.Time `json:"expire_before"`
}
// ListResourceLong 分页长效套餐
func ListResourceLong(c *fiber.Ctx) error {
// PageResourceLong 分页查询当前用户长效套餐
func PageResourceLong(c *fiber.Ctx) error {
// 检查权限
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
@@ -106,7 +106,7 @@ func ListResourceLong(c *fiber.Ctx) error {
}
// 解析请求参数
req := new(ListResourceLongReq)
req := new(PageResourceLongReq)
if err := c.BodyParser(req); err != nil {
return err
}
@@ -168,7 +168,7 @@ func ListResourceLong(c *fiber.Ctx) error {
})
}
type ListResourceLongReq struct {
type PageResourceLongReq struct {
core.PageReq
ResourceNo *string `json:"resource_no"`
Active *bool `json:"active"`
@@ -179,6 +179,56 @@ type ListResourceLongReq struct {
ExpireBefore *time.Time `json:"expire_before"`
}
// PageResourceShortByAdmin 分页查询全部短效套餐
func PageResourceShortByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin()
if err != nil {
return err
}
req := new(struct{ core.PageReq })
if err = g.Validator.ParseBody(c, req); err != nil {
return err
}
list, total, err := q.Resource.
LeftJoin(q.ResourceShort, q.ResourceShort.ResourceID.EqCol(q.Resource.ID)).
Where(q.Resource.Type.Eq(int(m.ResourceTypeShort))).
FindByPage(req.GetOffset(), req.GetLimit())
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
}
// PageResourceLongByAdmin 分页查询全部短效套餐
func PageResourceLongByAdmin(c *fiber.Ctx) error {
_, err := auth.GetAuthCtx(c).PermitAdmin()
if err != nil {
return err
}
req := new(struct{ core.PageReq })
if err = g.Validator.ParseBody(c, req); err != nil {
return err
}
list, total, err := q.Resource.
LeftJoin(q.ResourceLong, q.ResourceLong.ResourceID.EqCol(q.Resource.ID)).
Where(q.Resource.Type.Eq(int(m.ResourceTypeLong))).
FindByPage(req.GetOffset(), req.GetLimit())
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
}
// AllActiveResource 所有可用套餐
func AllActiveResource(c *fiber.Ctx) error {
// 检查权限

View File

@@ -9,6 +9,7 @@ import (
"platform/web/core"
g "platform/web/globals"
m "platform/web/models"
q "platform/web/queries"
s "platform/web/services"
"time"
@@ -16,18 +17,36 @@ import (
"github.com/valyala/fasthttp"
)
type TradeCreateReq struct {
s.CreateTradeData
Type m.TradeType `json:"type" validate:"required"`
Resource *s.CreateResourceData `json:"resource,omitempty"`
Recharge *s.RechargeProductInfo `json:"recharge,omitempty"`
}
type TradeCreateResp struct {
PayUrl string `json:"pay_url"`
TradeNo string `json:"trade_no"`
// PageTradeByAdmin 分页查询所有订单
func PageTradeByAdmin(c *fiber.Ctx) error {
// 检查权限
_, err := auth.GetAuthCtx(c).PermitAdmin()
if err != nil {
return err
}
// 解析请求参数
req := new(core.PageReq)
if err := g.Validator.ParseBody(c, req); err != nil {
return err
}
// 查询用户列表
list, total, err := q.Trade.FindByPage(req.GetOffset(), req.GetLimit())
if err != nil {
return err
}
// 返回结果
return c.JSON(core.PageResp{
List: list,
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
})
}
// 创建订单
func TradeCreate(c *fiber.Ctx) error {
// 检查权限
authCtx, err := auth.GetAuthCtx(c).PermitUser()
@@ -67,10 +86,19 @@ func TradeCreate(c *fiber.Ctx) error {
})
}
type TradeCompleteReq struct {
s.ModifyTradeData
type TradeCreateReq struct {
s.CreateTradeData
Type m.TradeType `json:"type" validate:"required"`
Resource *s.CreateResourceData `json:"resource,omitempty"`
Recharge *s.RechargeProductInfo `json:"recharge,omitempty"`
}
type TradeCreateResp struct {
PayUrl string `json:"pay_url"`
TradeNo string `json:"trade_no"`
}
// 完成订单
func TradeComplete(c *fiber.Ctx) error {
// 检查权限
_, err := auth.GetAuthCtx(c).PermitUser()
@@ -93,10 +121,11 @@ func TradeComplete(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNoContent)
}
type TradeCancelReq struct {
type TradeCompleteReq struct {
s.ModifyTradeData
}
// 取消订单
func TradeCancel(c *fiber.Ctx) error {
// 检查权限
_, err := auth.GetAuthCtx(c).PermitUser()
@@ -120,10 +149,11 @@ func TradeCancel(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNoContent)
}
type TradeCheckReq struct {
type TradeCancelReq struct {
s.ModifyTradeData
}
// 检查订单
func TradeCheck(c *fiber.Ctx) error {
// 解析请求参数
req := new(TradeCheckReq)
@@ -170,3 +200,7 @@ func TradeCheck(c *fiber.Ctx) error {
return nil
}
type TradeCheckReq struct {
s.ModifyTradeData
}

View File

@@ -2,6 +2,8 @@ package handlers
import (
"platform/web/auth"
"platform/web/core"
g "platform/web/globals"
m "platform/web/models"
q "platform/web/queries"
s "platform/web/services"
@@ -10,15 +12,81 @@ import (
"golang.org/x/crypto/bcrypt"
)
// region /update
// 分页获取用户
func PageUserByAdmin(c *fiber.Ctx) error {
// 检查权限
_, err := auth.GetAuthCtx(c).PermitAdmin()
if err != nil {
return err
}
type UpdateUserReq struct {
Username string `json:"username" validate:"omitempty,min=3,max=20"`
Email string `json:"email" validate:"omitempty,email"`
ContactQQ string `json:"contact_qq" validate:"omitempty,qq"`
ContactWechat string `json:"contact_wechat" validate:"omitempty,wechat"`
// 解析请求参数
req := new(core.PageReq)
if err := g.Validator.ParseBody(c, req); err != nil {
return err
}
// 查询用户列表
users, total, err := q.User.
Preload(q.User.Admin).
Omit(q.User.Password).
FindByPage(req.GetOffset(), req.GetLimit())
if err != nil {
return err
}
for _, user := range users {
if user.Admin != nil {
user.Admin = &m.Admin{
Name: user.Admin.Name,
}
}
}
// 返回结果
return c.JSON(core.PageResp{
Total: int(total),
Page: req.GetPage(),
Size: req.GetSize(),
List: users,
})
}
// 绑定管理员
func BindAdmin(c *fiber.Ctx) error {
// 检查权限
authCtx, err := auth.GetAuthCtx(c).PermitAdmin()
if err != nil {
return err
}
// 解析请求参数
req := new(struct {
UserID int `json:"user_id" validate:"required"`
})
if err := g.Validator.ParseBody(c, req); err != nil {
return err
}
// 更新用户信息
result, err := q.User.Where(
q.User.ID.Eq(int32(req.UserID)),
q.User.AdminID.IsNull(),
).UpdateColumnSimple(
q.User.AdminID.Value(authCtx.Admin.ID),
)
if err != nil {
return err
}
if result.RowsAffected == 0 {
return core.NewBizErr("用户已绑定管理员")
}
// 返回结果
return c.SendStatus(fiber.StatusNoContent)
}
// 更新用户
func UpdateUser(c *fiber.Ctx) error {
// 检查权限
authCtx, err := auth.GetAuthCtx(c).PermitUser()
@@ -49,15 +117,14 @@ func UpdateUser(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNoContent)
}
// endregion
// region /update/account
type UpdateAccountReq struct {
type UpdateUserReq struct {
Username string `json:"username" validate:"omitempty,min=3,max=20"`
Password string `json:"password" validate:"omitempty,min=6,max=20"`
Email string `json:"email" validate:"omitempty,email"`
ContactQQ string `json:"contact_qq" validate:"omitempty,qq"`
ContactWechat string `json:"contact_wechat" validate:"omitempty,wechat"`
}
// 更新账号信息
func UpdateAccount(c *fiber.Ctx) error {
// 检查权限
authCtx, err := auth.GetAuthCtx(c).PermitUser()
@@ -86,16 +153,12 @@ func UpdateAccount(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNoContent)
}
// endregion
// region /update/password
type UpdatePasswordReq struct {
Phone string `json:"phone"`
Code string `json:"code"`
Password string `json:"password"`
type UpdateAccountReq struct {
Username string `json:"username" validate:"omitempty,min=3,max=20"`
Password string `json:"password" validate:"omitempty,min=6,max=20"`
}
// 更新账号密码
func UpdatePassword(c *fiber.Ctx) error {
// 检查权限
authCtx, err := auth.GetAuthCtx(c).PermitUser()
@@ -109,8 +172,13 @@ func UpdatePassword(c *fiber.Ctx) error {
return err
}
// 验证手机号
if req.Phone != authCtx.User.Phone {
return fiber.NewError(fiber.StatusBadRequest, "手机号码不正确")
}
// 验证手机令牌
if req.Phone == "" || req.Code == "" {
if req.Code == "" {
return fiber.NewError(fiber.StatusBadRequest, "手机号码和验证码不能为空")
}
err = s.Verifier.VerifySms(c.Context(), req.Phone, req.Code)
@@ -135,4 +203,8 @@ func UpdatePassword(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNoContent)
}
// endregion
type UpdatePasswordReq struct {
Phone string `json:"phone"`
Code string `json:"code"`
Password string `json:"password"`
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/gofiber/contrib/otelfiber/v2"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/fiber/v2/middleware/requestid"
@@ -19,6 +20,14 @@ func ApplyMiddlewares(app *fiber.App) {
EnableStackTrace: true,
}))
// cors
app.Use(cors.New(cors.Config{
AllowCredentials: true,
AllowOriginsFunc: func(origin string) bool {
return true
},
}))
// logger
app.Use(logger.New(logger.Config{
Next: func(c *fiber.Ctx) bool {

View File

@@ -10,12 +10,30 @@ import (
func ApplyRouters(app *fiber.App) {
api := app.Group("/api")
userRouter(api)
adminRouter(api)
// 回调
callbacks := app.Group("/callback")
callbacks.Get("/identify", handlers.IdentifyCallbackNew)
// 临时
if env.RunMode == env.RunModeDev {
debug := app.Group("/debug")
debug.Get("/sms/:phone", handlers.DebugGetSmsCode)
debug.Get("/proxy/register", handlers.DebugRegisterProxyBaiYin)
}
}
// 用户接口路由
func userRouter(api fiber.Router) {
// 认证
auth := api.Group("/auth")
auth.Get("/authorize", auth2.AuthorizeGet)
auth.Post("/authorize", auth2.AuthorizePost)
auth.Post("/token", auth2.Token)
auth.Post("/revoke", handlers.Revoke)
auth.Post("/introspect", handlers.Introspect)
auth.Post("/revoke", auth2.Revoke)
auth.Post("/introspect", auth2.Introspect)
auth.Post("/verify/sms", handlers.SmsCode)
// 用户
@@ -35,8 +53,8 @@ func ApplyRouters(app *fiber.App) {
// 套餐
resource := api.Group("/resource")
resource.Post("/all", handlers.AllActiveResource)
resource.Post("/list/short", handlers.ListResourceShort)
resource.Post("/list/long", handlers.ListResourceLong)
resource.Post("/list/short", handlers.PageResourceShort)
resource.Post("/list/long", handlers.PageResourceLong)
resource.Post("/create", handlers.CreateResource)
resource.Post("/price", handlers.ResourcePrice)
resource.Post("/statistics/free", handlers.StatisticResourceFree)
@@ -72,7 +90,7 @@ func ApplyRouters(app *fiber.App) {
proxy.Post("/online", handlers.ProxyReportOnline)
proxy.Post("/offline", handlers.ProxyReportOffline)
proxy.Post("/update", handlers.ProxyReportUpdate)
proxy.Post("/register", handlers.ProxyRegisterBaiYin)
proxy.Post("/register/baidyin", handlers.ProxyRegisterBaiYin)
// 节点
edge := api.Group("/edge")
@@ -82,15 +100,35 @@ func ApplyRouters(app *fiber.App) {
// 前台
inquiry := api.Group("/inquiry")
inquiry.Post("/create", handlers.CreateInquiry)
// 回调
callbacks := app.Group("/callback")
callbacks.Get("/identify", handlers.IdentifyCallbackNew)
// 临时
if env.RunMode == env.RunModeDev {
debug := app.Group("/debug")
debug.Get("/sms/:phone", handlers.DebugGetSmsCode)
debug.Get("/proxy/register", handlers.DebugRegisterProxyBaiYin)
}
}
// 管理员接口路由
func adminRouter(api fiber.Router) {
api = api.Group("/admin")
// user 用户
var user = api.Group("/user")
user.Post("/page", handlers.PageUserByAdmin)
user.Post("/bind", handlers.BindAdmin)
// resource 套餐
var resource = api.Group("/resource")
resource.Post("/short/page", handlers.PageResourceShortByAdmin)
resource.Post("/long/page", handlers.PageResourceLongByAdmin)
// batch 批次
var usage = api.Group("batch")
usage.Post("/page", handlers.PageBatchByAdmin)
// channel 通道
var channel = api.Group("/channel")
channel.Post("/page", handlers.PageChannelsByAdmin)
// trade 交易
var trade = api.Group("trade")
trade.Post("/page", handlers.PageTradeByAdmin)
// bill 账单
var bill = api.Group("/bill")
bill.Post("/page", handlers.PageBillByAdmin)
}