diff --git a/.gitignore b/.gitignore
index c3f812701..251e8ca55 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ node_modules
.vscode
.trunk
.env.dev
+.env.prod
\ No newline at end of file
diff --git a/backend/go.mod b/backend/go.mod
index b6164b1a0..4e6367057 100644
--- a/backend/go.mod
+++ b/backend/go.mod
@@ -14,6 +14,7 @@ require (
github.com/huandu/go-assert v1.1.6
github.com/mcnijman/go-emailaddress v1.1.1
github.com/mitchellh/mapstructure v1.5.0
+ github.com/resend/resend-go/v2 v2.5.0
github.com/spf13/viper v1.18.2
github.com/swaggo/swag v1.16.3
golang.org/x/crypto v0.19.0
diff --git a/backend/go.sum b/backend/go.sum
index 23bfc4b49..15a31dc06 100644
--- a/backend/go.sum
+++ b/backend/go.sum
@@ -100,6 +100,8 @@ github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/resend/resend-go/v2 v2.5.0 h1:XzTtzQ9YB2LlGHWjS5AVyUqV9cVbDU+6Z4XgCKsJh4g=
+github.com/resend/resend-go/v2 v2.5.0/go.mod h1:ihnxc7wPpSgans8RV8d8dIF4hYWVsqMK5KxXAr9LIos=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
diff --git a/backend/src/auth/email.go b/backend/src/auth/email.go
new file mode 100644
index 000000000..36c55d946
--- /dev/null
+++ b/backend/src/auth/email.go
@@ -0,0 +1,35 @@
+package auth
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/resend/resend-go/v2"
+)
+
+type EmailService struct {
+ Client *resend.Client
+}
+
+func NewEmailService() *EmailService {
+ client := resend.NewClient(os.Getenv("SAC_RESEND_API_KEY"))
+ return &EmailService{
+ Client: client,
+ }
+}
+
+func (e *EmailService) SendPasswordResetEmail(name, email, token string) error {
+ params := &resend.SendEmailRequest{
+ From: "onboarding@resend.dev",
+ To: []string{"generatesac@gmail.com"},
+ Subject: "Password Reset",
+ Html: fmt.Sprintf("
Hello %s,
Forgot your password? you can reset your password by clicking on this link: Reset Password You can also copy and paste the following link into your browser: https://sac.resend.dev/reset-password/%s
If you did not request a password reset, please ignore this email.
", name, token, token),
+ }
+
+ _, err := e.Client.Emails.Send(params)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/backend/src/auth/password.go b/backend/src/auth/hash.go
similarity index 81%
rename from backend/src/auth/password.go
rename to backend/src/auth/hash.go
index 852d5c2ed..bb05b9811 100644
--- a/backend/src/auth/password.go
+++ b/backend/src/auth/hash.go
@@ -19,7 +19,16 @@ type params struct {
keyLength uint32
}
-func ComputePasswordHash(password string) (*string, error) {
+func GeneratePasswordResetToken() (string, error) {
+ token := make([]byte, 64)
+ if _, err := rand.Read(token); err != nil {
+ return "", err
+ }
+
+ return base64.RawURLEncoding.EncodeToString(token), nil
+}
+
+func ComputeHash(data string) (*string, error) {
p := ¶ms{
memory: 64 * 1024,
iterations: 3,
@@ -34,7 +43,7 @@ func ComputePasswordHash(password string) (*string, error) {
return nil, err
}
- hash := argon2.IDKey([]byte(password),
+ hash := argon2.IDKey([]byte(data),
salt,
p.iterations,
p.memory,
@@ -56,13 +65,13 @@ var (
ErrIncompatibleVersion = errors.New("incompatible version of argon2")
)
-func ComparePasswordAndHash(password string, encodedHash string) (bool, error) {
+func CompareHash(data string, encodedHash string) (bool, error) {
p, salt, hash, err := decodeHash(encodedHash)
if err != nil {
return false, err
}
- otherHash := argon2.IDKey([]byte(password), salt, p.iterations, p.memory, p.parallelism, p.keyLength)
+ otherHash := argon2.IDKey([]byte(data), salt, p.iterations, p.memory, p.parallelism, p.keyLength)
if subtle.ConstantTimeCompare(hash, otherHash) == 1 {
return true, nil
diff --git a/backend/src/config/config.go b/backend/src/config/config.go
index bef5661ef..e58e3ba5e 100644
--- a/backend/src/config/config.go
+++ b/backend/src/config/config.go
@@ -13,6 +13,7 @@ type Settings struct {
Auth AuthSettings
PineconeSettings PineconeSettings
OpenAISettings OpenAISettings
+ ResendSettings ResendSettings
}
type intermediateSettings struct {
diff --git a/backend/src/config/local.go b/backend/src/config/local.go
index ad2f458f0..f5214c8a4 100644
--- a/backend/src/config/local.go
+++ b/backend/src/config/local.go
@@ -51,5 +51,12 @@ func readLocal(v *viper.Viper, path string, useDevDotEnv bool) (*Settings, error
settings.OpenAISettings = *openAISettings
+ resendSettings, err := readResendSettings()
+ if err != nil {
+ return nil, fmt.Errorf("failed to read Resend settings: %w", err)
+ }
+
+ settings.ResendSettings = *resendSettings
+
return settings, nil
}
diff --git a/backend/src/config/resend.go b/backend/src/config/resend.go
new file mode 100644
index 000000000..be10b52f0
--- /dev/null
+++ b/backend/src/config/resend.go
@@ -0,0 +1,28 @@
+package config
+
+import (
+ "errors"
+ "os"
+
+ m "github.com/garrettladley/mattress"
+)
+
+type ResendSettings struct {
+ APIKey *m.Secret[string]
+}
+
+func readResendSettings() (*ResendSettings, error) {
+ apiKey := os.Getenv("SAC_RESEND_API_KEY")
+ if apiKey == "" {
+ return nil, errors.New("SAC_RESEND_API_KEY is not set")
+ }
+
+ secretAPIKey, err := m.NewSecret(apiKey)
+ if err != nil {
+ return nil, errors.New("failed to create secret from api key")
+ }
+
+ return &ResendSettings{
+ APIKey: secretAPIKey,
+ }, nil
+}
diff --git a/backend/src/controllers/auth.go b/backend/src/controllers/auth.go
index 713f39303..b68c391d7 100644
--- a/backend/src/controllers/auth.go
+++ b/backend/src/controllers/auth.go
@@ -181,3 +181,59 @@ func (a *AuthController) UpdatePassword(c *fiber.Ctx) error {
return utilities.FiberMessage(c, fiber.StatusOK, "success")
}
+
+// ForgotPassword godoc
+//
+// @Summary Generates a password reset token
+// @Description Generates a password reset token
+// @ID forgot-password
+// @Tags auth
+// @Accept json
+// @Produce json
+// @Param userBody body models.PasswordResetRequestBody true "User Body"
+// @Success 200 {object} utilities.SuccessResponse
+// @Failure 400 {object} errors.Error
+// @Failure 429 {object} errors.Error
+// @Failure 500 {object} errors.Error
+// @Router /auth/forgot-password [post]
+func (a *AuthController) ForgotPassword(c *fiber.Ctx) error {
+ var userBody models.PasswordResetRequestBody
+
+ if err := c.BodyParser(&userBody); err != nil {
+ return errors.FailedToParseRequestBody.FiberError(c)
+ }
+
+ if err := a.authService.ForgotPassword(userBody); err != nil {
+ return err.FiberError(c)
+ }
+
+ return utilities.FiberMessage(c, fiber.StatusOK, "success")
+}
+
+// VerifyPasswordResetToken godoc
+//
+// @Summary Verifies a password reset token
+// @Description Verifies a password reset token
+// @ID verify-password-reset-token
+// @Tags auth
+// @Accept json
+// @Produce json
+// @Param tokenBody body models.VerifyPasswordResetTokenRequestBody true "Token Body"
+// @Success 200 {object} utilities.SuccessResponse
+// @Failure 400 {object} errors.Error
+// @Failure 429 {object} errors.Error
+// @Failure 500 {object} errors.Error
+// @Router /auth/verify-reset [post]
+func (a *AuthController) VerifyPasswordResetToken(c *fiber.Ctx) error {
+ var tokenBody models.VerifyPasswordResetTokenRequestBody
+
+ if err := c.BodyParser(&tokenBody); err != nil {
+ return errors.FailedToParseRequestBody.FiberError(c)
+ }
+
+ if err := a.authService.VerifyPasswordResetToken(tokenBody); err != nil {
+ return err.FiberError(c)
+ }
+
+ return utilities.FiberMessage(c, fiber.StatusOK, "success")
+}
diff --git a/backend/src/database/db.go b/backend/src/database/db.go
index 4466df11f..d4f06e0ed 100644
--- a/backend/src/database/db.go
+++ b/backend/src/database/db.go
@@ -76,6 +76,7 @@ func MigrateDB(settings config.Settings, db *gorm.DB) error {
&models.PointOfContact{},
&models.Tag{},
&models.User{},
+ &models.PasswordReset{},
&models.Series{},
&models.EventInstanceException{},
&models.EventSeries{},
diff --git a/backend/src/database/super.go b/backend/src/database/super.go
index 93c065b66..43af7be9d 100644
--- a/backend/src/database/super.go
+++ b/backend/src/database/super.go
@@ -11,7 +11,7 @@ import (
var SuperUserUUID uuid.UUID
func SuperUser(superUserSettings config.SuperUserSettings) (*models.User, *errors.Error) {
- passwordHash, err := auth.ComputePasswordHash(superUserSettings.Password.Expose())
+ passwordHash, err := auth.ComputeHash(superUserSettings.Password.Expose())
if err != nil {
return nil, &errors.FailedToComputePasswordHash
}
diff --git a/backend/src/errors/auth.go b/backend/src/errors/auth.go
index e53e5f03d..7a164337e 100644
--- a/backend/src/errors/auth.go
+++ b/backend/src/errors/auth.go
@@ -11,4 +11,24 @@ var (
StatusCode: fiber.StatusInternalServerError,
Message: "failed to cast to custom claims",
}
+ FailedToCreatePasswordReset = Error{
+ StatusCode: fiber.StatusInternalServerError,
+ Message: "failed to create password reset",
+ }
+ FailedToDeletePasswordReset = Error{
+ StatusCode: fiber.StatusInternalServerError,
+ Message: "failed to delete password reset",
+ }
+ TokenExpired = Error{
+ StatusCode: fiber.StatusUnauthorized,
+ Message: "token expired",
+ }
+ FailedToGetPasswordResetToken = Error{
+ StatusCode: fiber.StatusInternalServerError,
+ Message: "failed to get password reset token",
+ }
+ PasswordResetTokenNotFound = Error{
+ StatusCode: fiber.StatusNotFound,
+ Message: "password reset token not found",
+ }
)
diff --git a/backend/src/errors/common.go b/backend/src/errors/common.go
index 5b93b0b14..8b67ef563 100644
--- a/backend/src/errors/common.go
+++ b/backend/src/errors/common.go
@@ -67,4 +67,12 @@ var (
StatusCode: fiber.StatusUnauthorized,
Message: "failed to validate access token",
}
+ FailedToGenerateToken = Error{
+ StatusCode: fiber.StatusInternalServerError,
+ Message: "failed to generate token",
+ }
+ FailedToSendEmail = Error{
+ StatusCode: fiber.StatusInternalServerError,
+ Message: "failed to send email",
+ }
)
diff --git a/backend/src/middleware/auth.go b/backend/src/middleware/auth.go
index 93a07b5f6..35108cbbf 100644
--- a/backend/src/middleware/auth.go
+++ b/backend/src/middleware/auth.go
@@ -18,12 +18,9 @@ var paths = []string{
"/api/v1/auth/refresh",
"/api/v1/users/",
"/api/v1/auth/logout",
-}
-
-func (m *AuthMiddlewareService) DisableAuth(h fiber.Handler) fiber.Handler {
- return func(c *fiber.Ctx) error {
- return h(c)
- }
+ "/api/v1/auth/forgot-password",
+ "/api/v1/auth/verify-reset",
+ "/api/v1/auth/verify-email",
}
func (m *AuthMiddlewareService) IsSuper(c *fiber.Ctx) bool {
diff --git a/backend/src/models/password_reset.go b/backend/src/models/password_reset.go
new file mode 100644
index 000000000..efc35032d
--- /dev/null
+++ b/backend/src/models/password_reset.go
@@ -0,0 +1,23 @@
+package models
+
+import (
+ "time"
+
+ "github.com/google/uuid"
+)
+
+type PasswordReset struct {
+ UserID uuid.UUID `gorm:"type:varchar(36);not null;primaryKey" json:"user_id" validate:"required,uuid4"`
+ Token string `gorm:"type:varchar(255);unique" json:"token" validate:"required,max=255"`
+ ExpiresAt time.Time `gorm:"type:timestamp;not null;primaryKey" json:"expires_at" validate:"required"`
+}
+
+type PasswordResetRequestBody struct {
+ Email string `json:"email" validate:"required,email"`
+}
+
+type VerifyPasswordResetTokenRequestBody struct {
+ Token string `json:"token" validate:"required"`
+ NewPassword string `json:"new_password" validate:"required,min=8,password"`
+ VerifyNewPassword string `json:"verify_new_password" validate:"required,min=8,password,eqfield=NewPassword"`
+}
diff --git a/backend/src/models/user.go b/backend/src/models/user.go
index 6371b393f..fa9ed635c 100644
--- a/backend/src/models/user.go
+++ b/backend/src/models/user.go
@@ -64,7 +64,7 @@ type CreateUserRequestBody struct {
FirstName string `json:"first_name" validate:"required,max=255"`
LastName string `json:"last_name" validate:"required,max=255"`
Email string `json:"email" validate:"required,email,neu_email,max=255"`
- Password string `json:"password" validate:"required,password"`
+ Password string `json:"password" validate:"required,password,min=8,max=255"`
College College `json:"college" validate:"required,oneof=CAMD DMSB KCCS CE BCHS SL CPS CS CSSH"`
Year Year `json:"year" validate:"required,min=1,max=6"`
}
diff --git a/backend/src/server/routes/auth.go b/backend/src/server/routes/auth.go
index fd866caff..948e0c3ad 100644
--- a/backend/src/server/routes/auth.go
+++ b/backend/src/server/routes/auth.go
@@ -21,5 +21,6 @@ func Auth(router fiber.Router, authService services.AuthServiceInterface, settin
auth.Get("/refresh", authController.Refresh)
auth.Get("/me", authController.Me)
auth.Post("/update-password/:userID", authMiddleware.Limiter(2, 1*time.Minute), authMiddleware.UserAuthorizeById, authController.UpdatePassword)
- // auth.Post("/reset-password/:userID", middleware.Skip(authMiddleware.UserAuthorizeById), authController.ResetPassword)
+ auth.Post("/forgot-password", authController.ForgotPassword)
+ auth.Post("/verify-reset", authController.VerifyPasswordResetToken)
}
diff --git a/backend/src/server/server.go b/backend/src/server/server.go
index 278cbf3d6..c409804af 100644
--- a/backend/src/server/server.go
+++ b/backend/src/server/server.go
@@ -3,6 +3,7 @@ package server
import (
"fmt"
+ "github.com/GenerateNU/sac/backend/src/auth"
"github.com/GenerateNU/sac/backend/src/config"
"github.com/GenerateNU/sac/backend/src/middleware"
"github.com/GenerateNU/sac/backend/src/server/routes"
@@ -35,12 +36,13 @@ func Init(db *gorm.DB, settings config.Settings) *fiber.App {
}
authMiddleware := middleware.NewAuthAuthMiddlewareService(db, validate, settings.Auth)
+ emailService := auth.NewEmailService()
apiv1 := app.Group("/api/v1")
apiv1.Use(authMiddleware.Authenticate)
routes.Utility(app)
- routes.Auth(apiv1, services.NewAuthService(db, validate), settings.Auth, authMiddleware)
+ routes.Auth(apiv1, services.NewAuthService(db, validate, emailService), settings.Auth, authMiddleware)
routes.UserRoutes(apiv1, db, validate, authMiddleware)
routes.Contact(apiv1, services.NewContactService(db, validate), authMiddleware)
routes.ClubRoutes(apiv1, db, validate, authMiddleware)
diff --git a/backend/src/services/auth.go b/backend/src/services/auth.go
index 176312c07..0bff1d376 100644
--- a/backend/src/services/auth.go
+++ b/backend/src/services/auth.go
@@ -1,6 +1,8 @@
package services
import (
+ "time"
+
"github.com/GenerateNU/sac/backend/src/auth"
"github.com/GenerateNU/sac/backend/src/errors"
"github.com/GenerateNU/sac/backend/src/models"
@@ -15,17 +17,21 @@ type AuthServiceInterface interface {
Me(id string) (*models.User, *errors.Error)
Login(userBody models.LoginUserResponseBody) (*models.User, *errors.Error)
UpdatePassword(id string, passwordBody models.UpdatePasswordRequestBody) *errors.Error
+ ForgotPassword(userBody models.PasswordResetRequestBody) *errors.Error
+ VerifyPasswordResetToken(passwordBody models.VerifyPasswordResetTokenRequestBody) *errors.Error
}
type AuthService struct {
DB *gorm.DB
Validate *validator.Validate
+ Email *auth.EmailService
}
-func NewAuthService(db *gorm.DB, validate *validator.Validate) *AuthService {
+func NewAuthService(db *gorm.DB, validate *validator.Validate, email *auth.EmailService) *AuthService {
return &AuthService{
DB: db,
Validate: validate,
+ Email: email,
}
}
@@ -53,7 +59,7 @@ func (a *AuthService) Login(userBody models.LoginUserResponseBody) (*models.User
return nil, err
}
- correct, passwordErr := auth.ComparePasswordAndHash(userBody.Password, user.PasswordHash)
+ correct, passwordErr := auth.CompareHash(userBody.Password, user.PasswordHash)
if passwordErr != nil || !correct {
return nil, &errors.FailedToValidateUser
}
@@ -83,7 +89,6 @@ func (a *AuthService) UpdatePassword(id string, passwordBody models.UpdatePasswo
return idErr
}
- // TODO: Validate password
if err := a.Validate.Struct(passwordBody); err != nil {
return &errors.FailedToValidateUser
}
@@ -93,12 +98,12 @@ func (a *AuthService) UpdatePassword(id string, passwordBody models.UpdatePasswo
return err
}
- correct, passwordErr := auth.ComparePasswordAndHash(passwordBody.OldPassword, passwordHash)
+ correct, passwordErr := auth.CompareHash(passwordBody.OldPassword, passwordHash)
if passwordErr != nil || !correct {
return &errors.FailedToValidateUser
}
- hash, hashErr := auth.ComputePasswordHash(passwordBody.NewPassword)
+ hash, hashErr := auth.ComputeHash(passwordBody.NewPassword)
if hashErr != nil {
return &errors.FailedToValidateUser
}
@@ -110,3 +115,81 @@ func (a *AuthService) UpdatePassword(id string, passwordBody models.UpdatePasswo
return nil
}
+
+func (a *AuthService) ForgotPassword(userBody models.PasswordResetRequestBody) *errors.Error {
+ if err := a.Validate.Struct(userBody); err != nil {
+ return &errors.FailedToValidateUser
+ }
+
+ user, err := transactions.GetUserByEmail(a.DB, userBody.Email)
+ if err != nil {
+ return nil // Do not return error if user does not exist
+ }
+
+ // check if user has a password reset token, if not, generate one, if yes, use the existing one
+ activeToken, tokenErr := transactions.GetActivePasswordResetTokenByUserID(a.DB, user.ID)
+ if tokenErr != nil {
+ if tokenErr != &errors.PasswordResetTokenNotFound {
+ return tokenErr
+ }
+ }
+
+ if activeToken != nil {
+ return nil
+ }
+
+ token, generateErr := auth.GeneratePasswordResetToken()
+ if generateErr != nil {
+ return &errors.FailedToGenerateToken
+ }
+
+ // save token to db
+ saveErr := transactions.SavePasswordResetToken(a.DB, user.ID, token)
+ if saveErr != nil {
+ return saveErr
+ }
+
+ // PLEASE NOTE: don't overuse this email service in testing (we only have 1000 free emails per month)
+ sendErr := a.Email.SendPasswordResetEmail(user.FirstName, user.Email, token)
+ if sendErr != nil {
+ deleteErr := transactions.DeletePasswordResetToken(a.DB, token)
+ if deleteErr != nil {
+ return deleteErr
+ }
+ return &errors.FailedToSendEmail
+ }
+
+ return nil
+}
+
+func (a *AuthService) VerifyPasswordResetToken(passwordBody models.VerifyPasswordResetTokenRequestBody) *errors.Error {
+ if err := a.Validate.Struct(passwordBody); err != nil {
+ return &errors.FailedToValidateUser
+ }
+
+ token, tokenErr := transactions.GetPasswordResetToken(a.DB, passwordBody.Token)
+ if tokenErr != nil {
+ return tokenErr
+ }
+
+ if token.ExpiresAt.Before(time.Now().UTC()) {
+ return &errors.TokenExpired
+ }
+
+ hash, hashErr := auth.ComputeHash(passwordBody.NewPassword)
+ if hashErr != nil {
+ return &errors.FailedToValidateUser
+ }
+
+ updateErr := transactions.UpdatePassword(a.DB, token.UserID, *hash)
+ if updateErr != nil {
+ return updateErr
+ }
+
+ deleteErr := transactions.DeletePasswordResetToken(a.DB, passwordBody.Token)
+ if deleteErr != nil {
+ return deleteErr
+ }
+
+ return nil
+}
diff --git a/backend/src/services/user.go b/backend/src/services/user.go
index a940a6b33..579a935ed 100644
--- a/backend/src/services/user.go
+++ b/backend/src/services/user.go
@@ -40,7 +40,7 @@ func (u *UserService) CreateUser(userBody models.CreateUserRequestBody) (*models
return nil, &errors.FailedToMapRequestToModel
}
- passwordHash, err := auth.ComputePasswordHash(userBody.Password)
+ passwordHash, err := auth.ComputeHash(userBody.Password)
if err != nil {
return nil, &errors.FailedToComputePasswordHash
}
diff --git a/backend/src/transactions/password_reset.go b/backend/src/transactions/password_reset.go
new file mode 100644
index 000000000..7299247c5
--- /dev/null
+++ b/backend/src/transactions/password_reset.go
@@ -0,0 +1,56 @@
+package transactions
+
+import (
+ "time"
+
+ "github.com/GenerateNU/sac/backend/src/errors"
+ "github.com/GenerateNU/sac/backend/src/models"
+ "github.com/google/uuid"
+ "gorm.io/gorm"
+)
+
+func SavePasswordResetToken(db *gorm.DB, userID uuid.UUID, token string) *errors.Error {
+ passwordReset := models.PasswordReset{
+ UserID: userID,
+ Token: token,
+ ExpiresAt: time.Now().Add(time.Hour * 24).UTC(),
+ }
+
+ if err := db.Create(&passwordReset).Error; err != nil {
+ return &errors.FailedToCreatePasswordReset
+ }
+
+ return nil
+}
+
+func DeletePasswordResetToken(db *gorm.DB, token string) *errors.Error {
+ if err := db.Where("token = ?", token).Delete(&models.PasswordReset{}).Error; err != nil {
+ return &errors.FailedToDeletePasswordReset
+ }
+
+ return nil
+}
+
+func GetPasswordResetToken(db *gorm.DB, token string) (*models.PasswordReset, *errors.Error) {
+ passwordReset := models.PasswordReset{}
+ if err := db.Where("token = ?", token).First(&passwordReset).Error; err != nil {
+ if err == gorm.ErrRecordNotFound {
+ return nil, &errors.PasswordResetTokenNotFound
+ }
+ return nil, &errors.FailedToGetPasswordResetToken
+ }
+
+ return &passwordReset, nil
+}
+
+func GetActivePasswordResetTokenByUserID(db *gorm.DB, userID uuid.UUID) (*models.PasswordReset, *errors.Error) {
+ passwordReset := models.PasswordReset{}
+ if err := db.Where("user_id = ? AND expires_at > ?", userID, time.Now().UTC()).First(&passwordReset).Error; err != nil {
+ if err == gorm.ErrRecordNotFound {
+ return nil, &errors.PasswordResetTokenNotFound
+ }
+ return nil, &errors.FailedToGetPasswordResetToken
+ }
+
+ return &passwordReset, nil
+}
diff --git a/backend/tests/api/helpers/auth.go b/backend/tests/api/helpers/auth.go
index eb65640e3..c64a981d5 100644
--- a/backend/tests/api/helpers/auth.go
+++ b/backend/tests/api/helpers/auth.go
@@ -133,7 +133,7 @@ func (app *TestApp) authStudent() {
func SampleStudentFactory() (models.User, string) {
password := "1234567890&"
- hashedPassword, err := auth.ComputePasswordHash(password)
+ hashedPassword, err := auth.ComputeHash(password)
if err != nil {
panic(err)
}
diff --git a/backend/tests/api/user_test.go b/backend/tests/api/user_test.go
index 20e1d0ebe..507015c28 100644
--- a/backend/tests/api/user_test.go
+++ b/backend/tests/api/user_test.go
@@ -373,7 +373,7 @@ func AssertUserWithIDBodyRespDB(eaa h.ExistingAppAssert, resp *http.Response, bo
eaa.Assert.Equal(dbUser.College, respUser.College)
eaa.Assert.Equal(dbUser.Year, respUser.Year)
- match, err := auth.ComparePasswordAndHash((*body)["password"].(string), dbUser.PasswordHash)
+ match, err := auth.CompareHash((*body)["password"].(string), dbUser.PasswordHash)
eaa.Assert.NilError(err)
diff --git a/config/.env.template b/config/.env.template
index 977a98269..9bc755042 100644
--- a/config/.env.template
+++ b/config/.env.template
@@ -1,3 +1,4 @@
SAC_PINECONE_INDEX_HOST="https://SAC_PINECONE_INDEX_HOST.com"
SAC_PINECONE_API_KEY="SAC_PINECONE_API_KEY"
-SAC_OPENAI_API_KEY="SAC_OPENAI_API_KEY"
\ No newline at end of file
+SAC_OPENAI_API_KEY="SAC_OPENAI_API_KEY"
+SAC_RESEND_API_KEY="SAC_RESEND_API_KEY"
\ No newline at end of file