feat: 新增设备登录功能,包括API接口、RPC服务、逻辑处理和相关数据类型及错误码。
All checks were successful
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.queue image_name:ppanel-queue name:queue]) (push) Successful in 3m55s
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
All checks were successful
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.queue image_name:ppanel-queue name:queue]) (push) Successful in 3m55s
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
This commit is contained in:
parent
be4cc669d2
commit
3b4429bdd9
1
.serena/.gitignore
vendored
Normal file
1
.serena/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/cache
|
||||
126
.serena/project.yml
Normal file
126
.serena/project.yml
Normal file
@ -0,0 +1,126 @@
|
||||
# the name by which the project can be referenced within Serena
|
||||
project_name: "zero-ppanel"
|
||||
|
||||
|
||||
# list of languages for which language servers are started; choose from:
|
||||
# al bash clojure cpp csharp
|
||||
# csharp_omnisharp dart elixir elm erlang
|
||||
# fortran fsharp go groovy haskell
|
||||
# java julia kotlin lua markdown
|
||||
# matlab nix pascal perl php
|
||||
# php_phpactor powershell python python_jedi r
|
||||
# rego ruby ruby_solargraph rust scala
|
||||
# swift terraform toml typescript typescript_vts
|
||||
# vue yaml zig
|
||||
# (This list may be outdated. For the current list, see values of Language enum here:
|
||||
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
|
||||
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
|
||||
# Note:
|
||||
# - For C, use cpp
|
||||
# - For JavaScript, use typescript
|
||||
# - For Free Pascal/Lazarus, use pascal
|
||||
# Special requirements:
|
||||
# Some languages require additional setup/installations.
|
||||
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
|
||||
# When using multiple languages, the first language server that supports a given file will be used for that file.
|
||||
# The first language is the default language and the respective language server will be used as a fallback.
|
||||
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||
languages:
|
||||
- go
|
||||
|
||||
# the encoding used by text files in the project
|
||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||
encoding: "utf-8"
|
||||
|
||||
# The language backend to use for this project.
|
||||
# If not set, the global setting from serena_config.yml is used.
|
||||
# Valid values: LSP, JetBrains
|
||||
# Note: the backend is fixed at startup. If a project with a different backend
|
||||
# is activated post-init, an error will be returned.
|
||||
language_backend:
|
||||
|
||||
# whether to use project's .gitignore files to ignore files
|
||||
ignore_all_files_in_gitignore: true
|
||||
|
||||
# list of additional paths to ignore in this project.
|
||||
# Same syntax as gitignore, so you can use * and **.
|
||||
# Note: global ignored_paths from serena_config.yml are also applied additively.
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||
# Below is the complete list of tools for convenience.
|
||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||
# execute `uv run scripts/print_tool_overview.py`.
|
||||
#
|
||||
# * `activate_project`: Activates a project by name.
|
||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
# * `delete_lines`: Deletes a range of lines within a file.
|
||||
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||
# * `execute_shell_command`: Executes a shell command.
|
||||
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||
# Should only be used in settings where the system prompt cannot be set,
|
||||
# e.g. in clients you have no control over, like Claude Desktop.
|
||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||
# * `read_file`: Reads a file within the project directory.
|
||||
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||
# * `remove_project`: Removes a project from the Serena configuration.
|
||||
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||
# * `switch_modes`: Activates modes by providing a list of their names
|
||||
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
excluded_tools: []
|
||||
|
||||
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default)
|
||||
included_optional_tools: []
|
||||
|
||||
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
|
||||
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
|
||||
fixed_tools: []
|
||||
|
||||
# list of mode names to that are always to be included in the set of active modes
|
||||
# The full set of modes to be activated is base_modes + default_modes.
|
||||
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this setting overrides the global configuration.
|
||||
# Set this to [] to disable base modes for this project.
|
||||
# Set this to a list of mode names to always include the respective modes for this project.
|
||||
base_modes:
|
||||
|
||||
# list of mode names that are to be activated by default.
|
||||
# The full set of modes to be activated is base_modes + default_modes.
|
||||
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
|
||||
# This setting can, in turn, be overridden by CLI parameters (--mode).
|
||||
default_modes:
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
|
||||
# time budget (seconds) per tool call for the retrieval of additional symbol information
|
||||
# such as docstrings or parameter information.
|
||||
# This overrides the corresponding setting in the global configuration; see the documentation there.
|
||||
# If null or missing, use the setting from the global configuration.
|
||||
symbol_info_budget:
|
||||
@ -19,6 +19,14 @@ type (
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
DeviceLoginReq {
|
||||
Identifier string `json:"identifier" validate:"required"`
|
||||
UserAgent string `json:"user_agent" validate:"required"`
|
||||
ShortCode string `json:"short_code,optional"`
|
||||
CfToken string `json:"cf_token,optional"`
|
||||
IP string `header:"X-Original-Forwarded-For,optional"`
|
||||
}
|
||||
|
||||
UserRegisterReq {
|
||||
Identifier string `json:"identifier"`
|
||||
Email string `json:"email" validate:"required"`
|
||||
@ -58,3 +66,14 @@ service ppanel-api {
|
||||
post /reset (ResetPasswordReq) returns (LoginResp)
|
||||
|
||||
}
|
||||
|
||||
@server (
|
||||
prefix: /api/v1/auth
|
||||
group: auth
|
||||
middleware: DecryptMiddleware
|
||||
)
|
||||
service ppanel-api {
|
||||
@doc "设备登录"
|
||||
@handler DeviceLoginHandler
|
||||
post /login/device (DeviceLoginReq) returns (LoginResp)
|
||||
}
|
||||
|
||||
28
apps/api/internal/handler/auth/deviceLoginHandler.go
Normal file
28
apps/api/internal/handler/auth/deviceLoginHandler.go
Normal file
@ -0,0 +1,28 @@
|
||||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.9.2
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/zero-ppanel/zero-ppanel/apps/api/internal/logic/auth"
|
||||
"github.com/zero-ppanel/zero-ppanel/apps/api/internal/svc"
|
||||
"github.com/zero-ppanel/zero-ppanel/apps/api/internal/types"
|
||||
"github.com/zero-ppanel/zero-ppanel/pkg/result"
|
||||
)
|
||||
|
||||
// 设备登录
|
||||
func DeviceLoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.DeviceLoginReq
|
||||
if err := result.Parse(r, &req); err != nil {
|
||||
result.HttpResult(r, w, nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
l := auth.NewDeviceLoginLogic(r.Context(), svcCtx)
|
||||
resp, err := l.DeviceLogin(&req)
|
||||
result.HttpResult(r, w, resp, err)
|
||||
}
|
||||
}
|
||||
@ -38,6 +38,21 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
||||
rest.WithPrefix("/api/v1/auth"),
|
||||
)
|
||||
|
||||
server.AddRoutes(
|
||||
rest.WithMiddlewares(
|
||||
[]rest.Middleware{serverCtx.DecryptMiddleware},
|
||||
[]rest.Route{
|
||||
{
|
||||
// 设备登录
|
||||
Method: http.MethodPost,
|
||||
Path: "/login/device",
|
||||
Handler: auth.DeviceLoginHandler(serverCtx),
|
||||
},
|
||||
}...,
|
||||
),
|
||||
rest.WithPrefix("/api/v1/auth"),
|
||||
)
|
||||
|
||||
server.AddRoutes(
|
||||
[]rest.Route{
|
||||
{
|
||||
|
||||
87
apps/api/internal/logic/auth/deviceLoginLogic.go
Normal file
87
apps/api/internal/logic/auth/deviceLoginLogic.go
Normal file
@ -0,0 +1,87 @@
|
||||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.9.2
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/zero-ppanel/zero-ppanel/apps/api/internal/svc"
|
||||
"github.com/zero-ppanel/zero-ppanel/apps/api/internal/types"
|
||||
"github.com/zero-ppanel/zero-ppanel/apps/rpc/core/core"
|
||||
"github.com/zero-ppanel/zero-ppanel/pkg/jwtx"
|
||||
"github.com/zero-ppanel/zero-ppanel/pkg/xerr"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type DeviceLoginLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// 设备登录
|
||||
func NewDeviceLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeviceLoginLogic {
|
||||
return &DeviceLoginLogic{
|
||||
Logger: logx.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginReq) (resp *types.LoginResp, err error) {
|
||||
// 1. 调用 Core RPC 设备登录
|
||||
rpcResp, err := l.svcCtx.CoreRpc.DeviceLogin(l.ctx, &core.DeviceLoginReq{
|
||||
Identifier: req.Identifier,
|
||||
UserAgent: req.UserAgent,
|
||||
Ip: req.IP,
|
||||
ShortCode: req.ShortCode,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 检查用户是否禁用
|
||||
if rpcResp.IsDisabled {
|
||||
return nil, xerr.NewErrCode(xerr.UserDisabled)
|
||||
}
|
||||
|
||||
// 3. 生成 session ID
|
||||
sessionId := uuid.New().String()
|
||||
|
||||
// 4. 生成 JWT Token
|
||||
token, err := jwtx.GenerateToken(
|
||||
l.svcCtx.Config.JwtAuth.AccessSecret,
|
||||
rpcResp.UserId,
|
||||
"device",
|
||||
rpcResp.IsAdmin,
|
||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||
)
|
||||
if err != nil {
|
||||
l.Errorf("GenerateToken error: %v", err)
|
||||
return nil, xerr.NewErrCode(xerr.ServerError)
|
||||
}
|
||||
|
||||
// 5. 写入 Redis session
|
||||
expireDuration := time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire) * time.Second
|
||||
|
||||
// session:{sessionId} → userId
|
||||
sessionKey := fmt.Sprintf("session:%s", sessionId)
|
||||
if err := l.svcCtx.Redis.SetexCtx(l.ctx, sessionKey, fmt.Sprintf("%d", rpcResp.UserId), int(expireDuration.Seconds())); err != nil {
|
||||
l.Errorf("Redis set session error: %v", err)
|
||||
}
|
||||
|
||||
// device:{identifier} → sessionId
|
||||
deviceKey := fmt.Sprintf("device:%s", req.Identifier)
|
||||
if err := l.svcCtx.Redis.SetexCtx(l.ctx, deviceKey, sessionId, int(expireDuration.Seconds())); err != nil {
|
||||
l.Errorf("Redis set device session error: %v", err)
|
||||
}
|
||||
|
||||
return &types.LoginResp{
|
||||
Token: token,
|
||||
}, nil
|
||||
}
|
||||
@ -9,14 +9,16 @@ import (
|
||||
coreClient "github.com/zero-ppanel/zero-ppanel/apps/rpc/core/coreClient"
|
||||
"github.com/zero-ppanel/zero-ppanel/pkg/signature"
|
||||
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||
"github.com/zeromicro/go-zero/rest"
|
||||
"github.com/zeromicro/go-zero/zrpc"
|
||||
)
|
||||
|
||||
type ServiceContext struct {
|
||||
Config config.Config
|
||||
CoreRpc coreClient.Core
|
||||
Redis *redis.Redis
|
||||
SignatureMiddleware *middleware.SignatureMiddleware
|
||||
DecryptMiddleware *middleware.DecryptMiddleware
|
||||
DecryptMiddleware rest.Middleware
|
||||
}
|
||||
|
||||
func NewServiceContext(c config.Config) *ServiceContext {
|
||||
@ -26,7 +28,8 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
||||
return &ServiceContext{
|
||||
Config: c,
|
||||
CoreRpc: coreClient.NewCore(zrpc.MustNewClient(c.CoreRpc)),
|
||||
Redis: rds,
|
||||
SignatureMiddleware: middleware.NewSignatureMiddleware(c, nonceStore),
|
||||
DecryptMiddleware: middleware.NewDecryptMiddleware(c),
|
||||
DecryptMiddleware: middleware.NewDecryptMiddleware(c).Handle,
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,6 +31,14 @@ type CreateTicketReq struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type DeviceLoginReq struct {
|
||||
Identifier string `json:"identifier" validate:"required"`
|
||||
UserAgent string `json:"user_agent" validate:"required"`
|
||||
ShortCode string `json:"short_code,optional"`
|
||||
CfToken string `json:"cf_token,optional"`
|
||||
IP string `header:"X-Original-Forwarded-For,optional"`
|
||||
}
|
||||
|
||||
type DocumentDetailReq struct {
|
||||
Id int64 `path:"id"`
|
||||
}
|
||||
|
||||
@ -30,7 +30,6 @@ func main() {
|
||||
|
||||
ctx := svc.NewServiceContext(c)
|
||||
server.Use(ctx.SignatureMiddleware.Handle)
|
||||
server.Use(ctx.DecryptMiddleware.Handle)
|
||||
handler.RegisterHandlers(server, ctx)
|
||||
|
||||
// Registe global http error handler
|
||||
|
||||
@ -45,6 +45,23 @@ message GetNodeInfoResp {
|
||||
string status = 4;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Device 服务定义
|
||||
// ----------------------------------------------------------------------------
|
||||
message DeviceLoginReq {
|
||||
string identifier = 1;
|
||||
string user_agent = 2;
|
||||
string ip = 3;
|
||||
string short_code = 4;
|
||||
}
|
||||
|
||||
message DeviceLoginResp {
|
||||
int64 user_id = 1;
|
||||
bool is_admin = 2;
|
||||
bool is_disabled = 3;
|
||||
bool is_new_user = 4;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// 核心 RPC 服务接口
|
||||
// ----------------------------------------------------------------------------
|
||||
@ -57,4 +74,7 @@ service Core {
|
||||
|
||||
// 节点相关
|
||||
rpc GetNodeInfo(GetNodeInfoReq) returns(GetNodeInfoResp);
|
||||
|
||||
// 设备登录
|
||||
rpc DeviceLogin(DeviceLoginReq) returns(DeviceLoginResp);
|
||||
}
|
||||
|
||||
@ -374,6 +374,145 @@ func (x *GetNodeInfoResp) GetStatus() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Device 服务定义
|
||||
// ----------------------------------------------------------------------------
|
||||
type DeviceLoginReq struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Identifier string `protobuf:"bytes,1,opt,name=identifier,proto3" json:"identifier,omitempty"`
|
||||
UserAgent string `protobuf:"bytes,2,opt,name=user_agent,json=userAgent,proto3" json:"user_agent,omitempty"`
|
||||
Ip string `protobuf:"bytes,3,opt,name=ip,proto3" json:"ip,omitempty"`
|
||||
ShortCode string `protobuf:"bytes,4,opt,name=short_code,json=shortCode,proto3" json:"short_code,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeviceLoginReq) Reset() {
|
||||
*x = DeviceLoginReq{}
|
||||
mi := &file_core_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DeviceLoginReq) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DeviceLoginReq) ProtoMessage() {}
|
||||
|
||||
func (x *DeviceLoginReq) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_core_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DeviceLoginReq.ProtoReflect.Descriptor instead.
|
||||
func (*DeviceLoginReq) Descriptor() ([]byte, []int) {
|
||||
return file_core_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *DeviceLoginReq) GetIdentifier() string {
|
||||
if x != nil {
|
||||
return x.Identifier
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *DeviceLoginReq) GetUserAgent() string {
|
||||
if x != nil {
|
||||
return x.UserAgent
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *DeviceLoginReq) GetIp() string {
|
||||
if x != nil {
|
||||
return x.Ip
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *DeviceLoginReq) GetShortCode() string {
|
||||
if x != nil {
|
||||
return x.ShortCode
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type DeviceLoginResp struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||
IsAdmin bool `protobuf:"varint,2,opt,name=is_admin,json=isAdmin,proto3" json:"is_admin,omitempty"`
|
||||
IsDisabled bool `protobuf:"varint,3,opt,name=is_disabled,json=isDisabled,proto3" json:"is_disabled,omitempty"`
|
||||
IsNewUser bool `protobuf:"varint,4,opt,name=is_new_user,json=isNewUser,proto3" json:"is_new_user,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *DeviceLoginResp) Reset() {
|
||||
*x = DeviceLoginResp{}
|
||||
mi := &file_core_proto_msgTypes[7]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *DeviceLoginResp) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*DeviceLoginResp) ProtoMessage() {}
|
||||
|
||||
func (x *DeviceLoginResp) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_core_proto_msgTypes[7]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use DeviceLoginResp.ProtoReflect.Descriptor instead.
|
||||
func (*DeviceLoginResp) Descriptor() ([]byte, []int) {
|
||||
return file_core_proto_rawDescGZIP(), []int{7}
|
||||
}
|
||||
|
||||
func (x *DeviceLoginResp) GetUserId() int64 {
|
||||
if x != nil {
|
||||
return x.UserId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *DeviceLoginResp) GetIsAdmin() bool {
|
||||
if x != nil {
|
||||
return x.IsAdmin
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *DeviceLoginResp) GetIsDisabled() bool {
|
||||
if x != nil {
|
||||
return x.IsDisabled
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *DeviceLoginResp) GetIsNewUser() bool {
|
||||
if x != nil {
|
||||
return x.IsNewUser
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var File_core_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_core_proto_rawDesc = "" +
|
||||
@ -403,11 +542,27 @@ const file_core_proto_rawDesc = "" +
|
||||
"\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" +
|
||||
"\x04name\x18\x02 \x01(\tR\x04name\x12\x16\n" +
|
||||
"\x06server\x18\x03 \x01(\tR\x06server\x12\x16\n" +
|
||||
"\x06status\x18\x04 \x01(\tR\x06status2\xa8\x01\n" +
|
||||
"\x06status\x18\x04 \x01(\tR\x06status\"~\n" +
|
||||
"\x0eDeviceLoginReq\x12\x1e\n" +
|
||||
"\n" +
|
||||
"identifier\x18\x01 \x01(\tR\n" +
|
||||
"identifier\x12\x1d\n" +
|
||||
"\n" +
|
||||
"user_agent\x18\x02 \x01(\tR\tuserAgent\x12\x0e\n" +
|
||||
"\x02ip\x18\x03 \x01(\tR\x02ip\x12\x1d\n" +
|
||||
"\n" +
|
||||
"short_code\x18\x04 \x01(\tR\tshortCode\"\x86\x01\n" +
|
||||
"\x0fDeviceLoginResp\x12\x17\n" +
|
||||
"\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x19\n" +
|
||||
"\bis_admin\x18\x02 \x01(\bR\aisAdmin\x12\x1f\n" +
|
||||
"\vis_disabled\x18\x03 \x01(\bR\n" +
|
||||
"isDisabled\x12\x1e\n" +
|
||||
"\vis_new_user\x18\x04 \x01(\bR\tisNewUser2\xe4\x01\n" +
|
||||
"\x04Core\x12(\n" +
|
||||
"\x04Ping\x12\v.core.Empty\x1a\x13.core.BasicResponse\x12:\n" +
|
||||
"\vGetUserInfo\x12\x14.core.GetUserInfoReq\x1a\x15.core.GetUserInfoResp\x12:\n" +
|
||||
"\vGetNodeInfo\x12\x14.core.GetNodeInfoReq\x1a\x15.core.GetNodeInfoRespB\bZ\x06./coreb\x06proto3"
|
||||
"\vGetNodeInfo\x12\x14.core.GetNodeInfoReq\x1a\x15.core.GetNodeInfoResp\x12:\n" +
|
||||
"\vDeviceLogin\x12\x14.core.DeviceLoginReq\x1a\x15.core.DeviceLoginRespB\bZ\x06./coreb\x06proto3"
|
||||
|
||||
var (
|
||||
file_core_proto_rawDescOnce sync.Once
|
||||
@ -421,7 +576,7 @@ func file_core_proto_rawDescGZIP() []byte {
|
||||
return file_core_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_core_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
|
||||
var file_core_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
|
||||
var file_core_proto_goTypes = []any{
|
||||
(*Empty)(nil), // 0: core.Empty
|
||||
(*BasicResponse)(nil), // 1: core.BasicResponse
|
||||
@ -429,16 +584,20 @@ var file_core_proto_goTypes = []any{
|
||||
(*GetUserInfoResp)(nil), // 3: core.GetUserInfoResp
|
||||
(*GetNodeInfoReq)(nil), // 4: core.GetNodeInfoReq
|
||||
(*GetNodeInfoResp)(nil), // 5: core.GetNodeInfoResp
|
||||
(*DeviceLoginReq)(nil), // 6: core.DeviceLoginReq
|
||||
(*DeviceLoginResp)(nil), // 7: core.DeviceLoginResp
|
||||
}
|
||||
var file_core_proto_depIdxs = []int32{
|
||||
0, // 0: core.Core.Ping:input_type -> core.Empty
|
||||
2, // 1: core.Core.GetUserInfo:input_type -> core.GetUserInfoReq
|
||||
4, // 2: core.Core.GetNodeInfo:input_type -> core.GetNodeInfoReq
|
||||
1, // 3: core.Core.Ping:output_type -> core.BasicResponse
|
||||
3, // 4: core.Core.GetUserInfo:output_type -> core.GetUserInfoResp
|
||||
5, // 5: core.Core.GetNodeInfo:output_type -> core.GetNodeInfoResp
|
||||
3, // [3:6] is the sub-list for method output_type
|
||||
0, // [0:3] is the sub-list for method input_type
|
||||
6, // 3: core.Core.DeviceLogin:input_type -> core.DeviceLoginReq
|
||||
1, // 4: core.Core.Ping:output_type -> core.BasicResponse
|
||||
3, // 5: core.Core.GetUserInfo:output_type -> core.GetUserInfoResp
|
||||
5, // 6: core.Core.GetNodeInfo:output_type -> core.GetNodeInfoResp
|
||||
7, // 7: core.Core.DeviceLogin:output_type -> core.DeviceLoginResp
|
||||
4, // [4:8] is the sub-list for method output_type
|
||||
0, // [0:4] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
@ -455,7 +614,7 @@ func file_core_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_core_proto_rawDesc), len(file_core_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 6,
|
||||
NumMessages: 8,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
||||
@ -22,6 +22,7 @@ const (
|
||||
Core_Ping_FullMethodName = "/core.Core/Ping"
|
||||
Core_GetUserInfo_FullMethodName = "/core.Core/GetUserInfo"
|
||||
Core_GetNodeInfo_FullMethodName = "/core.Core/GetNodeInfo"
|
||||
Core_DeviceLogin_FullMethodName = "/core.Core/DeviceLogin"
|
||||
)
|
||||
|
||||
// CoreClient is the client API for Core service.
|
||||
@ -38,6 +39,8 @@ type CoreClient interface {
|
||||
GetUserInfo(ctx context.Context, in *GetUserInfoReq, opts ...grpc.CallOption) (*GetUserInfoResp, error)
|
||||
// 节点相关
|
||||
GetNodeInfo(ctx context.Context, in *GetNodeInfoReq, opts ...grpc.CallOption) (*GetNodeInfoResp, error)
|
||||
// 设备登录
|
||||
DeviceLogin(ctx context.Context, in *DeviceLoginReq, opts ...grpc.CallOption) (*DeviceLoginResp, error)
|
||||
}
|
||||
|
||||
type coreClient struct {
|
||||
@ -78,6 +81,16 @@ func (c *coreClient) GetNodeInfo(ctx context.Context, in *GetNodeInfoReq, opts .
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *coreClient) DeviceLogin(ctx context.Context, in *DeviceLoginReq, opts ...grpc.CallOption) (*DeviceLoginResp, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(DeviceLoginResp)
|
||||
err := c.cc.Invoke(ctx, Core_DeviceLogin_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// CoreServer is the server API for Core service.
|
||||
// All implementations must embed UnimplementedCoreServer
|
||||
// for forward compatibility.
|
||||
@ -92,6 +105,8 @@ type CoreServer interface {
|
||||
GetUserInfo(context.Context, *GetUserInfoReq) (*GetUserInfoResp, error)
|
||||
// 节点相关
|
||||
GetNodeInfo(context.Context, *GetNodeInfoReq) (*GetNodeInfoResp, error)
|
||||
// 设备登录
|
||||
DeviceLogin(context.Context, *DeviceLoginReq) (*DeviceLoginResp, error)
|
||||
mustEmbedUnimplementedCoreServer()
|
||||
}
|
||||
|
||||
@ -111,6 +126,9 @@ func (UnimplementedCoreServer) GetUserInfo(context.Context, *GetUserInfoReq) (*G
|
||||
func (UnimplementedCoreServer) GetNodeInfo(context.Context, *GetNodeInfoReq) (*GetNodeInfoResp, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetNodeInfo not implemented")
|
||||
}
|
||||
func (UnimplementedCoreServer) DeviceLogin(context.Context, *DeviceLoginReq) (*DeviceLoginResp, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeviceLogin not implemented")
|
||||
}
|
||||
func (UnimplementedCoreServer) mustEmbedUnimplementedCoreServer() {}
|
||||
func (UnimplementedCoreServer) testEmbeddedByValue() {}
|
||||
|
||||
@ -186,6 +204,24 @@ func _Core_GetNodeInfo_Handler(srv interface{}, ctx context.Context, dec func(in
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Core_DeviceLogin_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(DeviceLoginReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(CoreServer).DeviceLogin(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Core_DeviceLogin_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(CoreServer).DeviceLogin(ctx, req.(*DeviceLoginReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// Core_ServiceDesc is the grpc.ServiceDesc for Core service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@ -205,6 +241,10 @@ var Core_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "GetNodeInfo",
|
||||
Handler: _Core_GetNodeInfo_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "DeviceLogin",
|
||||
Handler: _Core_DeviceLogin_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "core.proto",
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
// goctl 1.9.2
|
||||
// Source: core.proto
|
||||
|
||||
package coreclient
|
||||
package coreClient
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -15,6 +15,8 @@ import (
|
||||
|
||||
type (
|
||||
BasicResponse = core.BasicResponse
|
||||
DeviceLoginReq = core.DeviceLoginReq
|
||||
DeviceLoginResp = core.DeviceLoginResp
|
||||
Empty = core.Empty
|
||||
GetNodeInfoReq = core.GetNodeInfoReq
|
||||
GetNodeInfoResp = core.GetNodeInfoResp
|
||||
@ -28,6 +30,8 @@ type (
|
||||
GetUserInfo(ctx context.Context, in *GetUserInfoReq, opts ...grpc.CallOption) (*GetUserInfoResp, error)
|
||||
// 节点相关
|
||||
GetNodeInfo(ctx context.Context, in *GetNodeInfoReq, opts ...grpc.CallOption) (*GetNodeInfoResp, error)
|
||||
// 设备登录
|
||||
DeviceLogin(ctx context.Context, in *DeviceLoginReq, opts ...grpc.CallOption) (*DeviceLoginResp, error)
|
||||
}
|
||||
|
||||
defaultCore struct {
|
||||
@ -58,3 +62,9 @@ func (m *defaultCore) GetNodeInfo(ctx context.Context, in *GetNodeInfoReq, opts
|
||||
client := core.NewCoreClient(m.cli.Conn())
|
||||
return client.GetNodeInfo(ctx, in, opts...)
|
||||
}
|
||||
|
||||
// 设备登录
|
||||
func (m *defaultCore) DeviceLogin(ctx context.Context, in *DeviceLoginReq, opts ...grpc.CallOption) (*DeviceLoginResp, error) {
|
||||
client := core.NewCoreClient(m.cli.Conn())
|
||||
return client.DeviceLogin(ctx, in, opts...)
|
||||
}
|
||||
|
||||
104
apps/rpc/core/internal/logic/deviceLoginLogic.go
Normal file
104
apps/rpc/core/internal/logic/deviceLoginLogic.go
Normal file
@ -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
|
||||
}
|
||||
155
apps/rpc/core/internal/repo/device_repo.go
Normal file
155
apps/rpc/core/internal/repo/device_repo.go
Normal 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
|
||||
@ -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
|
||||
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,
|
||||
DB: db,
|
||||
Redis: rds,
|
||||
DeviceRepo: repo.NewDeviceRepo(db),
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,9 +13,10 @@ type Claims struct {
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func GenerateToken(secret string, userId int64, isAdmin bool, expire int64) (string, error) {
|
||||
func GenerateToken(secret string, userId int64, loginType string, isAdmin bool, expire int64) (string, error) {
|
||||
claims := Claims{
|
||||
UserId: userId,
|
||||
LoginType: loginType,
|
||||
IsAdmin: isAdmin,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expire) * time.Second)),
|
||||
|
||||
@ -24,4 +24,6 @@ const (
|
||||
SignatureInvalid = 7003
|
||||
SignatureReplay = 7004
|
||||
DecryptFailed = 7005
|
||||
DeviceLoginDisabled = 8001
|
||||
DeviceNotEnabled = 8002
|
||||
)
|
||||
|
||||
@ -24,6 +24,8 @@ var codeText = map[int]string{
|
||||
SignatureInvalid: "签名错误",
|
||||
SignatureReplay: "重放攻击",
|
||||
DecryptFailed: "解密失败",
|
||||
DeviceLoginDisabled: "设备登录未启用",
|
||||
DeviceNotEnabled: "设备已禁用",
|
||||
}
|
||||
|
||||
func IsCodeErr(errcode uint32) bool {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user