fix(mpc-system): 修复通过邀请码加入会话时 invalid token 错误
问题: 通过邀请码查询会话后加入时报 "13 INTERNAL: invalid token" 原因: GetSessionByInviteCode API 没有返回 join_token 修复: - account-service: GetSessionByInviteCode 在查询时生成新的 wildcard join token - account-service: CoManagedHTTPHandler 添加 jwtService 依赖注入 - service-party-app: validateInviteCode 返回 join_token - service-party-app: Join.tsx 保存并使用 joinToken 和 partyId - service-party-app: preload.ts joinSession 使用正确的参数格式 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
21985abde5
commit
af08f0f9c6
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
grpcclient "github.com/rwadurian/mpc-system/services/account/adapters/output/grpc"
|
grpcclient "github.com/rwadurian/mpc-system/services/account/adapters/output/grpc"
|
||||||
|
"github.com/rwadurian/mpc-system/pkg/jwt"
|
||||||
"github.com/rwadurian/mpc-system/pkg/logger"
|
"github.com/rwadurian/mpc-system/pkg/logger"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
@ -20,7 +21,8 @@ import (
|
||||||
// This is a completely independent handler that does not affect existing functionality
|
// This is a completely independent handler that does not affect existing functionality
|
||||||
type CoManagedHTTPHandler struct {
|
type CoManagedHTTPHandler struct {
|
||||||
sessionCoordinatorClient *grpcclient.SessionCoordinatorClient
|
sessionCoordinatorClient *grpcclient.SessionCoordinatorClient
|
||||||
db *sql.DB // Database connection for invite_code lookups
|
db *sql.DB // Database connection for invite_code lookups
|
||||||
|
jwtService *jwt.JWTService // JWT service for generating join tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCoManagedHTTPHandler creates a new CoManagedHTTPHandler
|
// NewCoManagedHTTPHandler creates a new CoManagedHTTPHandler
|
||||||
|
|
@ -37,10 +39,12 @@ func NewCoManagedHTTPHandler(
|
||||||
func NewCoManagedHTTPHandlerWithDB(
|
func NewCoManagedHTTPHandlerWithDB(
|
||||||
sessionCoordinatorClient *grpcclient.SessionCoordinatorClient,
|
sessionCoordinatorClient *grpcclient.SessionCoordinatorClient,
|
||||||
db *sql.DB,
|
db *sql.DB,
|
||||||
|
jwtService *jwt.JWTService,
|
||||||
) *CoManagedHTTPHandler {
|
) *CoManagedHTTPHandler {
|
||||||
return &CoManagedHTTPHandler{
|
return &CoManagedHTTPHandler{
|
||||||
sessionCoordinatorClient: sessionCoordinatorClient,
|
sessionCoordinatorClient: sessionCoordinatorClient,
|
||||||
db: db,
|
db: db,
|
||||||
|
jwtService: jwtService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -380,7 +384,7 @@ func (h *CoManagedHTTPHandler) GetSessionByInviteCode(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get wildcard join token from session coordinator
|
// Get session status from session coordinator
|
||||||
statusResp, err := h.sessionCoordinatorClient.GetSessionStatus(ctx, sessionID)
|
statusResp, err := h.sessionCoordinatorClient.GetSessionStatus(ctx, sessionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to get session status from coordinator",
|
logger.Error("Failed to get session status from coordinator",
|
||||||
|
|
@ -398,10 +402,30 @@ func (h *CoManagedHTTPHandler) GetSessionByInviteCode(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate a wildcard join token for this session
|
||||||
|
// This allows any participant to join using this token
|
||||||
|
var joinToken string
|
||||||
|
if h.jwtService != nil {
|
||||||
|
sessionUUID, err := uuid.Parse(sessionID)
|
||||||
|
if err == nil {
|
||||||
|
// Token valid until session expires
|
||||||
|
tokenExpiry := time.Until(expiresAt)
|
||||||
|
if tokenExpiry > 0 {
|
||||||
|
joinToken, err = h.jwtService.GenerateJoinToken(sessionUUID, "*", tokenExpiry)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("Failed to generate join token",
|
||||||
|
zap.String("session_id", sessionID),
|
||||||
|
zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.Info("Found session for invite_code",
|
logger.Info("Found session for invite_code",
|
||||||
zap.String("invite_code", inviteCode),
|
zap.String("invite_code", inviteCode),
|
||||||
zap.String("session_id", sessionID),
|
zap.String("session_id", sessionID),
|
||||||
zap.String("wallet_name", walletName))
|
zap.String("wallet_name", walletName),
|
||||||
|
zap.Bool("has_join_token", joinToken != ""))
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"session_id": sessionID,
|
"session_id": sessionID,
|
||||||
|
|
@ -412,6 +436,7 @@ func (h *CoManagedHTTPHandler) GetSessionByInviteCode(c *gin.Context) {
|
||||||
"completed_parties": statusResp.CompletedParties,
|
"completed_parties": statusResp.CompletedParties,
|
||||||
"total_parties": statusResp.TotalParties,
|
"total_parties": statusResp.TotalParties,
|
||||||
"expires_at": expiresAt.UnixMilli(),
|
"expires_at": expiresAt.UnixMilli(),
|
||||||
|
"join_token": joinToken,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -303,8 +303,8 @@ func startHTTPServer(
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create co-managed wallet handler (independent from existing functionality)
|
// Create co-managed wallet handler (independent from existing functionality)
|
||||||
// Uses database connection for invite_code lookups
|
// Uses database connection for invite_code lookups and JWT service for generating join tokens
|
||||||
coManagedHandler := httphandler.NewCoManagedHTTPHandlerWithDB(sessionCoordinatorClient, db)
|
coManagedHandler := httphandler.NewCoManagedHTTPHandlerWithDB(sessionCoordinatorClient, db, jwtService)
|
||||||
|
|
||||||
// Configure authentication middleware
|
// Configure authentication middleware
|
||||||
// Skip paths that don't require authentication
|
// Skip paths that don't require authentication
|
||||||
|
|
|
||||||
|
|
@ -235,8 +235,10 @@ function setupIpcHandlers() {
|
||||||
n: result?.threshold_n,
|
n: result?.threshold_n,
|
||||||
},
|
},
|
||||||
status: result?.status,
|
status: result?.status,
|
||||||
currentParticipants: result?.joined_parties || 0,
|
currentParticipants: result?.completed_parties || result?.joined_parties || 0,
|
||||||
|
totalParticipants: result?.total_parties || result?.threshold_n || 0,
|
||||||
},
|
},
|
||||||
|
joinToken: result?.join_token,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, error: (error as Error).message };
|
return { success: false, error: (error as Error).message };
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,9 @@ export interface GetSessionByInviteCodeResponse {
|
||||||
invite_code: string;
|
invite_code: string;
|
||||||
expires_at: number;
|
expires_at: number;
|
||||||
joined_parties: number;
|
joined_parties: number;
|
||||||
|
completed_parties?: number;
|
||||||
|
total_parties?: number;
|
||||||
|
join_token?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sign 会话相关
|
// Sign 会话相关
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
initiatorName: string;
|
initiatorName: string;
|
||||||
}) => ipcRenderer.invoke('grpc:createSession', params),
|
}) => ipcRenderer.invoke('grpc:createSession', params),
|
||||||
|
|
||||||
joinSession: (sessionId: string, participantName: string) =>
|
joinSession: (params: { sessionId: string; partyId: string; joinToken: string }) =>
|
||||||
ipcRenderer.invoke('grpc:joinSession', { sessionId, participantName }),
|
ipcRenderer.invoke('grpc:joinSession', params),
|
||||||
|
|
||||||
validateInviteCode: (code: string) =>
|
validateInviteCode: (code: string) =>
|
||||||
ipcRenderer.invoke('grpc:validateInviteCode', { code }),
|
ipcRenderer.invoke('grpc:validateInviteCode', { code }),
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,14 @@ interface SessionInfo {
|
||||||
initiator: string;
|
initiator: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
currentParticipants: number;
|
currentParticipants: number;
|
||||||
|
totalParticipants?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ValidateResult {
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
sessionInfo?: SessionInfo;
|
||||||
|
joinToken?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Join() {
|
export default function Join() {
|
||||||
|
|
@ -20,6 +28,8 @@ export default function Join() {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [sessionInfo, setSessionInfo] = useState<SessionInfo | null>(null);
|
const [sessionInfo, setSessionInfo] = useState<SessionInfo | null>(null);
|
||||||
|
const [joinToken, setJoinToken] = useState<string | null>(null);
|
||||||
|
const [partyId, setPartyId] = useState<string | null>(null);
|
||||||
const [step, setStep] = useState<'input' | 'confirm' | 'joining'>('input');
|
const [step, setStep] = useState<'input' | 'confirm' | 'joining'>('input');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -38,10 +48,22 @@ export default function Join() {
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 获取当前 partyId
|
||||||
|
const partyResult = await window.electronAPI.grpc.getPartyId();
|
||||||
|
if (!partyResult.success || !partyResult.partyId) {
|
||||||
|
setError('请先连接到消息路由器');
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPartyId(partyResult.partyId);
|
||||||
|
|
||||||
// 解析邀请码获取会话信息
|
// 解析邀请码获取会话信息
|
||||||
const result = await window.electronAPI.grpc.validateInviteCode(codeToValidate);
|
const result: ValidateResult = await window.electronAPI.grpc.validateInviteCode(codeToValidate);
|
||||||
if (result.success && result.sessionInfo) {
|
if (result.success && result.sessionInfo) {
|
||||||
setSessionInfo(result.sessionInfo);
|
setSessionInfo(result.sessionInfo);
|
||||||
|
if (result.joinToken) {
|
||||||
|
setJoinToken(result.joinToken);
|
||||||
|
}
|
||||||
setStep('confirm');
|
setStep('confirm');
|
||||||
} else {
|
} else {
|
||||||
setError(result.error || '无效的邀请码');
|
setError(result.error || '无效的邀请码');
|
||||||
|
|
@ -59,15 +81,26 @@ export default function Join() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!partyId) {
|
||||||
|
setError('未获取到 Party ID,请重试');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!joinToken) {
|
||||||
|
setError('未获取到加入令牌,请重新验证邀请码');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setStep('joining');
|
setStep('joining');
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.electronAPI.grpc.joinSession(
|
const result = await window.electronAPI.grpc.joinSession({
|
||||||
sessionInfo.sessionId,
|
sessionId: sessionInfo.sessionId,
|
||||||
participantName.trim()
|
partyId: partyId,
|
||||||
);
|
joinToken: joinToken,
|
||||||
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
navigate(`/session/${sessionInfo.sessionId}`);
|
navigate(`/session/${sessionInfo.sessionId}`);
|
||||||
|
|
@ -190,6 +223,8 @@ export default function Join() {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setStep('input');
|
setStep('input');
|
||||||
setSessionInfo(null);
|
setSessionInfo(null);
|
||||||
|
setJoinToken(null);
|
||||||
|
setPartyId(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
}}
|
}}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue