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,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
|
||||
Reference in New Issue
Block a user