1331 lines
44 KiB
Go
1331 lines
44 KiB
Go
package http
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"github.com/rwadurian/mpc-system/pkg/logger"
|
|
grpcclient "github.com/rwadurian/mpc-system/services/account/adapters/output/grpc"
|
|
"github.com/rwadurian/mpc-system/services/account/application/ports"
|
|
"github.com/rwadurian/mpc-system/services/account/application/use_cases"
|
|
"github.com/rwadurian/mpc-system/services/account/domain/entities"
|
|
"github.com/rwadurian/mpc-system/services/account/domain/repositories"
|
|
"github.com/rwadurian/mpc-system/services/account/domain/value_objects"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// AccountHTTPHandler handles HTTP requests for accounts
|
|
type AccountHTTPHandler struct {
|
|
accountRepo repositories.AccountRepository
|
|
sessionEventRepo repositories.SessionEventRepository
|
|
createAccountUC *use_cases.CreateAccountUseCase
|
|
getAccountUC *use_cases.GetAccountUseCase
|
|
updateAccountUC *use_cases.UpdateAccountUseCase
|
|
listAccountsUC *use_cases.ListAccountsUseCase
|
|
getAccountSharesUC *use_cases.GetAccountSharesUseCase
|
|
deactivateShareUC *use_cases.DeactivateShareUseCase
|
|
loginUC *use_cases.LoginUseCase
|
|
refreshTokenUC *use_cases.RefreshTokenUseCase
|
|
generateChallengeUC *use_cases.GenerateChallengeUseCase
|
|
initiateRecoveryUC *use_cases.InitiateRecoveryUseCase
|
|
completeRecoveryUC *use_cases.CompleteRecoveryUseCase
|
|
getRecoveryStatusUC *use_cases.GetRecoveryStatusUseCase
|
|
cancelRecoveryUC *use_cases.CancelRecoveryUseCase
|
|
sessionCoordinatorClient *grpcclient.SessionCoordinatorClient
|
|
}
|
|
|
|
// NewAccountHTTPHandler creates a new AccountHTTPHandler
|
|
func NewAccountHTTPHandler(
|
|
accountRepo repositories.AccountRepository,
|
|
sessionEventRepo repositories.SessionEventRepository,
|
|
createAccountUC *use_cases.CreateAccountUseCase,
|
|
getAccountUC *use_cases.GetAccountUseCase,
|
|
updateAccountUC *use_cases.UpdateAccountUseCase,
|
|
listAccountsUC *use_cases.ListAccountsUseCase,
|
|
getAccountSharesUC *use_cases.GetAccountSharesUseCase,
|
|
deactivateShareUC *use_cases.DeactivateShareUseCase,
|
|
loginUC *use_cases.LoginUseCase,
|
|
refreshTokenUC *use_cases.RefreshTokenUseCase,
|
|
generateChallengeUC *use_cases.GenerateChallengeUseCase,
|
|
initiateRecoveryUC *use_cases.InitiateRecoveryUseCase,
|
|
completeRecoveryUC *use_cases.CompleteRecoveryUseCase,
|
|
getRecoveryStatusUC *use_cases.GetRecoveryStatusUseCase,
|
|
cancelRecoveryUC *use_cases.CancelRecoveryUseCase,
|
|
sessionCoordinatorClient *grpcclient.SessionCoordinatorClient,
|
|
) *AccountHTTPHandler {
|
|
return &AccountHTTPHandler{
|
|
accountRepo: accountRepo,
|
|
sessionEventRepo: sessionEventRepo,
|
|
createAccountUC: createAccountUC,
|
|
getAccountUC: getAccountUC,
|
|
updateAccountUC: updateAccountUC,
|
|
listAccountsUC: listAccountsUC,
|
|
getAccountSharesUC: getAccountSharesUC,
|
|
deactivateShareUC: deactivateShareUC,
|
|
loginUC: loginUC,
|
|
refreshTokenUC: refreshTokenUC,
|
|
generateChallengeUC: generateChallengeUC,
|
|
initiateRecoveryUC: initiateRecoveryUC,
|
|
completeRecoveryUC: completeRecoveryUC,
|
|
getRecoveryStatusUC: getRecoveryStatusUC,
|
|
cancelRecoveryUC: cancelRecoveryUC,
|
|
sessionCoordinatorClient: sessionCoordinatorClient,
|
|
}
|
|
}
|
|
|
|
// RegisterRoutes registers HTTP routes
|
|
func (h *AccountHTTPHandler) RegisterRoutes(router *gin.RouterGroup) {
|
|
accounts := router.Group("/accounts")
|
|
{
|
|
accounts.POST("", h.CreateAccount)
|
|
accounts.POST("/from-keygen", h.CreateAccountFromKeygen)
|
|
accounts.GET("", h.ListAccounts)
|
|
accounts.GET("/:id", h.GetAccount)
|
|
accounts.PUT("/:id", h.UpdateAccount)
|
|
accounts.GET("/:id/shares", h.GetAccountShares)
|
|
accounts.DELETE("/:id/shares/:shareId", h.DeactivateShare)
|
|
// Signing parties configuration (use username as path parameter)
|
|
accounts.POST("/by-username/:username/signing-config", h.SetSigningParties)
|
|
accounts.PUT("/by-username/:username/signing-config", h.UpdateSigningParties)
|
|
accounts.DELETE("/by-username/:username/signing-config", h.ClearSigningParties)
|
|
accounts.GET("/by-username/:username/signing-config", h.GetSigningParties)
|
|
}
|
|
|
|
auth := router.Group("/auth")
|
|
{
|
|
auth.POST("/challenge", h.GenerateChallenge)
|
|
auth.POST("/login", h.Login)
|
|
auth.POST("/refresh", h.RefreshToken)
|
|
}
|
|
|
|
recovery := router.Group("/recovery")
|
|
{
|
|
recovery.POST("", h.InitiateRecovery)
|
|
recovery.GET("/:id", h.GetRecoveryStatus)
|
|
recovery.POST("/:id/complete", h.CompleteRecovery)
|
|
recovery.POST("/:id/cancel", h.CancelRecovery)
|
|
}
|
|
|
|
// MPC session management
|
|
mpc := router.Group("/mpc")
|
|
{
|
|
mpc.POST("/keygen", h.CreateKeygenSession)
|
|
mpc.POST("/sign", h.CreateSigningSession)
|
|
mpc.GET("/sessions/:id", h.GetSessionStatus)
|
|
}
|
|
}
|
|
|
|
// CreateAccountRequest represents the request for creating an account
|
|
type CreateAccountRequest struct {
|
|
Username string `json:"username" binding:"required"`
|
|
Email string `json:"email" binding:"omitempty,email"`
|
|
Phone *string `json:"phone"`
|
|
PublicKey string `json:"publicKey" binding:"required"`
|
|
KeygenSessionID string `json:"keygenSessionId" binding:"required"`
|
|
ThresholdN int `json:"thresholdN" binding:"required,min=1"`
|
|
ThresholdT int `json:"thresholdT" binding:"required,min=1"`
|
|
Shares []ShareInput `json:"shares" binding:"required,min=1"`
|
|
}
|
|
|
|
// ShareInput represents a share in the request
|
|
type ShareInput struct {
|
|
ShareType string `json:"shareType" binding:"required"`
|
|
PartyID string `json:"partyId" binding:"required"`
|
|
PartyIndex int `json:"partyIndex" binding:"required,min=0"`
|
|
DeviceType *string `json:"deviceType"`
|
|
DeviceID *string `json:"deviceId"`
|
|
}
|
|
|
|
// CreateAccount handles account creation
|
|
func (h *AccountHTTPHandler) CreateAccount(c *gin.Context) {
|
|
var req CreateAccountRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
keygenSessionID, err := uuid.Parse(req.KeygenSessionID)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid keygen session ID"})
|
|
return
|
|
}
|
|
|
|
shares := make([]ports.ShareInput, len(req.Shares))
|
|
for i, s := range req.Shares {
|
|
shares[i] = ports.ShareInput{
|
|
ShareType: value_objects.ShareType(s.ShareType),
|
|
PartyID: s.PartyID,
|
|
PartyIndex: s.PartyIndex,
|
|
DeviceType: s.DeviceType,
|
|
DeviceID: s.DeviceID,
|
|
}
|
|
}
|
|
|
|
// Decode hex-encoded public key
|
|
publicKeyBytes, err := hex.DecodeString(req.PublicKey)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid public key format"})
|
|
return
|
|
}
|
|
|
|
output, err := h.createAccountUC.Execute(c.Request.Context(), ports.CreateAccountInput{
|
|
Username: req.Username,
|
|
Email: req.Email,
|
|
Phone: req.Phone,
|
|
PublicKey: publicKeyBytes,
|
|
KeygenSessionID: keygenSessionID,
|
|
ThresholdN: req.ThresholdN,
|
|
ThresholdT: req.ThresholdT,
|
|
Shares: shares,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"account": output.Account,
|
|
"shares": output.Shares,
|
|
})
|
|
}
|
|
|
|
// GetAccount handles getting account by ID
|
|
func (h *AccountHTTPHandler) GetAccount(c *gin.Context) {
|
|
idStr := c.Param("id")
|
|
accountID, err := value_objects.AccountIDFromString(idStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account ID"})
|
|
return
|
|
}
|
|
|
|
output, err := h.getAccountUC.Execute(c.Request.Context(), ports.GetAccountInput{
|
|
AccountID: &accountID,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"account": output.Account,
|
|
"shares": output.Shares,
|
|
})
|
|
}
|
|
|
|
// UpdateAccountRequest represents the request for updating an account
|
|
type UpdateAccountRequest struct {
|
|
Phone *string `json:"phone"`
|
|
}
|
|
|
|
// UpdateAccount handles account updates
|
|
func (h *AccountHTTPHandler) UpdateAccount(c *gin.Context) {
|
|
idStr := c.Param("id")
|
|
accountID, err := value_objects.AccountIDFromString(idStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account ID"})
|
|
return
|
|
}
|
|
|
|
var req UpdateAccountRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
output, err := h.updateAccountUC.Execute(c.Request.Context(), ports.UpdateAccountInput{
|
|
AccountID: accountID,
|
|
Phone: req.Phone,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, output.Account)
|
|
}
|
|
|
|
// ListAccounts handles listing accounts
|
|
func (h *AccountHTTPHandler) ListAccounts(c *gin.Context) {
|
|
var offset, limit int
|
|
if o := c.Query("offset"); o != "" {
|
|
// Parse offset
|
|
}
|
|
if l := c.Query("limit"); l != "" {
|
|
// Parse limit
|
|
}
|
|
|
|
output, err := h.listAccountsUC.Execute(c.Request.Context(), use_cases.ListAccountsInput{
|
|
Offset: offset,
|
|
Limit: limit,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"accounts": output.Accounts,
|
|
"total": output.Total,
|
|
})
|
|
}
|
|
|
|
// GetAccountShares handles getting account shares
|
|
func (h *AccountHTTPHandler) GetAccountShares(c *gin.Context) {
|
|
idStr := c.Param("id")
|
|
accountID, err := value_objects.AccountIDFromString(idStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account ID"})
|
|
return
|
|
}
|
|
|
|
output, err := h.getAccountSharesUC.Execute(c.Request.Context(), accountID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"shares": output.Shares,
|
|
})
|
|
}
|
|
|
|
// DeactivateShare handles share deactivation
|
|
func (h *AccountHTTPHandler) DeactivateShare(c *gin.Context) {
|
|
idStr := c.Param("id")
|
|
accountID, err := value_objects.AccountIDFromString(idStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account ID"})
|
|
return
|
|
}
|
|
|
|
shareID := c.Param("shareId")
|
|
|
|
err = h.deactivateShareUC.Execute(c.Request.Context(), ports.DeactivateShareInput{
|
|
AccountID: accountID,
|
|
ShareID: shareID,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "share deactivated"})
|
|
}
|
|
|
|
// GenerateChallengeRequest represents the request for generating a challenge
|
|
type GenerateChallengeRequest struct {
|
|
Username string `json:"username" binding:"required"`
|
|
}
|
|
|
|
// GenerateChallenge handles challenge generation
|
|
func (h *AccountHTTPHandler) GenerateChallenge(c *gin.Context) {
|
|
var req GenerateChallengeRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
output, err := h.generateChallengeUC.Execute(c.Request.Context(), use_cases.GenerateChallengeInput{
|
|
Username: req.Username,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"challengeId": output.ChallengeID,
|
|
"challenge": hex.EncodeToString(output.Challenge),
|
|
"expiresAt": output.ExpiresAt,
|
|
})
|
|
}
|
|
|
|
// LoginRequest represents the request for login
|
|
type LoginRequest struct {
|
|
Username string `json:"username" binding:"required"`
|
|
Challenge string `json:"challenge" binding:"required"`
|
|
Signature string `json:"signature" binding:"required"`
|
|
}
|
|
|
|
// Login handles user login
|
|
func (h *AccountHTTPHandler) Login(c *gin.Context) {
|
|
var req LoginRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Decode hex-encoded challenge and signature
|
|
// Return 401 Unauthorized for invalid formats (treated as invalid credentials)
|
|
challengeBytes, err := hex.DecodeString(req.Challenge)
|
|
if err != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
|
|
return
|
|
}
|
|
|
|
signatureBytes, err := hex.DecodeString(req.Signature)
|
|
if err != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
|
|
return
|
|
}
|
|
|
|
output, err := h.loginUC.Execute(c.Request.Context(), ports.LoginInput{
|
|
Username: req.Username,
|
|
Challenge: challengeBytes,
|
|
Signature: signatureBytes,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"account": output.Account,
|
|
"accessToken": output.AccessToken,
|
|
"refreshToken": output.RefreshToken,
|
|
})
|
|
}
|
|
|
|
// RefreshTokenRequest represents the request for refreshing tokens
|
|
type RefreshTokenRequest struct {
|
|
RefreshToken string `json:"refreshToken" binding:"required"`
|
|
}
|
|
|
|
// RefreshToken handles token refresh
|
|
func (h *AccountHTTPHandler) RefreshToken(c *gin.Context) {
|
|
var req RefreshTokenRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
output, err := h.refreshTokenUC.Execute(c.Request.Context(), use_cases.RefreshTokenInput{
|
|
RefreshToken: req.RefreshToken,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"accessToken": output.AccessToken,
|
|
"refreshToken": output.RefreshToken,
|
|
})
|
|
}
|
|
|
|
// InitiateRecoveryRequest represents the request for initiating recovery
|
|
type InitiateRecoveryRequest struct {
|
|
AccountID string `json:"accountId" binding:"required"`
|
|
RecoveryType string `json:"recoveryType" binding:"required"`
|
|
OldShareType *string `json:"oldShareType"`
|
|
}
|
|
|
|
// InitiateRecovery handles recovery initiation
|
|
func (h *AccountHTTPHandler) InitiateRecovery(c *gin.Context) {
|
|
var req InitiateRecoveryRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
accountID, err := value_objects.AccountIDFromString(req.AccountID)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account ID"})
|
|
return
|
|
}
|
|
|
|
input := ports.InitiateRecoveryInput{
|
|
AccountID: accountID,
|
|
RecoveryType: value_objects.RecoveryType(req.RecoveryType),
|
|
}
|
|
|
|
if req.OldShareType != nil {
|
|
st := value_objects.ShareType(*req.OldShareType)
|
|
input.OldShareType = &st
|
|
}
|
|
|
|
output, err := h.initiateRecoveryUC.Execute(c.Request.Context(), input)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"recoverySession": output.RecoverySession,
|
|
})
|
|
}
|
|
|
|
// GetRecoveryStatus handles getting recovery status
|
|
func (h *AccountHTTPHandler) GetRecoveryStatus(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
output, err := h.getRecoveryStatusUC.Execute(c.Request.Context(), use_cases.GetRecoveryStatusInput{
|
|
RecoverySessionID: id,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, output.RecoverySession)
|
|
}
|
|
|
|
// CompleteRecoveryRequest represents the request for completing recovery
|
|
type CompleteRecoveryRequest struct {
|
|
NewPublicKey string `json:"newPublicKey" binding:"required"`
|
|
NewKeygenSessionID string `json:"newKeygenSessionId" binding:"required"`
|
|
NewShares []ShareInput `json:"newShares" binding:"required,min=1"`
|
|
}
|
|
|
|
// CompleteRecovery handles recovery completion
|
|
func (h *AccountHTTPHandler) CompleteRecovery(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
var req CompleteRecoveryRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
newKeygenSessionID, err := uuid.Parse(req.NewKeygenSessionID)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid keygen session ID"})
|
|
return
|
|
}
|
|
|
|
newShares := make([]ports.ShareInput, len(req.NewShares))
|
|
for i, s := range req.NewShares {
|
|
newShares[i] = ports.ShareInput{
|
|
ShareType: value_objects.ShareType(s.ShareType),
|
|
PartyID: s.PartyID,
|
|
PartyIndex: s.PartyIndex,
|
|
DeviceType: s.DeviceType,
|
|
DeviceID: s.DeviceID,
|
|
}
|
|
}
|
|
|
|
// Decode hex-encoded public key
|
|
newPublicKeyBytes, err := hex.DecodeString(req.NewPublicKey)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid public key format"})
|
|
return
|
|
}
|
|
|
|
output, err := h.completeRecoveryUC.Execute(c.Request.Context(), ports.CompleteRecoveryInput{
|
|
RecoverySessionID: id,
|
|
NewPublicKey: newPublicKeyBytes,
|
|
NewKeygenSessionID: newKeygenSessionID,
|
|
NewShares: newShares,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, output.Account)
|
|
}
|
|
|
|
// CancelRecovery handles recovery cancellation
|
|
func (h *AccountHTTPHandler) CancelRecovery(c *gin.Context) {
|
|
id := c.Param("id")
|
|
|
|
err := h.cancelRecoveryUC.Execute(c.Request.Context(), use_cases.CancelRecoveryInput{
|
|
RecoverySessionID: id,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "recovery cancelled"})
|
|
}
|
|
|
|
// ============================================
|
|
// MPC Session Management Endpoints
|
|
// ============================================
|
|
|
|
// CreateKeygenSessionRequest represents the request for creating a keygen session
|
|
// Coordinator will automatically select parties from registered pool
|
|
type CreateKeygenSessionRequest struct {
|
|
Username string `json:"username" binding:"required"` // Username - the unique identifier for all relationships
|
|
ThresholdN int `json:"threshold_n" binding:"required,min=2"` // Total number of parties (e.g., 3)
|
|
ThresholdT int `json:"threshold_t" binding:"required,min=1"` // Threshold for signing (e.g., 2)
|
|
RequireDelegate bool `json:"require_delegate"` // If true, one party will be delegate (returns share to user)
|
|
}
|
|
|
|
// CreateKeygenSession handles creating a new keygen session
|
|
// Parties are automatically selected by Coordinator from registered pool
|
|
func (h *AccountHTTPHandler) CreateKeygenSession(c *gin.Context) {
|
|
var req CreateKeygenSessionRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Validate threshold
|
|
if req.ThresholdT > req.ThresholdN {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "threshold_t cannot be greater than threshold_n"})
|
|
return
|
|
}
|
|
|
|
// Check if username already exists (keygen should be for new users)
|
|
exists, err := h.accountRepo.ExistsByUsername(c.Request.Context(), req.Username)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to check username"})
|
|
return
|
|
}
|
|
if exists {
|
|
c.JSON(http.StatusConflict, gin.H{"error": "username already exists, use sign API instead"})
|
|
return
|
|
}
|
|
|
|
// Call session coordinator via gRPC (no participants - coordinator selects automatically)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
logger.Info("Calling CreateKeygenSession via gRPC (auto party selection)",
|
|
zap.String("username", req.Username),
|
|
zap.Int("threshold_n", req.ThresholdN),
|
|
zap.Int("threshold_t", req.ThresholdT),
|
|
zap.Bool("require_delegate", req.RequireDelegate))
|
|
|
|
// Calculate party composition based on require_delegate
|
|
var persistentCount, delegateCount int
|
|
if req.RequireDelegate {
|
|
// One delegate party, rest are persistent
|
|
delegateCount = 1
|
|
persistentCount = req.ThresholdN - 1
|
|
} else {
|
|
// All persistent parties
|
|
persistentCount = req.ThresholdN
|
|
delegateCount = 0
|
|
}
|
|
|
|
resp, err := h.sessionCoordinatorClient.CreateKeygenSessionAuto(
|
|
ctx,
|
|
int32(req.ThresholdN),
|
|
int32(req.ThresholdT),
|
|
int32(persistentCount),
|
|
int32(delegateCount),
|
|
600, // 10 minutes expiry
|
|
)
|
|
|
|
if err != nil {
|
|
logger.Error("gRPC CreateKeygenSession failed", zap.Error(err))
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
logger.Info("gRPC CreateKeygenSession succeeded",
|
|
zap.String("session_id", resp.SessionID),
|
|
zap.Int("num_parties", len(resp.SelectedParties)))
|
|
|
|
// Record session_created event
|
|
sessionID, _ := uuid.Parse(resp.SessionID)
|
|
event := repositories.NewSessionEvent(
|
|
sessionID,
|
|
req.Username,
|
|
repositories.EventSessionCreated,
|
|
repositories.SessionTypeKeygen,
|
|
).WithThreshold(req.ThresholdN, req.ThresholdT).WithMetadata(map[string]interface{}{
|
|
"selected_parties": resp.SelectedParties,
|
|
"delegate_party": resp.DelegateParty,
|
|
"require_delegate": req.RequireDelegate,
|
|
"persistent_count": persistentCount,
|
|
"delegate_count": delegateCount,
|
|
})
|
|
if err := h.sessionEventRepo.Create(c.Request.Context(), event); err != nil {
|
|
logger.Error("Failed to record session event", zap.Error(err))
|
|
// Don't fail the request, just log the error
|
|
}
|
|
|
|
// Return response with selected parties info
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"session_id": resp.SessionID,
|
|
"session_type": "keygen",
|
|
"username": req.Username, // Include username in response for reference
|
|
"threshold_n": req.ThresholdN,
|
|
"threshold_t": req.ThresholdT,
|
|
"selected_parties": resp.SelectedParties,
|
|
"delegate_party": resp.DelegateParty, // The party that will return share to user
|
|
"status": "created",
|
|
})
|
|
}
|
|
|
|
// CreateSigningSessionRequest represents the request for creating a signing session
|
|
// Coordinator will automatically select parties based on account's registered shares
|
|
type CreateSigningSessionRequest struct {
|
|
Username string `json:"username" binding:"required"` // Username - the unique identifier for all relationships
|
|
MessageHash string `json:"message_hash" binding:"required"` // SHA-256 hash to sign (hex encoded)
|
|
UserShare string `json:"user_share"` // Required if account has delegate share: user's encrypted share (hex)
|
|
}
|
|
|
|
// CreateSigningSession handles creating a new signing session
|
|
// Parties are automatically selected based on the account's registered shares
|
|
func (h *AccountHTTPHandler) CreateSigningSession(c *gin.Context) {
|
|
var req CreateSigningSessionRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Decode message hash
|
|
messageHash, err := hex.DecodeString(req.MessageHash)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid message_hash format (expected hex)"})
|
|
return
|
|
}
|
|
|
|
if len(messageHash) != 32 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "message_hash must be 32 bytes (SHA-256)"})
|
|
return
|
|
}
|
|
|
|
// Get account by username to verify it exists and get share info
|
|
accountOutput, err := h.getAccountUC.Execute(c.Request.Context(), ports.GetAccountInput{
|
|
Username: &req.Username,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "account not found for username: " + req.Username})
|
|
return
|
|
}
|
|
|
|
// Build a map of active shares for validation
|
|
activeSharesMap := make(map[string]*entities.AccountShare)
|
|
var allActivePartyIDs []string
|
|
var delegateShare *entities.AccountShare
|
|
for _, share := range accountOutput.Shares {
|
|
if share.IsActive {
|
|
activeSharesMap[share.PartyID] = share
|
|
allActivePartyIDs = append(allActivePartyIDs, share.PartyID)
|
|
// Check if this is a delegate share
|
|
if share.ShareType == value_objects.ShareTypeDelegate {
|
|
delegateShare = share
|
|
}
|
|
}
|
|
}
|
|
|
|
// Determine which parties to use for signing
|
|
var partyIDs []string
|
|
if accountOutput.Account.HasSigningPartiesConfig() {
|
|
// Use configured signing parties
|
|
configuredParties := accountOutput.Account.GetSigningParties()
|
|
|
|
// Validate all configured parties are still active
|
|
for _, partyID := range configuredParties {
|
|
if _, exists := activeSharesMap[partyID]; !exists {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "configured signing party is no longer active",
|
|
"party_id": partyID,
|
|
"hint": "update signing-config with active parties",
|
|
})
|
|
return
|
|
}
|
|
}
|
|
partyIDs = configuredParties
|
|
|
|
logger.Info("Using configured signing parties",
|
|
zap.String("username", req.Username),
|
|
zap.Strings("configured_parties", partyIDs))
|
|
} else {
|
|
// Use all active parties (original behavior)
|
|
partyIDs = allActivePartyIDs
|
|
|
|
logger.Info("Using all active parties for signing",
|
|
zap.String("username", req.Username),
|
|
zap.Strings("active_parties", partyIDs))
|
|
}
|
|
|
|
// Check if any of the selected parties is a delegate
|
|
// (delegate share might not be in the configured signing parties)
|
|
var selectedDelegateShare *entities.AccountShare
|
|
for _, partyID := range partyIDs {
|
|
if share := activeSharesMap[partyID]; share != nil && share.ShareType == value_objects.ShareTypeDelegate {
|
|
selectedDelegateShare = share
|
|
break
|
|
}
|
|
}
|
|
|
|
// If selected parties include delegate share, user_share is required
|
|
if selectedDelegateShare != nil && req.UserShare == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "user_share is required for accounts with delegate party",
|
|
"delegate_party_id": selectedDelegateShare.PartyID,
|
|
})
|
|
return
|
|
}
|
|
|
|
// Use the selected delegate for signing (not necessarily the account's delegate)
|
|
delegateShare = selectedDelegateShare
|
|
|
|
// Validate we have enough parties
|
|
if len(partyIDs) < accountOutput.Account.ThresholdT {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "insufficient parties for signing",
|
|
"required": accountOutput.Account.ThresholdT,
|
|
"selected": len(partyIDs),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Call session coordinator via gRPC
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
// Prepare delegate user share if needed
|
|
var delegateUserShare *grpcclient.DelegateUserShareInput
|
|
if delegateShare != nil {
|
|
userShareBytes, err := hex.DecodeString(req.UserShare)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user_share format (expected hex)"})
|
|
return
|
|
}
|
|
delegateUserShare = &grpcclient.DelegateUserShareInput{
|
|
DelegatePartyID: delegateShare.PartyID,
|
|
EncryptedShare: userShareBytes,
|
|
PartyIndex: int32(delegateShare.PartyIndex),
|
|
}
|
|
logger.Info("Calling CreateSigningSession with delegate user share",
|
|
zap.String("username", req.Username),
|
|
zap.String("delegate_party_id", delegateShare.PartyID),
|
|
zap.Int("threshold_t", accountOutput.Account.ThresholdT),
|
|
zap.Int("available_parties", len(partyIDs)))
|
|
} else {
|
|
logger.Info("Calling CreateSigningSession via gRPC (auto party selection)",
|
|
zap.String("username", req.Username),
|
|
zap.Int("threshold_t", accountOutput.Account.ThresholdT),
|
|
zap.Int("available_parties", len(partyIDs)))
|
|
}
|
|
|
|
resp, err := h.sessionCoordinatorClient.CreateSigningSessionAuto(
|
|
ctx,
|
|
int32(accountOutput.Account.ThresholdT),
|
|
partyIDs,
|
|
messageHash,
|
|
600, // 10 minutes expiry
|
|
delegateUserShare,
|
|
)
|
|
|
|
if err != nil {
|
|
logger.Error("gRPC CreateSigningSession failed", zap.Error(err))
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
logger.Info("gRPC CreateSigningSession succeeded",
|
|
zap.String("session_id", resp.SessionID),
|
|
zap.Int("num_parties", len(resp.SelectedParties)))
|
|
|
|
// Record session_created event for sign
|
|
signSessionID, _ := uuid.Parse(resp.SessionID)
|
|
signEvent := repositories.NewSessionEvent(
|
|
signSessionID,
|
|
req.Username,
|
|
repositories.EventSessionCreated,
|
|
repositories.SessionTypeSign,
|
|
).WithThreshold(accountOutput.Account.ThresholdN, accountOutput.Account.ThresholdT).
|
|
WithMessageHash(messageHash).
|
|
WithMetadata(map[string]interface{}{
|
|
"selected_parties": resp.SelectedParties,
|
|
"has_delegate": delegateShare != nil,
|
|
"signing_parties_config": accountOutput.Account.HasSigningPartiesConfig(),
|
|
})
|
|
if delegateShare != nil {
|
|
signEvent = signEvent.WithParty(delegateShare.PartyID, delegateShare.PartyIndex)
|
|
}
|
|
if err := h.sessionEventRepo.Create(c.Request.Context(), signEvent); err != nil {
|
|
logger.Error("Failed to record session event", zap.Error(err))
|
|
// Don't fail the request, just log the error
|
|
}
|
|
|
|
response := gin.H{
|
|
"session_id": resp.SessionID,
|
|
"session_type": "sign",
|
|
"username": req.Username,
|
|
"message_hash": req.MessageHash,
|
|
"threshold_t": accountOutput.Account.ThresholdT,
|
|
"selected_parties": resp.SelectedParties,
|
|
"status": "created",
|
|
}
|
|
|
|
// Include has_delegate for sign sessions too
|
|
if delegateShare != nil {
|
|
response["has_delegate"] = true
|
|
response["delegate_party_id"] = delegateShare.PartyID
|
|
} else {
|
|
response["has_delegate"] = false
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, response)
|
|
}
|
|
|
|
// GetSessionStatus handles getting session status
|
|
func (h *AccountHTTPHandler) GetSessionStatus(c *gin.Context) {
|
|
sessionID := c.Param("id")
|
|
|
|
// Validate session ID format
|
|
if _, err := uuid.Parse(sessionID); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid session ID format"})
|
|
return
|
|
}
|
|
|
|
// Call session coordinator via gRPC
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
resp, err := h.sessionCoordinatorClient.GetSessionStatus(ctx, sessionID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
response := gin.H{
|
|
"session_id": sessionID,
|
|
"status": resp.Status,
|
|
"session_type": resp.SessionType, // "keygen" or "sign"
|
|
"completed_parties": resp.CompletedParties,
|
|
"total_parties": resp.TotalParties,
|
|
}
|
|
|
|
// Keygen-specific fields
|
|
if resp.SessionType == "keygen" {
|
|
// has_delegate only meaningful for keygen sessions
|
|
response["has_delegate"] = resp.HasDelegate
|
|
|
|
if len(resp.PublicKey) > 0 {
|
|
response["public_key"] = hex.EncodeToString(resp.PublicKey)
|
|
}
|
|
|
|
// Include delegate share if keygen session has delegate party
|
|
// has_delegate=true + delegate_share=null means share was already retrieved (one-time)
|
|
// has_delegate=false means no delegate party (pure persistent keygen)
|
|
if resp.HasDelegate && resp.DelegateShare != nil {
|
|
response["delegate_share"] = gin.H{
|
|
"encrypted_share": hex.EncodeToString(resp.DelegateShare.EncryptedShare),
|
|
"party_index": resp.DelegateShare.PartyIndex,
|
|
"party_id": resp.DelegateShare.PartyID,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sign-specific fields
|
|
if resp.SessionType == "sign" {
|
|
if len(resp.Signature) > 0 {
|
|
response["signature"] = hex.EncodeToString(resp.Signature)
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// ============================================
|
|
// Account Creation from Keygen (Internal API)
|
|
// ============================================
|
|
|
|
// CreateAccountFromKeygenRequest represents the request from Session Coordinator
|
|
// after keygen completion
|
|
type CreateAccountFromKeygenRequest struct {
|
|
PublicKey string `json:"public_key" binding:"required"`
|
|
KeygenSessionID string `json:"keygen_session_id" binding:"required"`
|
|
ThresholdN int `json:"threshold_n" binding:"required,min=2"`
|
|
ThresholdT int `json:"threshold_t" binding:"required,min=1"`
|
|
Shares []ShareInfoFromKeygenInput `json:"shares" binding:"required,min=1"`
|
|
}
|
|
|
|
// ShareInfoFromKeygenInput represents share info from keygen
|
|
type ShareInfoFromKeygenInput struct {
|
|
PartyID string `json:"party_id" binding:"required"`
|
|
PartyIndex int `json:"party_index"`
|
|
ShareType string `json:"share_type" binding:"required"` // "persistent" or "delegate"
|
|
}
|
|
|
|
// CreateAccountFromKeygen handles account creation after keygen completion
|
|
// This is called by Session Coordinator when all parties complete keygen
|
|
func (h *AccountHTTPHandler) CreateAccountFromKeygen(c *gin.Context) {
|
|
var req CreateAccountFromKeygenRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Validate threshold
|
|
if req.ThresholdT > req.ThresholdN {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "threshold_t cannot be greater than threshold_n"})
|
|
return
|
|
}
|
|
|
|
// Decode public key
|
|
publicKey, err := hex.DecodeString(req.PublicKey)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid public key format"})
|
|
return
|
|
}
|
|
|
|
// Parse keygen session ID
|
|
keygenSessionID, err := uuid.Parse(req.KeygenSessionID)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid keygen_session_id format"})
|
|
return
|
|
}
|
|
|
|
// Generate a unique username based on keygen session ID
|
|
// In production, you might want a different naming scheme
|
|
username := "wallet-" + req.KeygenSessionID[:8]
|
|
|
|
// Convert shares - map share type string to value_objects.ShareType
|
|
shares := make([]ports.ShareInput, len(req.Shares))
|
|
for i, s := range req.Shares {
|
|
var shareType value_objects.ShareType
|
|
switch s.ShareType {
|
|
case "persistent", "server":
|
|
shareType = value_objects.ShareTypeServer
|
|
case "delegate", "user_device":
|
|
shareType = value_objects.ShareTypeUserDevice
|
|
default:
|
|
shareType = value_objects.ShareTypeServer
|
|
}
|
|
shares[i] = ports.ShareInput{
|
|
ShareType: shareType,
|
|
PartyID: s.PartyID,
|
|
PartyIndex: s.PartyIndex,
|
|
}
|
|
}
|
|
|
|
logger.Info("Creating account from keygen",
|
|
zap.String("keygen_session_id", req.KeygenSessionID),
|
|
zap.String("username", username),
|
|
zap.Int("threshold_n", req.ThresholdN),
|
|
zap.Int("threshold_t", req.ThresholdT),
|
|
zap.Int("num_shares", len(shares)))
|
|
|
|
// Create account
|
|
output, err := h.createAccountUC.Execute(c.Request.Context(), ports.CreateAccountInput{
|
|
Username: username,
|
|
PublicKey: publicKey,
|
|
KeygenSessionID: keygenSessionID,
|
|
ThresholdN: req.ThresholdN,
|
|
ThresholdT: req.ThresholdT,
|
|
Shares: shares,
|
|
})
|
|
if err != nil {
|
|
logger.Error("Failed to create account from keygen",
|
|
zap.String("keygen_session_id", req.KeygenSessionID),
|
|
zap.Error(err))
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
logger.Info("Account created from keygen successfully",
|
|
zap.String("account_id", output.Account.ID.String()),
|
|
zap.String("username", output.Account.Username),
|
|
zap.String("keygen_session_id", req.KeygenSessionID))
|
|
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"account_id": output.Account.ID.String(),
|
|
"username": output.Account.Username,
|
|
"public_key": hex.EncodeToString(output.Account.PublicKey),
|
|
})
|
|
}
|
|
|
|
// ============================================
|
|
// Signing Parties Configuration Endpoints
|
|
// ============================================
|
|
|
|
// SigningPartiesRequest represents the request for setting/updating signing parties
|
|
type SigningPartiesRequest struct {
|
|
PartyIDs []string `json:"party_ids" binding:"required,min=1"`
|
|
}
|
|
|
|
// SetSigningParties handles setting signing parties for the first time
|
|
// POST /accounts/by-username/:username/signing-config
|
|
func (h *AccountHTTPHandler) SetSigningParties(c *gin.Context) {
|
|
username := c.Param("username")
|
|
if username == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "username is required"})
|
|
return
|
|
}
|
|
|
|
var req SigningPartiesRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Get account by username
|
|
accountOutput, err := h.getAccountUC.Execute(c.Request.Context(), ports.GetAccountInput{
|
|
Username: &username,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "account not found for username: " + username})
|
|
return
|
|
}
|
|
|
|
// Check if signing parties already configured
|
|
if accountOutput.Account.HasSigningPartiesConfig() {
|
|
c.JSON(http.StatusConflict, gin.H{
|
|
"error": "signing parties already configured, use PUT to update",
|
|
"current_signing_parties": accountOutput.Account.GetSigningParties(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Validate that all party IDs are valid shares for this account
|
|
validPartyIDs := make(map[string]bool)
|
|
for _, share := range accountOutput.Shares {
|
|
if share.IsActive {
|
|
validPartyIDs[share.PartyID] = true
|
|
}
|
|
}
|
|
|
|
for _, partyID := range req.PartyIDs {
|
|
if !validPartyIDs[partyID] {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "invalid party ID - not an active share for this account",
|
|
"party_id": partyID,
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
// Set signing parties
|
|
if err := accountOutput.Account.SetSigningParties(req.PartyIDs); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Update account in database
|
|
_, err = h.updateAccountUC.Execute(c.Request.Context(), ports.UpdateAccountInput{
|
|
AccountID: accountOutput.Account.ID,
|
|
SigningParties: req.PartyIDs,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
logger.Info("Signing parties configured",
|
|
zap.String("username", username),
|
|
zap.Strings("signing_parties", req.PartyIDs))
|
|
|
|
// Record signing_config_set event
|
|
event := repositories.NewSessionEvent(
|
|
uuid.New(), // Generate new event ID (no session for config changes)
|
|
username,
|
|
repositories.EventSigningConfigSet,
|
|
repositories.SessionTypeSign, // Configuration is for signing
|
|
).WithThreshold(accountOutput.Account.ThresholdN, accountOutput.Account.ThresholdT).
|
|
WithMetadata(map[string]interface{}{
|
|
"operation": "set",
|
|
"signing_parties": req.PartyIDs,
|
|
})
|
|
if err := h.sessionEventRepo.Create(c.Request.Context(), event); err != nil {
|
|
logger.Error("Failed to record signing config event", zap.Error(err))
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"message": "signing parties configured successfully",
|
|
"username": username,
|
|
"signing_parties": req.PartyIDs,
|
|
"threshold_t": accountOutput.Account.ThresholdT,
|
|
})
|
|
}
|
|
|
|
// UpdateSigningParties handles updating existing signing parties configuration
|
|
// PUT /accounts/by-username/:username/signing-config
|
|
func (h *AccountHTTPHandler) UpdateSigningParties(c *gin.Context) {
|
|
username := c.Param("username")
|
|
if username == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "username is required"})
|
|
return
|
|
}
|
|
|
|
var req SigningPartiesRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Get account by username
|
|
accountOutput, err := h.getAccountUC.Execute(c.Request.Context(), ports.GetAccountInput{
|
|
Username: &username,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "account not found for username: " + username})
|
|
return
|
|
}
|
|
|
|
// Check if signing parties are configured (must exist to update)
|
|
if !accountOutput.Account.HasSigningPartiesConfig() {
|
|
c.JSON(http.StatusNotFound, gin.H{
|
|
"error": "signing parties not configured, use POST to set",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Validate that all party IDs are valid shares for this account
|
|
validPartyIDs := make(map[string]bool)
|
|
for _, share := range accountOutput.Shares {
|
|
if share.IsActive {
|
|
validPartyIDs[share.PartyID] = true
|
|
}
|
|
}
|
|
|
|
for _, partyID := range req.PartyIDs {
|
|
if !validPartyIDs[partyID] {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": "invalid party ID - not an active share for this account",
|
|
"party_id": partyID,
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
// Set signing parties (validates count matches threshold)
|
|
if err := accountOutput.Account.SetSigningParties(req.PartyIDs); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Update account in database
|
|
_, err = h.updateAccountUC.Execute(c.Request.Context(), ports.UpdateAccountInput{
|
|
AccountID: accountOutput.Account.ID,
|
|
SigningParties: req.PartyIDs,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
logger.Info("Signing parties updated",
|
|
zap.String("username", username),
|
|
zap.Strings("signing_parties", req.PartyIDs))
|
|
|
|
// Record signing_config_set event for update
|
|
updateEvent := repositories.NewSessionEvent(
|
|
uuid.New(),
|
|
username,
|
|
repositories.EventSigningConfigSet,
|
|
repositories.SessionTypeSign,
|
|
).WithThreshold(accountOutput.Account.ThresholdN, accountOutput.Account.ThresholdT).
|
|
WithMetadata(map[string]interface{}{
|
|
"operation": "update",
|
|
"signing_parties": req.PartyIDs,
|
|
})
|
|
if err := h.sessionEventRepo.Create(c.Request.Context(), updateEvent); err != nil {
|
|
logger.Error("Failed to record signing config event", zap.Error(err))
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "signing parties updated successfully",
|
|
"username": username,
|
|
"signing_parties": req.PartyIDs,
|
|
"threshold_t": accountOutput.Account.ThresholdT,
|
|
})
|
|
}
|
|
|
|
// ClearSigningParties handles clearing signing parties configuration
|
|
// DELETE /accounts/by-username/:username/signing-config
|
|
func (h *AccountHTTPHandler) ClearSigningParties(c *gin.Context) {
|
|
username := c.Param("username")
|
|
if username == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "username is required"})
|
|
return
|
|
}
|
|
|
|
// Get account by username
|
|
accountOutput, err := h.getAccountUC.Execute(c.Request.Context(), ports.GetAccountInput{
|
|
Username: &username,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "account not found for username: " + username})
|
|
return
|
|
}
|
|
|
|
// Check if signing parties are configured
|
|
if !accountOutput.Account.HasSigningPartiesConfig() {
|
|
c.JSON(http.StatusNotFound, gin.H{
|
|
"error": "signing parties not configured",
|
|
})
|
|
return
|
|
}
|
|
|
|
// Clear signing parties - pass empty slice
|
|
_, err = h.updateAccountUC.Execute(c.Request.Context(), ports.UpdateAccountInput{
|
|
AccountID: accountOutput.Account.ID,
|
|
ClearSigningParties: true,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
logger.Info("Signing parties cleared",
|
|
zap.String("username", username))
|
|
|
|
// Record signing_config_cleared event
|
|
clearEvent := repositories.NewSessionEvent(
|
|
uuid.New(),
|
|
username,
|
|
repositories.EventSigningConfigCleared,
|
|
repositories.SessionTypeSign,
|
|
).WithThreshold(accountOutput.Account.ThresholdN, accountOutput.Account.ThresholdT).
|
|
WithMetadata(map[string]interface{}{
|
|
"operation": "clear",
|
|
})
|
|
if err := h.sessionEventRepo.Create(c.Request.Context(), clearEvent); err != nil {
|
|
logger.Error("Failed to record signing config event", zap.Error(err))
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "signing parties cleared - all active parties will be used for signing",
|
|
"username": username,
|
|
})
|
|
}
|
|
|
|
// GetSigningParties handles getting current signing parties configuration
|
|
// GET /accounts/by-username/:username/signing-config
|
|
func (h *AccountHTTPHandler) GetSigningParties(c *gin.Context) {
|
|
username := c.Param("username")
|
|
if username == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "username is required"})
|
|
return
|
|
}
|
|
|
|
// Get account by username
|
|
accountOutput, err := h.getAccountUC.Execute(c.Request.Context(), ports.GetAccountInput{
|
|
Username: &username,
|
|
})
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "account not found for username: " + username})
|
|
return
|
|
}
|
|
|
|
if accountOutput.Account.HasSigningPartiesConfig() {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"configured": true,
|
|
"username": username,
|
|
"signing_parties": accountOutput.Account.GetSigningParties(),
|
|
"threshold_t": accountOutput.Account.ThresholdT,
|
|
})
|
|
} else {
|
|
// Get all active parties
|
|
var activeParties []string
|
|
for _, share := range accountOutput.Shares {
|
|
if share.IsActive {
|
|
activeParties = append(activeParties, share.PartyID)
|
|
}
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"configured": false,
|
|
"username": username,
|
|
"message": "no signing parties configured - all active parties will be used",
|
|
"active_parties": activeParties,
|
|
"threshold_t": accountOutput.Account.ThresholdT,
|
|
})
|
|
}
|
|
}
|