diff --git a/db/auth.go b/db/auth.go index 579faad..189a43e 100644 --- a/db/auth.go +++ b/db/auth.go @@ -100,6 +100,7 @@ type Auth interface { InsertSession(session *Session) error GetSession(sessionId string) (*Session, error) + GetSessions(userId uuid.UUID) ([]*Session, error) DeleteSession(sessionId string) error DeleteOldSessions(userId uuid.UUID) error } @@ -416,9 +417,44 @@ func (db AuthSqlite) GetSession(sessionId string) (*Session, error) { return NewSession(sessionId, userId, createdAt, expiresAt), nil } +func (db AuthSqlite) GetSessions(userId uuid.UUID) ([]*Session, error) { + + sessions, err := db.db.Query(` + SELECT session_id, created_at, expires_at + FROM session + WHERE user_id = ?`, userId) + if err != nil { + log.Error("Could not get sessions: %v", err) + return nil, types.ErrInternal + } + + var result []*Session + + for sessions.Next() { + var ( + sessionId string + createdAt time.Time + expiresAt time.Time + ) + + err := sessions.Scan(&sessionId, &createdAt, &expiresAt) + if err != nil { + log.Error("Could not scan session: %v", err) + return nil, types.ErrInternal + } + + session := NewSession(sessionId, userId, createdAt, expiresAt) + result = append(result, session) + } + + return result, nil +} + func (db AuthSqlite) DeleteOldSessions(userId uuid.UUID) error { - // Delete old inactive sessions - _, err := db.db.Exec("DELETE FROM session WHERE created_at < datetime('now','-8 hours') AND user_id = ?", userId) + _, err := db.db.Exec(` + DELETE FROM session + WHERE expires_at < datetime('now') + AND user_id = ?`, userId) if err != nil { log.Error("Could not delete old sessions: %v", err) return types.ErrInternal diff --git a/handler/auth.go b/handler/auth.go index e027e1c..f533fa7 100644 --- a/handler/auth.go +++ b/handler/auth.go @@ -48,9 +48,9 @@ func (handler AuthImpl) Handle(router *http.ServeMux) { router.Handle("/auth/change-password", handler.handleChangePasswordPage()) router.Handle("/api/auth/change-password", handler.handleChangePasswordComp()) - router.Handle("/auth/reset-password", handler.handleResetPasswordPage()) - router.Handle("/api/auth/reset-password", handler.handleForgotPasswordComp()) - router.Handle("/api/auth/reset-password-actual", handler.handleForgotPasswordResponseComp()) + router.Handle("/auth/forgot-password", handler.handleForgotPasswordPage()) + router.Handle("/api/auth/forgot-password", handler.handleForgotPasswordComp()) + router.Handle("/api/auth/forgot-password-actual", handler.handleForgotPasswordResponseComp()) } var ( @@ -93,12 +93,10 @@ func (handler AuthImpl) handleSignIn() http.HandlerFunc { }) if err != nil { - if err == service.ErrInvaidCredentials { - utils.TriggerToast(w, r, "error", "Invalid email or password") - http.Error(w, "Invalid email or password", http.StatusUnauthorized) + if err == service.ErrInvalidCredentials { + utils.TriggerToast(w, r, "error", "Invalid email or password", http.StatusUnauthorized) } else { - log.Error("Error signing in: %v", err) - http.Error(w, "An error occurred", http.StatusInternalServerError) + utils.TriggerToast(w, r, "error", "An error occurred", http.StatusInternalServerError) } return } @@ -198,16 +196,16 @@ func (handler AuthImpl) handleSignUp() http.HandlerFunc { if err != nil { if errors.Is(err, types.ErrInternal) { - utils.TriggerToast(w, r, "error", "An error occurred") + utils.TriggerToast(w, r, "error", "An error occurred", http.StatusInternalServerError) return } else if errors.Is(err, service.ErrInvalidEmail) { - utils.TriggerToast(w, r, "error", "The email provided is invalid") + utils.TriggerToast(w, r, "error", "The email provided is invalid", http.StatusBadRequest) return } // If the "service.ErrAccountExists", then just continue } - utils.TriggerToast(w, r, "success", "A link to activate your account has been emailed to the address provided.") + utils.TriggerToast(w, r, "success", "A link to activate your account has been emailed to the address provided.", http.StatusOK) } } @@ -261,15 +259,13 @@ func (handler AuthImpl) handleDeleteAccountComp() http.HandlerFunc { password := r.FormValue("password") - _, err := handler.service.SignIn(user.Email, password) + err := handler.service.DeleteAccount(user, password) if err != nil { - utils.TriggerToast(w, r, "error", "Password not correct") - return - } - - err = handler.service.DeleteAccount(user) - if err != nil { - utils.TriggerToast(w, r, "error", "Internal Server Error") + if err == service.ErrInvalidCredentials { + utils.TriggerToast(w, r, "error", "Password not correct", http.StatusUnauthorized) + } else { + utils.TriggerToast(w, r, "error", "Internal Server Error", http.StatusInternalServerError) + } return } @@ -297,8 +293,8 @@ func (handler AuthImpl) handleChangePasswordPage() http.HandlerFunc { func (handler AuthImpl) handleChangePasswordComp() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - user := middleware.GetUser(r) - if user == nil { + session := middleware.GetSession(r) + if session == nil || session.User == nil { utils.DoRedirect(w, r, "/auth/signin") return } @@ -306,22 +302,22 @@ func (handler AuthImpl) handleChangePasswordComp() http.HandlerFunc { currPass := r.FormValue("current-password") newPass := r.FormValue("new-password") - err := handler.service.ChangePassword(user, currPass, newPass) + err := handler.service.ChangePassword(session, currPass, newPass) if err != nil { - utils.TriggerToast(w, r, "error", "Password not correct") + utils.TriggerToast(w, r, "error", "Password not correct", http.StatusUnauthorized) return } - utils.TriggerToast(w, r, "success", "Password changed") + utils.TriggerToast(w, r, "success", "Password changed", http.StatusOK) } } -func (handler AuthImpl) handleResetPasswordPage() http.HandlerFunc { +func (handler AuthImpl) handleForgotPasswordPage() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { user := middleware.GetUser(r) - if user == nil { - utils.DoRedirect(w, r, "/auth/signin") + if user != nil { + utils.DoRedirect(w, r, "/") return } @@ -335,15 +331,19 @@ func (handler AuthImpl) handleForgotPasswordComp() http.HandlerFunc { email := r.FormValue("email") if email == "" { - utils.TriggerToast(w, r, "error", "Please enter an email") + utils.TriggerToast(w, r, "error", "Please enter an email", http.StatusBadRequest) return } - err := handler.service.SendForgotPasswordMail(email) + _, err := utils.WaitMinimumTime(securityWaitDuration, func() (interface{}, error) { + err := handler.service.SendForgotPasswordMail(email) + return nil, err + }) + if err != nil { - utils.TriggerToast(w, r, "error", "Internal Server Error") + utils.TriggerToast(w, r, "error", "Internal Server Error", http.StatusInternalServerError) } else { - utils.TriggerToast(w, r, "info", "If the email exists, an email has been sent") + utils.TriggerToast(w, r, "info", "If the email exists, an email has been sent", http.StatusOK) } } } @@ -354,23 +354,18 @@ func (handler AuthImpl) handleForgotPasswordResponseComp() http.HandlerFunc { pageUrl, err := url.Parse(r.Header.Get("HX-Current-URL")) if err != nil { log.Error("Could not get current URL: %v", err) - utils.TriggerToast(w, r, "error", "Internal Server Error") + utils.TriggerToast(w, r, "error", "Internal Server Error", http.StatusInternalServerError) return } token := pageUrl.Query().Get("token") - if token == "" { - utils.TriggerToast(w, r, "error", "No token") - return - } - newPass := r.FormValue("new-password") err = handler.service.ForgotPassword(token, newPass) if err != nil { - utils.TriggerToast(w, r, "error", err.Error()) + utils.TriggerToast(w, r, "error", err.Error(), http.StatusInternalServerError) } else { - utils.TriggerToast(w, r, "success", "Password changed") + utils.TriggerToast(w, r, "success", "Password changed", http.StatusOK) } } } diff --git a/handler/middleware/cross_site_request_forgery.go b/handler/middleware/cross_site_request_forgery.go index 9fbca5f..1186228 100644 --- a/handler/middleware/cross_site_request_forgery.go +++ b/handler/middleware/cross_site_request_forgery.go @@ -61,7 +61,9 @@ func CrossSiteRequestForgery(auth service.Auth) func(http.Handler) http.Handler } } - if session == nil && (strings.Contains(r.RequestURI, "/auth/signup") || strings.Contains(r.RequestURI, "/auth/signin")) { + // Always sign in anonymous + // This way, there is no way to forget creating a csrf token + if session == nil { session, _ = auth.SignInAnonymous() cookie := CreateSessionCookie(session.Id) diff --git a/handler/middleware/security_headers.go b/handler/middleware/security_headers.go index 2dce7f3..474b402 100644 --- a/handler/middleware/security_headers.go +++ b/handler/middleware/security_headers.go @@ -14,13 +14,13 @@ func SecurityHeaders(serverSettings *types.Settings) func(http.Handler) http.Han w.Header().Set("Access-Control-Allow-Origin", serverSettings.BaseUrl) w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE") w.Header().Set("Content-Security-Policy", - "default-src 'none';"+ - "script-src 'self' https://umami.me-fit.eu"+ - "connect-src 'self' https://umami.me-fit.eu"+ - "img-src 'self'"+ - "style-src 'self'"+ - "form-action 'self'"+ - "frame-ancestors 'none'", + "default-src 'none'; "+ + "script-src 'self' https://umami.me-fit.eu; "+ + "connect-src 'self' https://umami.me-fit.eu; "+ + "img-src 'self'; "+ + "style-src 'self'; "+ + "form-action 'self'; "+ + "frame-ancestors 'none'; ", ) w.Header().Set("Cross-Origin-Resource-Policy", "same-origin") w.Header().Set("Cross-Origin-Opener-Policy", "same-origin") diff --git a/handler/workout.go b/handler/workout.go index ebb563f..169deff 100644 --- a/handler/workout.go +++ b/handler/workout.go @@ -2,7 +2,6 @@ package handler import ( "me-fit/handler/middleware" - "me-fit/log" "me-fit/service" "me-fit/template/workout" "me-fit/utils" @@ -67,7 +66,7 @@ func (handler WorkoutImpl) handleAddWorkout() http.HandlerFunc { wo := service.NewWorkoutDto("", dateStr, typeStr, setsStr, repsStr) wo, err := handler.service.AddWorkout(session.User, wo) if err != nil { - utils.TriggerToast(w, r, "error", "Invalid input values") + utils.TriggerToast(w, r, "error", "Invalid input values", http.StatusBadRequest) http.Error(w, "Invalid input values", http.StatusBadRequest) return } @@ -111,25 +110,19 @@ func (handler WorkoutImpl) handleDeleteWorkout() http.HandlerFunc { rowId := r.PathValue("id") if rowId == "" { - http.Error(w, "Missing required fields", http.StatusBadRequest) - log.Warn("Missing required fields for workout delete") - utils.TriggerToast(w, r, "error", "Missing ID field") + utils.TriggerToast(w, r, "error", "Missing ID field", http.StatusBadRequest) return } rowIdInt, err := strconv.Atoi(rowId) if err != nil { - http.Error(w, "Invalid ID", http.StatusBadRequest) - log.Warn("Invalid ID for workout delete") - utils.TriggerToast(w, r, "error", "Invalid ID") + utils.TriggerToast(w, r, "error", "Invalid ID", http.StatusBadRequest) return } err = handler.service.DeleteWorkout(session.User, rowIdInt) if err != nil { - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - log.Error("Could not delete workout: %v", err.Error()) - utils.TriggerToast(w, r, "error", "Internal Server Error") + utils.TriggerToast(w, r, "error", "Internal Server Error", http.StatusInternalServerError) return } } diff --git a/less b/less deleted file mode 100644 index a08a65b..0000000 --- a/less +++ /dev/null @@ -1,28 +0,0 @@ - - __ _ ___ - / /\ | | | |_) -/_/--\ |_| |_| \_ v1.52.3, built with Go go1.22.5 - -mkdir /home/tiwun/source/me-fit/tmp -watching . -watching db -watching handler -watching handler/middleware -watching log -watching migration -watching mocks -!exclude node_modules -watching service -!exclude static -watching template -watching template/auth -watching template/mail -watching template/workout -!exclude tmp -watching types -watching utils -building... -[32m(✓)[0m Complete [[2m updates=12[22m[2m duration=10.258748ms[22m ] -cleaning... -deleting /home/tiwun/source/me-fit/tmp -see you again~ diff --git a/main_test.go b/main_test.go index f99df06..40fabf2 100644 --- a/main_test.go +++ b/main_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "me-fit/db" "me-fit/service" "me-fit/types" @@ -81,13 +82,13 @@ func TestIntegrationSecurityHeader(t *testing.T) { assert.Equal(t, "GET, POST, DELETE", value) value = resp.Header.Get("Content-Security-Policy") - assert.Equal(t, "default-src 'none';"+ - "script-src 'self' https://umami.me-fit.eu"+ - "connect-src 'self' https://umami.me-fit.eu"+ - "img-src 'self'"+ - "style-src 'self'"+ - "form-action 'self'"+ - "frame-ancestors 'none'", value) + assert.Equal(t, "default-src 'none'; "+ + "script-src 'self' https://umami.me-fit.eu; "+ + "connect-src 'self' https://umami.me-fit.eu; "+ + "img-src 'self'; "+ + "style-src 'self'; "+ + "form-action 'self'; "+ + "frame-ancestors 'none';", value) value = resp.Header.Get("Cross-Origin-Resource-Policy") assert.Equal(t, "same-origin", value) @@ -159,6 +160,138 @@ func TestIntegrationAuth(t *testing.T) { assert.True(t, cookie.HttpOnly, "Cookie is not secure") assert.True(t, cookie.Secure, "Cookie is not secure") }) + t.Run("should change password and invalidate other sessions from user", func(t *testing.T) { + t.Parallel() + + db, basePath, ctx := setupIntegrationTest(t) + userId := uuid.New() + userIdOther := uuid.New() + + pass := service.GetHashPassword("password", []byte("salt")) + _, err := db.Exec(` + INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) + VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) + + sessionId := "session-id" + assert.Nil(t, err) + _, err = db.Exec(` + INSERT INTO session (session_id, user_id, created_at, expires_at) + VALUES (?, ?, datetime(), datetime("now", "+1 day"))`, sessionId, userId) + assert.Nil(t, err) + _, err = db.Exec(` + INSERT INTO session (session_id, user_id, created_at, expires_at) + VALUES ("second", ?, datetime(), datetime("now", "+1 day"))`, userId) + assert.Nil(t, err) + _, err = db.Exec(` + INSERT INTO session (session_id, user_id, created_at, expires_at) + VALUES ("other", ?, datetime(), datetime("now", "+1 day"))`, userIdOther) + assert.Nil(t, err) + + req, err := http.NewRequestWithContext(ctx, "GET", basePath+"/auth/change-password", nil) + assert.Nil(t, err) + req.Header.Set("Cookie", "id="+sessionId) + resp, err := httpClient.Do(req) + assert.Nil(t, err) + + html, err := html.Parse(resp.Body) + assert.Nil(t, err) + + csrfToken := findCsrfToken(html) + assert.NotEqual(t, "", csrfToken) + + formData := url.Values{ + "current-password": {"password"}, + "new-password": {"MyNewSecurePassword1!"}, + "csrf-token": {csrfToken}, + } + + req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/change-password", strings.NewReader(formData.Encode())) + assert.Nil(t, err) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Cookie", "id="+sessionId) + req.Header.Set("HX-Request", "true") + resp, err = httpClient.Do(req) + assert.Nil(t, err) + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var sessionIds []string + sessions, err := db.Query(`SELECT session_id FROM session WHERE NOT user_id = ? ORDER BY session_id`, uuid.Nil) + assert.Nil(t, err) + for sessions.Next() { + var sessionId string + err = sessions.Scan(&sessionId) + assert.Nil(t, err) + sessionIds = append(sessionIds, sessionId) + } + + assert.Equal(t, 2, len(sessionIds)) + assert.Equal(t, "other", sessionIds[0]) + assert.Equal(t, "session-id", sessionIds[1]) + }) + t.Run("should forget password and invalidate all user sessions", func(t *testing.T) { + t.Parallel() + + d, basePath, ctx := setupIntegrationTest(t) + userId := uuid.New() + + pass := service.GetHashPassword("password", []byte("salt")) + _, err := d.Exec(` + INSERT INTO user (user_id, email, email_verified, is_admin, password, salt, created_at) + VALUES (?, "mail@mail.de", FALSE, FALSE, ?, ?, datetime())`, userId, pass, []byte("salt")) + + assert.Nil(t, err) + _, err = d.Exec(` + INSERT INTO session (session_id, user_id, created_at, expires_at) + VALUES ("session-id", ?, datetime(), datetime("now", "+1 day"))`, userId) + assert.Nil(t, err) + + req, err := http.NewRequestWithContext(ctx, "GET", basePath+"/auth/forgot-password", nil) + assert.Nil(t, err) + resp, err := httpClient.Do(req) + assert.Nil(t, err) + + sessionId := findCookie(resp, "id").Value + html, err := html.Parse(resp.Body) + assert.Nil(t, err) + csrfToken := findCsrfToken(html) + assert.NotEqual(t, "", csrfToken) + + formData := url.Values{ + "email": {"mail@mail.de"}, + "csrf-token": {csrfToken}, + } + req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/forgot-password", strings.NewReader(formData.Encode())) + assert.Nil(t, err) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Cookie", "id="+sessionId) + req.Header.Set("HX-Request", "true") + resp, err = httpClient.Do(req) + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var token string + err = d.QueryRow("SELECT token FROM token WHERE type = ?", db.TokenTypePasswordReset).Scan(&token) + assert.Nil(t, err) + + formData = url.Values{ + "new-password": {"MyNewSecurePassword1!"}, + "csrf-token": {csrfToken}, + } + req, err = http.NewRequestWithContext(ctx, "POST", basePath+"/api/auth/forgot-password-actual", strings.NewReader(formData.Encode())) + assert.Nil(t, err) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Cookie", "id="+sessionId) + req.Header.Set("HX-Request", "true") + req.Header.Set("HX-Current-URL", basePath+"/auth/change-password?token="+url.QueryEscape(token)) + resp, err = httpClient.Do(req) + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + sessions, err := d.Query("SELECT session_id FROM session WHERE user_id = ?", userId) + assert.Nil(t, err) + assert.False(t, sessions.Next()) + }) } func findCookie(resp *http.Response, name string) *http.Cookie { diff --git a/service/auth.go b/service/auth.go index 6829614..04592fc 100644 --- a/service/auth.go +++ b/service/auth.go @@ -18,11 +18,12 @@ import ( ) var ( - ErrInvaidCredentials = errors.New("invalid email or password") - ErrInvalidPassword = errors.New("password needs to be 8 characters long, contain at least one number, one special, one uppercase and one lowercase character") - ErrInvalidEmail = errors.New("invalid email") - ErrAccountExists = errors.New("account already exists") - ErrSessionIdInvalid = errors.New("session ID is invalid") + ErrInvalidCredentials = errors.New("invalid email or password") + ErrInvalidPassword = errors.New("password needs to be 8 characters long, contain at least one number, one special, one uppercase and one lowercase character") + ErrInvalidEmail = errors.New("invalid email") + ErrAccountExists = errors.New("account already exists") + ErrSessionIdInvalid = errors.New("session ID is invalid") + ErrTokenInvalid = errors.New("token is invalid") ) type User struct { @@ -65,9 +66,9 @@ type Auth interface { SignInAnonymous() (*Session, error) SignOut(sessionId string) error - DeleteAccount(user *User) error + DeleteAccount(user *User, currPass string) error - ChangePassword(user *User, currPass, newPass string) error + ChangePassword(session *Session, currPass, newPass string) error SendForgotPasswordMail(email string) error ForgotPassword(token string, newPass string) error @@ -98,7 +99,7 @@ func (service AuthImpl) SignIn(email string, password string) (*Session, error) user, err := service.db.GetUserByEmail(email) if err != nil { if errors.Is(err, db.ErrNotFound) { - return nil, ErrInvaidCredentials + return nil, ErrInvalidCredentials } else { return nil, types.ErrInternal } @@ -107,7 +108,7 @@ func (service AuthImpl) SignIn(email string, password string) (*Session, error) hash := GetHashPassword(password, user.Salt) if subtle.ConstantTimeCompare(hash, user.Password) == 0 { - return nil, ErrInvaidCredentials + return nil, ErrInvalidCredentials } session, err := service.createSession(user.Id) @@ -297,9 +298,19 @@ func (service AuthImpl) SignOut(sessionId string) error { return service.db.DeleteSession(sessionId) } -func (service AuthImpl) DeleteAccount(user *User) error { +func (service AuthImpl) DeleteAccount(user *User, currPass string) error { - err := service.db.DeleteUser(user.Id) + userDb, err := service.db.GetUser(user.Id) + if err != nil { + return types.ErrInternal + } + + currHash := GetHashPassword(currPass, userDb.Salt) + if subtle.ConstantTimeCompare(currHash, userDb.Password) == 0 { + return ErrInvalidCredentials + } + + err = service.db.DeleteUser(user.Id) if err != nil { return err } @@ -309,7 +320,7 @@ func (service AuthImpl) DeleteAccount(user *User) error { return nil } -func (service AuthImpl) ChangePassword(user *User, currPass, newPass string) error { +func (service AuthImpl) ChangePassword(session *Session, currPass, newPass string) error { if !isPasswordValid(newPass) { return ErrInvalidPassword @@ -319,18 +330,18 @@ func (service AuthImpl) ChangePassword(user *User, currPass, newPass string) err return ErrInvalidPassword } - _, err := service.SignIn(user.Email, currPass) + userDb, err := service.db.GetUser(session.User.Id) if err != nil { return err } - userDb, err := service.db.GetUser(user.Id) - if err != nil { - return err + currHash := GetHashPassword(currPass, userDb.Salt) + + if subtle.ConstantTimeCompare(currHash, userDb.Password) == 0 { + return ErrInvalidCredentials } newHash := GetHashPassword(newPass, userDb.Salt) - userDb.Password = newHash err = service.db.UpdateUser(userDb) @@ -338,11 +349,23 @@ func (service AuthImpl) ChangePassword(user *User, currPass, newPass string) err return err } + sessions, err := service.db.GetSessions(userDb.Id) + if err != nil { + return types.ErrInternal + } + for _, s := range sessions { + if s.Id != session.Id { + err = service.db.DeleteSession(s.Id) + if err != nil { + return types.ErrInternal + } + } + } + return nil } func (service AuthImpl) SendForgotPasswordMail(email string) error { - tokenStr, err := service.random.String(32) if err != nil { return err @@ -383,7 +406,7 @@ func (service AuthImpl) ForgotPassword(tokenStr string, newPass string) error { token, err := service.db.GetToken(tokenStr) if err != nil { - return err + return ErrTokenInvalid } err = service.db.DeleteToken(tokenStr) @@ -391,6 +414,11 @@ func (service AuthImpl) ForgotPassword(tokenStr string, newPass string) error { return err } + if token.Type != db.TokenTypePasswordReset || + token.ExpiresAt.Before(service.clock.Now()) { + return ErrTokenInvalid + } + user, err := service.db.GetUser(token.UserId) if err != nil { log.Error("Could not get user from token: %v", err) @@ -405,6 +433,18 @@ func (service AuthImpl) ForgotPassword(tokenStr string, newPass string) error { return err } + sessions, err := service.db.GetSessions(user.Id) + if err != nil { + return types.ErrInternal + } + + for _, session := range sessions { + err = service.db.DeleteSession(session.Id) + if err != nil { + return types.ErrInternal + } + } + return nil } diff --git a/service/auth_test.go b/service/auth_test.go index 96ac697..513dcc8 100644 --- a/service/auth_test.go +++ b/service/auth_test.go @@ -80,7 +80,7 @@ func TestSignIn(t *testing.T) { _, err := underTest.SignIn("test@test.de", "wrong password") - assert.Equal(t, ErrInvaidCredentials, err) + assert.Equal(t, ErrInvalidCredentials, err) }) t.Run("should return ErrInvalidCretentials if user has not been found", func(t *testing.T) { t.Parallel() @@ -94,7 +94,7 @@ func TestSignIn(t *testing.T) { underTest := NewAuthImpl(mockAuthDb, mockRandom, mockClock, mockMail, &types.Settings{}) _, err := underTest.SignIn("test", "test") - assert.Equal(t, ErrInvaidCredentials, err) + assert.Equal(t, ErrInvalidCredentials, err) }) t.Run("should forward ErrInternal on any other error", func(t *testing.T) { t.Parallel() diff --git a/template/auth/change_password.templ b/template/auth/change_password.templ index fbd1637..eeacbff 100644 --- a/template/auth/change_password.templ +++ b/template/auth/change_password.templ @@ -4,7 +4,7 @@ templ ChangePasswordComp(isPasswordReset bool) {