feat: 新增设备登录功能,包括API接口、RPC服务、逻辑处理和相关数据类型及错误码。
Build docker and publish / prepare (20.15.1) (push) Successful in 11s
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.admin image_name:ppanel-admin name:admin]) (push) Successful in 4m32s
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.api image_name:ppanel-api name:api]) (push) Successful in 8m6s
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.node image_name:ppanel-node name:node]) (push) Successful in 4m26s
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.rpc-core image_name:ppanel-rpc-core name:rpc-core]) (push) Successful in 8m23s
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.scheduler image_name:ppanel-scheduler name:scheduler]) (push) Successful in 4m1s
Build docker and publish / deploy (push) Successful in 45s
Build docker and publish / notify (push) Successful in 3s
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.queue image_name:ppanel-queue name:queue]) (push) Successful in 3m55s
Build docker and publish / prepare (20.15.1) (push) Successful in 11s
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.admin image_name:ppanel-admin name:admin]) (push) Successful in 4m32s
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.api image_name:ppanel-api name:api]) (push) Successful in 8m6s
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.node image_name:ppanel-node name:node]) (push) Successful in 4m26s
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.rpc-core image_name:ppanel-rpc-core name:rpc-core]) (push) Successful in 8m23s
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.scheduler image_name:ppanel-scheduler name:scheduler]) (push) Successful in 4m1s
Build docker and publish / deploy (push) Successful in 45s
Build docker and publish / notify (push) Successful in 3s
Build docker and publish / build (map[dockerfile:deploy/Dockerfile.queue image_name:ppanel-queue name:queue]) (push) Successful in 3m55s
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"github.com/zero-ppanel/zero-ppanel/apps/rpc/core/core"
|
||||
"github.com/zero-ppanel/zero-ppanel/apps/rpc/core/internal/repo"
|
||||
"github.com/zero-ppanel/zero-ppanel/apps/rpc/core/internal/svc"
|
||||
"github.com/zero-ppanel/zero-ppanel/pkg/xerr"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type DeviceLoginLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
logx.Logger
|
||||
}
|
||||
|
||||
func NewDeviceLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeviceLoginLogic {
|
||||
return &DeviceLoginLogic{
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
Logger: logx.WithContext(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
// DeviceLogin 设备登录:查设备 → 不存在则注册 → 返回用户信息
|
||||
func (l *DeviceLoginLogic) DeviceLogin(in *core.DeviceLoginReq) (*core.DeviceLoginResp, error) {
|
||||
// 1. 查询设备
|
||||
device, err := l.svcCtx.DeviceRepo.FindDeviceByIdentifier(l.ctx, in.Identifier)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
l.Errorf("FindDeviceByIdentifier error: %v", err)
|
||||
return nil, status.Error(codes.Code(xerr.DatabaseQueryError), "查询设备失败")
|
||||
}
|
||||
|
||||
var userID int64
|
||||
var isNewUser bool
|
||||
var isAdmin bool
|
||||
|
||||
if errors.Is(err, sql.ErrNoRows) || device == nil {
|
||||
// 2. 设备不存在,创建用户+设备
|
||||
userID, err = l.svcCtx.DeviceRepo.CreateUserWithDevice(l.ctx, repo.CreateUserWithDeviceParams{
|
||||
Identifier: in.Identifier,
|
||||
UserAgent: in.UserAgent,
|
||||
IP: in.Ip,
|
||||
ShortCode: in.ShortCode,
|
||||
})
|
||||
if err != nil {
|
||||
l.Errorf("CreateUserWithDevice error: %v", err)
|
||||
return nil, status.Error(codes.Code(xerr.DatabaseInsertError), "创建用户设备失败")
|
||||
}
|
||||
isNewUser = true
|
||||
|
||||
// 记录注册日志
|
||||
_ = l.svcCtx.DeviceRepo.InsertLoginLog(l.ctx, repo.LoginLogParams{
|
||||
UserID: userID,
|
||||
Method: "device",
|
||||
IP: in.Ip,
|
||||
UserAgent: in.UserAgent,
|
||||
Success: true,
|
||||
})
|
||||
} else {
|
||||
// 3. 设备存在,检查是否启用
|
||||
if !device.Enabled {
|
||||
return nil, status.Error(codes.Code(xerr.DeviceNotEnabled), "设备已禁用")
|
||||
}
|
||||
|
||||
userID = device.UserID
|
||||
|
||||
// 查用户信息
|
||||
user, err := l.svcCtx.DeviceRepo.FindUserByID(l.ctx, userID)
|
||||
if err != nil {
|
||||
l.Errorf("FindUserByID error: %v", err)
|
||||
return nil, status.Error(codes.Code(xerr.DatabaseQueryError), "查询用户失败")
|
||||
}
|
||||
|
||||
if !user.Enable {
|
||||
return nil, status.Error(codes.Code(xerr.UserDisabled), "用户已禁用")
|
||||
}
|
||||
|
||||
isAdmin = user.IsAdmin
|
||||
}
|
||||
|
||||
// 4. 记录登录日志
|
||||
_ = l.svcCtx.DeviceRepo.InsertLoginLog(l.ctx, repo.LoginLogParams{
|
||||
UserID: userID,
|
||||
Method: "device",
|
||||
IP: in.Ip,
|
||||
UserAgent: in.UserAgent,
|
||||
Success: true,
|
||||
})
|
||||
|
||||
return &core.DeviceLoginResp{
|
||||
UserId: userID,
|
||||
IsAdmin: isAdmin,
|
||||
IsDisabled: false,
|
||||
IsNewUser: isNewUser,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/zero-ppanel/zero-ppanel/pkg/cryptox"
|
||||
"github.com/zeromicro/go-zero/core/stores/sqlx"
|
||||
)
|
||||
|
||||
// DeviceInfo represents a row from user_device table.
|
||||
type DeviceInfo struct {
|
||||
ID int64 `db:"id"`
|
||||
UserID int64 `db:"user_id"`
|
||||
Identifier string `db:"Identifier"`
|
||||
Enabled bool `db:"enabled"`
|
||||
}
|
||||
|
||||
// UserInfo represents minimal user fields needed for login.
|
||||
type UserInfo struct {
|
||||
ID int64 `db:"id"`
|
||||
Enable bool `db:"enable"`
|
||||
IsAdmin bool `db:"is_admin"`
|
||||
IsDeleted bool `db:"is_del"`
|
||||
}
|
||||
|
||||
// CreateUserWithDeviceParams holds parameters for creating a user and device in one transaction.
|
||||
type CreateUserWithDeviceParams struct {
|
||||
Identifier string
|
||||
UserAgent string
|
||||
IP string
|
||||
ShortCode string
|
||||
}
|
||||
|
||||
// LoginLogParams holds parameters for inserting a login log.
|
||||
type LoginLogParams struct {
|
||||
UserID int64
|
||||
Method string
|
||||
IP string
|
||||
UserAgent string
|
||||
Success bool
|
||||
}
|
||||
|
||||
// DeviceRepo defines device-related data access methods.
|
||||
type DeviceRepo interface {
|
||||
FindDeviceByIdentifier(ctx context.Context, identifier string) (*DeviceInfo, error)
|
||||
FindUserByID(ctx context.Context, userID int64) (*UserInfo, error)
|
||||
CreateUserWithDevice(ctx context.Context, params CreateUserWithDeviceParams) (int64, error)
|
||||
InsertLoginLog(ctx context.Context, params LoginLogParams) error
|
||||
}
|
||||
|
||||
type deviceRepo struct {
|
||||
conn sqlx.SqlConn
|
||||
}
|
||||
|
||||
func NewDeviceRepo(conn sqlx.SqlConn) DeviceRepo {
|
||||
return &deviceRepo{conn: conn}
|
||||
}
|
||||
|
||||
func (r *deviceRepo) FindDeviceByIdentifier(ctx context.Context, identifier string) (*DeviceInfo, error) {
|
||||
var d DeviceInfo
|
||||
err := r.conn.QueryRowCtx(ctx, &d,
|
||||
"SELECT `id`, `user_id`, `Identifier`, `enabled` FROM `user_device` WHERE `Identifier` = ? LIMIT 1",
|
||||
identifier,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
func (r *deviceRepo) FindUserByID(ctx context.Context, userID int64) (*UserInfo, error) {
|
||||
var u UserInfo
|
||||
err := r.conn.QueryRowCtx(ctx, &u,
|
||||
"SELECT `id`, `enable`, `is_admin`, COALESCE(`is_del`, 0) AS `is_del` FROM `user` WHERE `id` = ? LIMIT 1",
|
||||
userID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
func (r *deviceRepo) CreateUserWithDevice(ctx context.Context, params CreateUserWithDeviceParams) (int64, error) {
|
||||
// Generate a random password hash for device-only users
|
||||
randomPwd, err := cryptox.GeneratePasswordHash("device-placeholder-" + params.Identifier)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("hash password: %w", err)
|
||||
}
|
||||
|
||||
var userID int64
|
||||
err = r.conn.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error {
|
||||
// 1. Create user
|
||||
result, err := session.ExecCtx(ctx,
|
||||
"INSERT INTO `user` (`password`, `algo`, `salt`, `enable`, `is_admin`, `created_at`, `updated_at`) VALUES (?, 'bcrypt', 'default', 1, 0, NOW(3), NOW(3))",
|
||||
randomPwd,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert user: %w", err)
|
||||
}
|
||||
userID, err = result.LastInsertId()
|
||||
if err != nil {
|
||||
return fmt.Errorf("last insert id: %w", err)
|
||||
}
|
||||
|
||||
// 2. Create auth method
|
||||
_, err = session.ExecCtx(ctx,
|
||||
"INSERT INTO `user_auth_methods` (`user_id`, `auth_type`, `auth_identifier`, `verified`, `created_at`, `updated_at`) VALUES (?, 'device', ?, 1, NOW(3), NOW(3))",
|
||||
userID, params.Identifier,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert auth method: %w", err)
|
||||
}
|
||||
|
||||
// 3. Create device
|
||||
_, err = session.ExecCtx(ctx,
|
||||
"INSERT INTO `user_device` (`user_id`, `ip`, `Identifier`, `short_code`, `user_agent`, `online`, `enabled`, `created_at`, `updated_at`) VALUES (?, ?, ?, ?, ?, 0, 1, NOW(3), NOW(3))",
|
||||
userID, params.IP, params.Identifier, params.ShortCode, params.UserAgent,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert device: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return userID, err
|
||||
}
|
||||
|
||||
func (r *deviceRepo) InsertLoginLog(ctx context.Context, params LoginLogParams) error {
|
||||
content, _ := json.Marshal(map[string]interface{}{
|
||||
"method": params.Method,
|
||||
"login_ip": params.IP,
|
||||
"user_agent": params.UserAgent,
|
||||
"success": params.Success,
|
||||
"timestamp": time.Now().UnixMilli(),
|
||||
})
|
||||
|
||||
_, err := r.conn.ExecCtx(ctx,
|
||||
"INSERT INTO `system_logs` (`type`, `date`, `object_id`, `content`, `created_at`) VALUES (?, ?, ?, ?, NOW(3))",
|
||||
6, // type=6 is Login log
|
||||
time.Now().Format("2006-01-02"),
|
||||
params.UserID,
|
||||
string(content),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert login log: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ErrNotFound is re-exported for convenience.
|
||||
var ErrNotFound = sql.ErrNoRows
|
||||
@@ -40,3 +40,9 @@ func (s *CoreServer) GetNodeInfo(ctx context.Context, in *core.GetNodeInfoReq) (
|
||||
l := logic.NewGetNodeInfoLogic(ctx, s.svcCtx)
|
||||
return l.GetNodeInfo(in)
|
||||
}
|
||||
|
||||
// 设备登录
|
||||
func (s *CoreServer) DeviceLogin(ctx context.Context, in *core.DeviceLoginReq) (*core.DeviceLoginResp, error) {
|
||||
l := logic.NewDeviceLoginLogic(ctx, s.svcCtx)
|
||||
return l.DeviceLogin(in)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
package svc
|
||||
|
||||
import "github.com/zero-ppanel/zero-ppanel/apps/rpc/core/internal/config"
|
||||
import (
|
||||
"github.com/zero-ppanel/zero-ppanel/apps/rpc/core/internal/config"
|
||||
"github.com/zero-ppanel/zero-ppanel/apps/rpc/core/internal/repo"
|
||||
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||
"github.com/zeromicro/go-zero/core/stores/sqlx"
|
||||
)
|
||||
|
||||
type ServiceContext struct {
|
||||
Config config.Config
|
||||
Config config.Config
|
||||
DB sqlx.SqlConn
|
||||
Redis *redis.Redis
|
||||
DeviceRepo repo.DeviceRepo
|
||||
}
|
||||
|
||||
func NewServiceContext(c config.Config) *ServiceContext {
|
||||
db := sqlx.NewMysql(c.MySQL.DataSource)
|
||||
rds := redis.MustNewRedis(c.CacheRedis)
|
||||
|
||||
return &ServiceContext{
|
||||
Config: c,
|
||||
Config: c,
|
||||
DB: db,
|
||||
Redis: rds,
|
||||
DeviceRepo: repo.NewDeviceRepo(db),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user