487 lines
15 KiB
Go
487 lines
15 KiB
Go
package http
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"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/value_objects"
|
|
)
|
|
|
|
// AccountHTTPHandler handles HTTP requests for accounts
|
|
type AccountHTTPHandler struct {
|
|
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
|
|
}
|
|
|
|
// NewAccountHTTPHandler creates a new AccountHTTPHandler
|
|
func NewAccountHTTPHandler(
|
|
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,
|
|
) *AccountHTTPHandler {
|
|
return &AccountHTTPHandler{
|
|
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,
|
|
}
|
|
}
|
|
|
|
// RegisterRoutes registers HTTP routes
|
|
func (h *AccountHTTPHandler) RegisterRoutes(router *gin.RouterGroup) {
|
|
accounts := router.Group("/accounts")
|
|
{
|
|
accounts.POST("", h.CreateAccount)
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
// CreateAccountRequest represents the request for creating an account
|
|
type CreateAccountRequest struct {
|
|
Username string `json:"username" binding:"required"`
|
|
Email string `json:"email" binding:"required,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,
|
|
}
|
|
}
|
|
|
|
output, err := h.createAccountUC.Execute(c.Request.Context(), ports.CreateAccountInput{
|
|
Username: req.Username,
|
|
Email: req.Email,
|
|
Phone: req.Phone,
|
|
PublicKey: []byte(req.PublicKey),
|
|
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": 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
|
|
}
|
|
|
|
output, err := h.loginUC.Execute(c.Request.Context(), ports.LoginInput{
|
|
Username: req.Username,
|
|
Challenge: []byte(req.Challenge),
|
|
Signature: []byte(req.Signature),
|
|
})
|
|
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,
|
|
}
|
|
}
|
|
|
|
output, err := h.completeRecoveryUC.Execute(c.Request.Context(), ports.CompleteRecoveryInput{
|
|
RecoverySessionID: id,
|
|
NewPublicKey: []byte(req.NewPublicKey),
|
|
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"})
|
|
}
|