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/google/uuid"
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"
"go.uber.org/zap"
)
@ -20,7 +21,8 @@ import (
// This is a completely independent handler that does not affect existing functionality
type CoManagedHTTPHandler struct {
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
@ -37,10 +39,12 @@ func NewCoManagedHTTPHandler(
func NewCoManagedHTTPHandlerWithDB(
sessionCoordinatorClient *grpcclient.SessionCoordinatorClient,
db *sql.DB,
jwtService *jwt.JWTService,
) *CoManagedHTTPHandler {
return &CoManagedHTTPHandler{
sessionCoordinatorClient: sessionCoordinatorClient,
db: db,
jwtService: jwtService,
}
}
@ -380,7 +384,7 @@ func (h *CoManagedHTTPHandler) GetSessionByInviteCode(c *gin.Context) {
return
}
// Get wildcard join token from session coordinator
// Get session status from session coordinator
statusResp, err := h.sessionCoordinatorClient.GetSessionStatus(ctx, sessionID)
if err != nil {
logger.Error("Failed to get session status from coordinator",
@ -398,10 +402,30 @@ func (h *CoManagedHTTPHandler) GetSessionByInviteCode(c *gin.Context) {
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",
zap.String("invite_code", inviteCode),
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{
"session_id": sessionID,
@ -412,6 +436,7 @@ func (h *CoManagedHTTPHandler) GetSessionByInviteCode(c *gin.Context) {
"completed_parties": statusResp.CompletedParties,
"total_parties": statusResp.TotalParties,
"expires_at": expiresAt.UnixMilli(),
"join_token": joinToken,
})
}

View File

@ -303,8 +303,8 @@ func startHTTPServer(
})
// Create co-managed wallet handler (independent from existing functionality)
// Uses database connection for invite_code lookups
coManagedHandler := httphandler.NewCoManagedHTTPHandlerWithDB(sessionCoordinatorClient, db)
// Uses database connection for invite_code lookups and JWT service for generating join tokens
coManagedHandler := httphandler.NewCoManagedHTTPHandlerWithDB(sessionCoordinatorClient, db, jwtService)
// Configure authentication middleware
// Skip paths that don't require authentication

View File

@ -235,8 +235,10 @@ function setupIpcHandlers() {
n: result?.threshold_n,
},
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) {
return { success: false, error: (error as Error).message };

View File

@ -82,6 +82,9 @@ export interface GetSessionByInviteCodeResponse {
invite_code: string;
expires_at: number;
joined_parties: number;
completed_parties?: number;
total_parties?: number;
join_token?: string;
}
// Sign 会话相关

View File

@ -16,8 +16,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
initiatorName: string;
}) => ipcRenderer.invoke('grpc:createSession', params),
joinSession: (sessionId: string, participantName: string) =>
ipcRenderer.invoke('grpc:joinSession', { sessionId, participantName }),
joinSession: (params: { sessionId: string; partyId: string; joinToken: string }) =>
ipcRenderer.invoke('grpc:joinSession', params),
validateInviteCode: (code: string) =>
ipcRenderer.invoke('grpc:validateInviteCode', { code }),

View File

@ -9,6 +9,14 @@ interface SessionInfo {
initiator: string;
createdAt: string;
currentParticipants: number;
totalParticipants?: number;
}
interface ValidateResult {
success: boolean;
error?: string;
sessionInfo?: SessionInfo;
joinToken?: string;
}
export default function Join() {
@ -20,6 +28,8 @@ export default function Join() {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | 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');
useEffect(() => {
@ -38,10 +48,22 @@ export default function Join() {
setError(null);
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) {
setSessionInfo(result.sessionInfo);
if (result.joinToken) {
setJoinToken(result.joinToken);
}
setStep('confirm');
} else {
setError(result.error || '无效的邀请码');
@ -59,15 +81,26 @@ export default function Join() {
return;
}
if (!partyId) {
setError('未获取到 Party ID请重试');
return;
}
if (!joinToken) {
setError('未获取到加入令牌,请重新验证邀请码');
return;
}
setStep('joining');
setIsLoading(true);
setError(null);
try {
const result = await window.electronAPI.grpc.joinSession(
sessionInfo.sessionId,
participantName.trim()
);
const result = await window.electronAPI.grpc.joinSession({
sessionId: sessionInfo.sessionId,
partyId: partyId,
joinToken: joinToken,
});
if (result.success) {
navigate(`/session/${sessionInfo.sessionId}`);
@ -190,6 +223,8 @@ export default function Join() {
onClick={() => {
setStep('input');
setSessionInfo(null);
setJoinToken(null);
setPartyId(null);
setError(null);
}}
disabled={isLoading}