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

This commit is contained in:
2026-03-01 18:51:12 -08:00
parent be4cc669d2
commit 3b4429bdd9
20 changed files with 824 additions and 25 deletions
+155
View File
@@ -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