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:
hailin 2025-12-29 03:40:36 -08:00
parent 21985abde5
commit af08f0f9c6
6 changed files with 78 additions and 13 deletions

View File

@ -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,
}) })
} }

View File

@ -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

View File

@ -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 };

View File

@ -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 会话相关

View File

@ -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 }),

View File

@ -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}