package api import ( "database/sql" "errors" "fmt" "net/http" "strconv" "strings" "github.com/ente-io/museum/ente" "github.com/ente-io/museum/ente/jwt" "github.com/ente-io/museum/pkg/controller/user" "github.com/ente-io/museum/pkg/utils/auth" "github.com/ente-io/museum/pkg/utils/handler" "github.com/ente-io/stacktrace" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // UserHandler exposes request handlers for all user related requests type UserHandler struct { UserController *user.UserController } // SendOTT generates and sends an OTT to the provided email address func (h *UserHandler) SendOTT(c *gin.Context) { var request ente.SendOTTRequest if err := c.ShouldBindJSON(&request); err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } email := strings.ToLower(request.Email) if len(email) == 0 { handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "Email id is missing")) return } err := h.UserController.SendEmailOTT(c, email, request.Client, request.Purpose) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } else { c.Status(http.StatusOK) } } // Logout removes the auth token from (instance) cache & database. func (h *UserHandler) Logout(c *gin.Context) { err := h.UserController.Logout(c) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, gin.H{}) } // GetDetails returns details about the requesting user func (h *UserHandler) GetDetails(c *gin.Context) { details, err := h.UserController.GetDetails(c) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, gin.H{ "details": details, }) } // GetDetailsV2 returns details about the requesting user func (h *UserHandler) GetDetailsV2(c *gin.Context) { userID := auth.GetUserID(c.Request.Header) fetchMemoryCount, _ := strconv.ParseBool(c.DefaultQuery("memoryCount", "true")) enteApp := auth.GetApp(c) details, err := h.UserController.GetDetailsV2(c, userID, fetchMemoryCount, enteApp) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, details) } // SetAttributes sets the attributes for a user func (h *UserHandler) SetAttributes(c *gin.Context) { userID := auth.GetUserID(c.Request.Header) var request ente.SetUserAttributesRequest if err := c.ShouldBindJSON(&request); err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } err := h.UserController.SetAttributes(userID, request) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.Status(http.StatusOK) } func (h *UserHandler) UpdateEmailMFA(c *gin.Context) { userID := auth.GetUserID(c.Request.Header) var request ente.UpdateEmailMFA if err := c.ShouldBindJSON(&request); err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } err := h.UserController.UpdateEmailMFA(c, userID, *request.IsEnabled) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.Status(http.StatusOK) } // UpdateKeys updates the user key attributes on password change func (h *UserHandler) UpdateKeys(c *gin.Context) { userID := auth.GetUserID(c.Request.Header) var request ente.UpdateKeysRequest if err := c.ShouldBindJSON(&request); err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } token := auth.GetToken(c) err := h.UserController.UpdateKeys(c, userID, request, token) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.Status(http.StatusOK) } // SetRecoveryKey sets the recovery key attributes for a user. func (h *UserHandler) SetRecoveryKey(c *gin.Context) { userID := auth.GetUserID(c.Request.Header) var request ente.SetRecoveryKeyRequest if err := c.ShouldBindJSON(&request); err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } err := h.UserController.SetRecoveryKey(userID, request) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.Status(http.StatusOK) } // GetPublicKey returns the public key of a user func (h *UserHandler) GetPublicKey(c *gin.Context) { email := strings.ToLower(c.Query("email")) publicKey, err := h.UserController.GetPublicKey(email) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, gin.H{ "publicKey": publicKey, }) } // GetRoadmapURL redirects the user to the feedback page func (h *UserHandler) GetRoadmapURL(c *gin.Context) { userID := auth.GetUserID(c.Request.Header) redirectURL, err := h.UserController.GetRoadmapURL(userID) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.Redirect(http.StatusTemporaryRedirect, redirectURL) } // GetRoadmapURLV2 returns the jwt token attached redirect url to roadmap func (h *UserHandler) GetRoadmapURLV2(c *gin.Context) { userID := auth.GetUserID(c.Request.Header) roadmapURL, err := h.UserController.GetRoadmapURL(userID) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, gin.H{ "url": roadmapURL, }) } // GetSessionValidityV2 verifies the user's session token and returns if the user has set their keys or not func (h *UserHandler) GetSessionValidityV2(c *gin.Context) { userID := auth.GetUserID(c.Request.Header) _, err := h.UserController.GetAttributes(userID) if err == nil { c.JSON(http.StatusOK, gin.H{ "hasSetKeys": true, }) } else { if errors.Is(err, sql.ErrNoRows) { c.JSON(http.StatusOK, gin.H{ "hasSetKeys": false, }) } else { handler.Error(c, stacktrace.Propagate(err, "")) } } } // VerifyEmail validates that the OTT provided in the request is valid for the // provided email address and if yes returns the users credentials func (h *UserHandler) VerifyEmail(c *gin.Context) { var request ente.EmailVerificationRequest if err := c.ShouldBindJSON(&request); err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } response, err := h.UserController.VerifyEmail(c, request) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, response) } // ChangeEmail validates that the OTT provided in the request is valid for the // provided email address and if yes updates the user's existing email address func (h *UserHandler) ChangeEmail(c *gin.Context) { var request ente.EmailVerificationRequest if err := c.ShouldBindJSON(&request); err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } err := h.UserController.ChangeEmail(c, request) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.Status(http.StatusOK) } // GetTwoFactorStatus returns a user's two factor status func (h *UserHandler) GetTwoFactorStatus(c *gin.Context) { userID := auth.GetUserID(c.Request.Header) status, err := h.UserController.GetTwoFactorStatus(userID) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, gin.H{"status": status}) } func (h *UserHandler) GetTwoFactorRecoveryStatus(c *gin.Context) { res, err := h.UserController.GetTwoFactorRecoveryStatus(c) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, res) } // ConfigurePasskeyRecovery configures the passkey skip challenge for a user. In case the user does not // have access to passkey, the user can bypass the passkey by providing the recovery key func (h *UserHandler) ConfigurePasskeyRecovery(c *gin.Context) { var request ente.SetPasskeyRecoveryRequest if err := c.ShouldBindJSON(&request); err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } err := h.UserController.ConfigurePasskeyRecovery(c, &request) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, gin.H{}) } // SetupTwoFactor generates a two factor secret and sends it to user to setup his authenticator app with func (h *UserHandler) SetupTwoFactor(c *gin.Context) { userID := auth.GetUserID(c.Request.Header) response, err := h.UserController.SetupTwoFactor(userID) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, response) } // EnableTwoFactor handles the two factor activation request after user has setup his two factor by validing a totp request func (h *UserHandler) EnableTwoFactor(c *gin.Context) { userID := auth.GetUserID(c.Request.Header) var request ente.TwoFactorEnableRequest if err := c.ShouldBindJSON(&request); err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } err := h.UserController.EnableTwoFactor(userID, request) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.Status(http.StatusOK) } // VerifyTwoFactor handles the two factor validation request func (h *UserHandler) VerifyTwoFactor(c *gin.Context) { var request ente.TwoFactorVerificationRequest if err := c.ShouldBindJSON(&request); err != nil { handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("Failed to bind request: %s", err))) return } response, err := h.UserController.VerifyTwoFactor(c, request.SessionID, request.Code) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, response) } // BeginPasskeyRegistrationCeremony handles the request to begin the passkey registration ceremony func (h *UserHandler) BeginPasskeyAuthenticationCeremony(c *gin.Context) { var request ente.PasskeyTwoFactorBeginAuthenticationCeremonyRequest if err := c.ShouldBindJSON(&request); err != nil { handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("Failed to bind request: %s", err))) return } userID, err := h.UserController.PasskeyRepo.GetUserIDWithPasskeyTwoFactorSession(request.SessionID) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } user, err := h.UserController.UserRepo.Get(userID) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } options, _, ceremonySessionID, err := h.UserController.PasskeyRepo.CreateBeginAuthenticationData(&user) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, gin.H{ "options": options, "ceremonySessionID": ceremonySessionID, }) } func (h *UserHandler) FinishPasskeyAuthenticationCeremony(c *gin.Context) { var request ente.PasskeyTwoFactorFinishAuthenticationCeremonyRequest if err := c.ShouldBindQuery(&request); err != nil { handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("Failed to bind request: %s", err))) return } userID, err := h.UserController.PasskeyRepo.GetUserIDWithPasskeyTwoFactorSession(request.SessionID) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } user, err := h.UserController.UserRepo.Get(userID) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } err = h.UserController.PasskeyRepo.FinishAuthentication(&user, c.Request, uuid.MustParse(request.CeremonySessionID)) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } response, err := h.UserController.GetKeyAttributeAndToken(c, userID) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, response) } func (h *UserHandler) IsPasskeyRecoveryEnabled(c *gin.Context) { userID := auth.GetUserID(c.Request.Header) response, err := h.UserController.GetKeyAttributeAndToken(c, userID) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, response) } // DisableTwoFactor disables the two factor authentication for a user func (h *UserHandler) DisableTwoFactor(c *gin.Context) { userID := auth.GetUserID(c.Request.Header) err := h.UserController.DisableTwoFactor(userID) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.Status(http.StatusOK) } // RecoverTwoFactor handles the two factor recovery request by sending the // recoveryKeyEncryptedTwoFactorSecret for the user to decrypt it and make twoFactor removal api call func (h *UserHandler) RecoverTwoFactor(c *gin.Context) { sessionID := c.Query("sessionID") twoFactorType := c.Query("twoFactorType") var response *ente.TwoFactorRecoveryResponse var err error if twoFactorType == "passkey" { response, err = h.UserController.GetPasskeyRecoveryResponse(c, sessionID) } else { response, err = h.UserController.RecoverTwoFactor(sessionID) } if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, response) } // RemoveTwoFactor handles two factor deactivation request if user lost his device // by authenticating him using his twoFactorsessionToken and twoFactor secret func (h *UserHandler) RemoveTwoFactor(c *gin.Context) { var request ente.TwoFactorRemovalRequest if err := c.ShouldBindJSON(&request); err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } var response *ente.TwoFactorAuthorizationResponse var err error if request.TwoFactorType == "passkey" { response, err = h.UserController.SkipPasskeyVerification(c, &request) } else { response, err = h.UserController.RemoveTOTPTwoFactor(c, request.SessionID, request.Secret) } if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, response) } func (h *UserHandler) ReportEvent(c *gin.Context) { var request ente.EventReportRequest if err := c.ShouldBindJSON(&request); err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.Status(http.StatusOK) } func (h *UserHandler) GetPaymentToken(c *gin.Context) { userID := auth.GetUserID(c.Request.Header) token, err := h.UserController.GetJWTToken(userID, jwt.PAYMENT) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, gin.H{ "paymentToken": token, }) } func (h *UserHandler) GetFamiliesToken(c *gin.Context) { userID := auth.GetUserID(c.Request.Header) token, err := h.UserController.GetJWTToken(userID, jwt.FAMILIES) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, gin.H{ "familiesToken": token, }) } func (h *UserHandler) GetAccountsToken(c *gin.Context) { userID := auth.GetUserID(c.Request.Header) token, err := h.UserController.GetJWTToken(userID, jwt.ACCOUNTS) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, gin.H{ "accountsToken": token, }) } func (h *UserHandler) GetActiveSessions(c *gin.Context) { userID := auth.GetUserID(c.Request.Header) sessions, err := h.UserController.GetActiveSessions(c, userID) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, gin.H{ "sessions": sessions, }) } // TerminateSession removes the auth token from (instance) cache & database. func (h *UserHandler) TerminateSession(c *gin.Context) { userID := auth.GetUserID(c.Request.Header) token := c.Query("token") err := h.UserController.TerminateSession(userID, token) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, gin.H{}) } // GetDeleteChallenge responds with flag to indicate if account deletion is enabled. // When enabled, it returns a challenge/encrypted token which clients need to decrypt // and send-back while confirming deletion func (h *UserHandler) GetDeleteChallenge(c *gin.Context) { response, err := h.UserController.GetDeleteChallengeToken(c) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, response) } // DeleteUser api for deleting a user func (h *UserHandler) DeleteUser(c *gin.Context) { var request ente.DeleteAccountRequest if err := c.ShouldBindJSON(&request); err != nil { handler.Error(c, stacktrace.Propagate(err, "Could not bind request params")) return } response, err := h.UserController.SelfDeleteAccount(c, request) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, response) } // GetSRPAttributes returns the SRP attributes for a user func (h *UserHandler) GetSRPAttributes(c *gin.Context) { var request ente.GetSRPAttributesRequest if err := c.ShouldBindQuery(&request); err != nil { handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("Request binding failed %s", err))) return } response, err := h.UserController.GetSRPAttributes(c, request.Email) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, gin.H{"attributes": response}) } // SetupSRP sets the SRP attributes for a user func (h *UserHandler) SetupSRP(c *gin.Context) { var request ente.SetupSRPRequest if err := c.ShouldBindJSON(&request); err != nil { handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("Request binding failed %s", err))) return } userID := auth.GetUserID(c.Request.Header) resp, err := h.UserController.SetupSRP(c, userID, request) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, resp) } // CompleteSRPSetup completes the SRP setup for a user func (h *UserHandler) CompleteSRPSetup(c *gin.Context) { var request ente.CompleteSRPSetupRequest if err := c.ShouldBindJSON(&request); err != nil { handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("Request binding failed %s", err))) return } resp, err := h.UserController.CompleteSRPSetup(c, request) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, resp) } // UpdateSrpAndKeyAttributes updates the SRP setup for a user and key attributes func (h *UserHandler) UpdateSrpAndKeyAttributes(c *gin.Context) { var request ente.UpdateSRPAndKeysRequest if err := c.ShouldBindJSON(&request); err != nil { handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("Request binding failed %s", err))) return } userID := auth.GetUserID(c.Request.Header) // default to true clearTokens := true if request.LogOutOtherDevices != nil { clearTokens = *request.LogOutOtherDevices } resp, err := h.UserController.UpdateSrpAndKeyAttributes(c, userID, request, clearTokens) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, resp) } // CreateSRPSession set the SRP A value on the server and returns the SRP B value to the client func (h *UserHandler) CreateSRPSession(c *gin.Context) { var request ente.CreateSRPSessionRequest if err := c.ShouldBindJSON(&request); err != nil { handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("Request binding failed %s", err))) return } resp, err := h.UserController.CreateSrpSession(c, request) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, resp) } // VerifySRPSession checks the M1 value to determine if user actually knows the password func (h *UserHandler) VerifySRPSession(c *gin.Context) { var request ente.VerifySRPSessionRequest if err := c.ShouldBindJSON(&request); err != nil { handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("Request binding failed %s", err))) return } response, err := h.UserController.VerifySRPSession(c, request) if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return } c.JSON(http.StatusOK, response) }