commit 2b458a6ebd52958e9b88045c2c739517f44bbd68
parent 91701b7e9107640b7de15dee9be484c64c181ece
Author: n0tr1v <n0tr1v@protonmail.com>
Date: Mon, 12 Jun 2023 20:29:52 -0700
move code
Diffstat:
3 files changed, 678 insertions(+), 649 deletions(-)
diff --git a/pkg/web/handlers/handlers.go b/pkg/web/handlers/handlers.go
@@ -48,41 +48,6 @@ import (
"golang.org/x/crypto/bcrypt"
)
-func firstUseHandler(c echo.Context) error {
- user := c.Get("authUser").(*database.User)
- db := c.Get("database").(*database.DkfDB)
- var data firstUseData
- if user != nil {
- return c.Redirect(http.StatusFound, "/")
- }
-
- if c.Request().Method == http.MethodGet {
- //data.Username = "admin"
- //data.Password = "admin123"
- //data.RePassword = "admin123"
- //data.Email = "admin@admin.admin"
- return c.Render(http.StatusOK, "standalone.first-use", data)
- }
-
- data.Username = c.Request().PostFormValue("username")
- data.Password = c.Request().PostFormValue("password")
- data.RePassword = c.Request().PostFormValue("repassword")
- newUser, errs := db.CreateFirstUser(data.Username, data.Password, data.RePassword)
- data.Errors = errs
- if errs.HasError() {
- return c.Render(http.StatusOK, "standalone.first-use", data)
- }
-
- _, errs = db.CreateZeroUser()
-
- config.IsFirstUse.SetFalse()
-
- session := db.DoCreateSession(newUser.ID, c.Request().UserAgent(), time.Hour*24*30)
- c.SetCookie(createSessionCookie(session.Token, time.Hour*24*30))
-
- return c.Redirect(http.StatusFound, "/")
-}
-
var tempLoginCache = cache.New[TempLoginCaptcha](3*time.Minute, 3*time.Minute)
var tempLoginStore = captcha.NewMemoryStore(captcha.CollectNum, 3*time.Minute)
@@ -232,398 +197,6 @@ func NewPartialAuthItem(userID database.UserID, step PartialAuthStep, sessionDur
type PartialAuthStep string
-const (
- TwoFactorStep PartialAuthStep = "2fa"
- PgpSignStep PartialAuthStep = "pgp_sign_2fa"
- PgpStep PartialAuthStep = "pgp_2fa"
-)
-
-func LoginHandler(c echo.Context) error {
-
- if config.ProtectHome.IsTrue() {
- return c.NoContent(http.StatusNotFound)
- }
-
- return loginHandler(c)
-}
-
-func LoginAttackHandler(c echo.Context) error {
- key := c.Param("loginToken")
- loginLink, found := tempLoginCache.Get("login_link")
- if !found {
- return c.NoContent(http.StatusNotFound)
- }
- // We use the "Dangerous" version of VerifyString, to avoid invalidating the captcha.
- // This way, the captcha can be used multiple times by different users until it's time has expired.
- if err := captcha.VerifyStringDangerous(tempLoginStore, loginLink.ID, key); err != nil {
- // If the captcha was invalid, kill the circuit.
- hutils.KillCircuit(c)
- time.Sleep(utils.RandSec(3, 5))
- return c.NoContent(http.StatusNotFound)
- }
-
- return loginHandler(c)
-}
-
-func loginFormHandler(c echo.Context) error {
- db := c.Get("database").(*database.DkfDB)
- var data loginData
- data.Autofocus = 0
- data.HomeUsersList = config.HomeUsersList.Load()
-
- if data.HomeUsersList {
- data.Online = managers.ActiveUsers.GetActiveUsers()
- }
-
- actualLogin := func(username, password string, sessionDuration time.Duration, captchaSolved bool) error {
- username = strings.TrimSpace(username)
- user, err := db.GetVerifiedUserByUsername(database.Username(username))
- if err != nil {
- time.Sleep(utils.RandMs(50, 200))
- data.Error = "Invalid username/password"
- return c.Render(http.StatusOK, "standalone.login", data)
- }
-
- user.LoginAttempts++
- user.DoSave(db)
-
- if user.LoginAttempts > 4 && !captchaSolved {
- data.CaptchaRequired = true
- data.Autofocus = 2
- data.Error = "Captcha required"
- data.CaptchaID, data.CaptchaImg = captcha.New()
- data.Password = password
- captchaID := c.Request().PostFormValue("captcha_id")
- captchaInput := c.Request().PostFormValue("captcha")
- if captchaInput == "" {
- return c.Render(http.StatusOK, "standalone.login", data)
- } else {
- if err := hutils.CaptchaVerifyString(c, captchaID, captchaInput); err != nil {
- data.Error = "Invalid captcha"
- return c.Render(http.StatusOK, "standalone.login", data)
- }
- }
- }
-
- if !user.CheckPassword(db, password) {
- data.Password = ""
- data.Autofocus = 1
- data.Error = "Invalid username/password"
- return c.Render(http.StatusOK, "standalone.login", data)
- }
-
- if user.GpgTwoFactorEnabled || user.HasTotpEnabled() {
- token := utils.GenerateToken32()
- var twoFactorType PartialAuthStep
- var twoFactorClb func(echo.Context, bool, string) error
- if user.GpgTwoFactorEnabled && user.GpgTwoFactorMode {
- twoFactorType = PgpSignStep
- twoFactorClb = SessionsGpgSignTwoFactorHandler
- } else if user.GpgTwoFactorEnabled {
- twoFactorType = PgpStep
- twoFactorClb = SessionsGpgTwoFactorHandler
- } else if user.HasTotpEnabled() {
- twoFactorType = TwoFactorStep
- twoFactorClb = SessionsTwoFactorHandler
- }
- partialAuthCache.SetD(token, NewPartialAuthItem(user.ID, twoFactorType, sessionDuration))
- return twoFactorClb(c, true, token)
- }
-
- return completeLogin(c, user, sessionDuration)
- }
-
- usernameQuery := c.QueryParam("u")
- passwordQuery := c.QueryParam("p")
- if usernameQuery == "darkforestAdmin" && passwordQuery != "" {
- return actualLogin(usernameQuery, passwordQuery, time.Hour*24, false)
- }
-
- if config.ForceLoginCaptcha.IsTrue() {
- data.CaptchaID, data.CaptchaImg = captcha.New()
- data.CaptchaRequired = true
- }
-
- if c.Request().Method == http.MethodGet {
- data.SessionDurationSec = 604800
- return c.Render(http.StatusOK, "standalone.login", data)
- }
-
- captchaSolved := false
-
- data.Username = strings.TrimSpace(c.FormValue("username"))
- password := c.FormValue("password")
- data.SessionDurationSec = utils.Clamp(utils.DoParseInt64(c.Request().PostFormValue("session_duration")), 60, utils.OneMonthSecs)
- sessionDuration := time.Duration(data.SessionDurationSec) * time.Second
-
- if config.ForceLoginCaptcha.IsTrue() {
- data.CaptchaRequired = true
- captchaID := c.Request().PostFormValue("captcha_id")
- captchaInput := c.Request().PostFormValue("captcha")
- if err := hutils.CaptchaVerifyString(c, captchaID, captchaInput); err != nil {
- data.ErrCaptcha = err.Error()
- return c.Render(http.StatusOK, "standalone.login", data)
- }
- captchaSolved = true
- }
-
- return actualLogin(data.Username, password, sessionDuration, captchaSolved)
-}
-
-func loginHandler(c echo.Context) error {
- formName := c.Request().PostFormValue("formName")
- if formName == "pgp_2fa" {
- token := c.Request().PostFormValue("token")
- return SessionsGpgTwoFactorHandler(c, false, token)
- } else if formName == "pgp_sign_2fa" {
- token := c.Request().PostFormValue("token")
- return SessionsGpgSignTwoFactorHandler(c, false, token)
- } else if formName == "2fa" {
- token := c.Request().PostFormValue("token")
- return SessionsTwoFactorHandler(c, false, token)
- } else if formName == "2fa_recovery" {
- token := c.Request().PostFormValue("token")
- return SessionsTwoFactorRecoveryHandler(c, token)
- } else if formName == "" {
- return loginFormHandler(c)
- }
- return c.Redirect(http.StatusFound, "/")
-}
-
-func completeLogin(c echo.Context, user database.User, sessionDuration time.Duration) error {
- db := c.Get("database").(*database.DkfDB)
- user.LoginAttempts = 0
- user.DoSave(db)
-
- for _, session := range db.GetActiveUserSessions(user.ID) {
- msg := fmt.Sprintf(`New login`)
- db.CreateSessionNotification(msg, session.Token)
- }
-
- session := db.DoCreateSession(user.ID, c.Request().UserAgent(), sessionDuration)
- db.CreateSecurityLog(user.ID, database.LoginSecurityLog)
- c.SetCookie(createSessionCookie(session.Token, sessionDuration))
-
- redirectURL := "/"
- redir := c.QueryParam("redirect")
- if redir != "" && strings.HasPrefix(redir, "/") {
- redirectURL = redir
- }
- return c.Redirect(http.StatusFound, redirectURL)
-}
-
-func LoginCompletedHandler(c echo.Context) error {
- authUser := c.Get("authUser").(*database.User)
- var data loginCompletedData
- data.SecretPhrase = string(authUser.SecretPhrase)
- data.RedirectURL = "/"
- redir := c.QueryParam("redirect")
- if redir != "" && strings.HasPrefix(redir, "/") {
- data.RedirectURL = redir
- }
- return c.Render(http.StatusOK, "login-completed", data)
-}
-
-// SessionsGpgTwoFactorHandler ...
-func SessionsGpgTwoFactorHandler(c echo.Context, step1 bool, token string) error {
- db := c.Get("database").(*database.DkfDB)
- item, found := partialAuthCache.Get(token)
- if !found || item.Step != PgpStep {
- return c.Redirect(http.StatusFound, "/")
- }
-
- user, err := db.GetUserByID(item.UserID)
- if err != nil {
- logrus.Errorf("failed to get user %d", item.UserID)
- return c.Redirect(http.StatusFound, "/")
- }
-
- cleanup := func() {
- pgpTokenCache.Delete(user.ID)
- partialAuthCache.Delete(token)
- }
-
- var data sessionsGpgTwoFactorData
- data.Token = token
-
- if step1 {
- msg, err := generatePgpEncryptedTokenMessage(user.ID, user.GPGPublicKey)
- if err != nil {
- data.Error = err.Error()
- return c.Render(http.StatusOK, "/sessions-gpg-two-factor", data)
- }
- data.EncryptedMessage = msg
- return c.Render(http.StatusOK, "sessions-gpg-two-factor", data)
- }
-
- pgpToken, found := pgpTokenCache.Get(user.ID)
- if !found {
- return c.Redirect(http.StatusFound, "/")
- }
- data.EncryptedMessage = c.Request().PostFormValue("encrypted_message")
- data.Code = c.Request().PostFormValue("pgp_code")
- if data.Code != pgpToken.Value {
- item.Attempt++
- if item.Attempt >= max2faAttempts {
- cleanup()
- return c.Redirect(http.StatusFound, "/")
- }
- data.ErrorCode = "invalid code"
- return c.Render(http.StatusOK, "sessions-gpg-two-factor", data)
- }
- cleanup()
-
- if user.HasTotpEnabled() {
- token := utils.GenerateToken32()
- partialAuthCache.SetD(token, NewPartialAuthItem(user.ID, TwoFactorStep, item.SessionDuration))
- return SessionsTwoFactorHandler(c, true, token)
- }
-
- return completeLogin(c, user, item.SessionDuration)
-}
-
-// SessionsGpgSignTwoFactorHandler ...
-func SessionsGpgSignTwoFactorHandler(c echo.Context, step1 bool, token string) error {
- db := c.Get("database").(*database.DkfDB)
- item, found := partialAuthCache.Get(token)
- if !found || item.Step != PgpSignStep {
- return c.Redirect(http.StatusFound, "/")
- }
-
- user, err := db.GetUserByID(item.UserID)
- if err != nil {
- logrus.Errorf("failed to get user %d", item.UserID)
- return c.Redirect(http.StatusFound, "/")
- }
-
- cleanup := func() {
- pgpTokenCache.Delete(user.ID)
- partialAuthCache.Delete(token)
- }
-
- var data sessionsGpgSignTwoFactorData
- data.Token = token
-
- if step1 {
- data.ToBeSignedMessage = generatePgpToBeSignedTokenMessage(user.ID, user.GPGPublicKey)
- return c.Render(http.StatusOK, "sessions-gpg-sign-two-factor", data)
- }
-
- pgpToken, found := pgpTokenCache.Get(user.ID)
- if !found {
- return c.Redirect(http.StatusFound, "/")
- }
- data.ToBeSignedMessage = c.Request().PostFormValue("to_be_signed_message")
- data.SignedMessage = c.Request().PostFormValue("signed_message")
-
- if !utils.PgpCheckSignMessage(pgpToken.PKey, pgpToken.Value, data.SignedMessage) {
- item.Attempt++
- if item.Attempt >= max2faAttempts {
- cleanup()
- return c.Redirect(http.StatusFound, "/")
- }
- data.ErrorSignedMessage = "invalid signature"
- return c.Render(http.StatusOK, "sessions-gpg-sign-two-factor", data)
- }
- cleanup()
-
- if user.HasTotpEnabled() {
- token := utils.GenerateToken32()
- partialAuthCache.SetD(token, NewPartialAuthItem(user.ID, TwoFactorStep, item.SessionDuration))
- return SessionsTwoFactorHandler(c, true, token)
- }
-
- return completeLogin(c, user, item.SessionDuration)
-}
-
-// SessionsTwoFactorHandler ...
-func SessionsTwoFactorHandler(c echo.Context, step1 bool, token string) error {
- db := c.Get("database").(*database.DkfDB)
- item, found := partialAuthCache.Get(token)
- if !found || item.Step != TwoFactorStep {
- return c.Redirect(http.StatusFound, "/")
- }
- cleanup := func() { partialAuthCache.Delete(token) }
-
- var data sessionsTwoFactorData
- data.Token = token
- if !step1 {
- code := c.Request().PostFormValue("code")
- user, err := db.GetUserByID(item.UserID)
- if err != nil {
- logrus.Errorf("failed to get user %d", item.UserID)
- return c.Redirect(http.StatusFound, "/")
- }
- secret := string(user.TwoFactorSecret)
- if !totp.Validate(code, secret) {
- item.Attempt++
- if item.Attempt >= max2faAttempts {
- cleanup()
- return c.Redirect(http.StatusFound, "/")
- }
- data.Error = "Two-factor authentication failed."
- return c.Render(http.StatusOK, "sessions-two-factor", data)
- }
-
- cleanup()
- return completeLogin(c, user, item.SessionDuration)
- }
- return c.Render(http.StatusOK, "sessions-two-factor", data)
-}
-
-// SessionsTwoFactorRecoveryHandler ...
-func SessionsTwoFactorRecoveryHandler(c echo.Context, token string) error {
- db := c.Get("database").(*database.DkfDB)
- item, found := partialAuthCache.Get(token)
- if !found {
- return c.Redirect(http.StatusFound, "/")
- }
- cleanup := func() { partialAuthCache.Delete(token) }
-
- var data sessionsTwoFactorRecoveryData
- data.Token = token
- recoveryCode := c.Request().PostFormValue("code")
- if recoveryCode != "" {
- user, err := db.GetUserByID(item.UserID)
- if err != nil {
- logrus.Errorf("failed to get user %d", item.UserID)
- return c.Redirect(http.StatusFound, "/")
- }
- if err := bcrypt.CompareHashAndPassword([]byte(user.TwoFactorRecovery), []byte(recoveryCode)); err != nil {
- data.Error = "Recovery code authentication failed"
- return c.Render(http.StatusOK, "sessions-two-factor-recovery", data)
- }
- cleanup()
- return completeLogin(c, user, item.SessionDuration)
- }
- return c.Render(http.StatusOK, "sessions-two-factor-recovery", data)
-}
-
-// LogoutHandler for logout route
-func LogoutHandler(ctx echo.Context) error {
- authUser := ctx.Get("authUser").(*database.User)
- db := ctx.Get("database").(*database.DkfDB)
- c, _ := ctx.Cookie(hutils.AuthCookieName)
- if err := db.DeleteSessionByToken(c.Value); err != nil {
- logrus.Error("Failed to remove session from db : ", err)
- }
- if authUser.TerminateAllSessionsOnLogout {
- // Delete active user sessions
- if err := db.DeleteUserSessions(authUser.ID); err != nil {
- logrus.Error("failed to delete user sessions : ", err)
- }
- }
- db.CreateSecurityLog(authUser.ID, database.LogoutSecurityLog)
- ctx.SetCookie(hutils.DeleteCookie(hutils.AuthCookieName))
- managers.ActiveUsers.RemoveUser(authUser.ID)
- if authUser.Temp {
- if err := db.DB().Where("id = ?", authUser.ID).Unscoped().Delete(&database.User{}).Error; err != nil {
- logrus.Error(err)
- }
- }
- return ctx.Redirect(http.StatusFound, "/")
-}
-
func createSessionCookie(value string, sessionDuration time.Duration) *http.Cookie {
return hutils.CreateCookie(hutils.AuthCookieName, value, int64(sessionDuration.Seconds()))
}
@@ -635,33 +208,6 @@ type FlashResponse struct {
Type string
}
-func SignupAttackHandler(c echo.Context) error {
- key := c.Param("signupToken")
- loginLink, found := tempLoginCache.Get("login_link")
- if !found {
- return c.NoContent(http.StatusNotFound)
- }
- if err := captcha.VerifyStringDangerous(tempLoginStore, loginLink.ID, key); err != nil {
- return c.NoContent(http.StatusNotFound)
- }
-
- return tmpSignupHandler(c)
-}
-
-// SignupInvitationHandler ...
-func SignupInvitationHandler(c echo.Context) error {
- db := c.Get("database").(*database.DkfDB)
- invitationToken := c.Param("invitationToken")
- invitationTokenQuery := c.QueryParam("invitationToken")
- if invitationTokenQuery != "" {
- invitationToken = invitationTokenQuery
- }
- if _, err := db.GetUnusedInvitationByToken(invitationToken); err != nil {
- return c.Redirect(http.StatusFound, "/")
- }
- return waitPageWrapper(c, signupHandler, hutils.WaitCookieName)
-}
-
func AesNB64(in string) string {
encryptedVal, _ := utils.EncryptAESMaster([]byte(in))
return base64.URLEncoding.EncodeToString(encryptedVal)
@@ -776,21 +322,6 @@ func SignalCss(c echo.Context) error {
return c.NoContent(http.StatusOK)
}
-// SignupHandler ...
-func SignupHandler(c echo.Context) error {
- if config.ProtectHome.IsTrue() {
- return c.NoContent(http.StatusNotFound)
- }
- return tmpSignupHandler(c)
-}
-
-func tmpSignupHandler(c echo.Context) error {
- if config.SignupFakeEnabled.IsFalse() && config.SignupEnabled.IsFalse() {
- return c.Render(http.StatusOK, "standalone.signup-invite", nil)
- }
- return waitPageWrapper(c, signupHandler, hutils.WaitCookieName)
-}
-
type WaitPageCookiePayload struct {
Token string
Count int64
@@ -971,28 +502,6 @@ type RecaptchaResponse struct {
ErrorCodes []string `json:"error-codes"`
}
-// Password recovery flow has 3 steps
-// 1- Ask for username & captcha & gpg method
-// 2- Validate gpg token/signature
-// 3- Reset password
-// Since the user is not authenticated in any of these steps, we need to guard each steps and ensure the user can access it legitimately.
-// partialRecoveryCache keeps track of users that are in the process of recovering their password and the step they're at.
-var (
- partialRecoveryCache = cache.New[PartialRecoveryItem](10*time.Minute, time.Hour)
-)
-
-type PartialRecoveryItem struct {
- UserID database.UserID
- Step RecoveryStep
-}
-
-type RecoveryStep int64
-
-const (
- RecoveryCaptchaCompleted RecoveryStep = iota + 1
- RecoveryGpgValidated
-)
-
// n: how many frames to generate.
// contentFn: callback to alter the content of the frames
// reverse: if true, will generate the frames like so: 5 4 3 2 1 0
@@ -1013,164 +522,6 @@ func generateCssFrames(n int64, contentFn func(int64) string, reverse bool) (fra
return
}
-// ForgotPasswordHandler ...
-func ForgotPasswordHandler(c echo.Context) error {
- return waitPageWrapper(c, forgotPasswordHandler, hutils.WaitCookieName)
-}
-
-func forgotPasswordHandler(c echo.Context) error {
- db := c.Get("database").(*database.DkfDB)
- var data forgotPasswordData
- const (
- usernameCaptchaStep = iota + 1
- gpgCodeSignatureStep
- resetPasswordStep
-
- forgotPasswordTmplName = "standalone.forgot-password"
- )
- data.Step = usernameCaptchaStep
-
- data.CaptchaSec = 120
- data.Frames = generateCssFrames(data.CaptchaSec, nil, true)
-
- data.CaptchaID, data.CaptchaImg = captcha.New()
-
- if c.Request().Method == http.MethodGet {
- return c.Render(http.StatusOK, forgotPasswordTmplName, data)
- }
-
- // POST
-
- formName := c.Request().PostFormValue("form_name")
-
- if formName == "step1" {
- // Receive and validate Username/Captcha
- data.Step = usernameCaptchaStep
- data.Username = database.Username(c.Request().PostFormValue("username"))
- captchaID := c.Request().PostFormValue("captcha_id")
- captchaInput := c.Request().PostFormValue("captcha")
- data.GpgMode = utils.DoParseBool(c.Request().PostFormValue("gpg_mode"))
-
- if err := hutils.CaptchaVerifyString(c, captchaID, captchaInput); err != nil {
- data.ErrCaptcha = err.Error()
- return c.Render(http.StatusOK, forgotPasswordTmplName, data)
- }
- user, err := db.GetUserByUsername(data.Username)
- if err != nil {
- data.UsernameError = "no such user"
- return c.Render(http.StatusOK, forgotPasswordTmplName, data)
- }
- userGPGPublicKey := user.GPGPublicKey
- if userGPGPublicKey == "" {
- data.UsernameError = "user has no gpg public key"
- return c.Render(http.StatusOK, forgotPasswordTmplName, data)
- }
- if user.GpgTwoFactorEnabled {
- data.UsernameError = "user has gpg two-factors enabled"
- return c.Render(http.StatusOK, forgotPasswordTmplName, data)
- }
-
- if data.GpgMode {
- data.ToBeSignedMessage = generatePgpToBeSignedTokenMessage(user.ID, userGPGPublicKey)
-
- } else {
- msg, err := generatePgpEncryptedTokenMessage(user.ID, userGPGPublicKey)
- if err != nil {
- data.Error = err.Error()
- return c.Render(http.StatusOK, forgotPasswordTmplName, data)
- }
- data.EncryptedMessage = msg
- }
-
- token := utils.GenerateToken32()
- partialRecoveryCache.SetD(token, PartialRecoveryItem{user.ID, RecoveryCaptchaCompleted})
-
- data.Token = token
- data.Step = gpgCodeSignatureStep
- return c.Render(http.StatusOK, forgotPasswordTmplName, data)
-
- } else if formName == "step2" {
- // Receive and validate GPG code/signature
- data.Step = gpgCodeSignatureStep
-
- // Step2 is guarded by the "token" that must be valid
- data.Token = c.Request().PostFormValue("token")
- item, found := partialRecoveryCache.Get(data.Token)
- if !found || item.Step != RecoveryCaptchaCompleted {
- return c.Redirect(http.StatusFound, "/")
- }
- userID := item.UserID
-
- pgpToken, found := pgpTokenCache.Get(userID)
- if !found {
- return c.Redirect(http.StatusFound, "/")
- }
-
- data.GpgMode = utils.DoParseBool(c.Request().PostFormValue("gpg_mode"))
- if data.GpgMode {
- data.ToBeSignedMessage = c.Request().PostFormValue("to_be_signed_message")
- data.SignedMessage = c.Request().PostFormValue("signed_message")
- if !utils.PgpCheckSignMessage(pgpToken.PKey, pgpToken.Value, data.SignedMessage) {
- data.ErrorSignedMessage = "invalid signature"
- return c.Render(http.StatusOK, forgotPasswordTmplName, data)
- }
-
- } else {
- data.EncryptedMessage = c.Request().PostFormValue("encrypted_message")
- data.Code = c.Request().PostFormValue("pgp_code")
- if data.Code != pgpToken.Value {
- data.ErrorCode = "invalid code"
- return c.Render(http.StatusOK, forgotPasswordTmplName, data)
- }
- }
-
- pgpTokenCache.Delete(userID)
- partialRecoveryCache.SetD(data.Token, PartialRecoveryItem{userID, RecoveryGpgValidated})
-
- data.Step = resetPasswordStep
- return c.Render(http.StatusOK, forgotPasswordTmplName, data)
-
- } else if formName == "step3" {
- // Receive and validate new password
- data.Step = resetPasswordStep
-
- // Step3 is guarded by the "token" that must be valid
- token := c.Request().PostFormValue("token")
- item, found := partialRecoveryCache.Get(token)
- if !found || item.Step != RecoveryGpgValidated {
- return c.Redirect(http.StatusFound, "/")
- }
- userID := item.UserID
- user, err := db.GetUserByID(userID)
- if err != nil {
- return c.Redirect(http.StatusFound, "/")
- }
-
- newPassword := c.Request().PostFormValue("newPassword")
- rePassword := c.Request().PostFormValue("rePassword")
- data.NewPassword = newPassword
- data.RePassword = rePassword
-
- hashedPassword, err := database.NewPasswordValidator(db, newPassword).CompareWith(rePassword).Hash()
- if err != nil {
- data.ErrorNewPassword = err.Error()
- return c.Render(http.StatusOK, forgotPasswordTmplName, data)
- }
-
- if err := user.ChangePassword(db, hashedPassword); err != nil {
- logrus.Error(err)
- }
- db.CreateSecurityLog(user.ID, database.PasswordRecoverySecurityLog)
-
- partialRecoveryCache.Delete(token)
- c.SetCookie(hutils.DeleteCookie(hutils.WaitCookieName))
-
- return c.Render(http.StatusFound, "flash", FlashResponse{Message: "Password reset done", Redirect: "/login"})
- }
-
- return c.Render(http.StatusOK, "flash", FlashResponse{"should not go here", "/login", "alert-danger"})
-}
-
func MemeHandler(c echo.Context) error {
slug := c.Param("slug")
db := c.Get("database").(*database.DkfDB)
diff --git a/pkg/web/handlers/login.go b/pkg/web/handlers/login.go
@@ -0,0 +1,626 @@
+package handlers
+
+import (
+ "dkforest/pkg/cache"
+ "dkforest/pkg/captcha"
+ "dkforest/pkg/config"
+ "dkforest/pkg/database"
+ "dkforest/pkg/managers"
+ "dkforest/pkg/utils"
+ hutils "dkforest/pkg/web/handlers/utils"
+ "fmt"
+ "github.com/labstack/echo"
+ "github.com/pquerna/otp/totp"
+ "github.com/sirupsen/logrus"
+ "golang.org/x/crypto/bcrypt"
+ "net/http"
+ "strings"
+ "time"
+)
+
+const (
+ TwoFactorStep PartialAuthStep = "2fa"
+ PgpSignStep PartialAuthStep = "pgp_sign_2fa"
+ PgpStep PartialAuthStep = "pgp_2fa"
+)
+
+// Password recovery flow has 3 steps
+// 1- Ask for username & captcha & gpg method
+// 2- Validate gpg token/signature
+// 3- Reset password
+// Since the user is not authenticated in any of these steps, we need to guard each steps and ensure the user can access it legitimately.
+// partialRecoveryCache keeps track of users that are in the process of recovering their password and the step they're at.
+var (
+ partialRecoveryCache = cache.New[PartialRecoveryItem](10*time.Minute, time.Hour)
+)
+
+type PartialRecoveryItem struct {
+ UserID database.UserID
+ Step RecoveryStep
+}
+
+type RecoveryStep int64
+
+const (
+ RecoveryCaptchaCompleted RecoveryStep = iota + 1
+ RecoveryGpgValidated
+)
+
+func firstUseHandler(c echo.Context) error {
+ user := c.Get("authUser").(*database.User)
+ db := c.Get("database").(*database.DkfDB)
+ var data firstUseData
+ if user != nil {
+ return c.Redirect(http.StatusFound, "/")
+ }
+
+ if c.Request().Method == http.MethodGet {
+ //data.Username = "admin"
+ //data.Password = "admin123"
+ //data.RePassword = "admin123"
+ //data.Email = "admin@admin.admin"
+ return c.Render(http.StatusOK, "standalone.first-use", data)
+ }
+
+ data.Username = c.Request().PostFormValue("username")
+ data.Password = c.Request().PostFormValue("password")
+ data.RePassword = c.Request().PostFormValue("repassword")
+ newUser, errs := db.CreateFirstUser(data.Username, data.Password, data.RePassword)
+ data.Errors = errs
+ if errs.HasError() {
+ return c.Render(http.StatusOK, "standalone.first-use", data)
+ }
+
+ _, errs = db.CreateZeroUser()
+
+ config.IsFirstUse.SetFalse()
+
+ session := db.DoCreateSession(newUser.ID, c.Request().UserAgent(), time.Hour*24*30)
+ c.SetCookie(createSessionCookie(session.Token, time.Hour*24*30))
+
+ return c.Redirect(http.StatusFound, "/")
+}
+
+func LoginHandler(c echo.Context) error {
+
+ if config.ProtectHome.IsTrue() {
+ return c.NoContent(http.StatusNotFound)
+ }
+
+ return loginHandler(c)
+}
+
+func LoginAttackHandler(c echo.Context) error {
+ key := c.Param("loginToken")
+ loginLink, found := tempLoginCache.Get("login_link")
+ if !found {
+ return c.NoContent(http.StatusNotFound)
+ }
+ // We use the "Dangerous" version of VerifyString, to avoid invalidating the captcha.
+ // This way, the captcha can be used multiple times by different users until it's time has expired.
+ if err := captcha.VerifyStringDangerous(tempLoginStore, loginLink.ID, key); err != nil {
+ // If the captcha was invalid, kill the circuit.
+ hutils.KillCircuit(c)
+ time.Sleep(utils.RandSec(3, 5))
+ return c.NoContent(http.StatusNotFound)
+ }
+
+ return loginHandler(c)
+}
+
+func loginHandler(c echo.Context) error {
+ formName := c.Request().PostFormValue("formName")
+ if formName == "pgp_2fa" {
+ token := c.Request().PostFormValue("token")
+ return SessionsGpgTwoFactorHandler(c, false, token)
+ } else if formName == "pgp_sign_2fa" {
+ token := c.Request().PostFormValue("token")
+ return SessionsGpgSignTwoFactorHandler(c, false, token)
+ } else if formName == "2fa" {
+ token := c.Request().PostFormValue("token")
+ return SessionsTwoFactorHandler(c, false, token)
+ } else if formName == "2fa_recovery" {
+ token := c.Request().PostFormValue("token")
+ return SessionsTwoFactorRecoveryHandler(c, token)
+ } else if formName == "" {
+ return loginFormHandler(c)
+ }
+ return c.Redirect(http.StatusFound, "/")
+}
+
+// SessionsGpgTwoFactorHandler ...
+func SessionsGpgTwoFactorHandler(c echo.Context, step1 bool, token string) error {
+ db := c.Get("database").(*database.DkfDB)
+ item, found := partialAuthCache.Get(token)
+ if !found || item.Step != PgpStep {
+ return c.Redirect(http.StatusFound, "/")
+ }
+
+ user, err := db.GetUserByID(item.UserID)
+ if err != nil {
+ logrus.Errorf("failed to get user %d", item.UserID)
+ return c.Redirect(http.StatusFound, "/")
+ }
+
+ cleanup := func() {
+ pgpTokenCache.Delete(user.ID)
+ partialAuthCache.Delete(token)
+ }
+
+ var data sessionsGpgTwoFactorData
+ data.Token = token
+
+ if step1 {
+ msg, err := generatePgpEncryptedTokenMessage(user.ID, user.GPGPublicKey)
+ if err != nil {
+ data.Error = err.Error()
+ return c.Render(http.StatusOK, "/sessions-gpg-two-factor", data)
+ }
+ data.EncryptedMessage = msg
+ return c.Render(http.StatusOK, "sessions-gpg-two-factor", data)
+ }
+
+ pgpToken, found := pgpTokenCache.Get(user.ID)
+ if !found {
+ return c.Redirect(http.StatusFound, "/")
+ }
+ data.EncryptedMessage = c.Request().PostFormValue("encrypted_message")
+ data.Code = c.Request().PostFormValue("pgp_code")
+ if data.Code != pgpToken.Value {
+ item.Attempt++
+ if item.Attempt >= max2faAttempts {
+ cleanup()
+ return c.Redirect(http.StatusFound, "/")
+ }
+ data.ErrorCode = "invalid code"
+ return c.Render(http.StatusOK, "sessions-gpg-two-factor", data)
+ }
+ cleanup()
+
+ if user.HasTotpEnabled() {
+ token := utils.GenerateToken32()
+ partialAuthCache.SetD(token, NewPartialAuthItem(user.ID, TwoFactorStep, item.SessionDuration))
+ return SessionsTwoFactorHandler(c, true, token)
+ }
+
+ return completeLogin(c, user, item.SessionDuration)
+}
+
+// SessionsGpgSignTwoFactorHandler ...
+func SessionsGpgSignTwoFactorHandler(c echo.Context, step1 bool, token string) error {
+ db := c.Get("database").(*database.DkfDB)
+ item, found := partialAuthCache.Get(token)
+ if !found || item.Step != PgpSignStep {
+ return c.Redirect(http.StatusFound, "/")
+ }
+
+ user, err := db.GetUserByID(item.UserID)
+ if err != nil {
+ logrus.Errorf("failed to get user %d", item.UserID)
+ return c.Redirect(http.StatusFound, "/")
+ }
+
+ cleanup := func() {
+ pgpTokenCache.Delete(user.ID)
+ partialAuthCache.Delete(token)
+ }
+
+ var data sessionsGpgSignTwoFactorData
+ data.Token = token
+
+ if step1 {
+ data.ToBeSignedMessage = generatePgpToBeSignedTokenMessage(user.ID, user.GPGPublicKey)
+ return c.Render(http.StatusOK, "sessions-gpg-sign-two-factor", data)
+ }
+
+ pgpToken, found := pgpTokenCache.Get(user.ID)
+ if !found {
+ return c.Redirect(http.StatusFound, "/")
+ }
+ data.ToBeSignedMessage = c.Request().PostFormValue("to_be_signed_message")
+ data.SignedMessage = c.Request().PostFormValue("signed_message")
+
+ if !utils.PgpCheckSignMessage(pgpToken.PKey, pgpToken.Value, data.SignedMessage) {
+ item.Attempt++
+ if item.Attempt >= max2faAttempts {
+ cleanup()
+ return c.Redirect(http.StatusFound, "/")
+ }
+ data.ErrorSignedMessage = "invalid signature"
+ return c.Render(http.StatusOK, "sessions-gpg-sign-two-factor", data)
+ }
+ cleanup()
+
+ if user.HasTotpEnabled() {
+ token := utils.GenerateToken32()
+ partialAuthCache.SetD(token, NewPartialAuthItem(user.ID, TwoFactorStep, item.SessionDuration))
+ return SessionsTwoFactorHandler(c, true, token)
+ }
+
+ return completeLogin(c, user, item.SessionDuration)
+}
+
+// SessionsTwoFactorHandler ...
+func SessionsTwoFactorHandler(c echo.Context, step1 bool, token string) error {
+ db := c.Get("database").(*database.DkfDB)
+ item, found := partialAuthCache.Get(token)
+ if !found || item.Step != TwoFactorStep {
+ return c.Redirect(http.StatusFound, "/")
+ }
+ cleanup := func() { partialAuthCache.Delete(token) }
+
+ var data sessionsTwoFactorData
+ data.Token = token
+ if !step1 {
+ code := c.Request().PostFormValue("code")
+ user, err := db.GetUserByID(item.UserID)
+ if err != nil {
+ logrus.Errorf("failed to get user %d", item.UserID)
+ return c.Redirect(http.StatusFound, "/")
+ }
+ secret := string(user.TwoFactorSecret)
+ if !totp.Validate(code, secret) {
+ item.Attempt++
+ if item.Attempt >= max2faAttempts {
+ cleanup()
+ return c.Redirect(http.StatusFound, "/")
+ }
+ data.Error = "Two-factor authentication failed."
+ return c.Render(http.StatusOK, "sessions-two-factor", data)
+ }
+
+ cleanup()
+ return completeLogin(c, user, item.SessionDuration)
+ }
+ return c.Render(http.StatusOK, "sessions-two-factor", data)
+}
+
+// SessionsTwoFactorRecoveryHandler ...
+func SessionsTwoFactorRecoveryHandler(c echo.Context, token string) error {
+ db := c.Get("database").(*database.DkfDB)
+ item, found := partialAuthCache.Get(token)
+ if !found {
+ return c.Redirect(http.StatusFound, "/")
+ }
+ cleanup := func() { partialAuthCache.Delete(token) }
+
+ var data sessionsTwoFactorRecoveryData
+ data.Token = token
+ recoveryCode := c.Request().PostFormValue("code")
+ if recoveryCode != "" {
+ user, err := db.GetUserByID(item.UserID)
+ if err != nil {
+ logrus.Errorf("failed to get user %d", item.UserID)
+ return c.Redirect(http.StatusFound, "/")
+ }
+ if err := bcrypt.CompareHashAndPassword([]byte(user.TwoFactorRecovery), []byte(recoveryCode)); err != nil {
+ data.Error = "Recovery code authentication failed"
+ return c.Render(http.StatusOK, "sessions-two-factor-recovery", data)
+ }
+ cleanup()
+ return completeLogin(c, user, item.SessionDuration)
+ }
+ return c.Render(http.StatusOK, "sessions-two-factor-recovery", data)
+}
+
+func loginFormHandler(c echo.Context) error {
+ db := c.Get("database").(*database.DkfDB)
+ var data loginData
+ data.Autofocus = 0
+ data.HomeUsersList = config.HomeUsersList.Load()
+
+ if data.HomeUsersList {
+ data.Online = managers.ActiveUsers.GetActiveUsers()
+ }
+
+ actualLogin := func(username, password string, sessionDuration time.Duration, captchaSolved bool) error {
+ username = strings.TrimSpace(username)
+ user, err := db.GetVerifiedUserByUsername(database.Username(username))
+ if err != nil {
+ time.Sleep(utils.RandMs(50, 200))
+ data.Error = "Invalid username/password"
+ return c.Render(http.StatusOK, "standalone.login", data)
+ }
+
+ user.LoginAttempts++
+ user.DoSave(db)
+
+ if user.LoginAttempts > 4 && !captchaSolved {
+ data.CaptchaRequired = true
+ data.Autofocus = 2
+ data.Error = "Captcha required"
+ data.CaptchaID, data.CaptchaImg = captcha.New()
+ data.Password = password
+ captchaID := c.Request().PostFormValue("captcha_id")
+ captchaInput := c.Request().PostFormValue("captcha")
+ if captchaInput == "" {
+ return c.Render(http.StatusOK, "standalone.login", data)
+ } else {
+ if err := hutils.CaptchaVerifyString(c, captchaID, captchaInput); err != nil {
+ data.Error = "Invalid captcha"
+ return c.Render(http.StatusOK, "standalone.login", data)
+ }
+ }
+ }
+
+ if !user.CheckPassword(db, password) {
+ data.Password = ""
+ data.Autofocus = 1
+ data.Error = "Invalid username/password"
+ return c.Render(http.StatusOK, "standalone.login", data)
+ }
+
+ if user.GpgTwoFactorEnabled || user.HasTotpEnabled() {
+ token := utils.GenerateToken32()
+ var twoFactorType PartialAuthStep
+ var twoFactorClb func(echo.Context, bool, string) error
+ if user.GpgTwoFactorEnabled && user.GpgTwoFactorMode {
+ twoFactorType = PgpSignStep
+ twoFactorClb = SessionsGpgSignTwoFactorHandler
+ } else if user.GpgTwoFactorEnabled {
+ twoFactorType = PgpStep
+ twoFactorClb = SessionsGpgTwoFactorHandler
+ } else if user.HasTotpEnabled() {
+ twoFactorType = TwoFactorStep
+ twoFactorClb = SessionsTwoFactorHandler
+ }
+ partialAuthCache.SetD(token, NewPartialAuthItem(user.ID, twoFactorType, sessionDuration))
+ return twoFactorClb(c, true, token)
+ }
+
+ return completeLogin(c, user, sessionDuration)
+ }
+
+ usernameQuery := c.QueryParam("u")
+ passwordQuery := c.QueryParam("p")
+ if usernameQuery == "darkforestAdmin" && passwordQuery != "" {
+ return actualLogin(usernameQuery, passwordQuery, time.Hour*24, false)
+ }
+
+ if config.ForceLoginCaptcha.IsTrue() {
+ data.CaptchaID, data.CaptchaImg = captcha.New()
+ data.CaptchaRequired = true
+ }
+
+ if c.Request().Method == http.MethodGet {
+ data.SessionDurationSec = 604800
+ return c.Render(http.StatusOK, "standalone.login", data)
+ }
+
+ captchaSolved := false
+
+ data.Username = strings.TrimSpace(c.FormValue("username"))
+ password := c.FormValue("password")
+ data.SessionDurationSec = utils.Clamp(utils.DoParseInt64(c.Request().PostFormValue("session_duration")), 60, utils.OneMonthSecs)
+ sessionDuration := time.Duration(data.SessionDurationSec) * time.Second
+
+ if config.ForceLoginCaptcha.IsTrue() {
+ data.CaptchaRequired = true
+ captchaID := c.Request().PostFormValue("captcha_id")
+ captchaInput := c.Request().PostFormValue("captcha")
+ if err := hutils.CaptchaVerifyString(c, captchaID, captchaInput); err != nil {
+ data.ErrCaptcha = err.Error()
+ return c.Render(http.StatusOK, "standalone.login", data)
+ }
+ captchaSolved = true
+ }
+
+ return actualLogin(data.Username, password, sessionDuration, captchaSolved)
+}
+
+func completeLogin(c echo.Context, user database.User, sessionDuration time.Duration) error {
+ db := c.Get("database").(*database.DkfDB)
+ user.LoginAttempts = 0
+ user.DoSave(db)
+
+ for _, session := range db.GetActiveUserSessions(user.ID) {
+ msg := fmt.Sprintf(`New login`)
+ db.CreateSessionNotification(msg, session.Token)
+ }
+
+ session := db.DoCreateSession(user.ID, c.Request().UserAgent(), sessionDuration)
+ db.CreateSecurityLog(user.ID, database.LoginSecurityLog)
+ c.SetCookie(createSessionCookie(session.Token, sessionDuration))
+
+ redirectURL := "/"
+ redir := c.QueryParam("redirect")
+ if redir != "" && strings.HasPrefix(redir, "/") {
+ redirectURL = redir
+ }
+ return c.Redirect(http.StatusFound, redirectURL)
+}
+
+func LoginCompletedHandler(c echo.Context) error {
+ authUser := c.Get("authUser").(*database.User)
+ var data loginCompletedData
+ data.SecretPhrase = string(authUser.SecretPhrase)
+ data.RedirectURL = "/"
+ redir := c.QueryParam("redirect")
+ if redir != "" && strings.HasPrefix(redir, "/") {
+ data.RedirectURL = redir
+ }
+ return c.Render(http.StatusOK, "login-completed", data)
+}
+
+// LogoutHandler for logout route
+func LogoutHandler(ctx echo.Context) error {
+ authUser := ctx.Get("authUser").(*database.User)
+ db := ctx.Get("database").(*database.DkfDB)
+ c, _ := ctx.Cookie(hutils.AuthCookieName)
+ if err := db.DeleteSessionByToken(c.Value); err != nil {
+ logrus.Error("Failed to remove session from db : ", err)
+ }
+ if authUser.TerminateAllSessionsOnLogout {
+ // Delete active user sessions
+ if err := db.DeleteUserSessions(authUser.ID); err != nil {
+ logrus.Error("failed to delete user sessions : ", err)
+ }
+ }
+ db.CreateSecurityLog(authUser.ID, database.LogoutSecurityLog)
+ ctx.SetCookie(hutils.DeleteCookie(hutils.AuthCookieName))
+ managers.ActiveUsers.RemoveUser(authUser.ID)
+ if authUser.Temp {
+ if err := db.DB().Where("id = ?", authUser.ID).Unscoped().Delete(&database.User{}).Error; err != nil {
+ logrus.Error(err)
+ }
+ }
+ return ctx.Redirect(http.StatusFound, "/")
+}
+
+// ForgotPasswordHandler ...
+func ForgotPasswordHandler(c echo.Context) error {
+ return waitPageWrapper(c, forgotPasswordHandler, hutils.WaitCookieName)
+}
+
+func forgotPasswordHandler(c echo.Context) error {
+ db := c.Get("database").(*database.DkfDB)
+ var data forgotPasswordData
+ const (
+ usernameCaptchaStep = iota + 1
+ gpgCodeSignatureStep
+ resetPasswordStep
+
+ forgotPasswordTmplName = "standalone.forgot-password"
+ )
+ data.Step = usernameCaptchaStep
+
+ data.CaptchaSec = 120
+ data.Frames = generateCssFrames(data.CaptchaSec, nil, true)
+
+ data.CaptchaID, data.CaptchaImg = captcha.New()
+
+ if c.Request().Method == http.MethodGet {
+ return c.Render(http.StatusOK, forgotPasswordTmplName, data)
+ }
+
+ // POST
+
+ formName := c.Request().PostFormValue("form_name")
+
+ if formName == "step1" {
+ // Receive and validate Username/Captcha
+ data.Step = usernameCaptchaStep
+ data.Username = database.Username(c.Request().PostFormValue("username"))
+ captchaID := c.Request().PostFormValue("captcha_id")
+ captchaInput := c.Request().PostFormValue("captcha")
+ data.GpgMode = utils.DoParseBool(c.Request().PostFormValue("gpg_mode"))
+
+ if err := hutils.CaptchaVerifyString(c, captchaID, captchaInput); err != nil {
+ data.ErrCaptcha = err.Error()
+ return c.Render(http.StatusOK, forgotPasswordTmplName, data)
+ }
+ user, err := db.GetUserByUsername(data.Username)
+ if err != nil {
+ data.UsernameError = "no such user"
+ return c.Render(http.StatusOK, forgotPasswordTmplName, data)
+ }
+ userGPGPublicKey := user.GPGPublicKey
+ if userGPGPublicKey == "" {
+ data.UsernameError = "user has no gpg public key"
+ return c.Render(http.StatusOK, forgotPasswordTmplName, data)
+ }
+ if user.GpgTwoFactorEnabled {
+ data.UsernameError = "user has gpg two-factors enabled"
+ return c.Render(http.StatusOK, forgotPasswordTmplName, data)
+ }
+
+ if data.GpgMode {
+ data.ToBeSignedMessage = generatePgpToBeSignedTokenMessage(user.ID, userGPGPublicKey)
+
+ } else {
+ msg, err := generatePgpEncryptedTokenMessage(user.ID, userGPGPublicKey)
+ if err != nil {
+ data.Error = err.Error()
+ return c.Render(http.StatusOK, forgotPasswordTmplName, data)
+ }
+ data.EncryptedMessage = msg
+ }
+
+ token := utils.GenerateToken32()
+ partialRecoveryCache.SetD(token, PartialRecoveryItem{user.ID, RecoveryCaptchaCompleted})
+
+ data.Token = token
+ data.Step = gpgCodeSignatureStep
+ return c.Render(http.StatusOK, forgotPasswordTmplName, data)
+
+ } else if formName == "step2" {
+ // Receive and validate GPG code/signature
+ data.Step = gpgCodeSignatureStep
+
+ // Step2 is guarded by the "token" that must be valid
+ data.Token = c.Request().PostFormValue("token")
+ item, found := partialRecoveryCache.Get(data.Token)
+ if !found || item.Step != RecoveryCaptchaCompleted {
+ return c.Redirect(http.StatusFound, "/")
+ }
+ userID := item.UserID
+
+ pgpToken, found := pgpTokenCache.Get(userID)
+ if !found {
+ return c.Redirect(http.StatusFound, "/")
+ }
+
+ data.GpgMode = utils.DoParseBool(c.Request().PostFormValue("gpg_mode"))
+ if data.GpgMode {
+ data.ToBeSignedMessage = c.Request().PostFormValue("to_be_signed_message")
+ data.SignedMessage = c.Request().PostFormValue("signed_message")
+ if !utils.PgpCheckSignMessage(pgpToken.PKey, pgpToken.Value, data.SignedMessage) {
+ data.ErrorSignedMessage = "invalid signature"
+ return c.Render(http.StatusOK, forgotPasswordTmplName, data)
+ }
+
+ } else {
+ data.EncryptedMessage = c.Request().PostFormValue("encrypted_message")
+ data.Code = c.Request().PostFormValue("pgp_code")
+ if data.Code != pgpToken.Value {
+ data.ErrorCode = "invalid code"
+ return c.Render(http.StatusOK, forgotPasswordTmplName, data)
+ }
+ }
+
+ pgpTokenCache.Delete(userID)
+ partialRecoveryCache.SetD(data.Token, PartialRecoveryItem{userID, RecoveryGpgValidated})
+
+ data.Step = resetPasswordStep
+ return c.Render(http.StatusOK, forgotPasswordTmplName, data)
+
+ } else if formName == "step3" {
+ // Receive and validate new password
+ data.Step = resetPasswordStep
+
+ // Step3 is guarded by the "token" that must be valid
+ token := c.Request().PostFormValue("token")
+ item, found := partialRecoveryCache.Get(token)
+ if !found || item.Step != RecoveryGpgValidated {
+ return c.Redirect(http.StatusFound, "/")
+ }
+ userID := item.UserID
+ user, err := db.GetUserByID(userID)
+ if err != nil {
+ return c.Redirect(http.StatusFound, "/")
+ }
+
+ newPassword := c.Request().PostFormValue("newPassword")
+ rePassword := c.Request().PostFormValue("rePassword")
+ data.NewPassword = newPassword
+ data.RePassword = rePassword
+
+ hashedPassword, err := database.NewPasswordValidator(db, newPassword).CompareWith(rePassword).Hash()
+ if err != nil {
+ data.ErrorNewPassword = err.Error()
+ return c.Render(http.StatusOK, forgotPasswordTmplName, data)
+ }
+
+ if err := user.ChangePassword(db, hashedPassword); err != nil {
+ logrus.Error(err)
+ }
+ db.CreateSecurityLog(user.ID, database.PasswordRecoverySecurityLog)
+
+ partialRecoveryCache.Delete(token)
+ c.SetCookie(hutils.DeleteCookie(hutils.WaitCookieName))
+
+ return c.Render(http.StatusFound, "flash", FlashResponse{Message: "Password reset done", Redirect: "/login"})
+ }
+
+ return c.Render(http.StatusOK, "flash", FlashResponse{"should not go here", "/login", "alert-danger"})
+}
diff --git a/pkg/web/handlers/signup.go b/pkg/web/handlers/signup.go
@@ -0,0 +1,52 @@
+package handlers
+
+import (
+ "dkforest/pkg/captcha"
+ "dkforest/pkg/config"
+ "dkforest/pkg/database"
+ hutils "dkforest/pkg/web/handlers/utils"
+ "github.com/labstack/echo"
+ "net/http"
+)
+
+// SignupHandler ...
+func SignupHandler(c echo.Context) error {
+ if config.ProtectHome.IsTrue() {
+ return c.NoContent(http.StatusNotFound)
+ }
+ return tmpSignupHandler(c)
+}
+
+func SignupAttackHandler(c echo.Context) error {
+ key := c.Param("signupToken")
+ loginLink, found := tempLoginCache.Get("login_link")
+ if !found {
+ return c.NoContent(http.StatusNotFound)
+ }
+ if err := captcha.VerifyStringDangerous(tempLoginStore, loginLink.ID, key); err != nil {
+ return c.NoContent(http.StatusNotFound)
+ }
+
+ return tmpSignupHandler(c)
+}
+
+// SignupInvitationHandler ...
+func SignupInvitationHandler(c echo.Context) error {
+ db := c.Get("database").(*database.DkfDB)
+ invitationToken := c.Param("invitationToken")
+ invitationTokenQuery := c.QueryParam("invitationToken")
+ if invitationTokenQuery != "" {
+ invitationToken = invitationTokenQuery
+ }
+ if _, err := db.GetUnusedInvitationByToken(invitationToken); err != nil {
+ return c.Redirect(http.StatusFound, "/")
+ }
+ return waitPageWrapper(c, signupHandler, hutils.WaitCookieName)
+}
+
+func tmpSignupHandler(c echo.Context) error {
+ if config.SignupFakeEnabled.IsFalse() && config.SignupEnabled.IsFalse() {
+ return c.Render(http.StatusOK, "standalone.signup-invite", nil)
+ }
+ return waitPageWrapper(c, signupHandler, hutils.WaitCookieName)
+}