From 70794791379d31346b07d4dea8480ead8b59c4d8 Mon Sep 17 00:00:00 2001 From: Garrett Ladley <92384606+garrettladley@users.noreply.github.com> Date: Sun, 9 Jun 2024 21:43:57 -0400 Subject: [PATCH 1/6] =?UTF-8?q?=F0=9F=93=9D=20feat:=20oauth2=20providers?= =?UTF-8?q?=20(#973)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/auth/hash.go | 140 ------ backend/auth/jwt.go | 297 ----------- backend/auth/password.go | 50 -- backend/background/job.go | 17 + backend/background/jobs/jobs.go | 15 + backend/background/jobs/welcome_sender.go | 107 ++++ backend/config/app.go | 12 + backend/config/auth.go | 34 -- backend/config/oauth.go | 24 - backend/config/oauth_google.go | 38 +- backend/config/oauth_microsoft.go | 36 ++ backend/config/oauth_outlook.go | 37 -- backend/config/redis.go | 40 +- backend/config/settings.go | 88 ++-- backend/constants/jobs.go | 10 + backend/constants/redis.go | 2 - backend/database/store/active_token.go | 39 -- backend/database/store/blacklist.go | 36 -- backend/database/store/limiter.go | 50 -- backend/database/store/redis.go | 80 --- backend/database/store/store.go | 77 --- backend/database/store/storer.go | 71 +++ backend/database/store/stores.go | 11 + backend/database/super.go | 12 +- backend/docker-compose.yml | 30 +- backend/entities/auth/base/controller.go | 244 --------- backend/entities/auth/base/handlers.go | 113 +++++ backend/entities/auth/base/models.go | 20 - backend/entities/auth/base/routes.go | 46 +- backend/entities/auth/base/service.go | 461 ------------------ backend/entities/auth/base/transactions.go | 68 +-- backend/entities/categories/base/routes.go | 8 +- backend/entities/clubs/base/controller.go | 4 +- backend/entities/clubs/base/routes.go | 6 +- backend/entities/clubs/base/transactions.go | 3 - .../entities/clubs/recruitment/controller.go | 18 +- backend/entities/models/oauth.go | 35 -- backend/entities/models/tokens.go | 6 - backend/entities/models/user.go | 30 +- backend/entities/models/verification.go | 21 - backend/entities/models/welcome_task.go | 7 + backend/entities/oauth/base/controller.go | 90 ---- backend/entities/oauth/base/models.go | 6 - backend/entities/oauth/base/routes.go | 14 - backend/entities/oauth/base/service.go | 194 -------- backend/entities/oauth/base/transactions.go | 59 --- backend/entities/socials/base/routes.go | 4 +- backend/entities/tags/base/routes.go | 8 +- backend/entities/users/base/controller.go | 62 --- backend/entities/users/base/models.go | 17 +- backend/entities/users/base/routes.go | 6 +- backend/entities/users/base/service.go | 39 -- backend/entities/users/base/transactions.go | 14 +- backend/entities/users/transactions.go | 30 +- backend/go.mod | 14 +- backend/go.sum | 69 ++- backend/integrations/email/email.go | 74 +-- backend/integrations/expose.go | 4 +- backend/integrations/oauth/README.md | 3 + backend/integrations/oauth/google.go | 47 -- backend/integrations/oauth/oauth.go | 18 - backend/integrations/oauth/outlook.go | 34 -- backend/integrations/oauth/soth/goog/goog.go | 216 ++++++++ .../integrations/oauth/soth/goog/session.go | 64 +++ backend/integrations/oauth/soth/msft/msft.go | 190 ++++++++ .../integrations/oauth/soth/msft/session.go | 63 +++ backend/integrations/oauth/soth/provider.go | 67 +++ backend/integrations/oauth/soth/session.go | 21 + .../integrations/oauth/soth/sothic/params.go | 11 + .../integrations/oauth/soth/sothic/sothic.go | 419 ++++++++++++++++ backend/integrations/oauth/soth/user.go | 42 ++ backend/locals/claims.go | 27 - backend/locals/type.go | 3 +- backend/locals/user.go | 31 ++ backend/locals/user_id.go | 27 - backend/main.go | 103 ++-- backend/middleware/auth/auth.go | 107 +--- backend/middleware/auth/club.go | 70 +-- backend/middleware/auth/event.go | 68 +-- backend/middleware/auth/middleware.go | 35 +- backend/middleware/auth/user.go | 52 +- backend/middleware/utility/limiter.go | 4 +- backend/middleware/utility/middleware.go | 12 +- backend/middleware/utility/paginator.go | 4 +- backend/migrations/000001_init.down.sql | 137 +----- backend/migrations/000001_init.up.sql | 33 +- .../permission.go} | 2 +- backend/server/server.go | 41 +- .../emails/password_change_complete.templ | 74 --- .../emails/password_change_complete_templ.go | 89 ---- backend/templates/emails/password_reset.templ | 76 --- .../templates/emails/password_reset_templ.go | 100 ---- backend/tests/api/helpers/auth.go | 104 +--- backend/tests/api/helpers/dependencies.go | 4 +- backend/tests/api/helpers/redis.go | 18 +- backend/tests/api/helpers/requests.go | 4 - backend/tests/api/mocks/aws.go | 26 + backend/tests/api/mocks/aws_mock.go | 26 - backend/tests/api/mocks/jwt_mock.go | 36 -- backend/tests/api/mocks/redis.go | 31 ++ backend/tests/api/mocks/redis_mock.go | 124 ----- backend/tests/api/mocks/resend.go | 17 + backend/tests/api/mocks/resend_mock.go | 27 - backend/types/params.go | 13 +- backend/utilities/api_error.go | 4 - backend/utilities/http.go | 8 +- config/.env.template | 34 +- go.work.sum | 103 +--- 108 files changed, 2201 insertions(+), 3815 deletions(-) delete mode 100644 backend/auth/hash.go delete mode 100644 backend/auth/jwt.go delete mode 100644 backend/auth/password.go create mode 100644 backend/background/job.go create mode 100644 backend/background/jobs/jobs.go create mode 100644 backend/background/jobs/welcome_sender.go delete mode 100644 backend/config/auth.go delete mode 100644 backend/config/oauth.go create mode 100644 backend/config/oauth_microsoft.go delete mode 100644 backend/config/oauth_outlook.go create mode 100644 backend/constants/jobs.go delete mode 100644 backend/database/store/active_token.go delete mode 100644 backend/database/store/blacklist.go delete mode 100644 backend/database/store/limiter.go delete mode 100644 backend/database/store/redis.go delete mode 100644 backend/database/store/store.go create mode 100644 backend/database/store/storer.go create mode 100644 backend/database/store/stores.go delete mode 100644 backend/entities/auth/base/controller.go create mode 100644 backend/entities/auth/base/handlers.go delete mode 100644 backend/entities/auth/base/models.go delete mode 100644 backend/entities/auth/base/service.go delete mode 100644 backend/entities/models/oauth.go delete mode 100644 backend/entities/models/tokens.go delete mode 100644 backend/entities/models/verification.go create mode 100644 backend/entities/models/welcome_task.go delete mode 100644 backend/entities/oauth/base/controller.go delete mode 100644 backend/entities/oauth/base/models.go delete mode 100644 backend/entities/oauth/base/routes.go delete mode 100644 backend/entities/oauth/base/service.go delete mode 100644 backend/entities/oauth/base/transactions.go create mode 100644 backend/integrations/oauth/README.md delete mode 100644 backend/integrations/oauth/google.go delete mode 100644 backend/integrations/oauth/oauth.go delete mode 100644 backend/integrations/oauth/outlook.go create mode 100644 backend/integrations/oauth/soth/goog/goog.go create mode 100644 backend/integrations/oauth/soth/goog/session.go create mode 100644 backend/integrations/oauth/soth/msft/msft.go create mode 100644 backend/integrations/oauth/soth/msft/session.go create mode 100644 backend/integrations/oauth/soth/provider.go create mode 100644 backend/integrations/oauth/soth/session.go create mode 100644 backend/integrations/oauth/soth/sothic/params.go create mode 100644 backend/integrations/oauth/soth/sothic/sothic.go create mode 100644 backend/integrations/oauth/soth/user.go delete mode 100644 backend/locals/claims.go create mode 100644 backend/locals/user.go delete mode 100644 backend/locals/user_id.go rename backend/{auth/permissions.go => permission/permission.go} (99%) delete mode 100644 backend/templates/emails/password_change_complete.templ delete mode 100644 backend/templates/emails/password_change_complete_templ.go delete mode 100644 backend/templates/emails/password_reset.templ delete mode 100644 backend/templates/emails/password_reset_templ.go create mode 100644 backend/tests/api/mocks/aws.go delete mode 100644 backend/tests/api/mocks/aws_mock.go delete mode 100644 backend/tests/api/mocks/jwt_mock.go create mode 100644 backend/tests/api/mocks/redis.go delete mode 100644 backend/tests/api/mocks/redis_mock.go create mode 100644 backend/tests/api/mocks/resend.go delete mode 100644 backend/tests/api/mocks/resend_mock.go diff --git a/backend/auth/hash.go b/backend/auth/hash.go deleted file mode 100644 index be04f4385..000000000 --- a/backend/auth/hash.go +++ /dev/null @@ -1,140 +0,0 @@ -package auth - -import ( - "crypto/rand" - "crypto/subtle" - "encoding/base64" - "errors" - "fmt" - "strings" - - "golang.org/x/crypto/argon2" -) - -type params struct { - memory uint32 - iterations uint32 - parallelism uint8 - saltLength uint32 - keyLength uint32 -} - -func GenerateURLSafeToken(length int) (*string, error) { - token := make([]byte, length) - if _, err := rand.Read(token); err != nil { - return nil, err - } - - encodedToken := base64.RawURLEncoding.EncodeToString(token) - return &encodedToken, nil -} - -func GenerateOTP(length int) (*string, error) { - digits := "0123456789" - otp := make([]byte, length) - if _, err := rand.Read(otp); err != nil { - return nil, err - } - - for i := 0; i < length; i++ { - otp[i] = digits[int(otp[i])%10] - } - - outOtp := string(otp) - - return &outOtp, nil -} - -func ComputeHash(data string) (*string, error) { - p := ¶ms{ - memory: 64 * 1024, - iterations: 3, - parallelism: 2, - saltLength: 16, - keyLength: 32, - } - - salt := make([]byte, p.saltLength) - - if _, err := rand.Read(salt); err != nil { - return nil, err - } - - hash := argon2.IDKey([]byte(data), - salt, - p.iterations, - p.memory, - p.parallelism, - p.keyLength, - ) - - b64Salt := base64.RawStdEncoding.EncodeToString(salt) - - b64Hash := base64.RawStdEncoding.EncodeToString(hash) - - encodedHash := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, p.memory, p.iterations, p.parallelism, b64Salt, b64Hash) - - return &encodedHash, nil -} - -var ( - ErrInvalidHash = errors.New("the encoded hash is not in the correct format") - ErrIncompatibleVersion = errors.New("incompatible version of argon2") -) - -func CompareHash(data string, encodedHash string) (bool, error) { - p, salt, hash, err := decodeHash(encodedHash) - if err != nil { - return false, err - } - - otherHash := argon2.IDKey([]byte(data), salt, p.iterations, p.memory, p.parallelism, p.keyLength) - - if subtle.ConstantTimeCompare(hash, otherHash) == 1 { - return true, nil - } - - return false, nil -} - -func decodeHash(encodedHash string) (p *params, salt []byte, hash []byte, err error) { - vals := strings.Split(encodedHash, "$") - - if len(vals) != 6 { - return nil, nil, nil, ErrInvalidHash - } - - var version int - - _, err = fmt.Sscanf(vals[2], "v=%d", &version) - if err != nil { - return nil, nil, nil, err - } - - if version != argon2.Version { - return nil, nil, nil, ErrIncompatibleVersion - } - - p = ¶ms{} - - _, err = fmt.Sscanf(vals[3], "m=%d,t=%d,p=%d", &p.memory, &p.iterations, &p.parallelism) - if err != nil { - return nil, nil, nil, err - } - - salt, err = base64.RawStdEncoding.Strict().DecodeString(vals[4]) - if err != nil { - return nil, nil, nil, err - } - - p.saltLength = uint32(len(salt)) - - hash, err = base64.RawStdEncoding.Strict().DecodeString(vals[5]) - if err != nil { - return nil, nil, nil, err - } - - p.keyLength = uint32(len(hash)) - - return p, salt, hash, nil -} diff --git a/backend/auth/jwt.go b/backend/auth/jwt.go deleted file mode 100644 index fb66f5803..000000000 --- a/backend/auth/jwt.go +++ /dev/null @@ -1,297 +0,0 @@ -package auth - -import ( - "fmt" - "time" - - "github.com/GenerateNU/sac/backend/config" - "github.com/GenerateNU/sac/backend/constants" - - "github.com/GenerateNU/sac/backend/utilities" - m "github.com/garrettladley/mattress" - "github.com/gofiber/fiber/v2" - "github.com/golang-jwt/jwt" -) - -type CustomClaims struct { - jwt.StandardClaims - Role string `json:"role"` -} - -type JWTType string - -const ( - AccessToken JWTType = "access" - RefreshToken JWTType = "refresh" -) - -type Token struct { - AccessToken []byte - RefreshToken []byte -} - -type Claims struct { - StandardClaims *jwt.StandardClaims - CustomClaims *jwt.MapClaims -} - -type JWTClientInterface interface { - GenerateTokenPair(accessClaims, refreshClaims Claims) (*Token, error) - GenerateToken(claims Claims, tokenType JWTType) ([]byte, error) - RefreshToken(token, refreshToken string, tokenType JWTType, newClaims jwt.MapClaims) ([]byte, error) - ExtractClaims(tokenString string, tokenType JWTType) (jwt.MapClaims, error) - ParseToken(tokenString string, tokenType JWTType) (*jwt.Token, error) - IsTokenValid(tokenString string, tokenType JWTType) (bool, error) -} - -type JWTClient struct { - RefreshExp time.Duration - AccessExp time.Duration - RefreshKey *m.Secret[string] - AccessKey *m.Secret[string] - SigningMethod jwt.SigningMethod -} - -func NewJWTClient(authSettings config.AuthSettings, signingMethod jwt.SigningMethod) JWTClientInterface { - return &JWTClient{ - RefreshExp: constants.REFRESH_TOKEN_EXPIRY, - AccessExp: constants.ACCESS_TOKEN_EXPIRY, - RefreshKey: authSettings.RefreshKey, - AccessKey: authSettings.AccessKey, - SigningMethod: signingMethod, - } -} - -func (j *JWTClient) GenerateTokenPair(accessClaims, refreshClaims Claims) (*Token, error) { - accessToken, err := j.GenerateToken(accessClaims, AccessToken) - if err != nil { - return nil, err - } - - refreshToken, err := j.GenerateToken(refreshClaims, RefreshToken) - if err != nil { - return nil, err - } - - return &Token{ - AccessToken: accessToken, - RefreshToken: refreshToken, - }, nil -} - -// GenerateToken generates a token with the claims passed in. -// It returns the token if successful, otherwise it returns an error. -func (j *JWTClient) GenerateToken(claims Claims, tokenType JWTType) ([]byte, error) { - // create a new map to store the combined claims - combinedClaims := make(jwt.MapClaims) - - // copy the standard claims to the combined claims if they are present - if claims.CustomClaims != nil { - copyCustomClaims(&combinedClaims, *claims.CustomClaims) - } - - // copy the custom claims to the combined claims if they are present - if claims.StandardClaims != nil { - copyStandardClaims(&combinedClaims, *claims.StandardClaims) - } - - // get the secret key for the token type - secretKey, err := j.getSecretKey(tokenType) - if err != nil { - return nil, err - } - - // get the expiry for the token type - exp := j.getExpiry(tokenType) - - // set the expiry of the token if its a value in the client, otherwise use whatever was passed in - if exp != 0 { - combinedClaims["exp"] = time.Now().Add(exp).Unix() - } - - // create a new token with the combined claims - token := jwt.NewWithClaims(j.SigningMethod, combinedClaims) - signedToken, err := token.SignedString([]byte(secretKey)) - if err != nil { - return nil, fmt.Errorf("failed to sign token: %w", err) - } - - return []byte(signedToken), nil -} - -// ParseToken parses the token string and returns the token if successful, otherwise it returns an error. -// It uses the secret key for the token type to parse the token. -func (j *JWTClient) ParseToken(tokenString string, tokenType JWTType) (*jwt.Token, error) { - secretKey, err := j.getSecretKey(tokenType) - if err != nil { - return nil, err - } - - token, parseErr := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - return []byte(secretKey), nil - }) - if parseErr != nil { - return nil, fmt.Errorf("failed to parse token: %w", parseErr) - } - - return token, nil -} - -// ExtractClaims extracts the claims from the token. -// It returns the claims if successful, otherwise it returns an error. -func (j *JWTClient) ExtractClaims(tokenString string, tokenType JWTType) (jwt.MapClaims, error) { - token, err := j.ParseToken(tokenString, tokenType) - if err != nil { - return nil, err - } - - claims, ok := token.Claims.(jwt.MapClaims) - if !ok { - return nil, fmt.Errorf("failed to extract claims from token. got: %T", token.Claims) - } - - return claims, nil -} - -// IsTokenValid checks if the token is valid. -// It returns true if the token is valid, otherwise it returns false. -func (j *JWTClient) IsTokenValid(tokenString string, tokenType JWTType) (bool, error) { - token, err := j.ParseToken(tokenString, tokenType) - if err != nil { - return false, err - } - - return token.Valid, nil -} - -// RefreshToken generates a new access token using the refresh token. -// It checks if the refresh token is valid and extracts the claims from the access token. -// It then updates the issued at and expires at claims and gives the new claims priority over the old claims. -// It returns the new access token if successful, otherwise it returns an error. -func (j *JWTClient) RefreshToken(token, refreshToken string, tokenType JWTType, newClaims jwt.MapClaims) ([]byte, error) { - ok, err := j.IsTokenValid(refreshToken, RefreshToken) - if err != nil || !ok { - return nil, err - } - - claims, err := j.ExtractClaims(token, tokenType) - if err != nil { - return nil, err - } - - claims = updateIssuedAt(claims) - claims = updateExpiresAt(claims, j.getExpiry(tokenType)) - claims = emendClaims(claims, newClaims) - - newToken, err := j.GenerateToken(Claims{CustomClaims: &claims}, tokenType) - if err != nil { - return nil, err - } - - return newToken, nil -} - -// getSecretKey returns the secret key for the token type. -// If the token type is not present in the client, it returns an error. -func (j *JWTClient) getSecretKey(tokenType JWTType) (string, error) { - switch tokenType { - case AccessToken: - return j.AccessKey.Expose(), nil - case RefreshToken: - return j.RefreshKey.Expose(), nil - } - - return "", utilities.BadRequest(fmt.Errorf("invalid token type: %s", tokenType)) -} - -// getExpiry returns the expiry time for the token type. -// If the token type is not present in the client, it returns 0. -func (j *JWTClient) getExpiry(tokenType JWTType) time.Duration { - switch tokenType { - case AccessToken: - return j.AccessExp - case RefreshToken: - return j.RefreshExp - } - - return 0 -} - -// updateIssuedAt updates the issued at claim of the token. -// If the issued at claim is not present, it won't be added. -func updateIssuedAt(claims jwt.MapClaims) jwt.MapClaims { - if _, ok := claims["iat"]; ok { - claims["iat"] = time.Now().Unix() - } - return claims -} - -// updateExpiresAt updates the expires at claim of the token. -// If the expires at claim is not present, it won't be added. -func updateExpiresAt(claims jwt.MapClaims, exp time.Duration) jwt.MapClaims { - if _, ok := claims["exp"]; ok { - claims["exp"] = time.Now().Add(exp).Unix() - } - return claims -} - -// emendClaims updates the claims of the token with the new claims. -// If the claim is not present in the original claims, it won't be added. -func emendClaims(originalClaims, newClaims jwt.MapClaims) jwt.MapClaims { - for key, value := range newClaims { - if _, ok := originalClaims[key]; ok { - originalClaims[key] = value - } - } - return originalClaims -} - -// copyStandardClaims copies the standard claims from a jwt.StandardClaims instance to a jwt.MapClaims instance. -// It is a utility function used to copy standard claims to the token claims. -func copyStandardClaims(claims *jwt.MapClaims, standardClaims jwt.StandardClaims) { - claimMapping := map[string]interface{}{ - "exp": standardClaims.ExpiresAt, - "iss": standardClaims.Issuer, - "aud": standardClaims.Audience, - "iat": standardClaims.IssuedAt, - "nbf": standardClaims.NotBefore, - "sub": standardClaims.Subject, - "jti": standardClaims.Id, - } - - for key, value := range claimMapping { - if intValue, ok := value.(int64); ok && intValue != 0 { - (*claims)[key] = value - } else if strValue, ok := value.(string); ok && strValue != "" { - (*claims)[key] = value - } - } -} - -// copyCustomClaims copies the custom claims from a map[string]interface{} instance to a jwt.MapClaims instance. -// It is a utility function used to copy custom claims to the token claims. -func copyCustomClaims(claims *jwt.MapClaims, customClaims map[string]interface{}) { - for key, value := range customClaims { - (*claims)[key] = value - } -} - -func SetResponseTokens(c *fiber.Ctx, tokens *Token) { - c.Set("Authorization", fmt.Sprintf("Bearer %s", tokens.AccessToken)) - c.Cookie(&fiber.Cookie{ - Name: "refresh_token", - Value: string(tokens.RefreshToken), - Expires: time.Now().Add(constants.REFRESH_TOKEN_EXPIRY), - HTTPOnly: true, - }) -} - -func ExpireResponseTokens(c *fiber.Ctx) { - c.Set("Authorization", "") - c.Cookie(&fiber.Cookie{ - Name: "refresh_token", - Value: "", - Expires: time.Now().Add(-time.Hour), - HTTPOnly: true, - }) -} diff --git a/backend/auth/password.go b/backend/auth/password.go deleted file mode 100644 index 82856f1ef..000000000 --- a/backend/auth/password.go +++ /dev/null @@ -1,50 +0,0 @@ -package auth - -import ( - "errors" - "fmt" - "regexp" - "strings" - - "github.com/GenerateNU/sac/backend/constants" -) - -func ValidatePassword(password string) error { - var errs []string - - if len(password) < 8 { - errs = append(errs, "must be at least 8 characters long") - } - - if len(password) > 128 { // see https://github.com/OWASP/ASVS/issues/756 - errs = append(errs, "must be at most 128 characters long") - } - - if !hasDigit(password) { - errs = append(errs, "must contain at least one digit") - } - - if !hasSpecialChar(password) { - errs = append(errs, fmt.Sprintf("must contain at least one special character from: [%v]", string(constants.SPECIAL_CHARACTERS))) - } - - if len(errs) > 0 { - return errors.New(strings.Join(errs, ", ")) - } - - return nil -} - -func hasDigit(str string) bool { - return regexp.MustCompile(`[0-9]`).MatchString(str) -} - -func hasSpecialChar(str string) bool { - for _, c := range constants.SPECIAL_CHARACTERS { - if strings.Contains(str, string(c)) { - return true - } - } - - return false -} diff --git a/backend/background/job.go b/backend/background/job.go new file mode 100644 index 000000000..e2e48d757 --- /dev/null +++ b/backend/background/job.go @@ -0,0 +1,17 @@ +package background + +import "log/slog" + +type JobFunc func() + +func Go(fn JobFunc) { + go func() { + defer func() { + if r := recover(); r != nil { + slog.Error("panic in background job", r) + } + }() + + fn() + }() +} diff --git a/backend/background/jobs/jobs.go b/backend/background/jobs/jobs.go new file mode 100644 index 000000000..65ae2ad64 --- /dev/null +++ b/backend/background/jobs/jobs.go @@ -0,0 +1,15 @@ +package jobs + +import ( + "github.com/GenerateNU/sac/backend/integrations/email" + "gorm.io/gorm" +) + +type Jobs struct { + db *gorm.DB + emailer email.Emailer +} + +func New(db *gorm.DB) *Jobs { + return &Jobs{db: db} +} diff --git a/backend/background/jobs/welcome_sender.go b/backend/background/jobs/welcome_sender.go new file mode 100644 index 000000000..b98b73526 --- /dev/null +++ b/backend/background/jobs/welcome_sender.go @@ -0,0 +1,107 @@ +package jobs + +import ( + "context" + "log/slog" + "time" + + "github.com/GenerateNU/sac/backend/background" + "github.com/GenerateNU/sac/backend/constants" + "github.com/GenerateNU/sac/backend/entities/models" + "gorm.io/gorm" +) + +func (j *Jobs) WelcomeSender(ctx context.Context) background.JobFunc { + return func() { + t := time.NewTicker(constants.EMAIL_SENDER_INTERVAL) + + for range t.C { + slog.Info("sending welcome email") + + func() { + // start a separate transaction to increment the attempts + incrementTx := j.db.WithContext(ctx).Begin() + defer func() { + if r := recover(); r != nil { + incrementTx.Rollback() + } + }() + + task, err := j.dequeueWelcomeTask(incrementTx) + if err != nil { + incrementTx.Rollback() + return + } + + if task == nil { + slog.Info("no welcome tasks to send") + incrementTx.Rollback() + return + } + + // increment attempts + task.Attempts++ + if err := incrementTx.Save(task).Error; err != nil { + slog.Error("failed to increment attempts", "err", err) + incrementTx.Rollback() + return + } + + if err := incrementTx.Commit().Error; err != nil { + slog.Error("failed to commit increment transaction", "err", err) + return + } + + // start the main transaction + tx := j.db.WithContext(ctx).Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + // attempt to send the email + err = j.sendWelcomeEmail(ctx, task) + if err != nil { + slog.Error("failed to send welcome email", "err", err) + if task.Attempts >= constants.MAX_EMAIL_ATTEMPTS { + if err := j.deleteWelcomeTask(tx, task); err != nil { + slog.Error("failed to delete welcome task", "err", err) + } + } + } else { + if err := j.deleteWelcomeTask(tx, task); err != nil { + slog.Error("failed to delete welcome task", "err", err) + } + } + + if err := tx.Commit().Error; err != nil { + slog.Error("failed to commit transaction", "err", err) + } + }() + } + } +} + +func (j *Jobs) dequeueWelcomeTask(tx *gorm.DB) (*models.WelcomeTask, error) { + var task models.WelcomeTask + if err := tx.Raw("SELECT email, name, attempts FROM welcome_tasks FOR UPDATE SKIP LOCKED LIMIT 1").Scan(&task).Error; err != nil { + tx.Rollback() + return nil, err + } + + if task.Email == "" && task.Name == "" { + tx.Rollback() + return nil, nil + } + + return &task, nil +} + +func (j *Jobs) deleteWelcomeTask(tx *gorm.DB, task *models.WelcomeTask) error { + return tx.Delete(task).Error +} + +func (j *Jobs) sendWelcomeEmail(ctx context.Context, task *models.WelcomeTask) error { + return j.emailer.SendWelcome(ctx, task.Email, task.Name) +} diff --git a/backend/config/app.go b/backend/config/app.go index 24c1e6561..7b6f45acd 100644 --- a/backend/config/app.go +++ b/backend/config/app.go @@ -1,7 +1,19 @@ package config +import "fmt" + type ApplicationSettings struct { Port uint16 `env:"PORT"` Host string `env:"HOST"` BaseUrl string `env:"BASE_URL"` } + +func (s *ApplicationSettings) ApplicationURL() string { + var host string + if s.Host == "127.0.0.1" { + host = "localhost" + } else { + host = s.Host + } + return fmt.Sprintf("http://%s:%d", host, s.Port) +} diff --git a/backend/config/auth.go b/backend/config/auth.go deleted file mode 100644 index e359095c7..000000000 --- a/backend/config/auth.go +++ /dev/null @@ -1,34 +0,0 @@ -package config - -import ( - "fmt" - - m "github.com/garrettladley/mattress" -) - -type AuthSettings struct { - AccessKey *m.Secret[string] - RefreshKey *m.Secret[string] -} - -type intermediateAuthSettings struct { - AccessKey string `env:"ACCESS_KEY"` - RefreshKey string `env:"REFRESH_KEY"` -} - -func (i *intermediateAuthSettings) into() (*AuthSettings, error) { - accessKey, err := m.NewSecret(i.AccessKey) - if err != nil { - return nil, fmt.Errorf("failed to create secret from access key: %s", err.Error()) - } - - refreshKey, err := m.NewSecret(i.RefreshKey) - if err != nil { - return nil, fmt.Errorf("failed to create secret from refresh key: %s", err.Error()) - } - - return &AuthSettings{ - AccessKey: accessKey, - RefreshKey: refreshKey, - }, nil -} diff --git a/backend/config/oauth.go b/backend/config/oauth.go deleted file mode 100644 index ce6032734..000000000 --- a/backend/config/oauth.go +++ /dev/null @@ -1,24 +0,0 @@ -package config - -import ( - m "github.com/garrettladley/mattress" -) - -type OAuthSettings struct { - BaseURL string - TokenURL string - ClientID *m.Secret[string] - ClientSecret *m.Secret[string] - Scopes string - RedirectURI string - ResponseType string - ResponseMode string - AccessType string - IncludeGrantedScopes string - Prompt string -} - -type OauthProviderSettings struct { - GoogleOAuthSettings *OAuthSettings - OutlookOAuthSettings *OAuthSettings -} diff --git a/backend/config/oauth_google.go b/backend/config/oauth_google.go index a26101ab0..1e7c38a15 100644 --- a/backend/config/oauth_google.go +++ b/backend/config/oauth_google.go @@ -1,38 +1,30 @@ package config -import ( - "fmt" +import m "github.com/garrettladley/mattress" - m "github.com/garrettladley/mattress" -) +type GoogleOAuthSettings struct { + Key *m.Secret[string] + Secret *m.Secret[string] +} type intermediateGoogleOAuthSettings struct { - ClientID string `env:"CLIENT_ID"` - ClientSecret string `env:"CLIENT_SECRET"` - RedirectURI string `env:"REDIRECT_URI"` + Key string `env:"KEY"` + Secret string `env:"SECRET"` } -func (i *intermediateGoogleOAuthSettings) into() (*OAuthSettings, error) { - secretClientID, err := m.NewSecret(i.ClientID) +func (i *intermediateGoogleOAuthSettings) into() (*GoogleOAuthSettings, error) { + secretKey, err := m.NewSecret(i.Key) if err != nil { - return nil, fmt.Errorf("failed to create secret from client ID: %s", err.Error()) + return nil, err } - secretClientSecret, err := m.NewSecret(i.ClientSecret) + secretSecret, err := m.NewSecret(i.Secret) if err != nil { - return nil, fmt.Errorf("failed to create secret from client secret: %s", err.Error()) + return nil, err } - return &OAuthSettings{ - BaseURL: "https://accounts.google.com/o/oauth2/v2", - TokenURL: "https://oauth2.googleapis.com", - ClientID: secretClientID, - ClientSecret: secretClientSecret, - Scopes: "https://www.googleapis.com/auth/calendar.events https://www.googleapis.com/auth/calendar.readonly", - ResponseType: "code", - RedirectURI: i.RedirectURI, - IncludeGrantedScopes: "true", - AccessType: "offline", - Prompt: "consent", + return &GoogleOAuthSettings{ + Key: secretKey, + Secret: secretSecret, }, nil } diff --git a/backend/config/oauth_microsoft.go b/backend/config/oauth_microsoft.go new file mode 100644 index 000000000..011b9dda5 --- /dev/null +++ b/backend/config/oauth_microsoft.go @@ -0,0 +1,36 @@ +package config + +import m "github.com/garrettladley/mattress" + +const ( + tenantID string = "a8eec281-aaa3-4dae-ac9b-9a398b9215e7" +) + +type MicrosoftOAuthSettings struct { + Key *m.Secret[string] + Secret *m.Secret[string] + Tenant string +} + +type intermediateMicrosoftOAuthSetting struct { + Key string `env:"KEY"` + Secret string `env:"SECRET"` +} + +func (i *intermediateMicrosoftOAuthSetting) into() (*MicrosoftOAuthSettings, error) { + secretKey, err := m.NewSecret(i.Key) + if err != nil { + return nil, err + } + + secretSecret, err := m.NewSecret(i.Secret) + if err != nil { + return nil, err + } + + return &MicrosoftOAuthSettings{ + Key: secretKey, + Secret: secretSecret, + Tenant: tenantID, + }, nil +} diff --git a/backend/config/oauth_outlook.go b/backend/config/oauth_outlook.go deleted file mode 100644 index ce0cb11cc..000000000 --- a/backend/config/oauth_outlook.go +++ /dev/null @@ -1,37 +0,0 @@ -package config - -import ( - "fmt" - - m "github.com/garrettladley/mattress" -) - -type intermdeiateOutlookOAuthSettings struct { - ClientID string `env:"CLIENT_ID"` - ClientSecret string `env:"CLIENT_SECRET"` - RedirectURI string `env:"REDIRECT_URI"` -} - -func (i *intermdeiateOutlookOAuthSettings) into() (*OAuthSettings, error) { - secretClientID, err := m.NewSecret(i.ClientID) - if err != nil { - return nil, fmt.Errorf("failed to create secret from client ID: %s", err.Error()) - } - - secretClientSecret, err := m.NewSecret(i.ClientSecret) - if err != nil { - return nil, fmt.Errorf("failed to create secret from client secret: %s", err.Error()) - } - - return &OAuthSettings{ - BaseURL: "https://login.microsoftonline.com/common/oauth2/v2.0", - TokenURL: "https://login.microsoftonline.com/common/oauth2/v2.0", - ClientID: secretClientID, - ClientSecret: secretClientSecret, - Scopes: "offline_access user.read calendars.readwrite", - ResponseType: "code", - RedirectURI: i.RedirectURI, - ResponseMode: "query", - Prompt: "consent", - }, nil -} diff --git a/backend/config/redis.go b/backend/config/redis.go index 27a122312..e52abd69b 100644 --- a/backend/config/redis.go +++ b/backend/config/redis.go @@ -7,14 +7,34 @@ import ( ) type RedisSettings struct { - Username string - Password *m.Secret[string] - Host string - Port uint - DB int + username string + password *m.Secret[string] + host string + port uint + db int // TLSConfig *TLSConfig } +func (r RedisSettings) Username() string { + return r.username +} + +func (r RedisSettings) Password() *m.Secret[string] { + return r.password +} + +func (r RedisSettings) Host() string { + return r.host +} + +func (r RedisSettings) Port() uint { + return r.port +} + +func (r RedisSettings) DB() int { + return r.db +} + type intermediateRedisSettings struct { Username string `env:"USERNAME"` Password string `env:"PASSWORD"` @@ -31,11 +51,11 @@ func (i *intermediateRedisSettings) into() (*RedisSettings, error) { } return &RedisSettings{ - Username: i.Username, - Password: password, - Host: i.Host, - Port: i.Port, - DB: i.DB, + username: i.Username, + password: password, + host: i.Host, + port: i.Port, + db: i.DB, // TLSConfig: i.TLSConfig.into(), }, nil } diff --git a/backend/config/settings.go b/backend/config/settings.go index 40e05d218..123a71592 100644 --- a/backend/config/settings.go +++ b/backend/config/settings.go @@ -1,39 +1,33 @@ package config type Settings struct { - Application ApplicationSettings - Database DatabaseSettings - RedisActiveTokens RedisSettings - RedisBlacklist RedisSettings - RedisLimiter RedisSettings - SuperUser SuperUserSettings - Auth AuthSettings - Calendar CalendarSettings + Application ApplicationSettings + Database DatabaseSettings + RedisLimiter RedisSettings + SuperUser SuperUserSettings + Calendar CalendarSettings Integrations } type Integrations struct { - GoogleOauth OAuthSettings - OutlookOauth OAuthSettings - AWS AWSSettings - Resend ResendSettings - Search SearchSettings + Google GoogleOAuthSettings + Microsft MicrosoftOAuthSettings + AWS AWSSettings + Resend ResendSettings + Search SearchSettings } type intermediateSettings struct { - Application ApplicationSettings `envPrefix:"SAC_APPLICATION_"` - Database intermediateDatabaseSettings `envPrefix:"SAC_DB_"` - RedisActiveTokens intermediateRedisSettings `envPrefix:"SAC_REDIS_ACTIVE_TOKENS_"` - RedisBlacklist intermediateRedisSettings `envPrefix:"SAC_REDIS_BLACKLIST_"` - RedisLimiter intermediateRedisSettings `envPrefix:"SAC_REDIS_LIMITER_"` - SuperUser intermediateSuperUserSettings `envPrefix:"SAC_SUDO_"` - Auth intermediateAuthSettings `envPrefix:"SAC_AUTH_"` - AWS intermediateAWSSettings `envPrefix:"SAC_AWS_"` - Resend intermediateResendSettings `envPrefix:"SAC_RESEND_"` - Calendar intermediateCalendarSettings `envPrefix:"SAC_CALENDAR_"` - GoogleSettings intermediateGoogleOAuthSettings `envPrefix:"SAC_GOOGLE_OAUTH_"` - OutlookSettings intermdeiateOutlookOAuthSettings `envPrefix:"SAC_OUTLOOK_OAUTH_"` - Search SearchSettings `envPrefix:"SAC_SEARCH_"` + Application ApplicationSettings `envPrefix:"SAC_APPLICATION_"` + Database intermediateDatabaseSettings `envPrefix:"SAC_DB_"` + RedisLimiter intermediateRedisSettings `envPrefix:"SAC_REDIS_LIMITER_"` + SuperUser intermediateSuperUserSettings `envPrefix:"SAC_SUDO_"` + AWS intermediateAWSSettings `envPrefix:"SAC_AWS_"` + Resend intermediateResendSettings `envPrefix:"SAC_RESEND_"` + Calendar intermediateCalendarSettings `envPrefix:"SAC_CALENDAR_"` + Google intermediateGoogleOAuthSettings `envPrefix:"SAC_GOOGLE_OAUTH_"` + Microsft intermediateMicrosoftOAuthSetting `envPrefix:"SAC_MICROSOFT_OAUTH_"` + Search SearchSettings `envPrefix:"SAC_SEARCH_"` } func (i *intermediateSettings) into() (*Settings, error) { @@ -42,16 +36,6 @@ func (i *intermediateSettings) into() (*Settings, error) { return nil, err } - redisActiveTokens, err := i.RedisActiveTokens.into() - if err != nil { - return nil, err - } - - redisBlacklist, err := i.RedisBlacklist.into() - if err != nil { - return nil, err - } - redisLimiter, err := i.RedisLimiter.into() if err != nil { return nil, err @@ -62,11 +46,6 @@ func (i *intermediateSettings) into() (*Settings, error) { return nil, err } - auth, err := i.Auth.into() - if err != nil { - return nil, err - } - aws, err := i.AWS.into() if err != nil { return nil, err @@ -82,31 +61,28 @@ func (i *intermediateSettings) into() (*Settings, error) { return nil, err } - google, err := i.GoogleSettings.into() + google, err := i.Google.into() if err != nil { return nil, err } - outlook, err := i.OutlookSettings.into() + microsoft, err := i.Microsft.into() if err != nil { return nil, err } return &Settings{ - Application: i.Application, - Database: *database, - RedisActiveTokens: *redisActiveTokens, - RedisBlacklist: *redisBlacklist, - RedisLimiter: *redisLimiter, - SuperUser: *superUser, - Auth: *auth, - Calendar: *calendar, + Application: i.Application, + Database: *database, + RedisLimiter: *redisLimiter, + SuperUser: *superUser, + Calendar: *calendar, Integrations: Integrations{ - GoogleOauth: *google, - OutlookOauth: *outlook, - AWS: *aws, - Resend: *resend, - Search: i.Search, + Google: *google, + Microsft: *microsoft, + AWS: *aws, + Resend: *resend, + Search: i.Search, }, }, nil } diff --git a/backend/constants/jobs.go b/backend/constants/jobs.go new file mode 100644 index 000000000..49f8757e3 --- /dev/null +++ b/backend/constants/jobs.go @@ -0,0 +1,10 @@ +package constants + +import "time" + +const ( + DELETE_EXPIRED_VERIFICATION_INTERVAL time.Duration = 30 * time.Minute + DELETE_EXPIRED_VERIFICATION_LIMIT int = 100 + EMAIL_SENDER_INTERVAL time.Duration = 5 * time.Second + MAX_EMAIL_ATTEMPTS int = 3 +) diff --git a/backend/constants/redis.go b/backend/constants/redis.go index 229acff5b..6f6de9c42 100644 --- a/backend/constants/redis.go +++ b/backend/constants/redis.go @@ -5,8 +5,6 @@ import "time" const ( REDIS_MAX_IDLE_CONNECTIONS int = 10 REDIS_MAX_OPEN_CONNECTIONS int = 100 - TOKEN_BLACKLIST_SKEY string = "token_blacklist" - TOKEN_BLACKLIST_KEY string = "blacklisted" RATE_LIMIT_DURATION time.Duration = 5 * time.Minute RATE_LIMIT_MAX_REQUESTS int = 5 REDIS_TIMEOUT time.Duration = 200 * time.Millisecond diff --git a/backend/database/store/active_token.go b/backend/database/store/active_token.go deleted file mode 100644 index 0fd669d29..000000000 --- a/backend/database/store/active_token.go +++ /dev/null @@ -1,39 +0,0 @@ -package store - -import ( - "context" - "time" -) - -type ActiveTokenInterface interface { - StoreRefreshToken(ctx context.Context, token string, userID string, expiry time.Duration) error - IsActive(ctx context.Context, token string) (bool, error) - GetRefreshToken(ctx context.Context, token string) (string, error) - DeleteRefreshToken(ctx context.Context, token string) error -} - -type ActiveToken struct { - StoreClient StoreClientInterface -} - -func NewActiveToken(storeClient StoreClientInterface) *ActiveToken { - return &ActiveToken{ - StoreClient: storeClient, - } -} - -func (a *ActiveToken) StoreRefreshToken(ctx context.Context, token string, userID string, expiry time.Duration) error { - return a.StoreClient.Set(ctx, token, userID, expiry) -} - -func (a *ActiveToken) GetRefreshToken(ctx context.Context, token string) (string, error) { - return a.StoreClient.Get(ctx, token) -} - -func (a *ActiveToken) IsActive(ctx context.Context, token string) (bool, error) { - return a.StoreClient.Exists(ctx, token) -} - -func (a *ActiveToken) DeleteRefreshToken(ctx context.Context, token string) error { - return a.StoreClient.Del(ctx, token) -} diff --git a/backend/database/store/blacklist.go b/backend/database/store/blacklist.go deleted file mode 100644 index 22ff6ac67..000000000 --- a/backend/database/store/blacklist.go +++ /dev/null @@ -1,36 +0,0 @@ -package store - -import ( - "context" - "time" - - "github.com/GenerateNU/sac/backend/constants" -) - -type BlacklistInterface interface { - BlacklistToken(ctx context.Context, token string, expiry time.Duration) error - IsTokenBlacklisted(ctx context.Context, token string) (bool, error) -} - -type Blacklist struct { - storeClient StoreClientInterface -} - -func NewBlacklist(storeClient StoreClientInterface) *Blacklist { - return &Blacklist{ - storeClient: storeClient, - } -} - -func (b *Blacklist) BlacklistToken(ctx context.Context, token string, expiry time.Duration) error { - err := b.storeClient.SetAdd(ctx, constants.TOKEN_BLACKLIST_SKEY, token) - if err != nil { - return err - } - - return b.storeClient.Set(ctx, token, constants.TOKEN_BLACKLIST_KEY, expiry) -} - -func (b *Blacklist) IsTokenBlacklisted(ctx context.Context, token string) (bool, error) { - return b.storeClient.SetIsMember(ctx, constants.TOKEN_BLACKLIST_SKEY, token) -} diff --git a/backend/database/store/limiter.go b/backend/database/store/limiter.go deleted file mode 100644 index c7bd0e3f0..000000000 --- a/backend/database/store/limiter.go +++ /dev/null @@ -1,50 +0,0 @@ -package store - -import ( - "context" - "time" -) - -// LimiterInterface is an implementation of https://github.com/gofiber/storage -type LimiterInterface interface { - Get(key string) ([]byte, error) - Set(key string, val []byte, exp time.Duration) error - Delete(key string) error - Reset() error - Close() error -} - -type Limiter struct { - StoreClient StoreClientInterface -} - -func NewLimiter(storeClient StoreClientInterface) *Limiter { - return &Limiter{ - StoreClient: storeClient, - } -} - -func (l *Limiter) Get(key string) ([]byte, error) { - value, err := l.StoreClient.Get(context.Background(), key) - if err != nil { - return nil, err - } - - return []byte(value), nil -} - -func (l *Limiter) Set(key string, val []byte, exp time.Duration) error { - return l.StoreClient.Set(context.Background(), key, string(val), exp) -} - -func (l *Limiter) Delete(key string) error { - return l.StoreClient.Del(context.Background(), key) -} - -func (l *Limiter) Reset() error { - return l.StoreClient.FlushAll(context.Background()) -} - -func (l *Limiter) Close() error { - return l.StoreClient.Close(context.Background()) -} diff --git a/backend/database/store/redis.go b/backend/database/store/redis.go deleted file mode 100644 index e3bf3ec7a..000000000 --- a/backend/database/store/redis.go +++ /dev/null @@ -1,80 +0,0 @@ -package store - -import ( - "bytes" - "fmt" - "log/slog" - "os/exec" - - "github.com/GenerateNU/sac/backend/config" -) - -type Stores struct { - Limiter LimiterInterface - Blacklist BlacklistInterface - ActiveToken ActiveTokenInterface -} - -func NewStores(limiter LimiterInterface, blacklist BlacklistInterface, activeToken ActiveTokenInterface) *Stores { - return &Stores{ - Limiter: limiter, - Blacklist: blacklist, - ActiveToken: activeToken, - } -} - -func ConfigureRedis(settings config.Settings) *Stores { - stores := NewStores( - NewLimiter(NewRedisClient(settings.RedisLimiter.Username, settings.RedisLimiter.Password, settings.RedisLimiter.Host, settings.RedisLimiter.Port, settings.RedisLimiter.DB)), - NewBlacklist(NewRedisClient(settings.RedisBlacklist.Username, settings.RedisBlacklist.Password, settings.RedisBlacklist.Host, settings.RedisBlacklist.Port, settings.RedisBlacklist.DB)), - NewActiveToken(NewRedisClient(settings.RedisActiveTokens.Username, settings.RedisActiveTokens.Password, settings.RedisActiveTokens.Host, settings.RedisActiveTokens.Port, settings.RedisActiveTokens.DB)), - ) - - MustEstablishConn() - - return stores -} - -// TODO: this will be a part of a larger function that will check all services -func MustEstablishConn() { - isRunning, err := isDockerComposeRunning() - if err != nil { - panic(err) - } - - if !*isRunning { - if err := restartServices(); err != nil { - panic(err) - } - } -} - -func isDockerComposeRunning() (*bool, error) { - cmd := exec.Command("docker-compose", "ps", "-q") - - var out bytes.Buffer - cmd.Stdout = &out - err := cmd.Run() - if err != nil { - return nil, fmt.Errorf("error checking if docker-compose is running: %s", err.Error()) - } - - result := out.Len() > 0 - return &result, nil -} - -func restartServices() error { - cmd := exec.Command("docker-compose", "up", "-d") - - var out bytes.Buffer - cmd.Stdout = &out - cmd.Stderr = &out - - err := cmd.Run() - if err != nil { - return fmt.Errorf("error restarting services: %s\nconsole output: %s", err.Error(), out.String()) - } - - slog.Info("Services restarted successfully.") - return nil -} diff --git a/backend/database/store/store.go b/backend/database/store/store.go deleted file mode 100644 index 82d224aec..000000000 --- a/backend/database/store/store.go +++ /dev/null @@ -1,77 +0,0 @@ -package store - -import ( - "context" - "fmt" - "runtime" - "time" - - "github.com/GenerateNU/sac/backend/constants" - m "github.com/garrettladley/mattress" - "github.com/redis/go-redis/v9" -) - -type StoreClientInterface interface { - Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error - Get(ctx context.Context, key string) (string, error) - Del(ctx context.Context, key string) error - Exists(ctx context.Context, key string) (bool, error) - SetAdd(ctx context.Context, key string, members ...interface{}) error - SetIsMember(ctx context.Context, key string, member interface{}) (bool, error) - FlushAll(ctx context.Context) error - Close(ctx context.Context) error -} - -type RedisClient struct { - client *redis.Client -} - -func NewRedisClient(username string, password *m.Secret[string], host string, port uint, db int) *RedisClient { - client := redis.NewClient(&redis.Options{ - Username: username, - Password: password.Expose(), - Addr: fmt.Sprintf("%s:%d", host, port), - DB: db, - PoolSize: 10 * runtime.GOMAXPROCS(0), - MaxActiveConns: constants.REDIS_MAX_OPEN_CONNECTIONS, - MaxIdleConns: constants.REDIS_MAX_IDLE_CONNECTIONS, - ContextTimeoutEnabled: true, - }) - - return &RedisClient{ - client: client, - } -} - -func (r *RedisClient) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error { - return r.client.Set(ctx, key, value, expiration).Err() -} - -func (r *RedisClient) Get(ctx context.Context, key string) (string, error) { - return r.client.Get(ctx, key).Result() -} - -func (r *RedisClient) Del(ctx context.Context, key string) error { - return r.client.Del(ctx, key).Err() -} - -func (r *RedisClient) Exists(ctx context.Context, key string) (bool, error) { - result, err := r.client.Exists(ctx, key).Result() - return result > 0, err -} - -func (r *RedisClient) SetAdd(ctx context.Context, key string, members ...interface{}) error { - return r.client.SAdd(ctx, key, members...).Err() -} - -func (r *RedisClient) SetIsMember(ctx context.Context, key string, member interface{}) (bool, error) { - return r.client.SIsMember(ctx, key, member).Result() -} - -func (r *RedisClient) FlushAll(ctx context.Context) error { - return r.client.FlushAll(ctx).Err() -} - -func (r *RedisClient) Close(ctx context.Context) error { - return r.client.Close() -} diff --git a/backend/database/store/storer.go b/backend/database/store/storer.go new file mode 100644 index 000000000..838e121df --- /dev/null +++ b/backend/database/store/storer.go @@ -0,0 +1,71 @@ +package store + +import ( + "context" + "fmt" + "runtime" + "time" + + "github.com/GenerateNU/sac/backend/constants" + "github.com/redis/go-redis/v9" + + m "github.com/garrettladley/mattress" +) + +// an implementation of https://github.com/gofiber/storage +type Storer interface { + Get(key string) ([]byte, error) + Set(key string, val []byte, exp time.Duration) error + Delete(key string) error + Reset() error + Close() error +} + +type RedisClient struct { + client *redis.Client +} + +type RedisSettings interface { + Username() string + Password() *m.Secret[string] + Host() string + Port() uint + DB() int +} + +func NewRedisClient(settings RedisSettings) *RedisClient { + client := redis.NewClient(&redis.Options{ + Username: settings.Username(), + Password: settings.Password().Expose(), + Addr: fmt.Sprintf("%s:%d", settings.Host(), settings.Port()), + DB: settings.DB(), + PoolSize: 10 * runtime.GOMAXPROCS(0), + MaxActiveConns: constants.REDIS_MAX_OPEN_CONNECTIONS, + MaxIdleConns: constants.REDIS_MAX_IDLE_CONNECTIONS, + ContextTimeoutEnabled: true, + }) + + return &RedisClient{ + client: client, + } +} + +func (r *RedisClient) Get(key string) ([]byte, error) { + return r.client.Get(context.Background(), key).Bytes() +} + +func (r *RedisClient) Set(key string, val []byte, exp time.Duration) error { + return r.client.Set(context.Background(), key, val, exp).Err() +} + +func (r *RedisClient) Delete(key string) error { + return r.client.Del(context.Background(), key).Err() +} + +func (r *RedisClient) Reset() error { + return r.client.FlushAll(context.Background()).Err() +} + +func (r *RedisClient) Close() error { + return r.client.Close() +} diff --git a/backend/database/store/stores.go b/backend/database/store/stores.go new file mode 100644 index 000000000..b16a68f6c --- /dev/null +++ b/backend/database/store/stores.go @@ -0,0 +1,11 @@ +package store + +type Stores struct { + Limiter Storer +} + +func ConfigureStores(limiter RedisSettings) *Stores { + return &Stores{ + Limiter: NewRedisClient(limiter), + } +} diff --git a/backend/database/super.go b/backend/database/super.go index 6255b640c..3a0bf146f 100644 --- a/backend/database/super.go +++ b/backend/database/super.go @@ -1,9 +1,6 @@ package database import ( - "fmt" - - "github.com/GenerateNU/sac/backend/auth" "github.com/GenerateNU/sac/backend/config" "github.com/GenerateNU/sac/backend/entities/models" "github.com/google/uuid" @@ -12,17 +9,10 @@ import ( var SuperUserUUID uuid.UUID func SuperUser(superUserSettings config.SuperUserSettings) (*models.User, error) { - passwordHash, err := auth.ComputeHash(superUserSettings.Password.Expose()) - if err != nil { - return nil, fmt.Errorf("failed to hash super user password: %w", err) - } - return &models.User{ Role: models.Super, Email: "generatesac@gmail.com", - PasswordHash: *passwordHash, - FirstName: "SAC", - LastName: "Super", + Name: "SAC Super", Major0: models.ComputerScience, College: models.KCCS, GraduationCycle: models.May, diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 4870e808b..20ee008dd 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -1,32 +1,17 @@ services: - redis-active-tokens: + redis-session: build: context: . dockerfile: Dockerfile.redis - container_name: redis_active_tokens - ports: - - 6379:6379 - environment: - - REDIS_USERNAME=redis_active_tokens - - REDIS_PASSWORD=redis_active_tokens!#1 - - REDIS_DISABLE_DEFAULT_USER="true" - volumes: - - redis-active-data:/data - - redis-blacklist: - build: - context: . - dockerfile: Dockerfile.redis - container_name: redis_blacklist + container_name: redis_session ports: - 6380:6379 environment: - - REDIS_USERNAME=redis_blacklist - - REDIS_PASSWORD=redis_blacklist!#2 + - REDIS_USERNAME=redis_session + - REDIS_PASSWORD=redis_session!#1 - REDIS_DISABLE_DEFAULT_USER="true" volumes: - - redis-blacklist-data:/data - + - redis-session-data:/data redis-limiter: build: context: . @@ -36,7 +21,7 @@ services: - 6381:6379 environment: - REDIS_USERNAME=redis_limiter - - REDIS_PASSWORD=redis_limiter!#3 + - REDIS_PASSWORD=redis_limiter!#1 - REDIS_DISABLE_DEFAULT_USER="true" volumes: - redis-limiter-data:/data @@ -79,8 +64,7 @@ services: - opensearch-net volumes: - redis-active-data: - redis-blacklist-data: + redis-session-data: redis-limiter-data: opensearch-data1: diff --git a/backend/entities/auth/base/controller.go b/backend/entities/auth/base/controller.go deleted file mode 100644 index 52a671e97..000000000 --- a/backend/entities/auth/base/controller.go +++ /dev/null @@ -1,244 +0,0 @@ -package base - -import ( - "net/http" - - "github.com/GenerateNU/sac/backend/auth" - authEntities "github.com/GenerateNU/sac/backend/entities/auth" - users "github.com/GenerateNU/sac/backend/entities/users/base" - "github.com/GenerateNU/sac/backend/utilities" - "github.com/gofiber/fiber/v2" -) - -type AuthController struct { - authService AuthServiceInterface -} - -func NewAuthController(authService AuthServiceInterface) *AuthController { - return &AuthController{authService: authService} -} - -// Login godoc -// -// @Summary Logs in a user -// @Description Logs in a user -// @ID login-user -// @Tags auth -// @Accept json -// @Produce json -// @Param loginBody body authEntities.LoginResponseBody true "Login Body" -// @Success 200 {object} models.User -// @Failure 400 {object} error -// @Failure 404 {object} error -// @Failure 500 {object} error -// @Router /auth/login [post] -func (a *AuthController) Login(c *fiber.Ctx) error { - var userBody authEntities.LoginResponseBody - - if err := c.BodyParser(&userBody); err != nil { - return utilities.InvalidJSON() - } - - user, tokens, err := a.authService.Login(c.UserContext(), userBody) - if err != nil { - return err - } - - auth.SetResponseTokens(c, tokens) - - return c.Status(http.StatusOK).JSON(user) -} - -// Register godoc -// -// @Summary Registers a user -// @Description Registers a user -// @ID register-user -// @Tags auth -// @Accept json -// @Produce json -// @Param userBody body CreateUserRequestBody true "User Body" -// @Success 201 {object} models.User -// @Failure 400 {object} error -// @Failure 404 {object} error -// @Failure 500 {object} error -// @Router /auth/register [post] -func (a *AuthController) Register(c *fiber.Ctx) error { - var userBody users.CreateUserRequestBody - - if err := c.BodyParser(&userBody); err != nil { - return utilities.InvalidJSON() - } - - user, tokens, err := a.authService.Register(c.UserContext(), userBody) - if err != nil { - return err - } - - auth.SetResponseTokens(c, tokens) - - return c.Status(http.StatusCreated).JSON(user) -} - -// Refresh godoc -// -// @Summary Refreshes a user's access token and returns a new pair of tokens -// @Description Refreshes a user's access token and returns a new pair of tokens -// @ID refresh-user -// @Tags auth -// @Accept json -// @Produce json -// @Success 200 {object} utilities.SuccessResponse -// @Failure 400 {object} error -// @Failure 404 {object} error -// @Failure 500 {object} error -// @Router /auth/refresh [post] -func (a *AuthController) Refresh(c *fiber.Ctx) error { - var refreshBody RefreshTokenCookieBody - - if err := c.CookieParser(&refreshBody); err != nil { - return utilities.InvalidCookies() - } - - tokens, err := a.authService.Refresh(c.UserContext(), refreshBody.RefreshToken) - if err != nil { - return err - } - - auth.SetResponseTokens(c, tokens) - - return c.SendStatus(http.StatusOK) -} - -// Logout godoc -// -// @Summary Logs out a user -// @Description Logs out a user -// @ID logout-user -// @Tags auth -// @Accept json -// @Produce json -// @Success 200 {object} utilities.SuccessResponse -// @Router /auth/logout [post] -func (a *AuthController) Logout(c *fiber.Ctx) error { - if err := a.authService.Logout(c.UserContext(), c.Get("Authorization"), c.Cookies("refresh_token")); err != nil { - return err - } - - auth.ExpireResponseTokens(c) - - return c.SendStatus(http.StatusOK) -} - -// ForgotPassword godoc -// -// @Summary Generates a password reset token -// @Description Generates a password reset token -// @ID forgot-password -// @Tags auth -// @Accept json -// @Produce json -// @Param email body string true "Email" -// @Success 200 {object} utilities.SuccessResponse -// @Failure 400 {object} error -// @Failure 429 {object} error -// @Failure 500 {object} error -// @Router /auth/forgot-password [post] -func (a *AuthController) ForgotPassword(c *fiber.Ctx) error { - var emailBody EmailRequestBody - - if err := c.BodyParser(&emailBody); err != nil { - return utilities.InvalidJSON() - } - - if err := a.authService.ForgotPassword(emailBody.Email); err != nil { - return err - } - - return c.SendStatus(http.StatusOK) -} - -// 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 VerifyPasswordResetTokenRequestBody true "Password Reset Token Body" -// @Security Bearer -// @Success 200 {object} utilities.SuccessResponse -// @Failure 400 {object} error -// @Failure 429 {object} error -// @Failure 500 {object} error -// @Router /auth/verify-reset [post] -func (a *AuthController) VerifyPasswordResetToken(c *fiber.Ctx) error { - var tokenBody VerifyPasswordResetTokenRequestBody - - if err := c.BodyParser(&tokenBody); err != nil { - return utilities.InvalidJSON() - } - - if err := a.authService.VerifyPasswordResetToken(tokenBody); err != nil { - return err - } - - return c.SendStatus(http.StatusOK) -} - -// SendCode godoc -// -// @Summary Sends a verification code -// @Description Sends a verification code -// @ID send-verification-code -// @Tags auth -// @Accept json -// @Produce json -// @Param email body string true "Email" -// @Success 200 {object} utilities.SuccessResponse -// @Failure 400 {object} error -// @Failure 429 {object} error -// @Failure 500 {object} error -// @Router /auth/send-code [post] -func (a *AuthController) SendCode(c *fiber.Ctx) error { - var emailBody EmailRequestBody - - if err := c.BodyParser(&emailBody); err != nil { - return utilities.InvalidJSON() - } - - if err := a.authService.SendCode(emailBody.Email); err != nil { - return err - } - - return c.SendStatus(http.StatusOK) -} - -// VerifyEmail godoc -// -// @Summary Verifies an email -// @Description Verifies an email -// @ID verify-email -// @Tags auth -// @Accept json -// @Produce json -// @Param tokenBody body VerifyEmailRequestBody true "Email Verification Token Body" -// @Success 200 {object} utilities.SuccessResponse -// @Failure 400 {object} error -// @Failure 429 {object} error -// @Failure 500 {object} error -// @Router /auth/verify-email [post] -func (a *AuthController) VerifyEmail(c *fiber.Ctx) error { - var tokenBody VerifyEmailRequestBody - - if err := c.BodyParser(&tokenBody); err != nil { - return utilities.InvalidJSON() - } - - if err := a.authService.VerifyEmail(tokenBody); err != nil { - return err - } - - return c.SendStatus(http.StatusOK) -} diff --git a/backend/entities/auth/base/handlers.go b/backend/entities/auth/base/handlers.go new file mode 100644 index 000000000..6ecd5c816 --- /dev/null +++ b/backend/entities/auth/base/handlers.go @@ -0,0 +1,113 @@ +package base + +import ( + "context" + "log/slog" + "net/http" + "time" + + "github.com/GenerateNU/sac/backend/integrations/oauth/soth" + "github.com/GenerateNU/sac/backend/integrations/oauth/soth/sothic" + + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +type Service interface { + Login(c *fiber.Ctx) error + Provider(c *fiber.Ctx) error + ProviderCallback(c *fiber.Ctx) error + ProviderLogout(c *fiber.Ctx) error +} + +type Handler struct { + db *gorm.DB + authProvider soth.Provider +} + +func (h *Handler) Login(c *fiber.Ctx) error { + sothic.SetProvider(c, h.authProvider.Name()) + if gfUser, err := sothic.CompleteUserAuth(c); err == nil { + user, err := FindOrCreateUser(context.TODO(), h.db, gfUser) + if err != nil { + return err + } + + strUser, err := user.Marshal() + if err != nil { + return err + } + + if err := sothic.StoreInSession("user", *strUser, c); err != nil { + return err + } + + return nil + } else { + return sothic.BeginAuthHandler(c) + } +} + +func (h *Handler) Provider(c *fiber.Ctx) error { + if gfUser, err := sothic.CompleteUserAuth(c); err == nil { + user, err := FindOrCreateUser(context.TODO(), h.db, gfUser) + if err != nil { + return err + } + + strUser, err := user.Marshal() + if err != nil { + return err + } + + if err := sothic.StoreInSession("user", *strUser, c); err != nil { + return err + } + + return nil + } else { + return sothic.BeginAuthHandler(c) + } +} + +func (h *Handler) ProviderCallback(c *fiber.Ctx) error { + defer func() { + c.Cookie(&fiber.Cookie{ + Name: "redirect", + Value: "", + Expires: time.Now().Add(-1 * time.Hour), // expire the cookie immediately + // MARK: secure should be true in prod + // use go build tags to do this + HTTPOnly: true, + }) + }() + + gfUser, err := sothic.CompleteUserAuth(c) + if err != nil { + return err + } + + user, err := FindOrCreateUser(context.TODO(), h.db, gfUser) + if err != nil { + return err + } + + strUser, err := user.Marshal() + if err != nil { + return err + } + + if err := sothic.StoreInSession("user", *strUser, c); err != nil { + return err + } + + return c.Redirect(c.Cookies("redirect", "/")) +} + +func (h *Handler) ProviderLogout(c *fiber.Ctx) error { + if err := sothic.Logout(c); err != nil { + slog.Error("auth", "logout", err) + } + + return c.SendStatus(http.StatusOK) +} diff --git a/backend/entities/auth/base/models.go b/backend/entities/auth/base/models.go deleted file mode 100644 index 0657ac760..000000000 --- a/backend/entities/auth/base/models.go +++ /dev/null @@ -1,20 +0,0 @@ -package base - -type VerifyEmailRequestBody struct { - Email string `json:"email" validate:"required,email"` - Token string `json:"token" validate:"required,len=6"` -} - -type VerifyPasswordResetTokenRequestBody struct { - Token string `json:"token" validate:"required"` - NewPassword string `json:"new_password" validate:"required"` // MARK: must be validated manually - VerifyNewPassword string `json:"verify_new_password" validate:"required,eqfield=NewPassword"` // MARK: must be validated manually -} - -type EmailRequestBody struct { - Email string `json:"email" validate:"required,email"` -} - -type RefreshTokenCookieBody struct { - RefreshToken string `cookie:"refresh_token" validate:"required"` -} diff --git a/backend/entities/auth/base/routes.go b/backend/entities/auth/base/routes.go index 1e5c86744..f1df0ec6c 100644 --- a/backend/entities/auth/base/routes.go +++ b/backend/entities/auth/base/routes.go @@ -1,24 +1,42 @@ package base import ( - "github.com/GenerateNU/sac/backend/constants" - "github.com/GenerateNU/sac/backend/types" + "github.com/GenerateNU/sac/backend/integrations/email" + "github.com/GenerateNU/sac/backend/integrations/oauth/soth" + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" ) -func Auth(params types.RouteParams) { - authController := NewAuthController(NewAuthService(params.ServiceParams)) +type Params struct { + authProvider soth.Provider + providers []soth.Provider + applicationURL string + router fiber.Router + db *gorm.DB +} - // api/v1/auth/* - auth := params.Router.Group("/auth") +func NewParams(authProvider soth.Provider, applicationURL string, router fiber.Router, db *gorm.DB, emailer email.Emailer, validate *validator.Validate, providers ...soth.Provider) Params { + return Params{ + authProvider: authProvider, + providers: providers, + applicationURL: applicationURL, + router: router, + db: db, + } +} - auth.Post("/register", authController.Register) - auth.Post("/logout", authController.Logout) - auth.Post("/login", authController.Login) - auth.Post("/refresh", authController.Refresh) +func Auth(params Params) { + soth.UseProviders( + append(params.providers, params.authProvider)..., + ) - auth.Post("/send-code", params.UtilityMiddleware.Limiter(constants.RATE_LIMIT_MAX_REQUESTS, constants.RATE_LIMIT_DURATION), authController.SendCode) - auth.Post("/verify-email", authController.VerifyEmail) + handler := Handler{db: params.db, authProvider: params.authProvider} - auth.Post("/forgot-password", params.UtilityMiddleware.Limiter(constants.RATE_LIMIT_MAX_REQUESTS, constants.RATE_LIMIT_DURATION), authController.ForgotPassword) - auth.Post("/verify-reset", authController.VerifyPasswordResetToken) + params.router.Route("/auth", func(r fiber.Router) { + r.Get("/login", handler.Login) + r.Get("/:provider", handler.Provider) + r.Get(":provider/callback", handler.ProviderCallback) + r.Get("/logout", handler.ProviderLogout) + }) } diff --git a/backend/entities/auth/base/service.go b/backend/entities/auth/base/service.go deleted file mode 100644 index c8a0c5415..000000000 --- a/backend/entities/auth/base/service.go +++ /dev/null @@ -1,461 +0,0 @@ -package base - -import ( - "context" - "errors" - "fmt" - "log/slog" - "time" - - "github.com/GenerateNU/sac/backend/auth" - "github.com/GenerateNU/sac/backend/constants" - authEntities "github.com/GenerateNU/sac/backend/entities/auth" - "github.com/GenerateNU/sac/backend/entities/models" - "github.com/GenerateNU/sac/backend/entities/users" - usersEntities "github.com/GenerateNU/sac/backend/entities/users/base" - - "github.com/GenerateNU/sac/backend/errs" - "github.com/GenerateNU/sac/backend/types" - - "github.com/GenerateNU/sac/backend/utilities" - - "github.com/golang-jwt/jwt" - "gorm.io/gorm" -) - -type AuthServiceInterface interface { - GetRole(id string) (*models.UserRole, error) - Register(ctx context.Context, userBody usersEntities.CreateUserRequestBody) (*models.User, *auth.Token, error) - Login(ctx context.Context, userBody authEntities.LoginResponseBody) (*models.User, *auth.Token, error) - Refresh(ctx context.Context, refreshToken string) (*auth.Token, error) - Logout(ctx context.Context, accessToken string, refreshToken string) error - - SendCode(email string) error - VerifyEmail(emailBody VerifyEmailRequestBody) error - - ForgotPassword(email string) error - VerifyPasswordResetToken(passwordBody VerifyPasswordResetTokenRequestBody) error -} - -type AuthService struct { - types.ServiceParams -} - -func NewAuthService(serviceParams types.ServiceParams) AuthServiceInterface { - return &AuthService{serviceParams} -} - -func (a *AuthService) Login(ctx context.Context, loginBody authEntities.LoginResponseBody) (*models.User, *auth.Token, error) { - if err := utilities.Validate(a.Validate, loginBody, *utilities.NewMaybeError("password", auth.ValidatePassword(loginBody.Password))); err != nil { - return nil, nil, err - } - - user, err := users.GetUserByEmail(a.DB, loginBody.Email) - if err != nil { - return nil, nil, err - } - - correct, err := auth.CompareHash(loginBody.Password, user.PasswordHash) - if err != nil || !correct { - return nil, nil, err - } - - tokens, err := a.JWT.GenerateTokenPair(auth.Claims{ - StandardClaims: &jwt.StandardClaims{ - IssuedAt: time.Now().Unix(), - Issuer: user.ID.String(), - }, - CustomClaims: &jwt.MapClaims{ - "role": user.Role, - }, - }, auth.Claims{ - StandardClaims: &jwt.StandardClaims{ - IssuedAt: time.Now().Unix(), - Issuer: user.ID.String(), - }, - }) - if err != nil { - return nil, nil, err - } - - storeRefreshTokenCtx, storeRefreshTokenCancel := context.WithTimeoutCause(ctx, constants.REDIS_TIMEOUT, errs.ErrRedisTimeout) - defer storeRefreshTokenCancel() - if err := a.Stores.ActiveToken.StoreRefreshToken(storeRefreshTokenCtx, string(tokens.RefreshToken), user.ID.String(), constants.REFRESH_TOKEN_EXPIRY); err != nil { - return nil, nil, err - } - - return user, tokens, nil -} - -func (a *AuthService) Register(ctx context.Context, userBody usersEntities.CreateUserRequestBody) (*models.User, *auth.Token, error) { - if err := utilities.Validate(a.Validate, userBody, *utilities.NewMaybeError("password", auth.ValidatePassword(userBody.Password))); err != nil { - return nil, nil, err - } - - user, err := utilities.MapJsonTags(userBody, &models.User{}) - if err != nil { - return nil, nil, err - } - - user.Role = models.Student - - passwordHash, err := auth.ComputeHash(userBody.Password) - if err != nil { - return nil, nil, err - } - - user.Email = utilities.NormalizeEmail(userBody.Email) - user.PasswordHash = *passwordHash - - if err := a.Integrations.Email.SendWelcomeEmail(fmt.Sprintf("%s %s", user.FirstName, user.LastName), user.Email); err != nil { - return nil, nil, fmt.Errorf("failed to send welcome email: %w", err) - } - - user, err = CreateUser(a.DB, user) - if err != nil { - return nil, nil, err - } - - if err := a.SendCode(user.Email); err != nil { - return nil, nil, err - } - - loginCtx, loginCancel := context.WithTimeoutCause(ctx, constants.REDIS_TIMEOUT, errs.ErrRedisTimeout) - defer loginCancel() - _, tokens, err := a.Login(loginCtx, authEntities.LoginResponseBody{Email: user.Email, Password: userBody.Password}) - if err != nil { - return nil, nil, err - } - - return user, tokens, nil -} - -func (a *AuthService) Refresh(ctx context.Context, refreshToken string) (*auth.Token, error) { - isActiveCtx, isActiveCancel := context.WithTimeoutCause(ctx, constants.REDIS_TIMEOUT, errs.ErrRedisTimeout) - defer isActiveCancel() - isActive, err := a.Stores.ActiveToken.IsActive(isActiveCtx, refreshToken) - if err != nil { - return nil, err - } - - if !isActive { - return nil, utilities.Unauthorized() - } - - isBlacklistedCtx, isBlacklistedCancel := context.WithTimeoutCause(ctx, constants.REDIS_TIMEOUT, errs.ErrRedisTimeout) - defer isBlacklistedCancel() - isblacklisted, err := a.Stores.Blacklist.IsTokenBlacklisted(isBlacklistedCtx, refreshToken) - if err != nil { - return nil, err - } - - if isblacklisted { - return nil, utilities.Unauthorized() - } - - claims, err := a.JWT.ExtractClaims(refreshToken, auth.RefreshToken) - if err != nil { - return nil, err - } - - role, err := a.GetRole(claims["iss"].(string)) - if err != nil { - return nil, err - } - - tokens, err := a.JWT.GenerateTokenPair(auth.Claims{ - StandardClaims: &jwt.StandardClaims{ - IssuedAt: time.Now().Unix(), - Issuer: claims["iss"].(string), - }, - CustomClaims: &jwt.MapClaims{ - "role": role, - }, - }, auth.Claims{ - StandardClaims: &jwt.StandardClaims{ - IssuedAt: time.Now().Unix(), - Issuer: claims["iss"].(string), - }, - }) - if err != nil { - return nil, err - } - - blackListCtx, blackListCancel := context.WithTimeoutCause(ctx, constants.REDIS_TIMEOUT, errs.ErrRedisTimeout) - defer blackListCancel() - if err := a.Stores.Blacklist.BlacklistToken(blackListCtx, refreshToken, time.Until(time.Unix(int64(claims["exp"].(float64)), 0))); err != nil { - return nil, err - } - - deleteRefreshTokenCtx, deleteRefreshTokenCancel := context.WithTimeoutCause(ctx, constants.REDIS_TIMEOUT, errs.ErrRedisTimeout) - defer deleteRefreshTokenCancel() - if err := a.Stores.ActiveToken.DeleteRefreshToken(deleteRefreshTokenCtx, refreshToken); err != nil { - return nil, err - } - - storeRefreshTokenCtx, storeRefreshTokenCancel := context.WithTimeoutCause(ctx, constants.REDIS_TIMEOUT, errs.ErrRedisTimeout) - defer storeRefreshTokenCancel() - if err := a.Stores.ActiveToken.StoreRefreshToken(storeRefreshTokenCtx, string(tokens.RefreshToken), claims["iss"].(string), constants.REFRESH_TOKEN_EXPIRY); err != nil { - return nil, err - } - - return tokens, nil -} - -func (a *AuthService) GetRole(id string) (*models.UserRole, error) { - idAsUUID, err := utilities.ValidateID(id) - if err != nil { - return nil, err - } - - user, err := users.GetUser(a.DB, *idAsUUID) - if err != nil { - return nil, err - } - - return &user.Role, nil -} - -func (a *AuthService) ForgotPassword(email string) error { - user, err := users.GetUserByEmail(a.DB, email) - if err != nil { - return nil - } - - activeToken, err := GetActiveTokenByUserID(a.DB, user.ID, models.PasswordResetType) - if err != nil { - if !utilities.IsNotFound(err) { - return err - } - } - - if activeToken != nil { - err := a.Integrations.Email.SendPasswordResetEmail(user.FirstName, user.Email, activeToken.Token) - if err != nil { - return err - } - - return nil - } - - token, err := auth.GenerateURLSafeToken(64) - if err != nil { - return err - } - - if err := SaveToken(a.DB, user.ID, *token, models.PasswordResetType, time.Now().Add(time.Hour*24).UTC()); err != nil { - return err - } - - if err := a.Integrations.Email.SendPasswordResetEmail(user.FirstName, user.Email, *token); err != nil { - return err - } - - return nil -} - -func (a *AuthService) VerifyPasswordResetToken(passwordBody VerifyPasswordResetTokenRequestBody) error { - if err := utilities.Validate(a.Validate, passwordBody); err != nil { - return err - } - - token, err := GetToken(a.DB, passwordBody.Token, models.PasswordResetType) - if err != nil { - return err - } - - if token.ExpiresAt.Before(time.Now().UTC()) { - return utilities.Unauthorized() - } - - hash, err := auth.ComputeHash(passwordBody.NewPassword) - if err != nil { - return fmt.Errorf("failed to hash password: %w", err) - } - - tx := a.DB.Begin() - defer func() { - if r := recover(); r != nil { - tx.Rollback() - } - }() - - if err := users.UpdatePassword(tx, token.UserID, *hash); err != nil { - tx.Rollback() - return err - } - - if err := DeleteToken(tx, passwordBody.Token, models.PasswordResetType); err != nil { - tx.Rollback() - return err - } - - if err := tx.Commit().Error; err != nil { - tx.Rollback() - return err - } - - return nil -} - -func (a *AuthService) SendCode(email string) error { - user, err := users.GetUserByEmail(a.DB, email) - if err != nil { - return err - } - - if user.IsVerified { - return fmt.Errorf("user is already verified") - } - - activeOTP, err := GetActiveTokenByUserID(a.DB, user.ID, models.EmailVerificationType) - if err != nil { - if !utilities.IsNotFound(err) { - return err - } - } - - if activeOTP != nil { - if err := a.Integrations.Email.SendEmailVerification(user.Email, activeOTP.Token); err != nil { - return err - } - - return nil - } - - otp, err := auth.GenerateOTP(constants.OTP_LENGTH) - if err != nil { - return fmt.Errorf("failed to generate OTP: %w", err) - } - - if err := SaveToken(a.DB, user.ID, *otp, models.EmailVerificationType, time.Now().Add(constants.OTP_EXPIRY).UTC()); err != nil { - return err - } - - if err := a.Integrations.Email.SendEmailVerification(user.Email, *otp); err != nil { - return err - } - - return nil -} - -func verifyEmailHelper(user *models.User, token string, db *gorm.DB) error { - if user.IsVerified { - return fmt.Errorf("user is already verified") - } - - otp, otpErr := GetToken(db, token, models.EmailVerificationType) - if otpErr != nil { - return otpErr - } - - if otp.Token != token { - slog.Error("invalid otp", "otp", otp.Token, "token", token) - return utilities.BadRequest(errors.New("invalid otp")) - } - - if otp.ExpiresAt.Before(time.Now().UTC()) { - return utilities.BadRequest(errors.New("otp expired")) - } - - return nil -} - -func (a *AuthService) VerifyEmail(emailBody VerifyEmailRequestBody) error { - if err := utilities.Validate(a.Validate, emailBody); err != nil { - return err - } - - user, err := users.GetUserByEmail(a.DB, emailBody.Email) - if err != nil { - return err - } - - if err := verifyEmailHelper(user, emailBody.Token, a.DB); err != nil { - return err - } - - tx := a.DB.Begin() - defer func() { - if r := recover(); r != nil { - tx.Rollback() - } - }() - - if err := users.UpdateEmailVerification(tx, user.ID); err != nil { - tx.Rollback() - return err - } - - if err := DeleteToken(tx, emailBody.Token, models.EmailVerificationType); err != nil { - tx.Rollback() - return err - } - - if err := tx.Commit().Error; err != nil { - tx.Rollback() - return err - } - - return nil -} - -func (a *AuthService) Logout(ctx context.Context, accessToken string, refreshToken string) error { - if accessToken == "" { - return utilities.Unauthorized() - } - - token, err := utilities.SplitBearerToken(accessToken) - if err != nil { - return utilities.Unauthorized() - } - - blacklistAccessTokenCtx, blacklistAccessTokenCancel := context.WithTimeoutCause(ctx, constants.REDIS_TIMEOUT, errs.ErrRedisTimeout) - defer blacklistAccessTokenCancel() - if err = blacklist(blacklistAccessTokenCtx, a, token); err != nil { - return utilities.Unauthorized() - } - - if refreshToken == "" { - return utilities.Unauthorized() - } - - isActiveCtx, isActiveCancel := context.WithTimeoutCause(ctx, constants.REDIS_TIMEOUT, errs.ErrRedisTimeout) - defer isActiveCancel() - isActive, err := a.Stores.ActiveToken.IsActive(isActiveCtx, refreshToken) - if err != nil { - return utilities.Unauthorized() - } - - if !isActive { - return utilities.Unauthorized() - } - - blacklistRefreshTokenCtx, blacklistRefreshTokenCancel := context.WithTimeoutCause(ctx, constants.REDIS_TIMEOUT, errs.ErrRedisTimeout) - defer blacklistRefreshTokenCancel() - if err := blacklist(blacklistRefreshTokenCtx, a, &refreshToken); err != nil { - return utilities.Unauthorized() - } - - deleteRefreshTokenCtx, deleteRefreshTokenCancel := context.WithTimeoutCause(ctx, constants.REDIS_TIMEOUT, errs.ErrRedisTimeout) - defer deleteRefreshTokenCancel() - if err := a.Stores.ActiveToken.DeleteRefreshToken(deleteRefreshTokenCtx, refreshToken); err != nil { - return err - } - - return nil -} - -func blacklist(ctx context.Context, a *AuthService, token *string) error { - claims, err := a.JWT.ExtractClaims(*token, auth.AccessToken) - if err != nil { - return err - } else { - exp, ok := claims["exp"].(float64) - if ok { - _ = a.Stores.Blacklist.BlacklistToken(ctx, *token, time.Until(time.Unix(int64(exp), 0))) - } - } - - return nil -} diff --git a/backend/entities/auth/base/transactions.go b/backend/entities/auth/base/transactions.go index a8812c280..1dd839c52 100644 --- a/backend/entities/auth/base/transactions.go +++ b/backend/entities/auth/base/transactions.go @@ -1,69 +1,47 @@ package base import ( + "context" "errors" - "time" "github.com/GenerateNU/sac/backend/entities/models" - "github.com/GenerateNU/sac/backend/utilities" - "github.com/google/uuid" + "github.com/GenerateNU/sac/backend/integrations/oauth/soth" "gorm.io/gorm" ) -func CreateUser(db *gorm.DB, user *models.User) (*models.User, error) { - if err := db.Create(user).Error; err != nil { - if errors.Is(err, gorm.ErrDuplicatedKey) { - return nil, utilities.ErrDuplicate - } - return nil, err - } - - return user, nil -} - -func GetToken(db *gorm.DB, token string, tokenType models.VerificationType) (*models.Verification, error) { - tokenModel := models.Verification{} - if err := db.Where("token = ? AND type = ?", token, tokenType).First(&tokenModel).Error; err != nil { +func FindOrCreateUser(ctx context.Context, db *gorm.DB, user soth.User) (*models.User, error) { + var sacUser models.User + if err := db.WithContext(ctx).Where("email = ?", user.Email).First(&sacUser).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, utilities.ErrNotFound + if err := createUser(ctx, db, user.Into()); err != nil { + return nil, err + } + } else { + return nil, err } - return nil, err } - return &tokenModel, nil + return &sacUser, nil } -func GetActiveTokenByUserID(db *gorm.DB, userID uuid.UUID, tokenType models.VerificationType) (*models.Verification, error) { - token := models.Verification{} - if err := db.Where("user_id = ? AND expires_at > ? AND type = ?", userID, time.Now().UTC(), tokenType).First(&token).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, utilities.ErrNotFound +func createUser(ctx context.Context, db *gorm.DB, user *models.User) error { + tx := db.WithContext(ctx).Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() } - return nil, err - } + }() - return &token, nil -} - -func DeleteToken(db *gorm.DB, token string, tokenType models.VerificationType) error { - if err := db.Where("token = ? AND type = ?", token, tokenType).Delete(&models.Verification{}).Error; err != nil { + if err := tx.Create(user).Error; err != nil { + tx.Rollback() return err } - return nil -} - -func SaveToken(db *gorm.DB, userID uuid.UUID, token string, tokenType models.VerificationType, expiry time.Time) error { - tokenModel := models.Verification{ - UserID: userID, - Token: token, - ExpiresAt: expiry, - Type: tokenType, - } - - if err := db.Create(&tokenModel).Error; err != nil { + welcomeTask := models.WelcomeTask{Name: user.Name, Email: user.Email} + if err := tx.Create(&welcomeTask).Error; err != nil { + tx.Rollback() return err } - return nil + return tx.Commit().Error } diff --git a/backend/entities/categories/base/routes.go b/backend/entities/categories/base/routes.go index f41980ee0..a9458cb68 100644 --- a/backend/entities/categories/base/routes.go +++ b/backend/entities/categories/base/routes.go @@ -1,8 +1,8 @@ package base import ( - "github.com/GenerateNU/sac/backend/auth" "github.com/GenerateNU/sac/backend/entities/categories/tags" + "github.com/GenerateNU/sac/backend/permission" "github.com/GenerateNU/sac/backend/types" "github.com/gofiber/fiber/v2" ) @@ -22,15 +22,15 @@ func Category(categoryParams types.RouteParams) fiber.Router { // api/v1/categories/* categories := categoryParams.Router.Group("/categories") - categories.Post("/", categoryParams.AuthMiddleware.Authorize(auth.CreateAll), categoryController.CreateCategory) + categories.Post("/", categoryParams.AuthMiddleware.Authorize(permission.CreateAll), categoryController.CreateCategory) categories.Get("/", categoryParams.UtilityMiddleware.Paginator, categoryController.GetCategories) // api/v1/categories/:categoryID/* categoryID := categories.Group("/:categoryID") categoryID.Get("/", categoryController.GetCategory) - categoryID.Delete("/", categoryParams.AuthMiddleware.Authorize(auth.DeleteAll), categoryController.DeleteCategory) - categoryID.Patch("/", categoryParams.AuthMiddleware.Authorize(auth.WriteAll), categoryController.UpdateCategory) + categoryID.Delete("/", categoryParams.AuthMiddleware.Authorize(permission.DeleteAll), categoryController.DeleteCategory) + categoryID.Patch("/", categoryParams.AuthMiddleware.Authorize(permission.WriteAll), categoryController.UpdateCategory) return categoryID } diff --git a/backend/entities/clubs/base/controller.go b/backend/entities/clubs/base/controller.go index ae6e52bcc..a772c6b6b 100644 --- a/backend/entities/clubs/base/controller.go +++ b/backend/entities/clubs/base/controller.go @@ -66,12 +66,12 @@ func (cl *ClubController) CreateClub(c *fiber.Ctx) error { return utilities.InvalidJSON() } - userID, err := locals.UserIDFrom(c) + user, err := locals.UserFrom(c) if err != nil { return err } - club, err := cl.clubService.CreateClub(*userID, clubBody) + club, err := cl.clubService.CreateClub(user.ID, clubBody) if err != nil { return err } diff --git a/backend/entities/clubs/base/routes.go b/backend/entities/clubs/base/routes.go index d90ffd0e5..c3d234be9 100644 --- a/backend/entities/clubs/base/routes.go +++ b/backend/entities/clubs/base/routes.go @@ -1,7 +1,6 @@ package base import ( - p "github.com/GenerateNU/sac/backend/auth" "github.com/GenerateNU/sac/backend/entities/clubs/events" "github.com/GenerateNU/sac/backend/entities/clubs/followers" "github.com/GenerateNU/sac/backend/entities/clubs/leadership" @@ -10,6 +9,7 @@ import ( "github.com/GenerateNU/sac/backend/entities/clubs/socials" "github.com/GenerateNU/sac/backend/entities/clubs/tags" authMiddleware "github.com/GenerateNU/sac/backend/middleware/auth" + "github.com/GenerateNU/sac/backend/permission" "github.com/GenerateNU/sac/backend/types" "github.com/gofiber/fiber/v2" @@ -34,7 +34,7 @@ func ClubRouter(clubParams types.RouteParams) fiber.Router { clubs := clubParams.Router.Group("/clubs") clubs.Get("/", clubParams.UtilityMiddleware.Paginator, clubController.GetClubs) - clubs.Post("/", clubParams.AuthMiddleware.Authorize(p.CreateAll), clubController.CreateClub) + clubs.Post("/", clubParams.AuthMiddleware.Authorize(permission.CreateAll), clubController.CreateClub) // api/v1/clubs/:clubID/* clubID := clubs.Group("/:clubID") @@ -45,7 +45,7 @@ func ClubRouter(clubParams types.RouteParams) fiber.Router { authMiddleware.AttachExtractor(clubParams.AuthMiddleware.ClubAuthorizeById, authMiddleware.ExtractFromParams("clubID")), clubController.UpdateClub, ) - clubID.Delete("/", clubParams.AuthMiddleware.Authorize(p.DeleteAll), clubController.DeleteClub) + clubID.Delete("/", clubParams.AuthMiddleware.Authorize(permission.DeleteAll), clubController.DeleteClub) return clubID } diff --git a/backend/entities/clubs/base/transactions.go b/backend/entities/clubs/base/transactions.go index 65c924e07..0cb853e94 100644 --- a/backend/entities/clubs/base/transactions.go +++ b/backend/entities/clubs/base/transactions.go @@ -2,7 +2,6 @@ package base import ( "errors" - "log/slog" "github.com/GenerateNU/sac/backend/constants" "github.com/GenerateNU/sac/backend/utilities" @@ -48,7 +47,6 @@ func CreateClub(db *gorm.DB, userId uuid.UUID, club models.Club) (*models.Club, if err := tx.Create(&club).Error; err != nil { tx.Rollback() - slog.Info("err in create club", "err", err) return nil, err } @@ -60,7 +58,6 @@ func CreateClub(db *gorm.DB, userId uuid.UUID, club models.Club) (*models.Club, if err := tx.Create(&membership).Error; err != nil { tx.Rollback() - slog.Info("err in create membership", "err", err) return nil, err } diff --git a/backend/entities/clubs/recruitment/controller.go b/backend/entities/clubs/recruitment/controller.go index cf321d1f2..3d04efc79 100644 --- a/backend/entities/clubs/recruitment/controller.go +++ b/backend/entities/clubs/recruitment/controller.go @@ -22,7 +22,7 @@ func (cr *ClubRecruitmentController) CreateClubRecruitment(c *fiber.Ctx) error { return utilities.InvalidJSON() } - recruitment, err := cr.clubRecruitmentService.CreateClubRecruitment(c.UserContext(), c.Params("clubID"), body) + recruitment, err := cr.clubRecruitmentService.CreateClubRecruitment(c.Context(), c.Params("clubID"), body) if err != nil { return err } @@ -31,7 +31,7 @@ func (cr *ClubRecruitmentController) CreateClubRecruitment(c *fiber.Ctx) error { } func (cr *ClubRecruitmentController) GetClubRecruitment(c *fiber.Ctx) error { - recruitment, err := cr.clubRecruitmentService.GetClubRecruitment(c.UserContext(), c.Params("clubID")) + recruitment, err := cr.clubRecruitmentService.GetClubRecruitment(c.Context(), c.Params("clubID")) if err != nil { return err } @@ -45,7 +45,7 @@ func (cr *ClubRecruitmentController) UpdateClubRecruitment(c *fiber.Ctx) error { return utilities.InvalidJSON() } - recruitment, err := cr.clubRecruitmentService.UpdateClubRecruitment(c.UserContext(), c.Params("clubID"), body) + recruitment, err := cr.clubRecruitmentService.UpdateClubRecruitment(c.Context(), c.Params("clubID"), body) if err != nil { return err } @@ -54,7 +54,7 @@ func (cr *ClubRecruitmentController) UpdateClubRecruitment(c *fiber.Ctx) error { } func (cr *ClubRecruitmentController) DeleteClubRecruitment(c *fiber.Ctx) error { - if err := cr.clubRecruitmentService.DeleteClubRecruitment(c.UserContext(), c.Params("clubID")); err != nil { + if err := cr.clubRecruitmentService.DeleteClubRecruitment(c.Context(), c.Params("clubID")); err != nil { return err } @@ -67,7 +67,7 @@ func (cr *ClubRecruitmentController) CreateClubRecruitmentApplication(c *fiber.C return utilities.InvalidJSON() } - application, err := cr.clubRecruitmentService.CreateClubRecruitmentApplication(c.UserContext(), c.Params("clubID"), body) + application, err := cr.clubRecruitmentService.CreateClubRecruitmentApplication(c.Context(), c.Params("clubID"), body) if err != nil { return err } @@ -81,7 +81,7 @@ func (cr *ClubRecruitmentController) GetClubRecruitmentApplications(c *fiber.Ctx return utilities.ErrExpectedPageInfo } - applications, err := cr.clubRecruitmentService.GetClubRecruitmentApplications(c.UserContext(), c.Params("clubID"), *pageInfo) + applications, err := cr.clubRecruitmentService.GetClubRecruitmentApplications(c.Context(), c.Params("clubID"), *pageInfo) if err != nil { return err } @@ -90,7 +90,7 @@ func (cr *ClubRecruitmentController) GetClubRecruitmentApplications(c *fiber.Ctx } func (cr *ClubRecruitmentController) GetClubRecruitmentApplication(c *fiber.Ctx) error { - application, err := cr.clubRecruitmentService.GetClubRecruitmentApplication(c.UserContext(), c.Params("clubID"), c.Params("applicationID")) + application, err := cr.clubRecruitmentService.GetClubRecruitmentApplication(c.Context(), c.Params("clubID"), c.Params("applicationID")) if err != nil { return err } @@ -104,7 +104,7 @@ func (cr *ClubRecruitmentController) UpdateClubRecruitmentApplication(c *fiber.C return utilities.InvalidJSON() } - application, err := cr.clubRecruitmentService.UpdateClubRecruitmentApplication(c.UserContext(), c.Params("clubID"), c.Params("applicationID"), body) + application, err := cr.clubRecruitmentService.UpdateClubRecruitmentApplication(c.Context(), c.Params("clubID"), c.Params("applicationID"), body) if err != nil { return err } @@ -113,7 +113,7 @@ func (cr *ClubRecruitmentController) UpdateClubRecruitmentApplication(c *fiber.C } func (cr *ClubRecruitmentController) DeleteClubRecruitmentApplication(c *fiber.Ctx) error { - if err := cr.clubRecruitmentService.DeleteClubRecruitmentApplication(c.UserContext(), c.Params("clubID"), c.Params("applicationID")); err != nil { + if err := cr.clubRecruitmentService.DeleteClubRecruitmentApplication(c.Context(), c.Params("clubID"), c.Params("applicationID")); err != nil { return err } diff --git a/backend/entities/models/oauth.go b/backend/entities/models/oauth.go deleted file mode 100644 index acb736504..000000000 --- a/backend/entities/models/oauth.go +++ /dev/null @@ -1,35 +0,0 @@ -package models - -import ( - "time" - - "github.com/google/uuid" -) - -type OAuthResource string - -const ( - Google OAuthResource = "google" - Outlook OAuthResource = "outlook" -) - -type UserOAuthTokens struct { - UserID uuid.UUID `json:"user_id" validate:"required,uuid4"` - RefreshToken string `json:"refresh_token" validate:"max=255"` - AccessToken string `json:"access_token" validate:"max=255"` - CSRFToken string `json:"csrf_token" validate:"max=255"` - ResourceType OAuthResource `json:"resource_type" validate:"required"` - ExpiresAt time.Time `json:"expires_at" validate:"required"` -} - -type OAuthToken struct { - AccessToken string `json:"access_token" validate:"required"` - ExpiresIn int `json:"expires_in" validate:"required"` - RefreshToken string `json:"refresh_token" validate:"required"` - Scope string `json:"scope" validate:"required"` - TokenType string `json:"token_type" validate:"required"` -} - -func (UserOAuthTokens) TableName() string { - return "user_oauth_tokens" -} diff --git a/backend/entities/models/tokens.go b/backend/entities/models/tokens.go deleted file mode 100644 index b1b527f76..000000000 --- a/backend/entities/models/tokens.go +++ /dev/null @@ -1,6 +0,0 @@ -package models - -type Tokens struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` -} diff --git a/backend/entities/models/user.go b/backend/entities/models/user.go index bf1bb3b1e..a3b166fd9 100644 --- a/backend/entities/models/user.go +++ b/backend/entities/models/user.go @@ -1,24 +1,29 @@ package models import ( + "encoding/gob" + + go_json "github.com/goccy/go-json" "gorm.io/gorm" ) +func init() { + gob.Register(User{}) +} + type User struct { Model Role UserRole `json:"role"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` + Name string `json:"name"` Email string `json:"email"` - PasswordHash string `json:"-"` Major0 Major `json:"major0"` Major1 Major `json:"major1"` Major2 Major `json:"major2"` College College `json:"college"` GraduationCycle GraduationCycle `json:"graduation_cycle"` GraduationYear int16 `json:"graduation_year"` - IsVerified bool `json:"is_verified"` + AvatarURL string `json:"avatar_url"` Tag []Tag `gorm:"many2many:user_tags;" json:"-"` Admin []Club `gorm:"many2many:user_club_admins;" json:"-"` @@ -31,6 +36,23 @@ type User struct { Waitlist []Event `gorm:"many2many:user_event_waitlists;" json:"-"` } +func (u *User) Marshal() (*string, error) { + user, err := go_json.Marshal(u) + if err != nil { + return nil, err + } + strUser := string(user) + return &strUser, nil +} + +func UnmarshalUser(user string) *User { + u := User{} + if err := go_json.Unmarshal([]byte(user), &u); err != nil { + return nil + } + return &u +} + func (u *User) AfterCreate(tx *gorm.DB) (err error) { sac := &Club{} if err := tx.Where("name = ?", "SAC").First(sac).Error; err != nil { diff --git a/backend/entities/models/verification.go b/backend/entities/models/verification.go deleted file mode 100644 index 77546ec05..000000000 --- a/backend/entities/models/verification.go +++ /dev/null @@ -1,21 +0,0 @@ -package models - -import ( - "time" - - "github.com/google/uuid" -) - -type VerificationType string - -const ( - EmailVerificationType VerificationType = "email_verification" - PasswordResetType VerificationType = "password_reset" -) - -type Verification struct { - UserID uuid.UUID `json:"user_id"` - Token string `json:"token"` - ExpiresAt time.Time `json:"expires_at"` - Type VerificationType `json:"type"` -} diff --git a/backend/entities/models/welcome_task.go b/backend/entities/models/welcome_task.go new file mode 100644 index 000000000..fe4cce905 --- /dev/null +++ b/backend/entities/models/welcome_task.go @@ -0,0 +1,7 @@ +package models + +type WelcomeTask struct { + Email string `gorm:"primaryKey"` + Name string + Attempts int +} diff --git a/backend/entities/oauth/base/controller.go b/backend/entities/oauth/base/controller.go deleted file mode 100644 index 4c0343e41..000000000 --- a/backend/entities/oauth/base/controller.go +++ /dev/null @@ -1,90 +0,0 @@ -package base - -import ( - "errors" - "net/http" - - "github.com/GenerateNU/sac/backend/entities/models" - "github.com/GenerateNU/sac/backend/locals" - - "github.com/gofiber/fiber/v2" -) - -type OAuthController struct { - OAuthService OAuthServiceInterface -} - -func NewOAuthController(oauthService OAuthServiceInterface) *OAuthController { - return &OAuthController{OAuthService: oauthService} -} - -func (oc *OAuthController) Authorize(c *fiber.Ctx) error { - // Extract the resource type from the query params: - resourceType := models.OAuthResource(c.Query("type")) - if resourceType == "" { - return errors.New("resource type is required") - } - - // Extract the user making the call: - userID, err := locals.UserIDFrom(c) - if err != nil { - return err - } - - // Call the respective authorize method: - authUrl, err := oc.OAuthService.Authorize(*userID, resourceType) - if err != nil { - return err - } - - return c.Status(http.StatusOK).JSON(*authUrl) -} - -func (oc *OAuthController) Token(c *fiber.Ctx) error { - // Extract the resource type from the query params: - resourceType := models.OAuthResource(c.Query("type")) - if resourceType == "" { - return errors.New("resource type is required") - } - - // Parse the body of the request: - var tokenBody OAuthTokenRequestBody - if err := c.BodyParser(&tokenBody); err != nil { - return err - } - - // Extract the user making the call: - userID, err := locals.UserIDFrom(c) - if err != nil { - return err - } - - // Call the respective token method: - err = oc.OAuthService.Token(*userID, tokenBody, resourceType) - if err != nil { - return err - } - - return c.SendStatus(http.StatusNoContent) -} - -func (oc *OAuthController) Revoke(c *fiber.Ctx) error { - // Extract the resource type from the query params: - resourceType := models.OAuthResource(c.Query("type")) - if resourceType == "" { - return errors.New("resource type is required") - } - - // Extract the user making the call: - userID, err := locals.UserIDFrom(c) - if err != nil { - return err - } - - err = oc.OAuthService.Revoke(*userID, resourceType) - if err != nil { - return err - } - - return c.Status(http.StatusOK).JSON("Successfully revoked token") -} diff --git a/backend/entities/oauth/base/models.go b/backend/entities/oauth/base/models.go deleted file mode 100644 index ae04dca26..000000000 --- a/backend/entities/oauth/base/models.go +++ /dev/null @@ -1,6 +0,0 @@ -package base - -type OAuthTokenRequestBody struct { - Code string `json:"code" validate:"omitempty"` - State string `json:"state" validate:"omitempty"` -} diff --git a/backend/entities/oauth/base/routes.go b/backend/entities/oauth/base/routes.go deleted file mode 100644 index fc0971e22..000000000 --- a/backend/entities/oauth/base/routes.go +++ /dev/null @@ -1,14 +0,0 @@ -package base - -import "github.com/GenerateNU/sac/backend/types" - -func OAuth(params types.RouteParams) { - oauthController := NewOAuthController(&OAuthService{ServiceParams: params.ServiceParams}) - - // api/v1/calendar/* - calendar := params.Router.Group("/oauth") - - calendar.Get("/authorize", oauthController.Authorize) - calendar.Post("/token", oauthController.Token) - calendar.Delete("/revoke", oauthController.Revoke) -} diff --git a/backend/entities/oauth/base/service.go b/backend/entities/oauth/base/service.go deleted file mode 100644 index 7ff7ddd68..000000000 --- a/backend/entities/oauth/base/service.go +++ /dev/null @@ -1,194 +0,0 @@ -package base - -import ( - "errors" - "fmt" - "io" - "net/http" - "net/url" - - go_json "github.com/goccy/go-json" - - "github.com/GenerateNU/sac/backend/auth" - "github.com/GenerateNU/sac/backend/constants" - "github.com/GenerateNU/sac/backend/entities/models" - "github.com/GenerateNU/sac/backend/integrations/oauth" - "github.com/GenerateNU/sac/backend/types" - "github.com/GenerateNU/sac/backend/utilities" - "github.com/google/uuid" -) - -type OAuthServiceInterface interface { - Authorize(userID uuid.UUID, resource models.OAuthResource) (*string, error) - Token(userID uuid.UUID, tokenBody OAuthTokenRequestBody, resource models.OAuthResource) error - Revoke(userID uuid.UUID, resource models.OAuthResource) error -} - -type OAuthService struct { - ServiceParams types.ServiceParams -} - -func New(serviceParams types.ServiceParams) OAuthServiceInterface { - return &OAuthService{ - ServiceParams: serviceParams, - } -} - -func (e *OAuthService) Authorize(userID uuid.UUID, resource models.OAuthResource) (*string, error) { - // Parse the resource client: - client, err := e.parseResourceClient(resource) - if err != nil { - return nil, err - } - - // Create a CSRF Token: - csrfToken, err := auth.GenerateURLSafeToken(constants.CSRF_TOKEN_LENGTH) - if err != nil { - return nil, err - } - - // Save the CSRF Token to the database: - if err := CreateOAuthToken(e.ServiceParams.DB, - models.UserOAuthTokens{ - UserID: userID, - CSRFToken: *csrfToken, - ResourceType: resource, - }); err != nil { - return nil, err - } - - // Retrieve the authorize URL: - authorizeURL := client.ResourceClient.AuthorizeURL(*csrfToken) - - return &authorizeURL, nil -} - -func (e *OAuthService) Token(userID uuid.UUID, tokenBody OAuthTokenRequestBody, resource models.OAuthResource) error { - // Parse the resource client: - client, err := e.parseResourceClient(resource) - if err != nil { - return err - } - - // Retrieve the CSRF Token from the database: - oauthToken, err := GetOAuthToken(e.ServiceParams.DB, userID, resource) - if err != nil { - return err - } - - // Validate the CSRF Token: - if oauthToken.CSRFToken != tokenBody.State || oauthToken.CSRFToken == "" { - return errors.New("invalid CSRF token") - } - - // Exchange the code for an access token: - token, err := e.tokenExchange(client, tokenBody.Code) - if err != nil { - return err - } - - // Save the refresh token to the database: - if err := SaveOAuthTokens(e.ServiceParams.DB, userID, *token, resource); err != nil { - return err - } - - return nil -} - -func (e *OAuthService) Revoke(userID uuid.UUID, resource models.OAuthResource) error { - // Parse the resource client: - client, err := e.parseResourceClient(resource) - if err != nil { - return err - } - - // Retrieve access token: - oauthToken, err := GetOAuthToken(e.ServiceParams.DB, userID, resource) - if err != nil { - return err - } - - if oauthToken.AccessToken == "" { - return errors.New("user does not have an access token") - } - - // Revoke the token on resource server: - if err := client.ResourceClient.Revoke(oauthToken.AccessToken); err != nil { - return err - } - - // Remove refresh token from the database: - if err := DeleteOAuthToken(e.ServiceParams.DB, userID, resource); err != nil { - return err - } - - return nil -} - -func (e *OAuthService) parseResourceClient(resourceType models.OAuthResource) (*oauth.OAuthClient, error) { - var resourceClient oauth.OAuthResourceClientInterface - switch resourceType { - case "google": - resourceClient = &e.ServiceParams.Integrations.OAuth.Google - case "outlook": - resourceClient = &e.ServiceParams.Integrations.OAuth.Outlook - default: - return nil, errors.New("invalid resource type") - } - - return &oauth.OAuthClient{ - ResourceClient: resourceClient, - }, nil -} - -func (e *OAuthService) tokenExchange(client *oauth.OAuthClient, code string) (*models.OAuthToken, error) { - // Retrieve the client config - config := client.ResourceClient.GetConfig() - tokenUrl := fmt.Sprintf("%s/token", config.TokenURL) - - // Create the form data: - formData := url.Values{ - "client_id": {config.ClientID.Expose()}, - "client_secret": {config.ClientSecret.Expose()}, - "code": {code}, - "redirect_uri": {config.RedirectURI}, - "grant_type": {"authorization_code"}, - } - - encodedData := formData.Encode() - - resp, err := utilities.Request(http.MethodPost, tokenUrl, []byte(encodedData), utilities.FormURLEncoded()) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if !utilities.IsOk(resp.StatusCode) { - return nil, errors.New("failed to exchange code for token") - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - // Parse the data: - var oauthToken models.OAuthToken - if err = go_json.Unmarshal(body, &oauthToken); err != nil { - return nil, err - } - - if err := utilities.Validate(e.ServiceParams.Validate, oauthToken); err != nil { - return nil, err - } - - if oauthToken.AccessToken == "" { - return nil, errors.New("access token is empty") - } - - if oauthToken.RefreshToken == "" { - return nil, errors.New("refresh token is empty") - } - - return &oauthToken, nil -} diff --git a/backend/entities/oauth/base/transactions.go b/backend/entities/oauth/base/transactions.go deleted file mode 100644 index f2433369d..000000000 --- a/backend/entities/oauth/base/transactions.go +++ /dev/null @@ -1,59 +0,0 @@ -package base - -import ( - "errors" - "time" - - "github.com/GenerateNU/sac/backend/entities/models" - "github.com/GenerateNU/sac/backend/utilities" - "github.com/google/uuid" - "gorm.io/gorm" -) - -func CreateOAuthToken(db *gorm.DB, oauthToken models.UserOAuthTokens) error { - if err := db.Where("user_id = ? AND resource_type = ?", oauthToken.UserID, oauthToken.ResourceType).Save(&oauthToken).Error; err != nil { - return err - } - - return nil -} - -func GetOAuthToken(db *gorm.DB, userID uuid.UUID, resourceType models.OAuthResource) (*models.UserOAuthTokens, error) { - var oauthToken models.UserOAuthTokens - - if err := db.Where("user_id = ? AND resource_type = ?", userID, resourceType).First(&oauthToken).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, utilities.ErrNotFound - } - return nil, err - } - - return &oauthToken, nil -} - -func SaveOAuthTokens(db *gorm.DB, userID uuid.UUID, tokens models.OAuthToken, resourceType models.OAuthResource) error { - oauthToken := models.UserOAuthTokens{ - UserID: userID, - AccessToken: tokens.AccessToken, - RefreshToken: tokens.RefreshToken, - ExpiresAt: time.Now().Add(time.Duration(tokens.ExpiresIn) * time.Second), - ResourceType: resourceType, - } - - if err := db.Where("user_id = ? AND resource_type = ?", userID, resourceType).Save(&oauthToken).Error; err != nil { - return err - } - - return nil -} - -func DeleteOAuthToken(db *gorm.DB, userID uuid.UUID, resourceType models.OAuthResource) error { - if err := db.Where("user_id = ? AND resource_type = ?", userID, resourceType).Delete(&models.UserOAuthTokens{}).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return utilities.ErrNotFound - } - return err - } - - return nil -} diff --git a/backend/entities/socials/base/routes.go b/backend/entities/socials/base/routes.go index 2b0187240..3694aeb56 100644 --- a/backend/entities/socials/base/routes.go +++ b/backend/entities/socials/base/routes.go @@ -1,7 +1,7 @@ package base import ( - "github.com/GenerateNU/sac/backend/auth" + "github.com/GenerateNU/sac/backend/permission" "github.com/GenerateNU/sac/backend/types" ) @@ -13,5 +13,5 @@ func Social(socialParams types.RouteParams) { socials.Get("/", socialParams.UtilityMiddleware.Paginator, socialController.GetSocials) socials.Get("/:socialID", socialController.GetSocial) - socials.Delete("/:socialID", socialParams.AuthMiddleware.Authorize(auth.DeleteAll), socialController.DeleteSocial) + socials.Delete("/:socialID", socialParams.AuthMiddleware.Authorize(permission.DeleteAll), socialController.DeleteSocial) } diff --git a/backend/entities/tags/base/routes.go b/backend/entities/tags/base/routes.go index ca2050f20..1b4510baa 100644 --- a/backend/entities/tags/base/routes.go +++ b/backend/entities/tags/base/routes.go @@ -1,7 +1,7 @@ package base import ( - p "github.com/GenerateNU/sac/backend/auth" + "github.com/GenerateNU/sac/backend/permission" "github.com/GenerateNU/sac/backend/types" ) @@ -11,11 +11,11 @@ func Tag(tagParams types.RouteParams) { tags := tagParams.Router.Group("/tags") tags.Get("/", tagController.GetTags) - tags.Post("/", tagParams.AuthMiddleware.Authorize(p.CreateAll), tagController.CreateTag) + tags.Post("/", tagParams.AuthMiddleware.Authorize(permission.CreateAll), tagController.CreateTag) tagID := tags.Group("/:tagID") tagID.Get("/", tagController.GetTag) - tagID.Patch("/", tagParams.AuthMiddleware.Authorize(p.WriteAll), tagController.UpdateTag) - tagID.Delete("/", tagParams.AuthMiddleware.Authorize(p.DeleteAll), tagController.DeleteTag) + tagID.Patch("/", tagParams.AuthMiddleware.Authorize(permission.WriteAll), tagController.UpdateTag) + tagID.Delete("/", tagParams.AuthMiddleware.Authorize(permission.DeleteAll), tagController.DeleteTag) } diff --git a/backend/entities/users/base/controller.go b/backend/entities/users/base/controller.go index 202387350..f36aa84fc 100644 --- a/backend/entities/users/base/controller.go +++ b/backend/entities/users/base/controller.go @@ -3,8 +3,6 @@ package base import ( "net/http" - authEntities "github.com/GenerateNU/sac/backend/entities/auth" - "github.com/GenerateNU/sac/backend/locals" "github.com/GenerateNU/sac/backend/utilities" "github.com/garrettladley/fiberpaginate" @@ -47,35 +45,6 @@ func (u *UserController) GetUsers(c *fiber.Ctx) error { return c.Status(http.StatusOK).JSON(&users) } -// Me godoc -// -// @Summary Retrieves the currently authenticated user -// @Description Retrieves the currently authenticated user -// @ID get-me -// @Tags auth -// @Accept json -// @Produce json -// @Security Bearer -// @Success 200 {object} models.User -// @Failure 400 {object} error -// @Failure 401 {object} error -// @Failure 404 {object} error -// @Failure 500 {object} error -// @Router /auth/me [get] -func (u *UserController) GetMe(c *fiber.Ctx) error { - userID, err := locals.UserIDFrom(c) - if err != nil { - return err - } - - user, err := u.userService.GetMe(*userID) - if err != nil { - return err - } - - return c.Status(http.StatusOK).JSON(user) -} - // GetUser godoc // // @Summary Retrieve a user @@ -130,37 +99,6 @@ func (u *UserController) UpdateUser(c *fiber.Ctx) error { return c.Status(http.StatusOK).JSON(updatedUser) } -// UpdatePassword godoc -// -// @Summary Update a user's password -// @Description Updates a user's password -// @ID update-password -// @Tags user -// @Accept json -// @Produce json -// @Param userID path string true "User ID" -// @Param passwordBody body authEntities.UpdatePasswordRequestBody true "Password Body" -// @Success 200 {string} utilities.SuccessResponse -// @Failure 400 {object} error -// @Failure 401 {object} error -// @Failure 404 {object} error -// @Failure 500 {object} error -// @Router /users/{userID}/password [patch] -func (u *UserController) UpdatePassword(c *fiber.Ctx) error { - var passwordBody authEntities.UpdatePasswordRequestBody - - if err := c.BodyParser(&passwordBody); err != nil { - return utilities.InvalidJSON() - } - - err := u.userService.UpdatePassword(c.Params("userID"), passwordBody) - if err != nil { - return err - } - - return utilities.FiberMessage(c, http.StatusOK, "success") -} - // DeleteUser godoc // // @Summary Delete a user diff --git a/backend/entities/users/base/models.go b/backend/entities/users/base/models.go index d145e8993..44423bc56 100644 --- a/backend/entities/users/base/models.go +++ b/backend/entities/users/base/models.go @@ -2,23 +2,8 @@ package base import "github.com/GenerateNU/sac/backend/entities/models" -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,max=255"` // MARK: must be validated manually - // Optional fields - Major0 models.Major `json:"major0" validate:"not_equal_if_not_empty=Major1,not_equal_if_not_empty=Major2,omitempty,max=255,oneof=africanaStudies americanSignLanguage americanSignLanguage-EnglishInterpreting appliedPhysics architecturalStudies architecture art:ArtVisualStudies behavioralNeuroscience biochemistry bioengineering biology biomedicalPhysics businessAdministration businessAdministration:Accounting businessAdministration:AccountingAndAdvisoryServices businessAdministration:BrandManagement businessAdministration:BusinessAnalytics businessAdministration:CorporateInnovation businessAdministration:EntrepreneurialStartups businessAdministration:FamilyBusiness businessAdministration:Finance businessAdministration:Fintech businessAdministration:HealthcareManagementAndConsulting businessAdministration:Management businessAdministration:ManagementInformationSystems businessAdministration:Marketing businessAdministration:MarketingAnalytics businessAdministration:SocialInnovationAndEntrepreneurship businessAdministration:SupplyChainManagement cellAndMolecularBiology chemicalEngineering chemistry civilEngineering communicationStudies computerEngineering computerScience computingAndLaw criminologyAndCriminalJustice culturalAnthropology cybersecurity dataScience design economics electricalEngineering english environmentalAndSustainabilityStudies environmentalEngineering environmentalScience environmentalStudies gameArtAndAnimation gameDesign globalAsianStudies healthScience history historyCultureAndLaw humanServices industrialEngineering internationalAffairs internationalBusiness internationalBusiness:Accounting internationalBusiness:AccountingAndAdvisoryServices internationalBusiness:BrandManagement internationalBusiness:BusinessAnalytics internationalBusiness:CorporateInnovation internationalBusiness:EntrepreneurialStartups internationalBusiness:FamilyBusiness internationalBusiness:Finance internationalBusiness:Fintech internationalBusiness:HealthcareManagementAndConsulting internationalBusiness:Management internationalBusiness:ManagementInformationSystems internationalBusiness:Marketing internationalBusiness:MarketingAnalytics internationalBusiness:SocialInnovationAndEntrepreneurship internationalBusiness:SupplyChainManagement journalism landscapeArchitecture linguistics marineBiology mathematics mechanicalEngineering mediaAndScreenStudies mediaArts music musicTechnology nursing pharmaceuticalSciences pharmacy(PharmD) philosophy physics politicalScience politicsPhilosophyEconomics psychology publicHealth publicRelations religiousStudies sociology spanish speechLanguagePathologyAndAudiology theatre"` - Major1 models.Major `json:"major1" validate:"not_equal_if_not_empty=Major0,not_equal_if_not_empty=Major2,omitempty,max=255,oneof=africanaStudies americanSignLanguage americanSignLanguage-EnglishInterpreting appliedPhysics architecturalStudies architecture art:ArtVisualStudies behavioralNeuroscience biochemistry bioengineering biology biomedicalPhysics businessAdministration businessAdministration:Accounting businessAdministration:AccountingAndAdvisoryServices businessAdministration:BrandManagement businessAdministration:BusinessAnalytics businessAdministration:CorporateInnovation businessAdministration:EntrepreneurialStartups businessAdministration:FamilyBusiness businessAdministration:Finance businessAdministration:Fintech businessAdministration:HealthcareManagementAndConsulting businessAdministration:Management businessAdministration:ManagementInformationSystems businessAdministration:Marketing businessAdministration:MarketingAnalytics businessAdministration:SocialInnovationAndEntrepreneurship businessAdministration:SupplyChainManagement cellAndMolecularBiology chemicalEngineering chemistry civilEngineering communicationStudies computerEngineering computerScience computingAndLaw criminologyAndCriminalJustice culturalAnthropology cybersecurity dataScience design economics electricalEngineering english environmentalAndSustainabilityStudies environmentalEngineering environmentalScience environmentalStudies gameArtAndAnimation gameDesign globalAsianStudies healthScience history historyCultureAndLaw humanServices industrialEngineering internationalAffairs internationalBusiness internationalBusiness:Accounting internationalBusiness:AccountingAndAdvisoryServices internationalBusiness:BrandManagement internationalBusiness:BusinessAnalytics internationalBusiness:CorporateInnovation internationalBusiness:EntrepreneurialStartups internationalBusiness:FamilyBusiness internationalBusiness:Finance internationalBusiness:Fintech internationalBusiness:HealthcareManagementAndConsulting internationalBusiness:Management internationalBusiness:ManagementInformationSystems internationalBusiness:Marketing internationalBusiness:MarketingAnalytics internationalBusiness:SocialInnovationAndEntrepreneurship internationalBusiness:SupplyChainManagement journalism landscapeArchitecture linguistics marineBiology mathematics mechanicalEngineering mediaAndScreenStudies mediaArts music musicTechnology nursing pharmaceuticalSciences pharmacy(PharmD) philosophy physics politicalScience politicsPhilosophyEconomics psychology publicHealth publicRelations religiousStudies sociology spanish speechLanguagePathologyAndAudiology theatre"` - Major2 models.Major `json:"major2" validate:"not_equal_if_not_empty=Major0,not_equal_if_not_empty=Major1,omitempty,max=255,oneof=africanaStudies americanSignLanguage americanSignLanguage-EnglishInterpreting appliedPhysics architecturalStudies architecture art:ArtVisualStudies behavioralNeuroscience biochemistry bioengineering biology biomedicalPhysics businessAdministration businessAdministration:Accounting businessAdministration:AccountingAndAdvisoryServices businessAdministration:BrandManagement businessAdministration:BusinessAnalytics businessAdministration:CorporateInnovation businessAdministration:EntrepreneurialStartups businessAdministration:FamilyBusiness businessAdministration:Finance businessAdministration:Fintech businessAdministration:HealthcareManagementAndConsulting businessAdministration:Management businessAdministration:ManagementInformationSystems businessAdministration:Marketing businessAdministration:MarketingAnalytics businessAdministration:SocialInnovationAndEntrepreneurship businessAdministration:SupplyChainManagement cellAndMolecularBiology chemicalEngineering chemistry civilEngineering communicationStudies computerEngineering computerScience computingAndLaw criminologyAndCriminalJustice culturalAnthropology cybersecurity dataScience design economics electricalEngineering english environmentalAndSustainabilityStudies environmentalEngineering environmentalScience environmentalStudies gameArtAndAnimation gameDesign globalAsianStudies healthScience history historyCultureAndLaw humanServices industrialEngineering internationalAffairs internationalBusiness internationalBusiness:Accounting internationalBusiness:AccountingAndAdvisoryServices internationalBusiness:BrandManagement internationalBusiness:BusinessAnalytics internationalBusiness:CorporateInnovation internationalBusiness:EntrepreneurialStartups internationalBusiness:FamilyBusiness internationalBusiness:Finance internationalBusiness:Fintech internationalBusiness:HealthcareManagementAndConsulting internationalBusiness:Management internationalBusiness:ManagementInformationSystems internationalBusiness:Marketing internationalBusiness:MarketingAnalytics internationalBusiness:SocialInnovationAndEntrepreneurship internationalBusiness:SupplyChainManagement journalism landscapeArchitecture linguistics marineBiology mathematics mechanicalEngineering mediaAndScreenStudies mediaArts music musicTechnology nursing pharmaceuticalSciences pharmacy(PharmD) philosophy physics politicalScience politicsPhilosophyEconomics psychology publicHealth publicRelations religiousStudies sociology spanish speechLanguagePathologyAndAudiology theatre"` - College models.College `json:"college" validate:"omitempty,oneof=CAMD DMSB KCCS CE BCHS SL CPS CS CSSH"` - GraduationCycle models.GraduationCycle `json:"graduation_cycle" validate:"omitempty,max=255,oneof=december may"` - GraduationYear int16 `json:"graduation_year" validate:"omitempty"` -} - type UpdateUserRequestBody struct { - FirstName string `json:"first_name" validate:"omitempty,max=255"` - LastName string `json:"last_name" validate:"omitempty,max=255"` + Name string `json:"name" validate:"omitempty,max=255"` Major0 models.Major `json:"major0" validate:"not_equal_if_not_empty=Major1,not_equal_if_not_empty=Major2,omitempty,max=255,oneof=africanaStudies americanSignLanguage americanSignLanguage-EnglishInterpreting appliedPhysics architecturalStudies architecture art:ArtVisualStudies behavioralNeuroscience biochemistry bioengineering biology biomedicalPhysics businessAdministration businessAdministration:Accounting businessAdministration:AccountingAndAdvisoryServices businessAdministration:BrandManagement businessAdministration:BusinessAnalytics businessAdministration:CorporateInnovation businessAdministration:EntrepreneurialStartups businessAdministration:FamilyBusiness businessAdministration:Finance businessAdministration:Fintech businessAdministration:HealthcareManagementAndConsulting businessAdministration:Management businessAdministration:ManagementInformationSystems businessAdministration:Marketing businessAdministration:MarketingAnalytics businessAdministration:SocialInnovationAndEntrepreneurship businessAdministration:SupplyChainManagement cellAndMolecularBiology chemicalEngineering chemistry civilEngineering communicationStudies computerEngineering computerScience computingAndLaw criminologyAndCriminalJustice culturalAnthropology cybersecurity dataScience design economics electricalEngineering english environmentalAndSustainabilityStudies environmentalEngineering environmentalScience environmentalStudies gameArtAndAnimation gameDesign globalAsianStudies healthScience history historyCultureAndLaw humanServices industrialEngineering internationalAffairs internationalBusiness internationalBusiness:Accounting internationalBusiness:AccountingAndAdvisoryServices internationalBusiness:BrandManagement internationalBusiness:BusinessAnalytics internationalBusiness:CorporateInnovation internationalBusiness:EntrepreneurialStartups internationalBusiness:FamilyBusiness internationalBusiness:Finance internationalBusiness:Fintech internationalBusiness:HealthcareManagementAndConsulting internationalBusiness:Management internationalBusiness:ManagementInformationSystems internationalBusiness:Marketing internationalBusiness:MarketingAnalytics internationalBusiness:SocialInnovationAndEntrepreneurship internationalBusiness:SupplyChainManagement journalism landscapeArchitecture linguistics marineBiology mathematics mechanicalEngineering mediaAndScreenStudies mediaArts music musicTechnology nursing pharmaceuticalSciences pharmacy(PharmD) philosophy physics politicalScience politicsPhilosophyEconomics psychology publicHealth publicRelations religiousStudies sociology spanish speechLanguagePathologyAndAudiology theatre"` Major1 models.Major `json:"major1" validate:"not_equal_if_not_empty=Major0,not_equal_if_not_empty=Major2,omitempty,max=255,oneof=africanaStudies americanSignLanguage americanSignLanguage-EnglishInterpreting appliedPhysics architecturalStudies architecture art:ArtVisualStudies behavioralNeuroscience biochemistry bioengineering biology biomedicalPhysics businessAdministration businessAdministration:Accounting businessAdministration:AccountingAndAdvisoryServices businessAdministration:BrandManagement businessAdministration:BusinessAnalytics businessAdministration:CorporateInnovation businessAdministration:EntrepreneurialStartups businessAdministration:FamilyBusiness businessAdministration:Finance businessAdministration:Fintech businessAdministration:HealthcareManagementAndConsulting businessAdministration:Management businessAdministration:ManagementInformationSystems businessAdministration:Marketing businessAdministration:MarketingAnalytics businessAdministration:SocialInnovationAndEntrepreneurship businessAdministration:SupplyChainManagement cellAndMolecularBiology chemicalEngineering chemistry civilEngineering communicationStudies computerEngineering computerScience computingAndLaw criminologyAndCriminalJustice culturalAnthropology cybersecurity dataScience design economics electricalEngineering english environmentalAndSustainabilityStudies environmentalEngineering environmentalScience environmentalStudies gameArtAndAnimation gameDesign globalAsianStudies healthScience history historyCultureAndLaw humanServices industrialEngineering internationalAffairs internationalBusiness internationalBusiness:Accounting internationalBusiness:AccountingAndAdvisoryServices internationalBusiness:BrandManagement internationalBusiness:BusinessAnalytics internationalBusiness:CorporateInnovation internationalBusiness:EntrepreneurialStartups internationalBusiness:FamilyBusiness internationalBusiness:Finance internationalBusiness:Fintech internationalBusiness:HealthcareManagementAndConsulting internationalBusiness:Management internationalBusiness:ManagementInformationSystems internationalBusiness:Marketing internationalBusiness:MarketingAnalytics internationalBusiness:SocialInnovationAndEntrepreneurship internationalBusiness:SupplyChainManagement journalism landscapeArchitecture linguistics marineBiology mathematics mechanicalEngineering mediaAndScreenStudies mediaArts music musicTechnology nursing pharmaceuticalSciences pharmacy(PharmD) philosophy physics politicalScience politicsPhilosophyEconomics psychology publicHealth publicRelations religiousStudies sociology spanish speechLanguagePathologyAndAudiology theatre"` Major2 models.Major `json:"major2" validate:"not_equal_if_not_empty=Major0,not_equal_if_not_empty=Major1,omitempty,max=255,oneof=africanaStudies americanSignLanguage americanSignLanguage-EnglishInterpreting appliedPhysics architecturalStudies architecture art:ArtVisualStudies behavioralNeuroscience biochemistry bioengineering biology biomedicalPhysics businessAdministration businessAdministration:Accounting businessAdministration:AccountingAndAdvisoryServices businessAdministration:BrandManagement businessAdministration:BusinessAnalytics businessAdministration:CorporateInnovation businessAdministration:EntrepreneurialStartups businessAdministration:FamilyBusiness businessAdministration:Finance businessAdministration:Fintech businessAdministration:HealthcareManagementAndConsulting businessAdministration:Management businessAdministration:ManagementInformationSystems businessAdministration:Marketing businessAdministration:MarketingAnalytics businessAdministration:SocialInnovationAndEntrepreneurship businessAdministration:SupplyChainManagement cellAndMolecularBiology chemicalEngineering chemistry civilEngineering communicationStudies computerEngineering computerScience computingAndLaw criminologyAndCriminalJustice culturalAnthropology cybersecurity dataScience design economics electricalEngineering english environmentalAndSustainabilityStudies environmentalEngineering environmentalScience environmentalStudies gameArtAndAnimation gameDesign globalAsianStudies healthScience history historyCultureAndLaw humanServices industrialEngineering internationalAffairs internationalBusiness internationalBusiness:Accounting internationalBusiness:AccountingAndAdvisoryServices internationalBusiness:BrandManagement internationalBusiness:BusinessAnalytics internationalBusiness:CorporateInnovation internationalBusiness:EntrepreneurialStartups internationalBusiness:FamilyBusiness internationalBusiness:Finance internationalBusiness:Fintech internationalBusiness:HealthcareManagementAndConsulting internationalBusiness:Management internationalBusiness:ManagementInformationSystems internationalBusiness:Marketing internationalBusiness:MarketingAnalytics internationalBusiness:SocialInnovationAndEntrepreneurship internationalBusiness:SupplyChainManagement journalism landscapeArchitecture linguistics marineBiology mathematics mechanicalEngineering mediaAndScreenStudies mediaArts music musicTechnology nursing pharmaceuticalSciences pharmacy(PharmD) philosophy physics politicalScience politicsPhilosophyEconomics psychology publicHealth publicRelations religiousStudies sociology spanish speechLanguagePathologyAndAudiology theatre"` diff --git a/backend/entities/users/base/routes.go b/backend/entities/users/base/routes.go index 898a981ba..d6c7d8217 100644 --- a/backend/entities/users/base/routes.go +++ b/backend/entities/users/base/routes.go @@ -1,10 +1,10 @@ package base import ( - p "github.com/GenerateNU/sac/backend/auth" "github.com/GenerateNU/sac/backend/entities/users/followers" "github.com/GenerateNU/sac/backend/entities/users/members" "github.com/GenerateNU/sac/backend/entities/users/tags" + "github.com/GenerateNU/sac/backend/permission" "github.com/GenerateNU/sac/backend/types" "github.com/gofiber/fiber/v2" @@ -26,15 +26,13 @@ func UsersRouter(userParams types.RouteParams) fiber.Router { // api/v1/users/* users := userParams.Router.Group("/users") - users.Get("/", userParams.AuthMiddleware.Authorize(p.ReadAll), userParams.UtilityMiddleware.Paginator, userController.GetUsers) - users.Get("/me", userParams.AuthMiddleware.Authorize(p.UserRead), userController.GetMe) + users.Get("/", userParams.AuthMiddleware.Authorize(permission.ReadAll), userParams.UtilityMiddleware.Paginator, userController.GetUsers) // api/v1/users/:userID/* usersID := users.Group("/:userID") usersID.Get("/", userController.GetUser) usersID.Patch("/", userParams.AuthMiddleware.UserAuthorizeById, userController.UpdateUser) - usersID.Patch("/password", userParams.AuthMiddleware.UserAuthorizeById, userController.UpdatePassword) usersID.Delete("/", userParams.AuthMiddleware.UserAuthorizeById, userController.DeleteUser) return usersID diff --git a/backend/entities/users/base/service.go b/backend/entities/users/base/service.go index 272fe3612..4b9a93faa 100644 --- a/backend/entities/users/base/service.go +++ b/backend/entities/users/base/service.go @@ -1,12 +1,9 @@ package base import ( - "github.com/GenerateNU/sac/backend/auth" - authEntities "github.com/GenerateNU/sac/backend/entities/auth" "github.com/GenerateNU/sac/backend/entities/models" "github.com/GenerateNU/sac/backend/errs" "github.com/garrettladley/fiberpaginate" - "github.com/google/uuid" "github.com/GenerateNU/sac/backend/entities/users" "github.com/GenerateNU/sac/backend/types" @@ -15,10 +12,8 @@ import ( type UserServiceInterface interface { GetUsers(pageInfo fiberpaginate.PageInfo) ([]models.User, error) - GetMe(id uuid.UUID) (*models.User, error) GetUser(id string) (*models.User, error) UpdateUser(id string, userBody UpdateUserRequestBody) (*models.User, error) - UpdatePassword(id string, passwordBody authEntities.UpdatePasswordRequestBody) error DeleteUser(id string) error } @@ -34,10 +29,6 @@ func (u *UserService) GetUsers(pageInfo fiberpaginate.PageInfo) ([]models.User, return GetUsers(u.DB, pageInfo) } -func (u *UserService) GetMe(id uuid.UUID) (*models.User, error) { - return users.GetUser(u.DB, id) -} - func (u *UserService) GetUser(id string) (*models.User, error) { idAsUUID, err := utilities.ValidateID(id) if err != nil { @@ -69,36 +60,6 @@ func (u *UserService) UpdateUser(id string, userBody UpdateUserRequestBody) (*mo return UpdateUser(u.DB, *idAsUUID, *user) } -func (u *UserService) UpdatePassword(id string, passwordBody authEntities.UpdatePasswordRequestBody) error { - idAsUUID, err := utilities.ValidateID(id) - if err != nil { - return err - } - - if err := utilities.Validate(u.Validate, passwordBody, - *utilities.NewMaybeError("old_password", auth.ValidatePassword(passwordBody.OldPassword)), - *utilities.NewMaybeError("new_password", auth.ValidatePassword(passwordBody.NewPassword))); err != nil { - return err - } - - passwordHash, err := GetUserPasswordHash(u.DB, *idAsUUID) - if err != nil { - return err - } - - correct, passwordErr := auth.CompareHash(passwordBody.OldPassword, *passwordHash) - if passwordErr != nil || !correct { - return err - } - - hash, hashErr := auth.ComputeHash(passwordBody.NewPassword) - if hashErr != nil { - return err - } - - return users.UpdatePassword(u.DB, *idAsUUID, *hash) -} - func (u *UserService) DeleteUser(id string) error { idAsUUID, err := utilities.ValidateID(id) if err != nil { diff --git a/backend/entities/users/base/transactions.go b/backend/entities/users/base/transactions.go index e3acaf1bb..973bb063a 100644 --- a/backend/entities/users/base/transactions.go +++ b/backend/entities/users/base/transactions.go @@ -15,25 +15,13 @@ import ( func GetUsers(db *gorm.DB, pageInfo fiberpaginate.PageInfo) ([]models.User, error) { var users []models.User - if err := db.Omit("password_hash").Scopes(utilities.IntoScope(pageInfo, db)).Find(&users).Error; err != nil { + if err := db.Scopes(utilities.IntoScope(pageInfo, db)).Find(&users).Error; err != nil { return nil, err } return users, nil } -func GetUserPasswordHash(db *gorm.DB, id uuid.UUID) (*string, error) { - var user models.User - if err := db.Select("password_hash").First(&user, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, utilities.ErrNotFound - } - return nil, err - } - - return &user.PasswordHash, nil -} - func UpdateUser(db *gorm.DB, id uuid.UUID, user models.User) (*models.User, error) { var existingUser models.User diff --git a/backend/entities/users/transactions.go b/backend/entities/users/transactions.go index 38b093cc1..defa68007 100644 --- a/backend/entities/users/transactions.go +++ b/backend/entities/users/transactions.go @@ -1,6 +1,7 @@ package users import ( + "context" "errors" "github.com/GenerateNU/sac/backend/entities/models" @@ -19,7 +20,7 @@ func GetUser(db *gorm.DB, id uuid.UUID, preloads ...transactions.OptionalQuery) query = preload(query) } - if err := query.Omit("password_hash").First(&user, id).Error; err != nil { + if err := query.First(&user, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, utilities.ErrNotFound } @@ -29,10 +30,9 @@ func GetUser(db *gorm.DB, id uuid.UUID, preloads ...transactions.OptionalQuery) return &user, nil } -func GetUserByEmail(db *gorm.DB, email string) (*models.User, error) { +func GetUserByEmail(ctx context.Context, db *gorm.DB, email string) (*models.User, error) { var user models.User - - if err := db.Where("email = ?", email).First(&user).Error; err != nil { + if err := db.WithContext(ctx).Where("email = ?", email).First(&user).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, utilities.ErrNotFound } @@ -41,25 +41,3 @@ func GetUserByEmail(db *gorm.DB, email string) (*models.User, error) { return &user, nil } - -func UpdateEmailVerification(db *gorm.DB, id uuid.UUID) error { - result := db.Model(&models.User{}).Where("id = ?", id).Update("is_verified", true) - if result.RowsAffected == 0 { - if result.Error == nil { - return utilities.ErrNotFound - } - return result.Error - } - return nil -} - -func UpdatePassword(db *gorm.DB, id uuid.UUID, passwordHash string) error { - result := db.Model(&models.User{}).Where("id = ?", id).Update("password_hash", passwordHash) - if result.RowsAffected == 0 { - if result.Error == nil { - return utilities.ErrNotFound - } - return result.Error - } - return nil -} diff --git a/backend/go.mod b/backend/go.mod index 35e4f238c..27e8c9175 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -4,7 +4,6 @@ go 1.22.2 require ( github.com/a-h/templ v0.2.707 - github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 github.com/aws/aws-sdk-go v1.53.10 github.com/garrettladley/fiberpaginate v1.0.5 github.com/garrettladley/mattress v0.4.0 @@ -12,7 +11,6 @@ require ( github.com/goccy/go-json v0.10.3 github.com/gofiber/fiber/v2 v2.52.4 github.com/gofiber/swagger v1.0.0 - github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/uuid v1.6.0 github.com/huandu/go-assert v1.1.6 github.com/joho/godotenv v1.5.1 @@ -22,7 +20,7 @@ require ( github.com/swaggo/swag v1.16.3 go.opentelemetry.io/otel/sdk v1.27.0 go.opentelemetry.io/otel/trace v1.27.0 - golang.org/x/crypto v0.23.0 + golang.org/x/oauth2 v0.17.0 golang.org/x/text v0.15.0 gorm.io/driver/postgres v1.5.7 gorm.io/gorm v1.25.10 @@ -33,24 +31,30 @@ require ( github.com/awnumar/memguard v0.22.5 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/philhofer/fwd v1.1.2 // indirect - github.com/smartystreets/goconvey v1.8.1 // indirect github.com/tinylib/msgp v1.1.9 // indirect golang.org/x/sync v0.7.0 // indirect ) require ( + cloud.google.com/go/compute v1.23.3 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect go.opentelemetry.io/contrib v1.17.0 // indirect go.opentelemetry.io/otel/metric v1.27.0 // indirect go.uber.org/atomic v1.11.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/protobuf v1.33.0 // indirect ) require ( @@ -77,11 +81,11 @@ require ( github.com/klauspost/compress v1.17.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/markbates/going v1.0.3 github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.52.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index a4152969c..42e8d873a 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,3 +1,7 @@ +cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= +cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= @@ -6,8 +10,6 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/a-h/templ v0.2.707 h1:T1Gkd2ugbRglZ9rYw/VBchWOSZVKmetDbBkm4YubM7U= github.com/a-h/templ v0.2.707/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8= -github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 h1:rFw4nCn9iMW+Vajsk51NtYIcwSTkXr+JGrMd36kTDJw= -github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/awnumar/memcall v0.2.0 h1:sRaogqExTOOkkNwO9pzJsL8jrOV29UuUW7teRMfbqtI= @@ -40,6 +42,7 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/garrettladley/fiberpaginate v1.0.5 h1:H1Kj7UQBhaBxbCcDeSY3wlW9Hutq/hJ8NSEM86T9vAs= @@ -79,21 +82,24 @@ github.com/gofiber/swagger v1.0.0 h1:BzUzDS9ZT6fDUa692kxmfOjc1DZiloLiPK/W5z1H1tc github.com/gofiber/swagger v1.0.0/go.mod h1:QrYNF1Yrc7ggGK6ATsJ6yfH/8Zi5bu9lA7wB8TmCecg= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4= github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= -github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/go-assert v1.1.6 h1:oaAfYxq9KNDi9qswn/6aE0EydfxSa+tWZC1KabNitYs= github.com/huandu/go-assert v1.1.6/go.mod h1:JuIfbmYG9ykwvuxoJ3V8TB5QP+3+ajIA54Y44TmkMxs= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -116,8 +122,6 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -130,6 +134,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/markbates/going v1.0.3 h1:mY45T5TvW+Xz5A6jY7lf4+NLg9D8+iuStIHyR7M8qsE= +github.com/markbates/going v1.0.3/go.mod h1:fQiT6v6yQar9UD6bd/D4Z5Afbk9J6BBVBtLiyY4gp2o= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -143,6 +149,9 @@ github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= @@ -163,10 +172,7 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= -github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= -github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= -github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= +github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -184,6 +190,7 @@ github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7g github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/contrib v1.17.0 h1:lJJdtuNsP++XHD7tXDYEFSpsqIc7DzShuXMR5PwkmzA= go.opentelemetry.io/contrib v1.17.0/go.mod h1:gIzjwWFoGazJmtCaDgViqOSJPde2mCWzv60o0bWPcZs= go.opentelemetry.io/contrib/propagators/b3 v1.17.0 h1:ImOVvHnku8jijXqkwCSyYKRDt2YrnGXD4BbhcpfbfJo= @@ -204,27 +211,65 @@ go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5 go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= +golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/backend/integrations/email/email.go b/backend/integrations/email/email.go index b4bcc8601..704735ffe 100644 --- a/backend/integrations/email/email.go +++ b/backend/integrations/email/email.go @@ -11,38 +11,36 @@ import ( "github.com/a-h/templ" - "github.com/afex/hystrix-go/hystrix" "github.com/resend/resend-go/v2" ) -type EmailClientInterface interface { - SendPasswordResetEmail(name string, email string, token string) error - SendEmailVerification(email string, code string) error - SendWelcomeEmail(name string, email string) error - SendPasswordChangedEmail(name string, email string) error +type Emailer interface { + SendWelcome(ctx context.Context, email string, name string) error } type ResendClient struct { - Client *resend.Client - Dev bool + client *resend.Client + dev bool } -func NewResendClient(settings config.ResendSettings, dev bool) EmailClientInterface { - hystrix.ConfigureCommand("send-email", hystrix.CommandConfig{ - Timeout: 5000, - MaxConcurrentRequests: 100, - ErrorPercentThreshold: 25, - }) - +func NewResendClient(settings config.ResendSettings, dev bool) Emailer { return &ResendClient{ - Client: resend.NewClient(settings.APIKey.Expose()), - Dev: dev, + client: resend.NewClient(settings.APIKey.Expose()), + dev: dev, } } -func send(fromEmail string, toEmail string, subject string, template templ.Component, client *resend.Client) error { +func (r *ResendClient) SendWelcome(ctx context.Context, name string, email string) error { + if r.dev { + return nil + } + + return r.send(ctx, constants.ONBOARDING_EMAIL, email, "Welcome to Hippo", emails.Welcome(name)) +} + +func (r *ResendClient) send(ctx context.Context, fromEmail string, toEmail string, subject string, template templ.Component) error { buffer := new(bytes.Buffer) - err := template.Render(context.Background(), buffer) + err := template.Render(ctx, buffer) if err != nil { return fmt.Errorf("failed to render email template: %w", err) } @@ -54,45 +52,9 @@ func send(fromEmail string, toEmail string, subject string, template templ.Compo Html: buffer.String(), } - err = hystrix.Do("send-email", func() error { - _, err := client.Emails.Send(params) - return err - }, nil) - if err != nil { + if _, err := r.client.Emails.Send(params); err != nil { return fmt.Errorf("failed to send email: %w", err) } return nil } - -func (r *ResendClient) SendPasswordResetEmail(name string, email string, token string) error { - if r.Dev { - return nil - } - - return send(constants.DEFAULT_FROM_EMAIL, email, "Password Reset", emails.PasswordReset(name, fmt.Sprintf("https://hipponeu.com/reset/%s", token)), r.Client) -} - -func (r *ResendClient) SendEmailVerification(email string, code string) error { - if r.Dev { - return nil - } - - return send(constants.DEFAULT_FROM_EMAIL, email, "Email Verification", emails.Verification(code), r.Client) -} - -func (r *ResendClient) SendWelcomeEmail(name string, email string) error { - if r.Dev { - return nil - } - - return send(constants.ONBOARDING_EMAIL, email, "Welcome to Hippo", emails.Welcome(name), r.Client) -} - -func (r *ResendClient) SendPasswordChangedEmail(name string, email string) error { - if r.Dev { - return nil - } - - return send(constants.DEFAULT_FROM_EMAIL, email, "Password Changed", emails.PasswordChangeComplete(name), r.Client) -} diff --git a/backend/integrations/expose.go b/backend/integrations/expose.go index f7d138f98..be22f05eb 100644 --- a/backend/integrations/expose.go +++ b/backend/integrations/expose.go @@ -3,11 +3,9 @@ package integrations import ( "github.com/GenerateNU/sac/backend/integrations/email" "github.com/GenerateNU/sac/backend/integrations/file" - "github.com/GenerateNU/sac/backend/integrations/oauth" ) type Integrations struct { - Email email.EmailClientInterface + Email email.Emailer File file.FileClientInterface - OAuth oauth.OauthProviderSettings } diff --git a/backend/integrations/oauth/README.md b/backend/integrations/oauth/README.md new file mode 100644 index 000000000..99a15559d --- /dev/null +++ b/backend/integrations/oauth/README.md @@ -0,0 +1,3 @@ +# OAuth Acknowledgement + +## Forked code from @markbates' [goth package](https://github.com/markbates/goth/tree/v1.80.0) and @Shareed2k's [goth_fiber package](https://github.com/Shareed2k/goth_fiber/tree/master) to fit into our internal structure diff --git a/backend/integrations/oauth/google.go b/backend/integrations/oauth/google.go deleted file mode 100644 index 55dff4aa5..000000000 --- a/backend/integrations/oauth/google.go +++ /dev/null @@ -1,47 +0,0 @@ -package oauth - -import ( - "errors" - "fmt" - "net/http" - "net/url" - - "github.com/GenerateNU/sac/backend/config" - "github.com/GenerateNU/sac/backend/utilities" -) - -type GoogleOAuthClient struct { - OAuthConfig config.OAuthSettings -} - -func (client *GoogleOAuthClient) AuthorizeURL(state string) string { - values := url.Values{} - values.Set("client_id", client.OAuthConfig.ClientID.Expose()) - values.Set("redirect_uri", client.OAuthConfig.RedirectURI) - values.Set("response_type", client.OAuthConfig.ResponseType) - values.Set("scope", client.OAuthConfig.Scopes) - values.Set("access_type", client.OAuthConfig.AccessType) - values.Set("include_granted_scopes", client.OAuthConfig.IncludeGrantedScopes) - values.Set("prompt", client.OAuthConfig.Prompt) - values.Set("state", state) - - return fmt.Sprintf("%s/auth?%s", client.OAuthConfig.BaseURL, values.Encode()) -} - -func (client *GoogleOAuthClient) GetConfig() config.OAuthSettings { - return client.OAuthConfig -} - -func (client *GoogleOAuthClient) Revoke(token string) error { - resp, err := utilities.Request(http.MethodPost, fmt.Sprintf("%s/revoke?token=%s", client.OAuthConfig.TokenURL, token), nil, utilities.FormURLEncoded()) - if err != nil { - return err - } - defer resp.Body.Close() - - if !utilities.IsOk(resp.StatusCode) { - return errors.New("failed to revoke token") - } - - return nil -} diff --git a/backend/integrations/oauth/oauth.go b/backend/integrations/oauth/oauth.go deleted file mode 100644 index 7416d6190..000000000 --- a/backend/integrations/oauth/oauth.go +++ /dev/null @@ -1,18 +0,0 @@ -package oauth - -import "github.com/GenerateNU/sac/backend/config" - -type OAuthResourceClientInterface interface { - AuthorizeURL(state string) string - GetConfig() config.OAuthSettings - Revoke(token string) error -} - -type OAuthClient struct { - ResourceClient OAuthResourceClientInterface -} - -type OauthProviderSettings struct { - Outlook OutlookOAuthClient - Google GoogleOAuthClient -} diff --git a/backend/integrations/oauth/outlook.go b/backend/integrations/oauth/outlook.go deleted file mode 100644 index df6bc2cb0..000000000 --- a/backend/integrations/oauth/outlook.go +++ /dev/null @@ -1,34 +0,0 @@ -package oauth - -import ( - "fmt" - "net/url" - - "github.com/GenerateNU/sac/backend/config" -) - -type OutlookOAuthClient struct { - OAuthConfig config.OAuthSettings -} - -func (client *OutlookOAuthClient) AuthorizeURL(state string) string { - values := url.Values{} - values.Set("client_id", client.OAuthConfig.ClientID.Expose()) - values.Set("redirect_uri", client.OAuthConfig.RedirectURI) - values.Set("response_type", client.OAuthConfig.ResponseType) - values.Set("response_mode", client.OAuthConfig.ResponseMode) - values.Set("scope", client.OAuthConfig.Scopes) - values.Set("access_type", client.OAuthConfig.AccessType) - values.Set("state", state) - - return fmt.Sprintf("%s/authorize?%s", client.OAuthConfig.BaseURL, values.Encode()) -} - -func (client *OutlookOAuthClient) GetConfig() config.OAuthSettings { - return client.OAuthConfig -} - -func (client *OutlookOAuthClient) Revoke(token string) error { - // Outlook does not expose a revokation endpoint :/ - return nil -} diff --git a/backend/integrations/oauth/soth/goog/goog.go b/backend/integrations/oauth/soth/goog/goog.go new file mode 100644 index 000000000..d62e46352 --- /dev/null +++ b/backend/integrations/oauth/soth/goog/goog.go @@ -0,0 +1,216 @@ +package goog + +import ( + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/GenerateNU/sac/backend/integrations/oauth/soth" + go_json "github.com/goccy/go-json" + + "github.com/GenerateNU/sac/backend/utilities" + + "golang.org/x/oauth2" + + m "github.com/garrettladley/mattress" + goog "golang.org/x/oauth2/google" +) + +const ( + endpointProfile string = "https://www.googleapis.com/oauth2/v2/userinfo" +) + +var ( + endpoint oauth2.Endpoint = goog.Endpoint + defaultScopes []string = []string{"email", "https://www.googleapis.com/auth/calendar.events", "https://www.googleapis.com/auth/calendar.readonly"} +) + +// New creates a new Google provider, and sets up important connection details. +// You should always call `google.New` to get a new Provider. Never try to create +// one manually. +func New(clientKey *m.Secret[string], secret *m.Secret[string], callbackURL string, scopes ...string) *Provider { + p := &Provider{ + ClientKey: clientKey, + Secret: secret, + CallbackURL: callbackURL, + providerName: "google", + + // We can get a refresh token from Google by this option. + // See https://developers.google.com/identity/protocols/oauth2/openid-connect#access-type-param + authCodeOptions: []oauth2.AuthCodeOption{ + oauth2.AccessTypeOffline, + }, + } + p.config = newConfig(p, scopes) + return p +} + +// Provider is the implementation of `oauth.Provider` for accessing Google. +type Provider struct { + ClientKey *m.Secret[string] + Secret *m.Secret[string] + CallbackURL string + config *oauth2.Config + authCodeOptions []oauth2.AuthCodeOption + providerName string +} + +// Name is the name used to retrieve this provider later. +func (p *Provider) Name() string { + return p.providerName +} + +// SetName is to update the name of the provider (needed in case of multiple providers of 1 type) +func (p *Provider) SetName(name string) { + p.providerName = name +} + +// Debug is a no-op for the goog package. +func (p *Provider) Debug(debug bool) {} + +// BeginAuth asks Google for an authentication endpoint. +func (p *Provider) BeginAuth(state string) (soth.Session, error) { + url := p.config.AuthCodeURL(state, p.authCodeOptions...) + session := &Session{ + AuthURL: url, + } + return session, nil +} + +type googleUser struct { + ID string `json:"id"` + Email string `json:"email"` + Name string `json:"name"` + FirstName string `json:"given_name"` + LastName string `json:"family_name"` + Link string `json:"link"` + Picture string `json:"picture"` +} + +// FetchUser will go to Google and access basic information about the user. +func (p *Provider) FetchUser(session soth.Session) (soth.User, error) { + sess := session.(*Session) + user := soth.User{ + AccessToken: sess.AccessToken, + Provider: p.Name(), + RefreshToken: sess.RefreshToken, + ExpiresAt: sess.ExpiresAt, + IDToken: sess.IDToken, + } + + if user.AccessToken == "" { + // Data is not yet retrieved, since accessToken is still empty. + return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName) + } + + resp, err := utilities.Request(http.MethodGet, fmt.Sprintf("%s?access_token=%s", endpointProfile, url.QueryEscape(sess.AccessToken)), nil) + if err != nil { + return user, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, resp.StatusCode) + } + + responseBytes, err := io.ReadAll(resp.Body) + if err != nil { + return user, err + } + + var u googleUser + if err := go_json.Unmarshal(responseBytes, &u); err != nil { + return user, err + } + + // Extract the user data we got from Google into our oauth.User. + user.Name = u.Name + user.FirstName = u.FirstName + user.LastName = u.LastName + user.NickName = u.Name + user.Email = u.Email + user.AvatarURL = u.Picture + user.UserID = u.ID + // Google provides other useful fields such as 'hd'; get them from RawData + if err := go_json.Unmarshal(responseBytes, &user.RawData); err != nil { + return user, err + } + + return user, nil +} + +func newConfig(provider *Provider, scopes []string) *oauth2.Config { + c := &oauth2.Config{ + ClientID: provider.ClientKey.Expose(), + ClientSecret: provider.Secret.Expose(), + RedirectURL: provider.CallbackURL, + Endpoint: endpoint, + Scopes: []string{}, + } + + if len(scopes) > 0 { + c.Scopes = append(c.Scopes, scopes...) + } else { + c.Scopes = append(c.Scopes, defaultScopes...) + } + return c +} + +// RefreshTokenAvailable refresh token is provided by auth provider or not +func (p *Provider) RefreshTokenAvailable() bool { + return true +} + +// RefreshToken get new access token based on the refresh token +func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { + token := &oauth2.Token{RefreshToken: refreshToken} + ts := p.config.TokenSource(soth.ContextForClient(utilities.Client()), token) + newToken, err := ts.Token() + if err != nil { + return nil, err + } + return newToken, err +} + +// SetPrompt sets the prompt values for the google OAuth call. Use this to +// force users to choose and account every time by passing "select_account", +// for example. +// See https://developers.google.com/identity/protocols/OpenIDConnect#authenticationuriparameters +func (p *Provider) SetPrompt(prompt ...string) { + if len(prompt) == 0 { + return + } + p.authCodeOptions = append(p.authCodeOptions, oauth2.SetAuthURLParam("prompt", strings.Join(prompt, " "))) +} + +// SetHostedDomain sets the hd parameter for google OAuth call. +// Use this to force user to pick user from specific hosted domain. +// See https://developers.google.com/identity/protocols/oauth2/openid-connect#hd-param +func (p *Provider) SetHostedDomain(hd string) { + if hd == "" { + return + } + p.authCodeOptions = append(p.authCodeOptions, oauth2.SetAuthURLParam("hd", hd)) +} + +// SetLoginHint sets the login_hint parameter for the Google OAuth call. +// Use this to prompt the user to log in with a specific account. +// See https://developers.google.com/identity/protocols/oauth2/openid-connect#login-hint +func (p *Provider) SetLoginHint(loginHint string) { + if loginHint == "" { + return + } + p.authCodeOptions = append(p.authCodeOptions, oauth2.SetAuthURLParam("login_hint", loginHint)) +} + +// SetAccessType sets the access_type parameter for the Google OAuth call. +// If an access token is being requested, the client does not receive a refresh token unless a value of offline is specified. +// See https://developers.google.com/identity/protocols/oauth2/openid-connect#access-type-param +func (p *Provider) SetAccessType(at string) { + if at == "" { + return + } + p.authCodeOptions = append(p.authCodeOptions, oauth2.SetAuthURLParam("access_type", at)) +} diff --git a/backend/integrations/oauth/soth/goog/session.go b/backend/integrations/oauth/soth/goog/session.go new file mode 100644 index 000000000..7937f0ae8 --- /dev/null +++ b/backend/integrations/oauth/soth/goog/session.go @@ -0,0 +1,64 @@ +package goog + +import ( + "encoding/json" + "errors" + "strings" + "time" + + "github.com/GenerateNU/sac/backend/integrations/oauth/soth" + "github.com/GenerateNU/sac/backend/utilities" +) + +// Session stores data during the auth process with Google. +type Session struct { + AuthURL string + AccessToken string + RefreshToken string + ExpiresAt time.Time + IDToken string +} + +// GetAuthURL will return the URL set by calling the `BeginAuth` function on the Google provider. +func (s Session) GetAuthURL() (string, error) { + if s.AuthURL == "" { + return "", errors.New(soth.NoAuthUrlErrorMessage) + } + return s.AuthURL, nil +} + +// Authorize the session with Google and return the access token to be stored for future use. +func (s *Session) Authorize(provider soth.Provider, params soth.Params) (string, error) { + p := provider.(*Provider) + token, err := p.config.Exchange(soth.ContextForClient(utilities.Client()), params.Get("code")) + if err != nil { + return "", err + } + + if !token.Valid() { + return "", errors.New("invalid token received from provider") + } + + s.AccessToken = token.AccessToken + s.RefreshToken = token.RefreshToken + s.ExpiresAt = token.Expiry + s.IDToken = token.Extra("id_token").(string) + return token.AccessToken, err +} + +// Marshal the session into a string +func (s Session) Marshal() string { + b, _ := json.Marshal(s) + return string(b) +} + +func (s Session) String() string { + return s.Marshal() +} + +// UnmarshalSession will unmarshal a JSON string into a session. +func (p *Provider) UnmarshalSession(data string) (soth.Session, error) { + sess := &Session{} + err := json.NewDecoder(strings.NewReader(data)).Decode(sess) + return sess, err +} diff --git a/backend/integrations/oauth/soth/msft/msft.go b/backend/integrations/oauth/soth/msft/msft.go new file mode 100644 index 000000000..d77321600 --- /dev/null +++ b/backend/integrations/oauth/soth/msft/msft.go @@ -0,0 +1,190 @@ +package msft + +import ( + "bytes" + "errors" + "fmt" + "io" + "net/http" + + go_json "github.com/goccy/go-json" + + "github.com/GenerateNU/sac/backend/integrations/oauth/soth" + "github.com/GenerateNU/sac/backend/utilities" + + "github.com/markbates/going/defaults" + "golang.org/x/oauth2" + + m "github.com/garrettladley/mattress" +) + +const ( + // #nosec G101 + authURLFmt string = "https://login.microsoftonline.com/%s/oauth2/v2.0/authorize" + // #nosec G101 + tokenURLFmt string = "https://login.microsoftonline.com/%s/oauth2/v2.0/token" + endpointProfile string = "https://graph.microsoft.com/v1.0/me" +) + +var defaultScopes = []string{"openid", "offline_access", "user.read", "calendars.readwrite", "email", "profile"} + +// New creates a new microsoftonline Provider, and sets up important connection details. +// You should always call `msft.New` to get a new Provider. Never try to create +// one manually. +func New(clientKey *m.Secret[string], secret *m.Secret[string], callbackURL string, tenant string, scopes ...string) *Provider { + p := &Provider{ + ClientKey: clientKey, + Secret: secret, + CallbackURL: callbackURL, + ProviderName: "microsoftonline", + tenant: tenant, + } + + p.config = newConfig(p, scopes) + return p +} + +// Provider is the implementation of `soth.Provider` for accessing microsoftonline. +type Provider struct { + ClientKey *m.Secret[string] + Secret *m.Secret[string] + CallbackURL string + config *oauth2.Config + ProviderName string + tenant string +} + +// Name is the name used to retrieve this Provider later. +func (p *Provider) Name() string { + return p.ProviderName +} + +// SetName is to update the name of the Provider (needed in case of multiple Providers of 1 type) +func (p *Provider) SetName(name string) { + p.ProviderName = name +} + +// Debug is a no-op for the msft package. +func (p *Provider) Debug(debug bool) {} + +// BeginAuth asks MicrosoftOnline for an authentication end-point. +func (p *Provider) BeginAuth(state string) (soth.Session, error) { + return &Session{ + AuthURL: p.config.AuthCodeURL(state), + }, nil +} + +// FetchUser will go to MicrosoftOnline and access basic information about the user. +func (p *Provider) FetchUser(session soth.Session) (soth.User, error) { + msSession := session.(*Session) + user := soth.User{ + AccessToken: msSession.AccessToken, + Provider: p.Name(), + ExpiresAt: msSession.ExpiresAt, + } + + if user.AccessToken == "" { + return user, fmt.Errorf("%s cannot get user information without accessToken", p.ProviderName) + } + + resp, err := utilities.Request(http.MethodGet, endpointProfile, nil, utilities.Authorization(msSession.AccessToken)) + if err != nil { + return user, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.ProviderName, resp.StatusCode) + } + + user.AccessToken = msSession.AccessToken + + err = userFromReader(resp.Body, &user) + return user, err +} + +// RefreshTokenAvailable refresh token is provided by auth Provider or not +// available for microsoft online as session size hit the limit of max cookie size +func (p *Provider) RefreshTokenAvailable() bool { + return false +} + +// RefreshToken get new access token based on the refresh token +func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { + if refreshToken == "" { + return nil, errors.New("no refresh token provided") + } + + token := &oauth2.Token{RefreshToken: refreshToken} + ts := p.config.TokenSource(soth.ContextForClient(utilities.Client()), token) + newToken, err := ts.Token() + if err != nil { + return nil, err + } + return newToken, err +} + +func newConfig(provider *Provider, scopes []string) *oauth2.Config { + var ( + authURL string + tokenURL string + ) + if provider.tenant == "" { + authURL = fmt.Sprintf(authURLFmt, "common") + tokenURL = fmt.Sprintf(tokenURLFmt, "common") + } else { + authURL = fmt.Sprintf(authURLFmt, provider.tenant) + tokenURL = fmt.Sprintf(tokenURLFmt, provider.tenant) + } + + c := &oauth2.Config{ + ClientID: provider.ClientKey.Expose(), + ClientSecret: provider.Secret.Expose(), + RedirectURL: provider.CallbackURL, + Endpoint: oauth2.Endpoint{ + AuthURL: authURL, + TokenURL: tokenURL, + }, + Scopes: []string{}, + } + + c.Scopes = append(c.Scopes, scopes...) + if len(scopes) == 0 { + c.Scopes = append(c.Scopes, defaultScopes...) + } + + return c +} + +func userFromReader(r io.Reader, user *soth.User) error { + buf := &bytes.Buffer{} + tee := io.TeeReader(r, buf) + + u := struct { + ID string `json:"id"` + Name string `json:"displayName"` + Email string `json:"mail"` + FirstName string `json:"givenName"` + LastName string `json:"surname"` + UserPrincipalName string `json:"userPrincipalName"` + }{} + + if err := go_json.NewDecoder(tee).Decode(&u); err != nil { + return err + } + + raw := map[string]interface{}{} + if err := go_json.NewDecoder(buf).Decode(&raw); err != nil { + return err + } + + user.UserID = u.ID + user.Email = defaults.String(u.Email, u.UserPrincipalName) + user.Name = u.Name + user.NickName = u.Name + user.FirstName = u.FirstName + user.LastName = u.LastName + user.RawData = raw + + return nil +} diff --git a/backend/integrations/oauth/soth/msft/session.go b/backend/integrations/oauth/soth/msft/session.go new file mode 100644 index 000000000..e271d4c4c --- /dev/null +++ b/backend/integrations/oauth/soth/msft/session.go @@ -0,0 +1,63 @@ +package msft + +import ( + "encoding/json" + "errors" + "strings" + "time" + + "github.com/GenerateNU/sac/backend/integrations/oauth/soth" + "github.com/GenerateNU/sac/backend/utilities" +) + +// Session is the implementation of `soth.Session` for accessing microsoftonline. +// Refresh token not available for microsoft online: session size hit the limit of max cookie size +type Session struct { + AuthURL string + AccessToken string + ExpiresAt time.Time +} + +// GetAuthURL will return the URL set by calling the `BeginAuth` function on the Microsoft provider. +func (s Session) GetAuthURL() (string, error) { + if s.AuthURL == "" { + return "", errors.New(soth.NoAuthUrlErrorMessage) + } + + return s.AuthURL, nil +} + +// Authorize the session with Microsoft and return the access token to be stored for future use. +func (s *Session) Authorize(provider soth.Provider, params soth.Params) (string, error) { + p := provider.(*Provider) + token, err := p.config.Exchange(soth.ContextForClient(utilities.Client()), params.Get("code")) + if err != nil { + return "", err + } + + if !token.Valid() { + return "", errors.New("invalid token received from provider") + } + + s.AccessToken = token.AccessToken + s.ExpiresAt = token.Expiry + + return token.AccessToken, err +} + +// Marshal the session into a string +func (s Session) Marshal() string { + b, _ := json.Marshal(s) + return string(b) +} + +func (s Session) String() string { + return s.Marshal() +} + +// UnmarshalSession wil unmarshal a JSON string into a session. +func (p *Provider) UnmarshalSession(data string) (soth.Session, error) { + session := &Session{} + err := json.NewDecoder(strings.NewReader(data)).Decode(session) + return session, err +} diff --git a/backend/integrations/oauth/soth/provider.go b/backend/integrations/oauth/soth/provider.go new file mode 100644 index 000000000..521ac8339 --- /dev/null +++ b/backend/integrations/oauth/soth/provider.go @@ -0,0 +1,67 @@ +package soth + +import ( + "context" + "fmt" + "net/http" + + "golang.org/x/oauth2" +) + +// Provider needs to be implemented for each 3rd party authentication provider +// e.g. Facebook, Twitter, etc... +type Provider interface { + Name() string + SetName(name string) + BeginAuth(state string) (Session, error) + UnmarshalSession(string) (Session, error) + FetchUser(Session) (User, error) + Debug(bool) + RefreshToken(refreshToken string) (*oauth2.Token, error) // Get new access token based on the refresh token + RefreshTokenAvailable() bool // Refresh token is provided by auth provider or not +} + +const NoAuthUrlErrorMessage = "an AuthURL has not been set" + +// Providers is list of known/available providers. +type Providers map[string]Provider + +var providers = Providers{} + +// UseProviders adds a list of available providers for use with Soth. +// Can be called multiple times. If you pass the same provider more +// than once, the last will be used. +func UseProviders(viders ...Provider) { + for _, provider := range viders { + providers[provider.Name()] = provider + } +} + +// GetProviders returns a list of all the providers currently in use. +func GetProviders() Providers { + return providers +} + +// GetProvider returns a previously created provider. If Soth has not +// been told to use the named provider it will return an error. +func GetProvider(name string) (Provider, error) { + provider := providers[name] + if provider == nil { + return nil, fmt.Errorf("no provider for %s exists", name) + } + return provider, nil +} + +// ClearProviders will remove all providers currently in use. +// This is useful, mostly, for testing purposes. +func ClearProviders() { + providers = Providers{} +} + +// ContextForClient provides a context for use with oauth2. +func ContextForClient(h *http.Client) context.Context { + if h == nil { + return context.Background() + } + return context.WithValue(context.Background(), oauth2.HTTPClient, h) +} diff --git a/backend/integrations/oauth/soth/session.go b/backend/integrations/oauth/soth/session.go new file mode 100644 index 000000000..79115a8a5 --- /dev/null +++ b/backend/integrations/oauth/soth/session.go @@ -0,0 +1,21 @@ +package soth + +// Params is used to pass data to sessions for authorization. An existing +// implementation, and the one most likely to be used, is `url.Values`. +type Params interface { + Get(string) string +} + +// Session needs to be implemented as part of the provider package. +// It will be marshaled and persisted between requests to "tie" +// the start and the end of the authorization process with a +// 3rd party provider. +type Session interface { + // GetAuthURL returns the URL for the authentication end-point for the provider. + GetAuthURL() (string, error) + // Marshal generates a string representation of the Session for storing between requests. + Marshal() string + // Authorize should validate the data from the provider and return an access token + // that can be stored for later access to the provider. + Authorize(Provider, Params) (string, error) +} diff --git a/backend/integrations/oauth/soth/sothic/params.go b/backend/integrations/oauth/soth/sothic/params.go new file mode 100644 index 000000000..850975a15 --- /dev/null +++ b/backend/integrations/oauth/soth/sothic/params.go @@ -0,0 +1,11 @@ +package sothic + +import "github.com/gofiber/fiber/v2" + +type Params struct { + c *fiber.Ctx +} + +func (p *Params) Get(key string) string { + return p.c.Query(key) +} diff --git a/backend/integrations/oauth/soth/sothic/sothic.go b/backend/integrations/oauth/soth/sothic/sothic.go new file mode 100644 index 000000000..5cec4bd45 --- /dev/null +++ b/backend/integrations/oauth/soth/sothic/sothic.go @@ -0,0 +1,419 @@ +package sothic + +import ( + "bytes" + "compress/gzip" + "crypto/rand" + "encoding/base64" + "errors" + "fmt" + "io" + "net/url" + "path/filepath" + "strings" + + "github.com/GenerateNU/sac/backend/database/store" + "github.com/caarlos0/env/v11" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/session" + "github.com/joho/godotenv" + + "github.com/GenerateNU/sac/backend/integrations/oauth/soth" + m "github.com/garrettladley/mattress" +) + +type key int + +const ( + SessionName string = "_sothic_session" + + // ProviderParamKey can be used as a key in context when passing in a provider + ProviderParamKey key = iota +) + +// Session can/should be set by applications using gothic. The default is a cookie store. +var ( + SessionStore *session.Store +) + +type storageSettings struct { + username string + password *m.Secret[string] + host string + port uint + db int + // TLSConfig *TLSConfig +} + +func (s *storageSettings) Username() string { + return s.username +} + +func (s *storageSettings) Password() *m.Secret[string] { + return s.password +} + +func (s *storageSettings) Host() string { + return s.host +} + +func (s *storageSettings) Port() uint { + return s.port +} + +func (s *storageSettings) DB() int { + return s.db +} + +type intermediateStorageSettings struct { + Username string `env:"SAC_REDIS_SESSION_USERNAME"` + Password string `env:"SAC_REDIS_SESSION_PASSWORD"` + Host string `env:"SAC_REDIS_SESSION_HOST"` + Port uint `env:"SAC_REDIS_SESSION_PORT"` + DB int `env:"SAC_REDIS_SESSION_DB"` + // TLSConfig *intermediateTLSConfig `env:"TLS_CONFIG"` +} + +func (i *intermediateStorageSettings) into() (*storageSettings, error) { + password, err := m.NewSecret(i.Password) + if err != nil { + return nil, fmt.Errorf("failed to create secret from password: %s", err.Error()) + } + + return &storageSettings{ + username: i.Username, + password: password, + host: i.Host, + port: i.Port, + db: i.DB, + // TLSConfig: i.TLSConfig.into(), + }, nil +} + +func init() { + if err := godotenv.Load(filepath.Join("..", "config", ".env.dev")); err != nil { + panic(fmt.Errorf("failed to load environment variables: %s", err.Error())) + } + + intSettings, err := env.ParseAs[intermediateStorageSettings]() + if err != nil { + panic(fmt.Errorf("failed to parse environment variables: %s", err.Error())) + } + + settings, err := intSettings.into() + if err != nil { + panic(fmt.Errorf("failed to convert environment variables: %s", err.Error())) + } + + // optional config + config := session.Config{ + Storage: store.NewRedisClient(settings), + KeyLookup: fmt.Sprintf("cookie:%s", SessionName), + // for local + CookieHTTPOnly: true, + // MARK: secure in prod + // TODO: use build tags to set this + } + + SessionStore = session.New(config) +} + +/* +BeginAuthHandler is a convenience handler for starting the authentication process. +It expects to be able to get the name of the provider from the query parameters +as either "provider" or ":provider". + +BeginAuthHandler will redirect the user to the appropriate authentication end-point +for the requested provider. + +See https://github.com/markbates/goth/examples/main.go to see this in action. +*/ +func BeginAuthHandler(c *fiber.Ctx) error { + url, err := GetAuthURL(c) + if err != nil { + return c.Status(fiber.StatusBadRequest).SendString(err.Error()) + } + + return c.Redirect(url, fiber.StatusTemporaryRedirect) +} + +// SetState sets the state string associated with the given request. +// If no state string is associated with the request, one will be generated. +// This state is sent to the provider and can be retrieved during the +// callback. +func SetState(c *fiber.Ctx) string { + state := c.Query("state") + if len(state) > 0 { + return state + } + + // If a state query param is not passed in, generate a random + // base64-encoded nonce so that the state on the auth URL + // is unguessable, preventing CSRF attacks, as described in + // + // https://auth0.com/docs/protocols/oauth2/oauth-state#keep-reading + nonceBytes := make([]byte, 64) + _, err := io.ReadFull(rand.Reader, nonceBytes) + if err != nil { + panic("gothic: source of randomness unavailable: " + err.Error()) + } + return base64.URLEncoding.EncodeToString(nonceBytes) +} + +// GetState gets the state returned by the provider during the callback. +// This is used to prevent CSRF attacks, see +// http://tools.ietf.org/html/rfc6749#section-10.12 +func GetState(c *fiber.Ctx) string { + return c.Query("state") +} + +/* +GetAuthURL starts the authentication process with the requested provided. +It will return a URL that should be used to send users to. + +It expects to be able to get the name of the provider from the query parameters +as either "provider" or ":provider". + +I would recommend using the BeginAuthHandler instead of doing all of these steps +yourself, but that's entirely up to you. +*/ +func GetAuthURL(c *fiber.Ctx) (string, error) { + if SessionStore == nil { + return "", errors.New("session store unexpectedly nil") + } + + providerName, err := GetProviderName(c) + if err != nil { + return "", err + } + + provider, err := soth.GetProvider(providerName) + if err != nil { + return "", err + } + + sess, err := provider.BeginAuth(SetState(c)) + if err != nil { + return "", err + } + + url, err := sess.GetAuthURL() + if err != nil { + return "", err + } + + err = StoreInSession(providerName, sess.Marshal(), c) + if err != nil { + return "", err + } + + return url, err +} + +/* +CompleteUserAuth does what it says on the tin. It completes the authentication +process and fetches all of the basic information about the user from the provider. + +It expects to be able to get the name of the provider from the path parameter +":provider" or as set by SetProvider. + +This method automatically ends the session. You can prevent this behavior by +passing in options. Please note that any options provided in addition to the +first will be ignored. + +See https://github.com/markbates/goth/examples/main.go to see this in action. +*/ +func CompleteUserAuth(c *fiber.Ctx) (soth.User, error) { + if SessionStore == nil { + return soth.User{}, errors.New("session store unexpectedly nil") + } + + providerName, err := GetProviderName(c) + if err != nil { + return soth.User{}, err + } + + provider, err := soth.GetProvider(providerName) + if err != nil { + return soth.User{}, err + } + + value, err := GetFromSession(providerName, c) + if err != nil { + return soth.User{}, err + } + + sess, err := provider.UnmarshalSession(value) + if err != nil { + return soth.User{}, err + } + + err = validateState(c, sess) + if err != nil { + return soth.User{}, err + } + + user, err := provider.FetchUser(sess) + if err == nil { + // user can be found with existing session data + return user, err + } + + // get new token and retry fetch + _, err = sess.Authorize(provider, &Params{c: c}) + if err != nil { + return soth.User{}, err + } + + err = StoreInSession(providerName, sess.Marshal(), c) + if err != nil { + return soth.User{}, err + } + + gu, err := provider.FetchUser(sess) + return gu, err +} + +// validateState ensures that the state token param from the original +// AuthURL matches the one included in the current (callback) request. +func validateState(c *fiber.Ctx, sess soth.Session) error { + rawAuthURL, err := sess.GetAuthURL() + if err != nil { + return err + } + + authURL, err := url.Parse(rawAuthURL) + if err != nil { + return err + } + + originalState := authURL.Query().Get("state") + if originalState != "" && (originalState != c.Query("state")) { + return errors.New("state token mismatch") + } + return nil +} + +// Logout invalidates a user session. +func Logout(c *fiber.Ctx) error { + session, err := SessionStore.Get(c) + if err != nil { + return err + } + + if err := session.Destroy(); err != nil { + return err + } + + return nil +} + +// GetProviderName is a function used to get the name of a provider +// for a given request. By default, this provider is fetched from +// the URL query string. If you provide it in a different way, +// assign your own function to this variable that returns the provider +// name for your request. +func GetProviderName(c *fiber.Ctx) (string, error) { + // try to get it from the url param ":provider" + if p := c.Params("provider"); p != "" { + return p, nil + } + + // try to get it from the Fasthttp context's value of providerContextKey key + if p := c.Locals(fmt.Sprint(ProviderParamKey)); p != "" { + provider, ok := p.(string) + if ok && provider != "" { + return provider, nil + } + } + + // As a fallback, loop over the used providers, if we already have a valid session for any provider (ie. user has already begun authentication with a provider), then return that provider name + providers := soth.GetProviders() + session, err := SessionStore.Get(c) + if err != nil { + return "", err + } + + for _, provider := range providers { + p := provider.Name() + value := session.Get(p) + if _, ok := value.(string); ok { + return p, nil + } + } + + // if not found then return an empty string with the corresponding error + return "", errors.New("you must select a provider") +} + +func SetProvider(c *fiber.Ctx, provider string) { + c.Locals(fmt.Sprint(ProviderParamKey), provider) +} + +// StoreInSession stores a specified key/value pair in the session. +func StoreInSession(key string, value string, c *fiber.Ctx) error { + session, err := SessionStore.Get(c) + if err != nil { + return err + } + + if err := updateSessionValue(session, key, value); err != nil { + return err + } + + // saved here + return session.Save() +} + +// GetFromSession retrieves a previously-stored value from the session. +// If no value has previously been stored at the specified key, it will return an error. +func GetFromSession(key string, c *fiber.Ctx) (string, error) { + session, err := SessionStore.Get(c) + if err != nil { + return "", err + } + + value, err := getSessionValue(session, key) + if err != nil { + return "", errors.New("could not find a matching session for this request") + } + + return value, nil +} + +func getSessionValue(store *session.Session, key string) (string, error) { + value := store.Get(key) + if value == nil { + return "", errors.New("could not find a matching session for this request") + } + + rdata := strings.NewReader(value.(string)) + r, err := gzip.NewReader(rdata) + if err != nil { + return "", err + } + s, err := io.ReadAll(r) + if err != nil { + return "", err + } + + return string(s), nil +} + +func updateSessionValue(session *session.Session, key, value string) error { + var b bytes.Buffer + gz := gzip.NewWriter(&b) + if _, err := gz.Write([]byte(value)); err != nil { + return err + } + if err := gz.Flush(); err != nil { + return err + } + if err := gz.Close(); err != nil { + return err + } + + session.Set(key, b.String()) + + return nil +} diff --git a/backend/integrations/oauth/soth/user.go b/backend/integrations/oauth/soth/user.go new file mode 100644 index 000000000..16a321b42 --- /dev/null +++ b/backend/integrations/oauth/soth/user.go @@ -0,0 +1,42 @@ +package soth + +import ( + "encoding/gob" + "time" + + "github.com/GenerateNU/sac/backend/entities/models" +) + +func init() { + gob.Register(User{}) +} + +// User contains the information common amongst most OAuth and OAuth2 providers. +// All the "raw" data from the provider can be found in the `RawData` field. +type User struct { + RawData map[string]interface{} + Provider string + Email string + Name string + FirstName string + LastName string + NickName string + Description string + UserID string + AvatarURL string + Location string + AccessToken string + AccessTokenSecret string + RefreshToken string + ExpiresAt time.Time + IDToken string +} + +func (u *User) Into() *models.User { + return &models.User{ + Role: models.Student, + Name: u.Name, + Email: u.Email, + AvatarURL: u.AvatarURL, + } +} diff --git a/backend/locals/claims.go b/backend/locals/claims.go deleted file mode 100644 index 86f55273f..000000000 --- a/backend/locals/claims.go +++ /dev/null @@ -1,27 +0,0 @@ -package locals - -import ( - "fmt" - - "github.com/GenerateNU/sac/backend/auth" - "github.com/GenerateNU/sac/backend/utilities" - "github.com/gofiber/fiber/v2" -) - -func CustomClaimsFrom(c *fiber.Ctx) (*auth.CustomClaims, error) { - rawClaims := c.Locals(claimsKey) - if rawClaims == nil { - return nil, utilities.Forbidden() - } - - claims, ok := rawClaims.(*auth.CustomClaims) - if !ok { - return nil, fmt.Errorf("claims are not of type auth.CustomClaims. got: %T", rawClaims) - } - - return claims, nil -} - -func SetCustomClaims(c *fiber.Ctx, claims *auth.CustomClaims) { - c.Locals(claimsKey, claims) -} diff --git a/backend/locals/type.go b/backend/locals/type.go index 3c7c80364..bd23fa17a 100644 --- a/backend/locals/type.go +++ b/backend/locals/type.go @@ -3,6 +3,5 @@ package locals type localsKey byte const ( - claimsKey localsKey = 0 - userIDKey localsKey = 1 + userKey localsKey = 0 ) diff --git a/backend/locals/user.go b/backend/locals/user.go new file mode 100644 index 000000000..0397aeed2 --- /dev/null +++ b/backend/locals/user.go @@ -0,0 +1,31 @@ +package locals + +import ( + "fmt" + + "github.com/GenerateNU/sac/backend/entities/models" + "github.com/GenerateNU/sac/backend/utilities" + "github.com/gofiber/fiber/v2" +) + +func UserFrom(c *fiber.Ctx) (*models.User, error) { + lUser := c.Locals(userKey) + if lUser == nil { + return nil, utilities.Forbidden() + } + + user, ok := lUser.(*models.User) + if !ok { + return nil, fmt.Errorf("user is not of type models.User. got: %T", user) + } + + return user, nil +} + +func UserExists(c *fiber.Ctx) bool { + return c.Locals(userKey) != nil +} + +func SetUser(c *fiber.Ctx, user *models.User) { + c.Locals(userKey, user) +} diff --git a/backend/locals/user_id.go b/backend/locals/user_id.go deleted file mode 100644 index 8500a77be..000000000 --- a/backend/locals/user_id.go +++ /dev/null @@ -1,27 +0,0 @@ -package locals - -import ( - "fmt" - - "github.com/GenerateNU/sac/backend/utilities" - "github.com/gofiber/fiber/v2" - "github.com/google/uuid" -) - -func UserIDFrom(c *fiber.Ctx) (*uuid.UUID, error) { - userID := c.Locals(userIDKey) - if userID == nil { - return nil, utilities.Forbidden() - } - - id, ok := userID.(*uuid.UUID) - if !ok { - return nil, fmt.Errorf("userID is not of type uuid.UUID. got: %T", userID) - } - - return id, nil -} - -func SetUserID(c *fiber.Ctx, id *uuid.UUID) { - c.Locals(userIDKey, id) -} diff --git a/backend/main.go b/backend/main.go index b8b8f2cbe..b7c0771e6 100644 --- a/backend/main.go +++ b/backend/main.go @@ -4,10 +4,16 @@ import ( "context" "flag" "fmt" + "log" "log/slog" "net" + "os" + "os/signal" "path/filepath" + "syscall" + "github.com/GenerateNU/sac/backend/background" + "github.com/GenerateNU/sac/backend/background/jobs" "github.com/GenerateNU/sac/backend/config" "github.com/GenerateNU/sac/backend/constants" "github.com/GenerateNU/sac/backend/database" @@ -16,18 +22,17 @@ import ( "github.com/GenerateNU/sac/backend/integrations" "github.com/GenerateNU/sac/backend/integrations/email" "github.com/GenerateNU/sac/backend/integrations/file" - "github.com/GenerateNU/sac/backend/integrations/oauth" "github.com/GenerateNU/sac/backend/server" "github.com/GenerateNU/sac/backend/telemetry" "github.com/GenerateNU/sac/backend/utilities" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + sdktrace "go.opentelemetry.io/otel/sdk/trace" ) func main() { - onlyMigrate := flag.Bool("only-migrate", false, "Specify if you want to only perform the database migration") - seedSearch := flag.Bool("seed-search", true, "Specify if you want to seed the opensearch nodes.") - configPath := flag.String("config", filepath.Join("..", "config", ".env.dev"), "Specify the path to the config file (.env)") - - flag.Parse() + onlyMigrate, seedSearch, configPath := parseFlags() config, err := config.GetConfiguration(*configPath) if err != nil { utilities.Exit("Error getting configuration: %s", err.Error()) @@ -39,6 +44,8 @@ func main() { utilities.Exit("A server is already running on %s:%d.\n", config.Application.Host, config.Application.Port) } + ctx := context.Background() + db, err := database.ConfigureDB(*config) if err != nil { utilities.Exit("Error migrating database: %s", err.Error()) @@ -49,41 +56,34 @@ func main() { } if *seedSearch { - slog.Info("to appease linter", "seedSearch", *seedSearch) - // if err := search.SeedClubs(db); err != nil { - // return - // } - - // if err := search.SeedEvents(db); err != nil { - // return - // } + seedSearchData(db) } - err = database.ConnPooling(db) - if err != nil { + if err := database.ConnPooling(db); err != nil { utilities.Exit("Error with connection pooling: %s", err.Error()) } - stores := store.ConfigureRedis(*config) + startBackgroundJobs(ctx, db) + stores := store.ConfigureStores(config.RedisLimiter) integrations := configureIntegrations(&config.Integrations) tp := telemetry.InitTracer() + defer shutdownTracer(tp) - slog.Info("appease linter since we aren't tracing anything yet", "tracer", telemetry.Tracer) + app := server.Init(db, stores, *integrations, *config) - defer func() { - if err := tp.Shutdown(context.Background()); err != nil { - slog.Error("error shutting down tracer", "error", err) - } - }() + go startServer(app, config.Application.Host, config.Application.Port) - app := server.Init(db, stores, *integrations, *config) + waitForShutdown(app) +} - err = app.Listen(fmt.Sprintf("%s:%d", config.Application.Host, config.Application.Port)) - if err != nil { - utilities.Exit("Error starting server: %s", err.Error()) - } +func parseFlags() (onlyMigrate, seedSearch *bool, configPath *string) { + onlyMigrate = flag.Bool("only-migrate", false, "Specify if you want to only perform the database migration") + seedSearch = flag.Bool("seed-search", true, "Specify if you want to seed the opensearch nodes.") + configPath = flag.String("config", filepath.Join("..", "config", ".env.dev"), "Specify the path to the config file (.env)") + flag.Parse() + return } func checkServerRunning(host string, port uint16) error { @@ -96,17 +96,48 @@ func checkServerRunning(host string, port uint16) error { return nil } +func seedSearchData(db *gorm.DB) { + slog.Info("to appease linter", "seedSearch", true, "db", db) + // if err := search.SeedClubs(db); err != nil { + // return + // } + + // if err := search.SeedEvents(db); err != nil { + // return + // } +} + +func startBackgroundJobs(ctx context.Context, db *gorm.DB) { + jobs := jobs.New(db) + background.Go(jobs.WelcomeSender(ctx)) +} + func configureIntegrations(config *config.Integrations) *integrations.Integrations { return &integrations.Integrations{ File: file.NewAWSProvider(config.AWS), Email: email.NewResendClient(config.Resend, true), - OAuth: oauth.OauthProviderSettings{ - Google: oauth.GoogleOAuthClient{ - OAuthConfig: config.GoogleOauth, - }, - Outlook: oauth.OutlookOAuthClient{ - OAuthConfig: config.OutlookOauth, - }, - }, } } + +func shutdownTracer(tp *sdktrace.TracerProvider) { + if err := tp.Shutdown(context.Background()); err != nil { + slog.Error("error shutting down tracer", "error", err) + } +} + +func startServer(app *fiber.App, host string, port uint16) { + if err := app.Listen(fmt.Sprintf("%s:%d", host, port)); err != nil { + log.Fatalf("Failed to start server: %v", err) + } +} + +func waitForShutdown(app *fiber.App) { + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + slog.Info("Shutting down server") + if err := app.Shutdown(); err != nil { + slog.Error("failed to shutdown server", "error", err) + } + slog.Info("Server shutdown") +} diff --git a/backend/middleware/auth/auth.go b/backend/middleware/auth/auth.go index df01b1e1c..1d59b9a7b 100644 --- a/backend/middleware/auth/auth.go +++ b/backend/middleware/auth/auth.go @@ -1,113 +1,42 @@ package auth import ( - "context" - "fmt" "slices" - "strings" + "time" - "github.com/GenerateNU/sac/backend/auth" - "github.com/GenerateNU/sac/backend/constants" - "github.com/GenerateNU/sac/backend/errs" - "github.com/GenerateNU/sac/backend/locals" - "github.com/GenerateNU/sac/backend/utilities" - "github.com/google/uuid" + "github.com/GenerateNU/sac/backend/permission" "github.com/GenerateNU/sac/backend/entities/models" + "github.com/GenerateNU/sac/backend/integrations/oauth/soth/sothic" - "github.com/golang-jwt/jwt" + "github.com/GenerateNU/sac/backend/utilities" "github.com/gofiber/fiber/v2" ) -func (m *AuthMiddlewareService) IsSuper(c *fiber.Ctx) bool { - claims, err := locals.CustomClaimsFrom(c) - if err != nil { - return false - } - if claims == nil { - return false - } - return claims.Role == string(models.Super) -} - -func GetAuthorizationToken(c *fiber.Ctx) *string { - accessToken := c.Get("Authorization") - if accessToken == "" { - return nil - } - - token := strings.Split(accessToken, "Bearer ") - if len(token) != 2 { - return nil - } - - return &token[1] -} - -func (m *AuthMiddlewareService) Authenticate(c *fiber.Ctx) error { +func (m *AuthMiddlewareHandler) Authorize(requiredPermissions ...permission.Permission) fiber.Handler { return func(c *fiber.Ctx) error { - accessToken := GetAuthorizationToken(c) - if accessToken == nil { - return utilities.Unauthorized() - } - - token, err := func() (*jwt.Token, error) { - return jwt.ParseWithClaims(*accessToken, &auth.CustomClaims{}, func(token *jwt.Token) (interface{}, error) { - return []byte(m.Auth.AccessKey.Expose()), nil - }) - }() - if err != nil { - return utilities.Unauthorized() - } - - claims, ok := token.Claims.(*auth.CustomClaims) - if !ok || !token.Valid { - return utilities.Unauthorized() - } - - blacklistCtx, blacklistCancel := context.WithTimeoutCause(c.UserContext(), constants.REDIS_TIMEOUT, errs.ErrRedisTimeout) - defer blacklistCancel() - isBlacklisted, err := m.Stores.Blacklist.IsTokenBlacklisted(blacklistCtx, *accessToken) - if err != nil { - return utilities.InternalServerError() - } - - if isBlacklisted { - return utilities.Unauthorized() - } - - locals.SetCustomClaims(c, claims) - - rawUserID := claims.Issuer - userID, err := uuid.Parse(rawUserID) + strUser, err := sothic.GetFromSession("user", c) if err != nil { - return fmt.Errorf("invalid user id: %s", rawUserID) - } - - locals.SetUserID(c, &userID) - - return nil - }(c) -} + c.Cookie(&fiber.Cookie{ + Name: "redirect", + Value: c.OriginalURL(), + Expires: time.Now().Add(5 * time.Minute), + // MARK: secure should be true in prod + // use go build tags to do this + HTTPOnly: true, + }) -func (m *AuthMiddlewareService) Authorize(requiredPermissions ...auth.Permission) func(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - authErr := m.Authenticate(c) - if authErr != nil { - return utilities.Unauthorized() + return c.Redirect("/api/v1/auth/login") } - claims, err := locals.CustomClaimsFrom(c) - if err != nil { - return err - } + user := models.UnmarshalUser(strUser) - if claims != nil && claims.Role == string(models.Super) { + if user.Role == models.Super { return c.Next() } - userPermissions := auth.GetPermissions(models.UserRole(claims.Role)) + userPermissions := permission.GetPermissions(user.Role) for _, requiredPermission := range requiredPermissions { if !slices.Contains(userPermissions, requiredPermission) { diff --git a/backend/middleware/auth/club.go b/backend/middleware/auth/club.go index 56ae9473d..f37b89cf4 100644 --- a/backend/middleware/auth/club.go +++ b/backend/middleware/auth/club.go @@ -2,43 +2,49 @@ package auth import ( "slices" + "time" "github.com/GenerateNU/sac/backend/entities/clubs" - "github.com/GenerateNU/sac/backend/locals" + "github.com/GenerateNU/sac/backend/entities/models" + "github.com/GenerateNU/sac/backend/integrations/oauth/soth/sothic" "github.com/GenerateNU/sac/backend/utilities" "github.com/gofiber/fiber/v2" ) // Authorizes admins of the specific club to make this request, skips check if super user -func (m *AuthMiddlewareService) ClubAuthorizeById(c *fiber.Ctx, extractor ExtractID) error { - return func(c *fiber.Ctx) error { - if err := m.Authenticate(c); err != nil { - return utilities.Unauthorized() - } - - if m.IsSuper(c) { - return c.Next() - } - - clubUUID, err := extractor(c) - if err != nil { - return err - } - - userID, err := locals.UserIDFrom(c) - if err != nil { - return err - } - - clubAdmin, err := clubs.GetAdminIDs(m.DB, *clubUUID) - if err != nil { - return err - } - - if slices.Contains(clubAdmin, *userID) { - return c.Next() - } - - return utilities.Forbidden() - }(c) +func (m *AuthMiddlewareHandler) ClubAuthorizeById(c *fiber.Ctx, extractor ExtractID) error { + strUser, err := sothic.GetFromSession("user", c) + if err != nil { + c.Cookie(&fiber.Cookie{ + Name: "redirect", + Value: c.OriginalURL(), + Expires: time.Now().Add(5 * time.Minute), + // MARK: secure should be true in prod + // use go build tags to do this + HTTPOnly: true, + }) + return c.Redirect("/api/v1/auth/login") + } + + user := models.UnmarshalUser(strUser) + + if user.Role == models.Super { + return c.Next() + } + + clubUUID, err := extractor(c) + if err != nil { + return err + } + + clubAdmin, err := clubs.GetAdminIDs(m.db, *clubUUID) + if err != nil { + return err + } + + if slices.Contains(clubAdmin, user.ID) { + return c.Next() + } + + return utilities.Forbidden() } diff --git a/backend/middleware/auth/event.go b/backend/middleware/auth/event.go index c0fc03e9f..443c4b6ec 100644 --- a/backend/middleware/auth/event.go +++ b/backend/middleware/auth/event.go @@ -2,43 +2,47 @@ package auth import ( "slices" + "time" "github.com/GenerateNU/sac/backend/entities/events" - "github.com/GenerateNU/sac/backend/locals" + "github.com/GenerateNU/sac/backend/entities/models" + "github.com/GenerateNU/sac/backend/integrations/oauth/soth/sothic" "github.com/GenerateNU/sac/backend/utilities" + "github.com/gofiber/fiber/v2" ) // Authorizes admins of the host club of this event to make this request, skips check if super user -func (m *AuthMiddlewareService) EventAuthorizeById(c *fiber.Ctx, extractor ExtractID) error { - return func(c *fiber.Ctx) error { - if err := m.Authenticate(c); err != nil { - return utilities.Unauthorized() - } - - if m.IsSuper(c) { - return c.Next() - } - - eventUUID, err := extractor(c) - if err != nil { - return err - } - - userID, err := locals.UserIDFrom(c) - if err != nil { - return err - } - - eventHostAdmin, err := events.GetEventHostAdminIDs(m.DB, *eventUUID) - if err != nil { - return err - } - - if slices.Contains(eventHostAdmin, *userID) { - return c.Next() - } - - return utilities.Forbidden() - }(c) +func (m *AuthMiddlewareHandler) EventAuthorizeById(c *fiber.Ctx, extractor ExtractID) error { + strUser, err := sothic.GetFromSession("user", c) + if err != nil { + c.Cookie(&fiber.Cookie{ + Name: "redirect", + Value: c.OriginalURL(), + Expires: time.Now().Add(5 * time.Minute), + // MARK: secure should be true in prod + // use go build tags to do this + HTTPOnly: true, + }) + + return c.Redirect("/api/v1/auth/login") + } + + user := models.UnmarshalUser(strUser) + + eventUUID, err := extractor(c) + if err != nil { + return err + } + + eventHostAdmin, err := events.GetEventHostAdminIDs(m.db, *eventUUID) + if err != nil { + return err + } + + if slices.Contains(eventHostAdmin, user.ID) { + return c.Next() + } + + return utilities.Forbidden() } diff --git a/backend/middleware/auth/middleware.go b/backend/middleware/auth/middleware.go index 22045ac85..08f61133e 100644 --- a/backend/middleware/auth/middleware.go +++ b/backend/middleware/auth/middleware.go @@ -1,34 +1,31 @@ package auth import ( - "github.com/GenerateNU/sac/backend/auth" - "github.com/GenerateNU/sac/backend/config" - "github.com/GenerateNU/sac/backend/database/store" + "github.com/GenerateNU/sac/backend/integrations/oauth/soth" + + "github.com/GenerateNU/sac/backend/permission" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "gorm.io/gorm" ) -type AuthMiddlewareInterface interface { - ClubAuthorizeById(c *fiber.Ctx) error +type AuthMiddlewareService interface { + ClubAuthorizeById(c *fiber.Ctx, extractor ExtractID) error UserAuthorizeById(c *fiber.Ctx) error - Authenticate(c *fiber.Ctx) error - Authorize(requiredPermissions ...auth.Permission) fiber.Handler - IsSuper(c *fiber.Ctx) bool + EventAuthorizeById(c *fiber.Ctx, extractor ExtractID) error + Authorize(requiredPermissions ...permission.Permission) fiber.Handler } -type AuthMiddlewareService struct { - DB *gorm.DB - Validate *validator.Validate - Auth config.AuthSettings - Stores *store.Stores +type AuthMiddlewareHandler struct { + db *gorm.DB + validate *validator.Validate + provider soth.Provider } -func New(db *gorm.DB, validate *validator.Validate, authSettings config.AuthSettings, stores *store.Stores) *AuthMiddlewareService { - return &AuthMiddlewareService{ - DB: db, - Validate: validate, - Auth: authSettings, - Stores: stores, +func New(db *gorm.DB, validate *validator.Validate, provider soth.Provider) AuthMiddlewareService { + return &AuthMiddlewareHandler{ + db: db, + validate: validate, + provider: provider, } } diff --git a/backend/middleware/auth/user.go b/backend/middleware/auth/user.go index 9c06cc2d8..38b65f33f 100644 --- a/backend/middleware/auth/user.go +++ b/backend/middleware/auth/user.go @@ -1,35 +1,43 @@ package auth import ( - "github.com/GenerateNU/sac/backend/locals" + "time" + + "github.com/GenerateNU/sac/backend/entities/models" + "github.com/GenerateNU/sac/backend/integrations/oauth/soth/sothic" "github.com/GenerateNU/sac/backend/utilities" "github.com/gofiber/fiber/v2" ) -func (m *AuthMiddlewareService) UserAuthorizeById(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - if err := m.Authenticate(c); err != nil { - return utilities.Unauthorized() - } +func (m *AuthMiddlewareHandler) UserAuthorizeById(c *fiber.Ctx) error { + strUser, err := sothic.GetFromSession("user", c) + if err != nil { + c.Cookie(&fiber.Cookie{ + Name: "redirect", + Value: c.OriginalURL(), + Expires: time.Now().Add(5 * time.Minute), + // MARK: secure should be true in prod + // use go build tags to do this + HTTPOnly: true, + }) + + return c.Redirect("/api/v1/auth/login") + } - if m.IsSuper(c) { - return c.Next() - } + user := models.UnmarshalUser(strUser) - idAsUUID, err := utilities.ValidateID(c.Params("userID")) - if err != nil { - return err - } + if user.Role == models.Super { + return c.Next() + } - userID, err := locals.UserIDFrom(c) - if err != nil { - return err - } + idAsUUID, err := utilities.ValidateID(c.Params("userID")) + if err != nil { + return err + } - if idAsUUID == userID { - return c.Next() - } + if idAsUUID == &user.ID { + return c.Next() + } - return utilities.Forbidden() - }(c) + return utilities.Forbidden() } diff --git a/backend/middleware/utility/limiter.go b/backend/middleware/utility/limiter.go index 39d29bf30..84f88f510 100644 --- a/backend/middleware/utility/limiter.go +++ b/backend/middleware/utility/limiter.go @@ -11,7 +11,7 @@ import ( "github.com/gofiber/fiber/v2/middleware/limiter" ) -func (u *UtilityMiddlewareService) Limiter(rate int, expiration time.Duration) func(c *fiber.Ctx) error { +func (h *Handler) Limiter(rate int, expiration time.Duration) func(c *fiber.Ctx) error { return limiter.New(limiter.Config{ Max: rate, Expiration: expiration, @@ -21,6 +21,6 @@ func (u *UtilityMiddlewareService) Limiter(rate int, expiration time.Duration) f LimitReached: func(c *fiber.Ctx) error { return utilities.NewAPIError(http.StatusTooManyRequests, errors.New("too many requests")) }, - Storage: u.Stores.Limiter, + Storage: h.limiter, }) } diff --git a/backend/middleware/utility/middleware.go b/backend/middleware/utility/middleware.go index 2f582e913..36351ae8e 100644 --- a/backend/middleware/utility/middleware.go +++ b/backend/middleware/utility/middleware.go @@ -7,19 +7,19 @@ import ( "github.com/gofiber/fiber/v2" ) -type UtilityMiddlewareInterface interface { +type Service interface { Paginator(c *fiber.Ctx) error Limiter(rate int, duration time.Duration) fiber.Handler } -type UtilityMiddlewareService struct { +type Handler struct { paginator fiber.Handler - Stores *store.Stores + limiter store.Storer } -func New(paginator fiber.Handler, stores *store.Stores) *UtilityMiddlewareService { - return &UtilityMiddlewareService{ +func New(paginator fiber.Handler, limiter store.Storer) Service { + return &Handler{ paginator: paginator, - Stores: stores, + limiter: limiter, } } diff --git a/backend/middleware/utility/paginator.go b/backend/middleware/utility/paginator.go index 0e2827fda..fc54b851e 100644 --- a/backend/middleware/utility/paginator.go +++ b/backend/middleware/utility/paginator.go @@ -4,6 +4,6 @@ import ( "github.com/gofiber/fiber/v2" ) -func (u *UtilityMiddlewareService) Paginator(c *fiber.Ctx) error { - return u.paginator(c) +func (h *Handler) Paginator(c *fiber.Ctx) error { + return h.paginator(c) } diff --git a/backend/migrations/000001_init.down.sql b/backend/migrations/000001_init.down.sql index 7b7223876..2a37eecb7 100644 --- a/backend/migrations/000001_init.down.sql +++ b/backend/migrations/000001_init.down.sql @@ -1,132 +1,5 @@ BEGIN; -ALTER TABLE - "users" -ALTER COLUMN - id DROP DEFAULT; - -ALTER TABLE - recruitment -ALTER COLUMN - id DROP DEFAULT; - -ALTER TABLE - applications -ALTER COLUMN - id DROP DEFAULT; - -ALTER TABLE - clubs -ALTER COLUMN - id DROP DEFAULT; - -ALTER TABLE - series -ALTER COLUMN - id DROP DEFAULT; - -ALTER TABLE - EVENTS -ALTER COLUMN - id DROP DEFAULT; - -ALTER TABLE - categories -ALTER COLUMN - id DROP DEFAULT; - -ALTER TABLE - tags -ALTER COLUMN - id DROP DEFAULT; - -ALTER TABLE - club_events -ALTER COLUMN - club_id DROP DEFAULT, -ALTER COLUMN - event_id DROP DEFAULT; - -ALTER TABLE - club_tags -ALTER COLUMN - tag_id DROP DEFAULT, -ALTER COLUMN - club_id DROP DEFAULT; - -ALTER TABLE - contacts -ALTER COLUMN - id DROP DEFAULT; - -ALTER TABLE - event_tags -ALTER COLUMN - tag_id DROP DEFAULT, -ALTER COLUMN - event_id DROP DEFAULT; - -ALTER TABLE - files -ALTER COLUMN - id DROP DEFAULT; - --- ALTER TABLE notifications ALTER COLUMN id DROP DEFAULT; -- Uncomment if notifications table is created -ALTER TABLE - leadership -ALTER COLUMN - id DROP DEFAULT; - -ALTER TABLE - user_club_followers -ALTER COLUMN - user_id DROP DEFAULT, -ALTER COLUMN - club_id DROP DEFAULT; - -ALTER TABLE - user_club_intended_applicants -ALTER COLUMN - user_id DROP DEFAULT, -ALTER COLUMN - club_id DROP DEFAULT; - -ALTER TABLE - user_club_members -ALTER COLUMN - user_id DROP DEFAULT; - -ALTER TABLE - user_event_rsvps -ALTER COLUMN - user_id DROP DEFAULT, -ALTER COLUMN - event_id DROP DEFAULT; - -ALTER TABLE - user_event_waitlists -ALTER COLUMN - user_id DROP DEFAULT, -ALTER COLUMN - event_id DROP DEFAULT; - -ALTER TABLE - user_tags -ALTER COLUMN - user_id DROP DEFAULT, -ALTER COLUMN - tag_id DROP DEFAULT; - -ALTER TABLE - verifications -ALTER COLUMN - user_id DROP DEFAULT; - -ALTER TABLE - user_oauth_tokens -ALTER COLUMN - user_id DROP DEFAULT; - DROP TABLE IF EXISTS club_events CASCADE; DROP TABLE IF EXISTS club_tags CASCADE; @@ -152,11 +25,7 @@ DROP TABLE IF EXISTS user_event_waitlists CASCADE; DROP TABLE IF EXISTS user_tags CASCADE; -DROP TABLE IF EXISTS verifications CASCADE; - -DROP TABLE IF EXISTS user_oauth_tokens CASCADE; - -DROP TABLE IF EXISTS EVENTS CASCADE; +DROP TABLE IF EXISTS "events" CASCADE; DROP TABLE IF EXISTS series CASCADE; @@ -172,14 +41,14 @@ DROP TABLE IF EXISTS recruitment CASCADE; DROP TABLE IF EXISTS "users" CASCADE; +DROP TABLE IF EXISTS welcome_tasks CASCADE; + DROP TYPE IF EXISTS recruitment_cycle CASCADE; DROP TYPE IF EXISTS recruitment_type CASCADE; DROP TYPE IF EXISTS event_type CASCADE; -DROP TYPE IF EXISTS OAuthResourceType CASCADE; - DROP EXTENSION IF EXISTS "uuid-ossp"; COMMIT; \ No newline at end of file diff --git a/backend/migrations/000001_init.up.sql b/backend/migrations/000001_init.up.sql index f62171440..6af707ab5 100644 --- a/backend/migrations/000001_init.up.sql +++ b/backend/migrations/000001_init.up.sql @@ -7,17 +7,15 @@ CREATE TABLE IF NOT EXISTS "users"( created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, role varchar(255) NOT NULL DEFAULT 'student' :: character varying, - first_name varchar(255) NOT NULL, - last_name varchar(255) NOT NULL, + name varchar(255) NOT NULL, email varchar(255) NOT NULL, - password_hash varchar(97) NOT NULL, major0 varchar(255), major1 varchar(255), major2 varchar(255), college varchar(255), graduation_cycle varchar(255), graduation_year smallint, - is_verified boolean NOT NULL DEFAULT false, + avatar_url varchar(255), PRIMARY KEY(id) ); @@ -267,28 +265,11 @@ CREATE TABLE IF NOT EXISTS user_tags( CONSTRAINT fk_user_tags_tag FOREIGN KEY(tag_id) REFERENCES tags(id) ON UPDATE CASCADE ON DELETE CASCADE ); -CREATE UNIQUE INDEX IF NOT EXISTS uni_users_email ON "users" USING btree ("email"); - -CREATE TABLE IF NOT EXISTS verifications( - user_id uuid NOT NULL, - token varchar(255), - expires_at timestamp WITH time zone NOT NULL, - "type" varchar(255) NOT NULL, - PRIMARY KEY(user_id, expires_at) -); - -CREATE UNIQUE INDEX IF NOT EXISTS uni_verifications_token ON verifications USING btree ("token"); - -CREATE TYPE OAuthResourceType AS ENUM ('google', 'outlook'); - -CREATE TABLE IF NOT EXISTS user_oauth_tokens( - user_id uuid NOT NULL, - refresh_token varchar, - access_token varchar, - csrf_token varchar(255), - resource_type OAuthResourceType, - expires_at timestamp WITH time zone NOT NULL, - PRIMARY KEY(user_id, resource_type) +CREATE TABLE IF NOT EXISTS welcome_tasks( + email varchar(255) NOT NULL, + name varchar(255) NOT NULL, + attempts integer NOT NULL DEFAULT 0, + PRIMARY KEY(email) ); COMMIT; \ No newline at end of file diff --git a/backend/auth/permissions.go b/backend/permission/permission.go similarity index 99% rename from backend/auth/permissions.go rename to backend/permission/permission.go index e07bc61bf..7fe6484ca 100644 --- a/backend/auth/permissions.go +++ b/backend/permission/permission.go @@ -1,4 +1,4 @@ -package auth +package permission import "github.com/GenerateNU/sac/backend/entities/models" diff --git a/backend/server/server.go b/backend/server/server.go index bf466e1f1..a916d37c5 100644 --- a/backend/server/server.go +++ b/backend/server/server.go @@ -6,17 +6,18 @@ import ( "github.com/garrettladley/fiberpaginate" go_json "github.com/goccy/go-json" - authenticator "github.com/GenerateNU/sac/backend/auth" "github.com/GenerateNU/sac/backend/config" "github.com/GenerateNU/sac/backend/database/store" + "github.com/GenerateNU/sac/backend/integrations/oauth/soth/goog" + "github.com/GenerateNU/sac/backend/integrations/oauth/soth/msft" + auth "github.com/GenerateNU/sac/backend/entities/auth/base" categories "github.com/GenerateNU/sac/backend/entities/categories/base" clubs "github.com/GenerateNU/sac/backend/entities/clubs/base" events "github.com/GenerateNU/sac/backend/entities/events/base" files "github.com/GenerateNU/sac/backend/entities/files/base" leader "github.com/GenerateNU/sac/backend/entities/leadership/base" - oauth "github.com/GenerateNU/sac/backend/entities/oauth/base" socials "github.com/GenerateNU/sac/backend/entities/socials/base" tags "github.com/GenerateNU/sac/backend/entities/tags/base" users "github.com/GenerateNU/sac/backend/entities/users/base" @@ -33,7 +34,6 @@ import ( "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/requestid" - "github.com/golang-jwt/jwt" "gorm.io/gorm" ) @@ -55,9 +55,18 @@ func Init(db *gorm.DB, stores *store.Stores, integrations integrations.Integrati panic(fmt.Sprintf("Error registering custom validators: %s", err)) } - jwt := authenticator.NewJWTClient(settings.Auth, jwt.SigningMethodHS256) - authMiddleware := authMiddleware.New(db, validate, settings.Auth, stores) - utilityMiddleware := utilityMiddleware.New(fiberpaginate.New(), stores) + applicationURL := settings.Application.ApplicationURL() + + msftProvider := msft.New(settings.Microsft.Key, settings.Microsft.Secret, fmt.Sprintf("%s/api/v1/auth/microsoftonline/callback", applicationURL), settings.Microsft.Tenant) + googProvider := goog.New(settings.Google.Key, settings.Google.Secret, fmt.Sprintf("%s/api/v1/auth/google/callback", applicationURL)) + + authMiddleware := authMiddleware.New( + db, + validate, + msftProvider, + ) + + utilityMiddleware := utilityMiddleware.New(fiberpaginate.New(), stores.Limiter) apiv1 := app.Group("/api/v1") @@ -68,22 +77,29 @@ func Init(db *gorm.DB, stores *store.Stores, integrations integrations.Integrati types.NewServiceParams( db, validate, - &settings.Auth, - jwt, &settings.Calendar, stores, integrations, ), ) - allRoutes(app, routeParams) + allRoutes(app, routeParams, + auth.NewParams( + msftProvider, + applicationURL, + apiv1, + db, + integrations.Email, + validate, + googProvider, + )) return app } -func allRoutes(app *fiber.App, routeParams types.RouteParams) { +func allRoutes(app *fiber.App, routeParams types.RouteParams, authParams auth.Params) { Utility(app) - auth.Auth(routeParams) + auth.Auth(authParams) users.UserRoutes(routeParams) clubs.ClubRoutes(routeParams) socials.Social(routeParams) @@ -93,7 +109,6 @@ func allRoutes(app *fiber.App, routeParams types.RouteParams) { events.EventRoutes(routeParams) files.File(routeParams) search.SearchRoutes(routeParams) - oauth.OAuth(routeParams) } func newFiberApp(appSettings config.ApplicationSettings) *fiber.App { @@ -104,7 +119,7 @@ func newFiberApp(appSettings config.ApplicationSettings) *fiber.App { }) app.Use(cors.New(cors.Config{ - AllowOrigins: fmt.Sprintf("http://%s:%d", appSettings.Host, appSettings.Port), + AllowOrigins: appSettings.ApplicationURL(), AllowCredentials: true, AllowHeaders: "Origin, Content-Type, Accept, Authorization", AllowMethods: "GET, POST, PUT, DELETE, OPTIONS", diff --git a/backend/templates/emails/password_change_complete.templ b/backend/templates/emails/password_change_complete.templ deleted file mode 100644 index 5e21dadde..000000000 --- a/backend/templates/emails/password_change_complete.templ +++ /dev/null @@ -1,74 +0,0 @@ -package emails - -import "github.com/GenerateNU/sac/backend/templates/emails/layouts" - -templ PasswordChangeComplete(name string) { - @layouts.Base("Your Hippo Password Has Been Reset", passwordChangeCompleteStyles()) { -
-

Your Password Has Been Reset

-

Hi { name },

-

- This email confirms that your password for your Hippo account has been - successfully reset. -

-

You can now sign in using your new password:

- Sign in to Hippo -

- If you did not request this password reset, please contact our support - team immediately. -

-

Sincerely,

-

The Hippo Team

-
- } -} - -templ passwordChangeCompleteStyles() { - -} diff --git a/backend/templates/emails/password_change_complete_templ.go b/backend/templates/emails/password_change_complete_templ.go deleted file mode 100644 index 9dc55bc51..000000000 --- a/backend/templates/emails/password_change_complete_templ.go +++ /dev/null @@ -1,89 +0,0 @@ -// Code generated by templ - DO NOT EDIT. - -// templ: version: v0.2.639 -package emails - -//lint:file-ignore SA4006 This context is only used if a nested component is present. - -import "github.com/a-h/templ" -import "context" -import "io" -import "bytes" - -import "github.com/GenerateNU/sac/backend/templates/emails/layouts" - -func PasswordChangeComplete(name string) templ.Component { - return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) - if !templ_7745c5c3_IsBuffer { - templ_7745c5c3_Buffer = templ.GetBuffer() - defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var1 := templ.GetChildren(ctx) - if templ_7745c5c3_Var1 == nil { - templ_7745c5c3_Var1 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Var2 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) - if !templ_7745c5c3_IsBuffer { - templ_7745c5c3_Buffer = templ.GetBuffer() - defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Your Password Has Been Reset

Hi ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var3 string - templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(name) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/emails/password_change_complete.templ`, Line: 9, Col: 15} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(",

This email confirms that your password for your Hippo account has been successfully reset.

You can now sign in using your new password:

Sign in to Hippo

If you did not request this password reset, please contact our support team immediately.

Sincerely,

The Hippo Team

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if !templ_7745c5c3_IsBuffer { - _, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer) - } - return templ_7745c5c3_Err - }) - templ_7745c5c3_Err = layouts.Base("Your Hippo Password Has Been Reset", passwordChangeCompleteStyles()).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if !templ_7745c5c3_IsBuffer { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) - } - return templ_7745c5c3_Err - }) -} - -func passwordChangeCompleteStyles() templ.Component { - return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) - if !templ_7745c5c3_IsBuffer { - templ_7745c5c3_Buffer = templ.GetBuffer() - defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var4 := templ.GetChildren(ctx) - if templ_7745c5c3_Var4 == nil { - templ_7745c5c3_Var4 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if !templ_7745c5c3_IsBuffer { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) - } - return templ_7745c5c3_Err - }) -} diff --git a/backend/templates/emails/password_reset.templ b/backend/templates/emails/password_reset.templ deleted file mode 100644 index a6e0ed454..000000000 --- a/backend/templates/emails/password_reset.templ +++ /dev/null @@ -1,76 +0,0 @@ -package emails - -import ( - "github.com/GenerateNU/sac/backend/templates/emails/layouts" -) - -templ PasswordReset(name string, resetUrl string) { - @layouts.Base("Password Reset Email", passwordResetStyles()) { -
-

Your Hippo Account Needs a New Key!

-

Hi { name },

-

- Looks like you've misplaced the key to your Hippo account! Don't worry, - we can help you unlock it in no time. Just click the button below to - choose a new password: -

- Reset Your Password -

- Remember, if you didn't request this change, simply disregard this - message and your account remains secure. -

-

See you soon,

-

The Hippo Team

-
- } -} - -templ passwordResetStyles() { - -} diff --git a/backend/templates/emails/password_reset_templ.go b/backend/templates/emails/password_reset_templ.go deleted file mode 100644 index bc9ca12dd..000000000 --- a/backend/templates/emails/password_reset_templ.go +++ /dev/null @@ -1,100 +0,0 @@ -// Code generated by templ - DO NOT EDIT. - -// templ: version: v0.2.639 -package emails - -//lint:file-ignore SA4006 This context is only used if a nested component is present. - -import "github.com/a-h/templ" -import "context" -import "io" -import "bytes" - -import ( - "github.com/GenerateNU/sac/backend/templates/emails/layouts" -) - -func PasswordReset(name string, resetUrl string) templ.Component { - return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) - if !templ_7745c5c3_IsBuffer { - templ_7745c5c3_Buffer = templ.GetBuffer() - defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var1 := templ.GetChildren(ctx) - if templ_7745c5c3_Var1 == nil { - templ_7745c5c3_Var1 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Var2 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) - if !templ_7745c5c3_IsBuffer { - templ_7745c5c3_Buffer = templ.GetBuffer() - defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("

Your Hippo Account Needs a New Key!

Hi ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var3 string - templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(name) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/emails/password_reset.templ`, Line: 11, Col: 15} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(",

Looks like you've misplaced the key to your Hippo account! Don't worry, we can help you unlock it in no time. Just click the button below to choose a new password:

Reset Your Password

Remember, if you didn't request this change, simply disregard this message and your account remains secure.

See you soon,

The Hippo Team

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if !templ_7745c5c3_IsBuffer { - _, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer) - } - return templ_7745c5c3_Err - }) - templ_7745c5c3_Err = layouts.Base("Password Reset Email", passwordResetStyles()).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if !templ_7745c5c3_IsBuffer { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) - } - return templ_7745c5c3_Err - }) -} - -func passwordResetStyles() templ.Component { - return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) - if !templ_7745c5c3_IsBuffer { - templ_7745c5c3_Buffer = templ.GetBuffer() - defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var5 := templ.GetChildren(ctx) - if templ_7745c5c3_Var5 == nil { - templ_7745c5c3_Var5 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if !templ_7745c5c3_IsBuffer { - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) - } - return templ_7745c5c3_Err - }) -} diff --git a/backend/tests/api/helpers/auth.go b/backend/tests/api/helpers/auth.go index 15e945bc9..897e094fe 100644 --- a/backend/tests/api/helpers/auth.go +++ b/backend/tests/api/helpers/auth.go @@ -3,20 +3,16 @@ package helpers import ( "fmt" - "github.com/GenerateNU/sac/backend/auth" "github.com/GenerateNU/sac/backend/database" "github.com/GenerateNU/sac/backend/entities/models" - "github.com/goccy/go-json" "github.com/gofiber/fiber/v2" "github.com/google/uuid" ) type TestUser struct { - UUID uuid.UUID - Email string - Password string - AccessToken string - RefreshToken string + UUID uuid.UUID + Email string + AccessToken string } func (app *TestApp) Auth(role models.UserRole) { @@ -57,117 +53,41 @@ func (app *TestApp) authSuper() { accessToken = authHeader[len("Bearer "):] } - for _, cookie := range resp.Cookies() { - if cookie.Name == "refresh_token" { - refreshToken = cookie.Value - } - } - if accessToken == "" || refreshToken == "" { panic("Failed to authenticate super user") } app.TestUser = &TestUser{ - UUID: database.SuperUserUUID, - Email: email, - Password: password.Expose(), - AccessToken: accessToken, - RefreshToken: refreshToken, + UUID: database.SuperUserUUID, + Email: email, + AccessToken: accessToken, } } func (app *TestApp) authStudent() { - studentUser, rawPassword := SampleStudentFactory() - - resp, err := app.Send(TestRequest{ - Method: fiber.MethodPost, - Path: "/api/v1/users/", - Body: SampleStudentJSONFactory(studentUser, rawPassword), - }) - if err != nil { - panic(fmt.Sprintf("Failed to create sample student user: %v", err)) - } - var respBody map[string]interface{} - - err = json.NewDecoder(resp.Body).Decode(&respBody) - if err != nil { - panic(fmt.Sprintf("Failed to decode response body: %v", err)) - } - - rawStudentUserUUID := respBody["id"].(string) - studentUserUUID, err := uuid.Parse(rawStudentUserUUID) - if err != nil { - panic(fmt.Sprintf("Failed to parse student user UUID: %v", err)) - } - - resp, err = app.Send(TestRequest{ - Method: fiber.MethodPost, - Path: "/api/v1/auth/login", - Body: &map[string]interface{}{ - "email": studentUser.Email, - "password": rawPassword, - }, - }) - if err != nil { - panic(fmt.Sprintf("Failed to authenticate sample student user: %v", err)) - } - var accessToken string - var refreshToken string - - authHeader := resp.Header.Get("Authorization") - if authHeader != "" { - accessToken = authHeader[len("Bearer "):] - } - - for _, cookie := range resp.Cookies() { - if cookie.Name == "refresh_token" { - refreshToken = cookie.Value - } - } - - if accessToken == "" || refreshToken == "" { - panic("Failed to authenticate sample student user") - } - - app.TestUser = &TestUser{ - UUID: studentUserUUID, - Email: studentUser.Email, - Password: rawPassword, - AccessToken: accessToken, - RefreshToken: refreshToken, - } + app.TestUser = nil } -func SampleStudentFactory() (models.User, string) { - password := "1234567890&" - hashedPassword, err := auth.ComputeHash(password) - if err != nil { - panic(fmt.Sprintf("Failed to hash password: %v", err)) - } - +func SampleStudentFactory() models.User { return models.User{ Role: models.Student, - FirstName: "Jane", - LastName: "Doe", + Name: "Jane Doe", Email: "doe.jane@northeastern.edu", - PasswordHash: *hashedPassword, Major0: models.ComputerScience, Major1: models.Economics, College: models.KCCS, GraduationCycle: models.May, GraduationYear: 2025, - }, password + } } -func SampleStudentJSONFactory(sampleStudent models.User, rawPassword string) *map[string]interface{} { +func SampleStudentJSONFactory(sampleStudent models.User) *map[string]interface{} { if sampleStudent.Role != models.Student { panic("User is not a student") } return &map[string]interface{}{ - "first_name": sampleStudent.FirstName, - "last_name": sampleStudent.LastName, + "name": sampleStudent.Name, "email": sampleStudent.Email, - "password": rawPassword, "major0": string(sampleStudent.Major0), "major1": string(sampleStudent.Major1), "major2": string(sampleStudent.Major2), diff --git a/backend/tests/api/helpers/dependencies.go b/backend/tests/api/helpers/dependencies.go index de47fcfc0..1716037e2 100644 --- a/backend/tests/api/helpers/dependencies.go +++ b/backend/tests/api/helpers/dependencies.go @@ -7,7 +7,7 @@ import ( func NewMockDependencies() *integrations.Integrations { return &integrations.Integrations{ - Email: mocks.NewResendMockClient(), - File: mocks.NewAWSMockClient(), + Email: mocks.NewResendClient(), + File: mocks.NewAWSClient(), } } diff --git a/backend/tests/api/helpers/redis.go b/backend/tests/api/helpers/redis.go index 581b00ff3..f94861f72 100644 --- a/backend/tests/api/helpers/redis.go +++ b/backend/tests/api/helpers/redis.go @@ -6,25 +6,15 @@ import ( ) type StoresMock struct { - Limiter store.LimiterInterface - Blacklist store.BlacklistInterface - ActiveToken store.ActiveTokenInterface + Limiter store.Storer } -func NewStoresMock(limiter store.LimiterInterface, blacklist store.BlacklistInterface, activeToken store.ActiveTokenInterface) *store.Stores { +func NewStoresMock(session store.Storer, limiter store.Storer) *store.Stores { return &store.Stores{ - Limiter: limiter, - Blacklist: blacklist, - ActiveToken: activeToken, + Limiter: limiter, } } func ConfigureRedisMock() *store.Stores { - limiter := mocks.NewLimiterMock(mocks.NewRedisMockClient()) - blacklist := mocks.NewBlacklistMock(mocks.NewRedisMockClient()) - activeToken := mocks.NewActiveTokenMock(mocks.NewRedisMockClient()) - - stores := NewStoresMock(limiter, blacklist, activeToken) - - return stores + return NewStoresMock(mocks.NewRedisClient(), mocks.NewRedisClient()) } diff --git a/backend/tests/api/helpers/requests.go b/backend/tests/api/helpers/requests.go index fca088662..460ca3106 100644 --- a/backend/tests/api/helpers/requests.go +++ b/backend/tests/api/helpers/requests.go @@ -65,10 +65,6 @@ func (app TestApp) Send(request TestRequest) (*http.Response, error) { if app.TestUser != nil { req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", app.TestUser.AccessToken)) - req.AddCookie(&http.Cookie{ - Name: "refresh_token", - Value: app.TestUser.RefreshToken, - }) } resp, err := app.App.Test(req) diff --git a/backend/tests/api/mocks/aws.go b/backend/tests/api/mocks/aws.go new file mode 100644 index 000000000..5bb29a13b --- /dev/null +++ b/backend/tests/api/mocks/aws.go @@ -0,0 +1,26 @@ +package mocks + +import ( + "mime/multipart" + + "github.com/GenerateNU/sac/backend/entities/models" + "github.com/GenerateNU/sac/backend/integrations/file" +) + +type AWSClient struct{} + +func NewAWSClient() file.FileClientInterface { + return &AWSClient{} +} + +func (c *AWSClient) UploadFile(folder string, fileHeader *multipart.FileHeader, allowedTypes []file.FileType) (*models.FileInfo, error) { + return nil, nil +} + +func (c *AWSClient) DeleteFile(fileURL string) error { + return nil +} + +func (c *AWSClient) GetFileURL(fileURL string) *string { + return nil +} diff --git a/backend/tests/api/mocks/aws_mock.go b/backend/tests/api/mocks/aws_mock.go deleted file mode 100644 index 28d72e690..000000000 --- a/backend/tests/api/mocks/aws_mock.go +++ /dev/null @@ -1,26 +0,0 @@ -package mocks - -import ( - "mime/multipart" - - "github.com/GenerateNU/sac/backend/entities/models" - "github.com/GenerateNU/sac/backend/integrations/file" -) - -type AWSMockClient struct{} - -func NewAWSMockClient() file.FileClientInterface { - return &AWSMockClient{} -} - -func (c *AWSMockClient) UploadFile(folder string, fileHeader *multipart.FileHeader, allowedTypes []file.FileType) (*models.FileInfo, error) { - return nil, nil -} - -func (c *AWSMockClient) DeleteFile(fileURL string) error { - return nil -} - -func (c *AWSMockClient) GetFileURL(fileURL string) *string { - return nil -} diff --git a/backend/tests/api/mocks/jwt_mock.go b/backend/tests/api/mocks/jwt_mock.go deleted file mode 100644 index 221d3737b..000000000 --- a/backend/tests/api/mocks/jwt_mock.go +++ /dev/null @@ -1,36 +0,0 @@ -package mocks - -import ( - "github.com/GenerateNU/sac/backend/auth" - "github.com/golang-jwt/jwt" -) - -type JWTMockClient struct{} - -func NewJWTMockClient() auth.JWTClientInterface { - return &JWTMockClient{} -} - -func (c *JWTMockClient) GenerateTokenPair(accessClaims, refreshClaims auth.Claims) (*auth.Token, error) { - return &auth.Token{}, nil -} - -func (c *JWTMockClient) GenerateToken(claims auth.Claims, tokenType auth.JWTType) ([]byte, error) { - return []byte{}, nil -} - -func (c *JWTMockClient) RefreshToken(token, refreshToken string, tokenType auth.JWTType, newClaims jwt.MapClaims) ([]byte, error) { - return []byte{}, nil -} - -func (c *JWTMockClient) ExtractClaims(tokenString string, tokenType auth.JWTType) (jwt.MapClaims, error) { - return jwt.MapClaims{}, nil -} - -func (c *JWTMockClient) ParseToken(tokenString string, tokenType auth.JWTType) (*jwt.Token, error) { - return &jwt.Token{}, nil -} - -func (c *JWTMockClient) IsTokenValid(tokenString string, tokenType auth.JWTType) (bool, error) { - return true, nil -} diff --git a/backend/tests/api/mocks/redis.go b/backend/tests/api/mocks/redis.go new file mode 100644 index 000000000..4f0d96846 --- /dev/null +++ b/backend/tests/api/mocks/redis.go @@ -0,0 +1,31 @@ +package mocks + +import ( + "time" +) + +type RedisClient struct{} + +func NewRedisClient() *RedisClient { + return &RedisClient{} +} + +func (r *RedisClient) Get(key string) ([]byte, error) { + return []byte{}, nil +} + +func (r *RedisClient) Set(key string, val []byte, exp time.Duration) error { + return nil +} + +func (r *RedisClient) Delete(key string) error { + return nil +} + +func (r *RedisClient) Reset() error { + return nil +} + +func (r *RedisClient) Close() error { + return nil +} diff --git a/backend/tests/api/mocks/redis_mock.go b/backend/tests/api/mocks/redis_mock.go deleted file mode 100644 index db10d70fd..000000000 --- a/backend/tests/api/mocks/redis_mock.go +++ /dev/null @@ -1,124 +0,0 @@ -package mocks - -import ( - "context" - "time" - - "github.com/GenerateNU/sac/backend/database/store" -) - -// RedisMockClient implements StoreClientInterface -type RedisMockClient struct{} - -func NewRedisMockClient() *RedisMockClient { - return &RedisMockClient{} -} - -func (r *RedisMockClient) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error { - return nil -} - -func (r *RedisMockClient) Get(ctx context.Context, key string) (string, error) { - return "", nil -} - -func (r *RedisMockClient) Del(ctx context.Context, key string) error { - return nil -} - -func (r *RedisMockClient) Exists(ctx context.Context, key string) (bool, error) { - return false, nil -} - -func (r *RedisMockClient) SetAdd(ctx context.Context, key string, members ...interface{}) error { - return nil -} - -func (r *RedisMockClient) SetIsMember(ctx context.Context, key string, member interface{}) (bool, error) { - return false, nil -} - -func (r *RedisMockClient) FlushAll(ctx context.Context) error { - return nil -} - -func (r *RedisMockClient) Close(ctx context.Context) error { - return nil -} - -// ActiveTokenMock implements ActiveTokenInterface -type ActiveTokenMock struct { - StoreClient store.StoreClientInterface -} - -func NewActiveTokenMock(storeClient store.StoreClientInterface) *ActiveTokenMock { - return &ActiveTokenMock{ - StoreClient: storeClient, - } -} - -func (a *ActiveTokenMock) StoreRefreshToken(ctx context.Context, token string, userID string, expiry time.Duration) error { - return nil -} - -func (a *ActiveTokenMock) GetRefreshToken(ctx context.Context, token string) (string, error) { - return "", nil -} - -func (a *ActiveTokenMock) IsActive(ctx context.Context, token string) (bool, error) { - return false, nil -} - -func (a *ActiveTokenMock) DeleteRefreshToken(ctx context.Context, token string) error { - return nil -} - -// BlacklistMock implements BlacklistInterface -type BlacklistMock struct { - StoreClient store.StoreClientInterface -} - -func NewBlacklistMock(storeClient store.StoreClientInterface) *BlacklistMock { - return &BlacklistMock{ - StoreClient: storeClient, - } -} - -func (b *BlacklistMock) BlacklistToken(ctx context.Context, token string, expiry time.Duration) error { - return nil -} - -func (b *BlacklistMock) IsTokenBlacklisted(ctx context.Context, token string) (bool, error) { - return false, nil -} - -// LimiterMock implements LimiterInterface -type LimiterMock struct { - StoreClient store.StoreClientInterface -} - -func NewLimiterMock(storeClient store.StoreClientInterface) *LimiterMock { - return &LimiterMock{ - StoreClient: storeClient, - } -} - -func (l *LimiterMock) Get(key string) ([]byte, error) { - return nil, nil -} - -func (l *LimiterMock) Set(key string, val []byte, exp time.Duration) error { - return nil -} - -func (l *LimiterMock) Delete(key string) error { - return nil -} - -func (l *LimiterMock) Reset() error { - return nil -} - -func (l *LimiterMock) Close() error { - return nil -} diff --git a/backend/tests/api/mocks/resend.go b/backend/tests/api/mocks/resend.go new file mode 100644 index 000000000..90bd78f35 --- /dev/null +++ b/backend/tests/api/mocks/resend.go @@ -0,0 +1,17 @@ +package mocks + +import ( + "context" + + "github.com/GenerateNU/sac/backend/integrations/email" +) + +type ResendClient struct{} + +func NewResendClient() email.Emailer { + return &ResendClient{} +} + +func (c *ResendClient) SendWelcome(ctx context.Context, email string, name string) error { + return nil +} diff --git a/backend/tests/api/mocks/resend_mock.go b/backend/tests/api/mocks/resend_mock.go deleted file mode 100644 index 3482cb382..000000000 --- a/backend/tests/api/mocks/resend_mock.go +++ /dev/null @@ -1,27 +0,0 @@ -package mocks - -import ( - "github.com/GenerateNU/sac/backend/integrations/email" -) - -type ResendMockClient struct{} - -func NewResendMockClient() email.EmailClientInterface { - return &ResendMockClient{} -} - -func (c *ResendMockClient) SendPasswordResetEmail(name, email, token string) error { - return nil -} - -func (c *ResendMockClient) SendEmailVerification(email, code string) error { - return nil -} - -func (c *ResendMockClient) SendWelcomeEmail(name, email string) error { - return nil -} - -func (c *ResendMockClient) SendPasswordChangedEmail(name, email string) error { - return nil -} diff --git a/backend/types/params.go b/backend/types/params.go index 750c3f86d..339dc4cd6 100644 --- a/backend/types/params.go +++ b/backend/types/params.go @@ -1,7 +1,6 @@ package types import ( - "github.com/GenerateNU/sac/backend/auth" "github.com/GenerateNU/sac/backend/config" "github.com/GenerateNU/sac/backend/database/store" "github.com/GenerateNU/sac/backend/integrations" @@ -14,12 +13,12 @@ import ( type RouteParams struct { Router fiber.Router - AuthMiddleware *authMiddleware.AuthMiddlewareService - UtilityMiddleware *utilityMiddleware.UtilityMiddlewareService + AuthMiddleware authMiddleware.AuthMiddlewareService + UtilityMiddleware utilityMiddleware.Service ServiceParams ServiceParams } -func NewRouteParams(router fiber.Router, authMiddleware *authMiddleware.AuthMiddlewareService, utilityMiddleware *utilityMiddleware.UtilityMiddlewareService, serviceParams ServiceParams) RouteParams { +func NewRouteParams(router fiber.Router, authMiddleware authMiddleware.AuthMiddlewareService, utilityMiddleware utilityMiddleware.Service, serviceParams ServiceParams) RouteParams { return RouteParams{ Router: router, AuthMiddleware: authMiddleware, @@ -31,19 +30,15 @@ func NewRouteParams(router fiber.Router, authMiddleware *authMiddleware.AuthMidd type ServiceParams struct { DB *gorm.DB Validate *validator.Validate - Auth *config.AuthSettings - JWT auth.JWTClientInterface Calendar *config.CalendarSettings Stores *store.Stores Integrations integrations.Integrations } -func NewServiceParams(db *gorm.DB, validate *validator.Validate, auth *config.AuthSettings, jwt auth.JWTClientInterface, calendar *config.CalendarSettings, stores *store.Stores, integrations integrations.Integrations) ServiceParams { +func NewServiceParams(db *gorm.DB, validate *validator.Validate, calendar *config.CalendarSettings, stores *store.Stores, integrations integrations.Integrations) ServiceParams { return ServiceParams{ DB: db, Validate: validate, - Auth: auth, - JWT: jwt, Calendar: calendar, Stores: stores, Integrations: integrations, diff --git a/backend/utilities/api_error.go b/backend/utilities/api_error.go index 7d6e297e6..840a0e797 100644 --- a/backend/utilities/api_error.go +++ b/backend/utilities/api_error.go @@ -58,10 +58,6 @@ func InvalidJSON() APIError { return NewAPIError(http.StatusBadRequest, fmt.Errorf("invalid JSON request data")) } -func InvalidCookies() APIError { - return NewAPIError(http.StatusBadRequest, fmt.Errorf("invalid cookies")) -} - func Unauthorized() APIError { return NewAPIError(http.StatusUnauthorized, fmt.Errorf("unauthorized")) } diff --git a/backend/utilities/http.go b/backend/utilities/http.go index 48e3b919f..f7e5a4a03 100644 --- a/backend/utilities/http.go +++ b/backend/utilities/http.go @@ -23,6 +23,10 @@ func Request(method string, url string, body []byte, modifiers ...RequestModifie return httpClient.Do(req) } +func Client() *http.Client { + return httpClient +} + func IsOk(statusCode int) bool { return statusCode >= 200 && statusCode < 300 } @@ -48,10 +52,10 @@ func HeaderKV(key, value string) RequestModifier { } } -func Authorization(apiKey string) RequestModifier { +func Authorization(value string) RequestModifier { return HeaderKVModifier{ Key: "Authorization", - Value: fmt.Sprintf("Bearer %s", apiKey), + Value: fmt.Sprintf("Bearer %s", value), } } diff --git a/config/.env.template b/config/.env.template index 864f6a331..251cc9597 100644 --- a/config/.env.template +++ b/config/.env.template @@ -9,20 +9,17 @@ SAC_DB_HOST="127.0.0.1" SAC_DB_NAME="sac" SAC_DB_REQUIRE_SSL="false" -SAC_REDIS_ACTIVE_TOKENS_USERNAME="redis_active_tokens" -SAC_REDIS_ACTIVE_TOKENS_PASSWORD="redis_active_tokens!#1" -SAC_REDIS_ACTIVE_TOKENS_HOST="127.0.0.1" -SAC_REDIS_ACTIVE_TOKENS_PORT="6379" -SAC_REDIS_ACTIVE_TOKENS_DB="0" - -SAC_REDIS_BLACKLIST_USERNAME="redis_blacklist" -SAC_REDIS_BLACKLIST_PASSWORD="redis_blacklist!#2" -SAC_REDIS_BLACKLIST_HOST="127.0.0.1" -SAC_REDIS_BLACKLIST_PORT="6380" -SAC_REDIS_BLACKLIST_DB="0" +SAC_SESSION_SECRET="this is a super duper long secret 1234567890!@#$%^&*()" +SESSION_SECRET="this is a super duper long secret 1234567890!@#$%^&*()" + +SAC_REDIS_SESSION_USERNAME="redis_session" +SAC_REDIS_SESSION_PASSWORD="redis_session!#1" +SAC_REDIS_SESSION_HOST="127.0.0.1" +SAC_REDIS_SESSION_PORT="6380" +SAC_REDIS_SESSION_DB="0" SAC_REDIS_LIMITER_USERNAME="redis_limiter" -SAC_REDIS_LIMITER_PASSWORD="redis_limiter!#3" +SAC_REDIS_LIMITER_PASSWORD="redis_limiter!#1" SAC_REDIS_LIMITER_HOST="127.0.0.1" SAC_REDIS_LIMITER_PORT="6381" SAC_REDIS_LIMITER_DB="0" @@ -34,9 +31,6 @@ SAC_AWS_REGION="SAC_AWS_REGION" SAC_SUDO_PASSWORD="Password#!1" -SAC_AUTH_ACCESS_KEY="g(r|##*?>\Qp}h37e+,T2" -SAC_AUTH_REFRESH_KEY="amk*2!gG}1i\"8D9RwJS\$p" - SAC_AWS_BUCKET_NAME="SAC_AWS_BUCKET_NAME" SAC_AWS_ID="SAC_AWS_ID" SAC_AWS_SECRET="SAC_AWS_SECRET" @@ -46,13 +40,11 @@ SAC_RESEND_API_KEY="SAC_RESEND_API_KEY" SAC_CALENDAR_MAX_TERMINATION_DATE="12-31-2024" -SAC_GOOGLE_OAUTH_CLIENT_ID=GOOGLE_OAUTH_CLIENT_ID -SAC_GOOGLE_OAUTH_CLIENT_SECRET=GOOGLE_OAUTH_CLIENT_SECRET +SAC_GOOGLE_OAUTH_KEY=GOOGLE_OAUTH_CLIENT_ID +SAC_GOOGLE_OAUTH_SECRET=GOOGLE_OAUTH_CLIENT_SECRET SAC_GOOGLE_API_KEY=GOOGLE_API_KEY -SAC_GOOGLE_OAUTH_REDIRECT_URI="http://127.0.0.1:3000" -SAC_OUTLOOK_OAUTH_CLIENT_ID=test -SAC_OUTLOOK_OAUTH_CLIENT_SECRET=test -SAC_OUTLOOK_OAUTH_REDIRECT_URI="http://127.0.0.1:3000" +SAC_MICROSOFT_OAUTH_KEY=test +SAC_MICROSOFT_OAUTH_SECRET=test SAC_SEARCH_URI="http://127.0.0.1:9200" \ No newline at end of file diff --git a/go.work.sum b/go.work.sum index 5fb52f710..d98dc8c5b 100644 --- a/go.work.sum +++ b/go.work.sum @@ -29,10 +29,6 @@ cloud.google.com/go/cloudtasks v1.12.4/go.mod h1:BEPu0Gtt2dU6FxZHNqqNdGqIG86qyWK cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= cloud.google.com/go/compute v1.23.1/go.mod h1:CqB3xpmPKKt3OJpW2ndFIXnA9A4xAy/F3Xp1ixncW78= -cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= -cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/contactcenterinsights v1.11.3/go.mod h1:HHX5wrz5LHVAwfI2smIotQG9x8Qd6gYilaHcLLLmNis= cloud.google.com/go/container v1.27.1/go.mod h1:b1A1gJeTBXVLQ6GGw9/9M4FG94BEGsqJ5+t4d/3N7O4= cloud.google.com/go/containeranalysis v0.11.3/go.mod h1:kMeST7yWFQMGjiG9K7Eov+fPNQcGhb8mXj/UcTiWw9U= @@ -158,7 +154,6 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/ClickHouse/clickhouse-go v1.4.3 h1:iAFMa2UrQdR5bHJ2/yaSLffZkxpcOYQMCUuKeNXGdqc= github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= -github.com/GenerateNU/sac/backend v0.0.0-20240427140745-74eb4ec0f597/go.mod h1:VEuNTR6WQ0OQ5fjMjoRcIymCMjRLdRUM611roK9yQYQ= github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= @@ -172,6 +167,8 @@ github.com/a-h/pathvars v0.0.14/go.mod h1:7rLTtvDVyKneR/N65hC0lh2sZ2KRyAmWFaOvv0 github.com/a-h/protocol v0.0.0-20230224160810-b4eec67c1c22 h1:ehNdbGOAR8KTrLY/S90/9RJ4p/cgeNdt1sRt0DSiRWs= github.com/a-h/protocol v0.0.0-20230224160810-b4eec67c1c22/go.mod h1:Gm0KywveHnkiIhqFSMZglXwWZRQICg3KDWLYdglv/d8= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 h1:rFw4nCn9iMW+Vajsk51NtYIcwSTkXr+JGrMd36kTDJw= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/apache/arrow/go/v10 v10.0.1 h1:n9dERvixoC/1JjDmBcs9FPaEryoANa2sCgVFo6ez9cI= @@ -232,12 +229,12 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 h1:XNT/Zf5l++1Pyg08/HV04ppB0gKxAqtZQBRYiYrUuYk= github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY= github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= @@ -252,8 +249,6 @@ github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/form3tech-oss/jwt-go v3.2.5+incompatible h1:/l4kBbb4/vGSsdtB5nUe8L7B9mImVMaBPw9L/0TBHU8= github.com/form3tech-oss/jwt-go v3.2.5+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fsouza/fake-gcs-server v1.17.0 h1:OeH75kBZcZa3ZE+zz/mFdJ2btt9FgqfjI7gIh9+5fvk= github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= @@ -269,6 +264,8 @@ github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556 h1:N/MD/sr6o61X+iZBAT2 github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= @@ -278,8 +275,6 @@ github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EO github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= @@ -304,10 +299,17 @@ github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56 github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720 h1:zC34cGQu69FG7qzJ3WiKW244WfhDC3xxYMeNOX2gtUQ= github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.1.1 h1:YMDmfaK68mUixINzY/XjscuJ47uXFWSSHzFbBQM0PrE= +github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= @@ -335,11 +337,9 @@ github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY= github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= @@ -354,6 +354,7 @@ github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU= github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= +github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/k0kubun/pp v2.3.0+incompatible h1:EKhKbi34VQDWJtq+zpsKSEhkHHs9w2P8Izbq8IhLVSo= github.com/k0kubun/pp v2.3.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= @@ -368,16 +369,18 @@ github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBF github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/ktrysmt/go-bitbucket v0.6.4 h1:C8dUGp0qkwncKtAnozHCbbqhptefzEd1I0sfnuy9rYQ= github.com/ktrysmt/go-bitbucket v0.6.4/go.mod h1:9u0v3hsd2rqCHRIpbir1oP7F58uo5dq19sBYvuMoyQ4= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx v1.2.29/go.mod h1:hU8k2l6WF0ncx20uQdOmik/Gjg6E3/wIRtXSNFeZuB8= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lyft/protoc-gen-star/v2 v2.0.3/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/markbates/pkger v0.15.1 h1:3MPelV53RnGSW07izx5xGxl4e/sdRD6zqseIk0rMASY= github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -391,11 +394,11 @@ github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcs github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/mutecomm/go-sqlcipher/v4 v4.4.0 h1:sV1tWCWGAVlPhNGT95Q+z/txFxuhAYWwHD1afF5bMZg= @@ -419,7 +422,6 @@ github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vv github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU= github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pierrec/lz4/v4 v4.1.16 h1:kQPfno+wyx6C5572ABwV+Uo3pDFzQ7yhyGchSyRda0c= github.com/pierrec/lz4/v4 v4.1.16/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= @@ -439,13 +441,8 @@ github.com/rqlite/gorqlite v0.0.0-20230708021416-2acd02b70b79/go.mod h1:xF/KoXmr github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/crypt v0.17.0 h1:ZA/7pXyjkHoK4bW4mIdnCLvL8hd+Nrbiw7Dqk7D4qUk= github.com/sagikazarmark/crypt v0.17.0/go.mod h1:SMtHTvdmsZMuY/bpZoqokSoChIrcJ/epOxZN58PbZDg= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= -github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= @@ -458,27 +455,16 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5I github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= +github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= github.com/snowflakedb/gosnowflake v1.6.19 h1:KSHXrQ5o7uso25hNIzi/RObXtnSGkFgie91X82KcvMY= github.com/snowflakedb/gosnowflake v1.6.19/go.mod h1:FM1+PWUdwB9udFDsXdfD58NONC0m+MlOSmQRvimobSM= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04= github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E= github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= @@ -493,7 +479,6 @@ github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCO github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs= @@ -523,7 +508,6 @@ go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZu go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= @@ -546,38 +530,10 @@ golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= -golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= -golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= -golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= -golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= -golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= -golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= -golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= -golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= -golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= @@ -608,18 +564,8 @@ golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= -golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= @@ -634,8 +580,6 @@ google.golang.org/api v0.150.0/go.mod h1:ccy+MJ6nrYFgE3WgRx/AMXOxOmU8Q4hSa+jjibz google.golang.org/api v0.152.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY= google.golang.org/api v0.153.0 h1:N1AwGhielyKFaUqH07/ZSIQR3uNPcV7NVw0vj+j4iR4= google.golang.org/api v0.153.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b/go.mod h1:CgAqfJo+Xmu0GwA0411Ht3OU3OntXwsGmrmjI8ioGXI= google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405/go.mod h1:3WDQMjmJk36UQhjQ89emUzb1mdaHcPeeAh4SCBKznB4= @@ -658,11 +602,8 @@ google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= lukechampine.com/frand v1.4.2/go.mod h1:4S/TM2ZgrKejMcKMbeLjISpJMO+/eZ1zu3vYX9dtj3s= lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= From aa99835d5c675383b6051d7cf36c491a26830d8f Mon Sep 17 00:00:00 2001 From: Garrett Ladley <92384606+garrettladley@users.noreply.github.com> Date: Sun, 9 Jun 2024 21:49:06 -0400 Subject: [PATCH 2/6] fix: mock out auth flow in test (#980) --- backend/tests/api/helpers/auth.go | 42 +------------------------------ 1 file changed, 1 insertion(+), 41 deletions(-) diff --git a/backend/tests/api/helpers/auth.go b/backend/tests/api/helpers/auth.go index 897e094fe..32b32ce70 100644 --- a/backend/tests/api/helpers/auth.go +++ b/backend/tests/api/helpers/auth.go @@ -1,11 +1,7 @@ package helpers import ( - "fmt" - - "github.com/GenerateNU/sac/backend/database" "github.com/GenerateNU/sac/backend/entities/models" - "github.com/gofiber/fiber/v2" "github.com/google/uuid" ) @@ -25,43 +21,7 @@ func (app *TestApp) Auth(role models.UserRole) { } func (app *TestApp) authSuper() { - superUser, superUserErr := database.SuperUser(app.Settings.SuperUser) - if superUserErr != nil { - panic(fmt.Sprintf("Failed to get super user: %v", superUserErr)) - } - - email := superUser.Email - password := app.Settings.SuperUser.Password - - resp, err := app.Send(TestRequest{ - Method: fiber.MethodPost, - Path: "/api/v1/auth/login", - Body: &map[string]interface{}{ - "email": email, - "password": password.Expose(), - }, - }) - if err != nil { - panic(fmt.Sprintf("Failed to authenticate super user: %v", err)) - } - - var accessToken string - var refreshToken string - - authHeader := resp.Header.Get("Authorization") - if authHeader != "" { - accessToken = authHeader[len("Bearer "):] - } - - if accessToken == "" || refreshToken == "" { - panic("Failed to authenticate super user") - } - - app.TestUser = &TestUser{ - UUID: database.SuperUserUUID, - Email: email, - AccessToken: accessToken, - } + app.TestUser = nil } func (app *TestApp) authStudent() { From 9f568034f409e1e915ab7441d431d895e793c4c8 Mon Sep 17 00:00:00 2001 From: Garrett Ladley <92384606+garrettladley@users.noreply.github.com> Date: Sun, 9 Jun 2024 22:01:39 -0400 Subject: [PATCH 3/6] =?UTF-8?q?=F0=9F=A7=B9=20chore:=20fix=20failing=20tes?= =?UTF-8?q?ts=20(#982)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/config/settings.go | 2 + .../integrations/oauth/soth/sothic/sothic.go | 79 +------------------ backend/main.go | 2 + config/.env.template | 3 - 4 files changed, 8 insertions(+), 78 deletions(-) diff --git a/backend/config/settings.go b/backend/config/settings.go index 123a71592..d780f26c4 100644 --- a/backend/config/settings.go +++ b/backend/config/settings.go @@ -3,6 +3,7 @@ package config type Settings struct { Application ApplicationSettings Database DatabaseSettings + RedisSession RedisSettings RedisLimiter RedisSettings SuperUser SuperUserSettings Calendar CalendarSettings @@ -20,6 +21,7 @@ type Integrations struct { type intermediateSettings struct { Application ApplicationSettings `envPrefix:"SAC_APPLICATION_"` Database intermediateDatabaseSettings `envPrefix:"SAC_DB_"` + RedisSession intermediateRedisSettings `envPrefix:"SAC_REDIS_SESSION_"` RedisLimiter intermediateRedisSettings `envPrefix:"SAC_REDIS_LIMITER_"` SuperUser intermediateSuperUserSettings `envPrefix:"SAC_SUDO_"` AWS intermediateAWSSettings `envPrefix:"SAC_AWS_"` diff --git a/backend/integrations/oauth/soth/sothic/sothic.go b/backend/integrations/oauth/soth/sothic/sothic.go index 5cec4bd45..cc402a04e 100644 --- a/backend/integrations/oauth/soth/sothic/sothic.go +++ b/backend/integrations/oauth/soth/sothic/sothic.go @@ -9,17 +9,14 @@ import ( "fmt" "io" "net/url" - "path/filepath" "strings" + "github.com/GenerateNU/sac/backend/config" "github.com/GenerateNU/sac/backend/database/store" - "github.com/caarlos0/env/v11" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/session" - "github.com/joho/godotenv" "github.com/GenerateNU/sac/backend/integrations/oauth/soth" - m "github.com/garrettladley/mattress" ) type key int @@ -36,78 +33,10 @@ var ( SessionStore *session.Store ) -type storageSettings struct { - username string - password *m.Secret[string] - host string - port uint - db int - // TLSConfig *TLSConfig -} - -func (s *storageSettings) Username() string { - return s.username -} - -func (s *storageSettings) Password() *m.Secret[string] { - return s.password -} - -func (s *storageSettings) Host() string { - return s.host -} - -func (s *storageSettings) Port() uint { - return s.port -} - -func (s *storageSettings) DB() int { - return s.db -} - -type intermediateStorageSettings struct { - Username string `env:"SAC_REDIS_SESSION_USERNAME"` - Password string `env:"SAC_REDIS_SESSION_PASSWORD"` - Host string `env:"SAC_REDIS_SESSION_HOST"` - Port uint `env:"SAC_REDIS_SESSION_PORT"` - DB int `env:"SAC_REDIS_SESSION_DB"` - // TLSConfig *intermediateTLSConfig `env:"TLS_CONFIG"` -} - -func (i *intermediateStorageSettings) into() (*storageSettings, error) { - password, err := m.NewSecret(i.Password) - if err != nil { - return nil, fmt.Errorf("failed to create secret from password: %s", err.Error()) - } - - return &storageSettings{ - username: i.Username, - password: password, - host: i.Host, - port: i.Port, - db: i.DB, - // TLSConfig: i.TLSConfig.into(), - }, nil -} - -func init() { - if err := godotenv.Load(filepath.Join("..", "config", ".env.dev")); err != nil { - panic(fmt.Errorf("failed to load environment variables: %s", err.Error())) - } - - intSettings, err := env.ParseAs[intermediateStorageSettings]() - if err != nil { - panic(fmt.Errorf("failed to parse environment variables: %s", err.Error())) - } - - settings, err := intSettings.into() - if err != nil { - panic(fmt.Errorf("failed to convert environment variables: %s", err.Error())) - } - - // optional config +// MUST be called before using the package +func Init(sessionSettings config.RedisSettings) { config := session.Config{ - Storage: store.NewRedisClient(settings), + Storage: store.NewRedisClient(sessionSettings), KeyLookup: fmt.Sprintf("cookie:%s", SessionName), // for local CookieHTTPOnly: true, diff --git a/backend/main.go b/backend/main.go index b7c0771e6..aea4c5603 100644 --- a/backend/main.go +++ b/backend/main.go @@ -22,6 +22,7 @@ import ( "github.com/GenerateNU/sac/backend/integrations" "github.com/GenerateNU/sac/backend/integrations/email" "github.com/GenerateNU/sac/backend/integrations/file" + "github.com/GenerateNU/sac/backend/integrations/oauth/soth/sothic" "github.com/GenerateNU/sac/backend/server" "github.com/GenerateNU/sac/backend/telemetry" "github.com/GenerateNU/sac/backend/utilities" @@ -66,6 +67,7 @@ func main() { startBackgroundJobs(ctx, db) stores := store.ConfigureStores(config.RedisLimiter) + sothic.Init(config.RedisLimiter) integrations := configureIntegrations(&config.Integrations) tp := telemetry.InitTracer() diff --git a/config/.env.template b/config/.env.template index 251cc9597..b52428215 100644 --- a/config/.env.template +++ b/config/.env.template @@ -9,9 +9,6 @@ SAC_DB_HOST="127.0.0.1" SAC_DB_NAME="sac" SAC_DB_REQUIRE_SSL="false" -SAC_SESSION_SECRET="this is a super duper long secret 1234567890!@#$%^&*()" -SESSION_SECRET="this is a super duper long secret 1234567890!@#$%^&*()" - SAC_REDIS_SESSION_USERNAME="redis_session" SAC_REDIS_SESSION_PASSWORD="redis_session!#1" SAC_REDIS_SESSION_HOST="127.0.0.1" From 734d3644661f20b6cc91b1f2d905cdb83be416ac Mon Sep 17 00:00:00 2001 From: Garrett Ladley <92384606+garrettladley@users.noreply.github.com> Date: Sun, 9 Jun 2024 22:16:42 -0400 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=93=9D=20feat:=20set=20user=20in=20lo?= =?UTF-8?q?cals=20(#984)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/middleware/auth/auth.go | 3 +++ backend/middleware/auth/club.go | 3 +++ backend/middleware/auth/event.go | 2 ++ backend/middleware/auth/user.go | 3 +++ 4 files changed, 11 insertions(+) diff --git a/backend/middleware/auth/auth.go b/backend/middleware/auth/auth.go index 1d59b9a7b..6130a1652 100644 --- a/backend/middleware/auth/auth.go +++ b/backend/middleware/auth/auth.go @@ -4,6 +4,7 @@ import ( "slices" "time" + "github.com/GenerateNU/sac/backend/locals" "github.com/GenerateNU/sac/backend/permission" "github.com/GenerateNU/sac/backend/entities/models" @@ -33,6 +34,7 @@ func (m *AuthMiddlewareHandler) Authorize(requiredPermissions ...permission.Perm user := models.UnmarshalUser(strUser) if user.Role == models.Super { + locals.SetUser(c, user) return c.Next() } @@ -44,6 +46,7 @@ func (m *AuthMiddlewareHandler) Authorize(requiredPermissions ...permission.Perm } } + locals.SetUser(c, user) return c.Next() } } diff --git a/backend/middleware/auth/club.go b/backend/middleware/auth/club.go index f37b89cf4..54d42980f 100644 --- a/backend/middleware/auth/club.go +++ b/backend/middleware/auth/club.go @@ -7,6 +7,7 @@ import ( "github.com/GenerateNU/sac/backend/entities/clubs" "github.com/GenerateNU/sac/backend/entities/models" "github.com/GenerateNU/sac/backend/integrations/oauth/soth/sothic" + "github.com/GenerateNU/sac/backend/locals" "github.com/GenerateNU/sac/backend/utilities" "github.com/gofiber/fiber/v2" ) @@ -29,6 +30,7 @@ func (m *AuthMiddlewareHandler) ClubAuthorizeById(c *fiber.Ctx, extractor Extrac user := models.UnmarshalUser(strUser) if user.Role == models.Super { + locals.SetUser(c, user) return c.Next() } @@ -43,6 +45,7 @@ func (m *AuthMiddlewareHandler) ClubAuthorizeById(c *fiber.Ctx, extractor Extrac } if slices.Contains(clubAdmin, user.ID) { + locals.SetUser(c, user) return c.Next() } diff --git a/backend/middleware/auth/event.go b/backend/middleware/auth/event.go index 443c4b6ec..619454931 100644 --- a/backend/middleware/auth/event.go +++ b/backend/middleware/auth/event.go @@ -7,6 +7,7 @@ import ( "github.com/GenerateNU/sac/backend/entities/events" "github.com/GenerateNU/sac/backend/entities/models" "github.com/GenerateNU/sac/backend/integrations/oauth/soth/sothic" + "github.com/GenerateNU/sac/backend/locals" "github.com/GenerateNU/sac/backend/utilities" "github.com/gofiber/fiber/v2" @@ -41,6 +42,7 @@ func (m *AuthMiddlewareHandler) EventAuthorizeById(c *fiber.Ctx, extractor Extra } if slices.Contains(eventHostAdmin, user.ID) { + locals.SetUser(c, user) return c.Next() } diff --git a/backend/middleware/auth/user.go b/backend/middleware/auth/user.go index 38b65f33f..c9ab01076 100644 --- a/backend/middleware/auth/user.go +++ b/backend/middleware/auth/user.go @@ -5,6 +5,7 @@ import ( "github.com/GenerateNU/sac/backend/entities/models" "github.com/GenerateNU/sac/backend/integrations/oauth/soth/sothic" + "github.com/GenerateNU/sac/backend/locals" "github.com/GenerateNU/sac/backend/utilities" "github.com/gofiber/fiber/v2" ) @@ -27,6 +28,7 @@ func (m *AuthMiddlewareHandler) UserAuthorizeById(c *fiber.Ctx) error { user := models.UnmarshalUser(strUser) if user.Role == models.Super { + locals.SetUser(c, user) return c.Next() } @@ -36,6 +38,7 @@ func (m *AuthMiddlewareHandler) UserAuthorizeById(c *fiber.Ctx) error { } if idAsUUID == &user.ID { + locals.SetUser(c, user) return c.Next() } From 473c425f735f7053be16ea5c8cb535736e5a90f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Jun 2024 02:19:39 +0000 Subject: [PATCH 5/6] Chore(deps): Bump the backend group across 1 directory with 6 updates (#979) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Garrett Ladley <92384606+garrettladley@users.noreply.github.com> --- backend/go.mod | 20 ++++++--------- backend/go.sum | 69 ++++++++++++-------------------------------------- 2 files changed, 24 insertions(+), 65 deletions(-) diff --git a/backend/go.mod b/backend/go.mod index 27e8c9175..04ddbcb69 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -4,10 +4,10 @@ go 1.22.2 require ( github.com/a-h/templ v0.2.707 - github.com/aws/aws-sdk-go v1.53.10 + github.com/aws/aws-sdk-go v1.53.19 github.com/garrettladley/fiberpaginate v1.0.5 github.com/garrettladley/mattress v0.4.0 - github.com/go-playground/validator/v10 v10.20.0 + github.com/go-playground/validator/v10 v10.21.0 github.com/goccy/go-json v0.10.3 github.com/gofiber/fiber/v2 v2.52.4 github.com/gofiber/swagger v1.0.0 @@ -15,13 +15,13 @@ require ( github.com/huandu/go-assert v1.1.6 github.com/joho/godotenv v1.5.1 github.com/mcnijman/go-emailaddress v1.1.1 - github.com/redis/go-redis/v9 v9.5.1 + github.com/redis/go-redis/v9 v9.5.3 github.com/resend/resend-go/v2 v2.6.0 github.com/swaggo/swag v1.16.3 go.opentelemetry.io/otel/sdk v1.27.0 go.opentelemetry.io/otel/trace v1.27.0 - golang.org/x/oauth2 v0.17.0 - golang.org/x/text v0.15.0 + golang.org/x/oauth2 v0.21.0 + golang.org/x/text v0.16.0 gorm.io/driver/postgres v1.5.7 gorm.io/gorm v1.25.10 ) @@ -36,13 +36,11 @@ require ( ) require ( - cloud.google.com/go/compute v1.23.3 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect + cloud.google.com/go/compute/metadata v0.3.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/golang/protobuf v1.5.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect @@ -53,8 +51,6 @@ require ( go.opentelemetry.io/otel/metric v1.27.0 // indirect go.uber.org/atomic v1.11.0 // indirect golang.org/x/crypto v0.23.0 // indirect - google.golang.org/appengine v1.6.8 // indirect - google.golang.org/protobuf v1.33.0 // indirect ) require ( @@ -69,7 +65,7 @@ require ( github.com/go-openapi/swag v0.23.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 + github.com/go-viper/mapstructure/v2 v2.0.0 github.com/gofiber/contrib/otelfiber v1.0.10 github.com/golang-migrate/migrate/v4 v4.17.1 github.com/jackc/pgpassfile v1.0.0 // indirect @@ -94,6 +90,6 @@ require ( go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.27.0 golang.org/x/net v0.25.0 // indirect golang.org/x/sys v0.20.0 // indirect - golang.org/x/tools v0.20.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index 42e8d873a..2de951db9 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,7 +1,5 @@ -cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= -cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= @@ -16,8 +14,8 @@ github.com/awnumar/memcall v0.2.0 h1:sRaogqExTOOkkNwO9pzJsL8jrOV29UuUW7teRMfbqtI github.com/awnumar/memcall v0.2.0/go.mod h1:S911igBPR9CThzd/hYQQmTc9SWNu3ZHIlCGaWsWsoJo= github.com/awnumar/memguard v0.22.5 h1:PH7sbUVERS5DdXh3+mLo8FDcl1eIeVjJVYMnyuYpvuI= github.com/awnumar/memguard v0.22.5/go.mod h1:+APmZGThMBWjnMlKiSM1X7MVpbIVewen2MTkqWkA/zE= -github.com/aws/aws-sdk-go v1.53.10 h1:3enP5l5WtezT9Ql+XZqs56JBf5YUd/FEzTCg///OIGY= -github.com/aws/aws-sdk-go v1.53.10/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= +github.com/aws/aws-sdk-go v1.53.19 h1:WEuWc918RXlIaPCyU11F7hH9H1ItK+8m2c/uoQNRUok= +github.com/aws/aws-sdk-go v1.53.19/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= @@ -68,10 +66,10 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= -github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= -github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-playground/validator/v10 v10.21.0 h1:4fZA11ovvtkdgaeev9RGWPgc1uj3H8W+rNYyH/ySBb0= +github.com/go-playground/validator/v10 v10.21.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= +github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gofiber/contrib/otelfiber v1.0.10 h1:Bu28Pi4pfYmGfIc/9+sNaBbFwTHGY/zpSIK5jBxuRtM= @@ -85,11 +83,6 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4= github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -163,8 +156,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE 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/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= -github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU= +github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/resend/resend-go/v2 v2.6.0 h1:bHwF79iCYC3V9H7/DL0MAIoz0hiAqM+Rq9G4EhgooyE= github.com/resend/resend-go/v2 v2.6.0/go.mod h1:ihnxc7wPpSgans8RV8d8dIF4hYWVsqMK5KxXAr9LIos= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -190,7 +183,6 @@ github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7g github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/contrib v1.17.0 h1:lJJdtuNsP++XHD7tXDYEFSpsqIc7DzShuXMR5PwkmzA= go.opentelemetry.io/contrib v1.17.0/go.mod h1:gIzjwWFoGazJmtCaDgViqOSJPde2mCWzv60o0bWPcZs= go.opentelemetry.io/contrib/propagators/b3 v1.17.0 h1:ImOVvHnku8jijXqkwCSyYKRDt2YrnGXD4BbhcpfbfJo= @@ -211,59 +203,30 @@ go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5 go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= -golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From 8cdc0a02b3e589e52a4a5a6a8d2cde93f5eae38a Mon Sep 17 00:00:00 2001 From: Tyler Schaefer Date: Mon, 10 Jun 2024 13:56:05 -0400 Subject: [PATCH 6/6] Club page integration (#976) Co-authored-by: Garrett Ladley <92384606+garrettladley@users.noreply.github.com> --- .../app/(app)/(tabs)/mockClubPageData.ts | 257 ++++++++++++++++++ .../components/ClubPage/ClubPage.tsx | 172 ++++++++++++ .../components/ClubPage/ClubPageHeader.tsx | 23 ++ .../RecruitmentInfo/ClubRecruitmentInfo.tsx | 6 +- .../RecruitmentItem/ClubRecruitmentItem.tsx | 12 +- .../components/EventCard/EventCardList.tsx | 42 +++ .../PointOfContactCard/PointOfContactCard.tsx | 40 +++ .../PointofContactsList.tsx | 46 ++++ frontend/mobile/app/(design-system)/index.ts | 5 +- .../app/(design-system)/shared/spacing.ts | 2 + frontend/mobile/yarn.lock | 95 ++++++- 11 files changed, 680 insertions(+), 20 deletions(-) create mode 100644 frontend/mobile/app/(app)/(tabs)/mockClubPageData.ts create mode 100644 frontend/mobile/app/(design-system)/components/ClubPage/ClubPage.tsx create mode 100644 frontend/mobile/app/(design-system)/components/ClubPage/ClubPageHeader.tsx create mode 100644 frontend/mobile/app/(design-system)/components/EventCard/EventCardList.tsx create mode 100644 frontend/mobile/app/(design-system)/components/PointOfContactCard/PointOfContactCard.tsx create mode 100644 frontend/mobile/app/(design-system)/components/PointOfContactCard/PointofContactsList.tsx diff --git a/frontend/mobile/app/(app)/(tabs)/mockClubPageData.ts b/frontend/mobile/app/(app)/(tabs)/mockClubPageData.ts new file mode 100644 index 000000000..8e276f166 --- /dev/null +++ b/frontend/mobile/app/(app)/(tabs)/mockClubPageData.ts @@ -0,0 +1,257 @@ +import { Club, Contact, Event, PointOfContact, Tag } from '@generatesac/lib'; + +const mockClub: Club = { + id: '1', + name: 'Mock Club', + preview: 'This is a mock club for demonstration purposes.', + description: + 'A mock club that serves as an example for the ClubPage component.', + is_recruiting: true, + recruitment_cycle: 'fall', + recruitment_type: 'application', + application_link: 'https://example.com/apply', + num_members: 50, + created_at: new Date(), + updated_at: new Date(), + logo: 'https://example.com/logo.png', + weekly_time_committment: 5, + one_word_to_describe_us: 'Innovative' +}; + +const mockTags: Tag[] = [ + { + id: '1', + name: 'Technology', + category_id: '1', + created_at: new Date(), + updated_at: new Date() + }, + { + id: '2', + name: 'Community', + category_id: '2', + created_at: new Date(), + updated_at: new Date() + }, + { + id: '3', + name: 'Social', + category_id: '3', + created_at: new Date(), + updated_at: new Date() + }, + { + id: '4', + name: 'Professional', + category_id: '4', + created_at: new Date(), + updated_at: new Date() + }, + { + id: '5', + name: 'Academic', + category_id: '5', + created_at: new Date(), + updated_at: new Date() + } +]; + +const mockEvents: Event[] = [ + { + id: '1', + name: 'Mock Event 1', + preview: 'This is the first mock event.', + content: 'Details about the first mock event.', + start_time: new Date(), + end_time: new Date(), + location: 'Online', + event_type: 'open', + is_recurring: false, + created_at: new Date(), + updated_at: new Date(), + host: 'Mock Host 1', + meeting_link: 'https://example.com/meeting1' + }, + { + id: '2', + name: 'Mock Event 2', + preview: 'This is the second mock event.', + content: 'Details about the second mock event.', + start_time: new Date(), + end_time: new Date(), + location: 'Offline', + event_type: 'membersOnly', + is_recurring: true, + created_at: new Date(), + updated_at: new Date(), + host: 'Mock Host 2', + meeting_link: 'https://example.com/meeting2' + }, + { + id: '3', + name: 'Mock Event 3', + preview: 'This is the third mock event.', + content: 'Details about the third mock event.', + start_time: new Date(), + end_time: new Date(), + location: 'Online', + event_type: 'open', + is_recurring: false, + created_at: new Date(), + updated_at: new Date(), + host: 'Mock Host 3', + meeting_link: 'https://example.com/meeting3' + }, + { + id: '4', + name: 'Mock Event 4', + preview: 'This is the fourth mock event.', + content: 'Details about the fourth mock event.', + start_time: new Date(), + end_time: new Date(), + location: 'Offline', + event_type: 'membersOnly', + is_recurring: true, + created_at: new Date(), + updated_at: new Date(), + host: 'Mock Host 4', + meeting_link: 'https://example.com/meeting4' + }, + { + id: '5', + name: 'Mock Event 5', + preview: 'This is the fifth mock event.', + content: 'Details about the fifth mock event.', + start_time: new Date(), + end_time: new Date(), + location: 'Online', + event_type: 'open', + is_recurring: false, + created_at: new Date(), + updated_at: new Date(), + host: 'Mock Host 5', + meeting_link: 'https://example.com/meeting5' + } +]; + +const mockContacts: Contact[] = [ + { + id: '1', + type: 'email', + content: 'contact@example.com', + created_at: new Date(), + updated_at: new Date() + }, + { + id: '2', + type: 'discord', + content: 'https://discord.gg/example', + created_at: new Date(), + updated_at: new Date() + } +]; + +const pointOfContactMocks: PointOfContact[] = [ + { + name: 'Jane Smith', + email: 'jane.smith@example.com', + position: 'Director', + photo_file: { + owner_id: 'd4f5a6e7-8b9c-10d1-1121-314151617181', + owner_type: 'user', + file_name: 'photo1.jpg', + file_type: 'image/jpeg', + file_size: 2048, + file_url: '/photos/photo1.jpg', + object_key: 'photos/photo1.jpg', + id: '12345', + created_at: new Date('2023-05-01T00:00:00Z'), + updated_at: new Date('2023-05-02T00:00:00Z') + }, + id: '54321', + created_at: new Date('2023-05-01T00:00:00Z'), + updated_at: new Date('2023-05-02T00:00:00Z') + }, + { + name: 'John Doe', + email: 'john.doe@example.com', + position: 'Manager', + photo_file: { + owner_id: 'd4f5a6e7-8b9c-10d1-1121-314151617182', + owner_type: 'user', + file_name: 'photo2.jpg', + file_type: 'image/jpeg', + file_size: 1024, + file_url: '/photos/photo2.jpg', + object_key: 'photos/photo2.jpg', + id: '12346', + created_at: new Date('2023-05-01T00:00:00Z'), + updated_at: new Date('2023-05-02T00:00:00Z') + }, + id: '54322', + created_at: new Date('2023-05-01T00:00:00Z'), + updated_at: new Date('2023-05-02T00:00:00Z') + }, + { + name: 'Alice Johnson', + email: 'alice.johnson@example.com', + position: 'Team Lead', + photo_file: { + owner_id: 'd4f5a6e7-8b9c-10d1-1121-314151617183', + owner_type: 'user', + file_name: 'photo3.jpg', + file_type: 'image/jpeg', + file_size: 3072, + file_url: '/photos/photo3.jpg', + object_key: 'photos/photo3.jpg', + id: '12347', + created_at: new Date('2023-05-01T00:00:00Z'), + updated_at: new Date('2023-05-02T00:00:00Z') + }, + id: '54323', + created_at: new Date('2023-05-01T00:00:00Z'), + updated_at: new Date('2023-05-02T00:00:00Z') + }, + { + name: 'Bob Brown', + email: 'bob.brown@example.com', + position: 'Senior Developer', + photo_file: { + owner_id: 'd4f5a6e7-8b9c-10d1-1121-314151617184', + owner_type: 'user', + file_name: 'photo4.jpg', + file_type: 'image/jpeg', + file_size: 4096, + file_url: '/photos/photo4.jpg', + object_key: 'photos/photo4.jpg', + id: '12348', + created_at: new Date('2023-05-01T00:00:00Z'), + updated_at: new Date('2023-05-02T00:00:00Z') + }, + id: '54324', + created_at: new Date('2023-05-01T00:00:00Z'), + updated_at: new Date('2023-05-02T00:00:00Z') + }, + { + name: 'Clara White', + email: 'clara.white@example.com', + position: 'Product Manager', + photo_file: { + owner_id: 'd4f5a6e7-8b9c-10d1-1121-314151617185', + owner_type: 'user', + file_name: 'photo5.jpg', + file_type: 'image/jpeg', + file_size: 5120, + file_url: '/photos/photo5.jpg', + object_key: 'photos/photo5.jpg', + id: '12349', + created_at: new Date('2023-05-01T00:00:00Z'), + updated_at: new Date('2023-05-02T00:00:00Z') + }, + id: '54325', + created_at: new Date('2023-05-01T00:00:00Z'), + updated_at: new Date('2023-05-02T00:00:00Z') + } +]; + +export { mockClub, mockTags, mockEvents, mockContacts, pointOfContactMocks }; diff --git a/frontend/mobile/app/(design-system)/components/ClubPage/ClubPage.tsx b/frontend/mobile/app/(design-system)/components/ClubPage/ClubPage.tsx new file mode 100644 index 000000000..8997194aa --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/ClubPage/ClubPage.tsx @@ -0,0 +1,172 @@ +import { Dimensions } from 'react-native'; +import { Linking } from 'react-native'; +import Animated, { + interpolate, + useAnimatedRef, + useAnimatedStyle, + useScrollViewOffset +} from 'react-native-reanimated'; + +import { faExternalLink } from '@fortawesome/free-solid-svg-icons'; +import { + Club, + Contact, + Event, + PointOfContact as POCProps, + Tag as TagProps +} from '@generatesac/lib'; + +import { SACColors } from '../../shared/colors'; +import { Box } from '../Box/Box'; +import { Button } from '../Button/Button'; +import { ClubIcon } from '../ClubIcon/ClubIcon'; +import { RecruitmentInfo } from '../ClubRecruitment/RecruitmentInfo/ClubRecruitmentInfo'; +import { EventCard } from '../EventCard/EventCard'; +import { EventCardList } from '../EventCard/EventCardList'; +import { PointOfContactList } from '../PointOfContactCard/PointofContactsList'; +import { Tag } from '../Tag/Tag'; +import { Text } from '../Text/Text'; +import { AnimatedImageBox } from './ClubPageHeader'; + +interface ClubPageProps { + club: Club; + tags: TagProps[]; + events: Event[]; + contacts?: Contact[]; + color: SACColors; + pointOfContacts: POCProps[]; +} + +export const ClubPage: React.FC = ({ + club, + tags, + events, + color, + pointOfContacts +}) => { + const { width } = Dimensions.get('window'); + const IMG_HEIGHT = width; + + const scrollRef = useAnimatedRef(); + const scrollOffset = useScrollViewOffset(scrollRef); + + const imageAnimatedStyle = useAnimatedStyle(() => { + return { + transform: [ + { + translateY: interpolate( + scrollOffset.value, + [-IMG_HEIGHT, 0, IMG_HEIGHT], + [-IMG_HEIGHT / 2, 0, IMG_HEIGHT * 0.75] + ) + }, + { + scale: interpolate( + scrollOffset.value, + [-IMG_HEIGHT, 0, IMG_HEIGHT], + [2, 1, 1] + ) + } + ] + }; + }); + + return ( + + + + + + + + + + + {club.name} + + {tags.map((tag) => ( + + {tag.name} + + ))} + + + About Us + {club.description} + + + + + + Recruiting + + + + Upcoming Events + + + + + Leadership + + + + + ); +}; diff --git a/frontend/mobile/app/(design-system)/components/ClubPage/ClubPageHeader.tsx b/frontend/mobile/app/(design-system)/components/ClubPage/ClubPageHeader.tsx new file mode 100644 index 000000000..ba81006b2 --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/ClubPage/ClubPageHeader.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import Animated from 'react-native-reanimated'; + +import { Box } from '../Box/Box'; + +interface AnimatedImageBoxProps { + uri: string; + animatedStyle: any; +} + +export const AnimatedImageBox: React.FC = ({ + uri, + animatedStyle +}) => { + return ( + + + + ); +}; diff --git a/frontend/mobile/app/(design-system)/components/ClubRecruitment/RecruitmentInfo/ClubRecruitmentInfo.tsx b/frontend/mobile/app/(design-system)/components/ClubRecruitment/RecruitmentInfo/ClubRecruitmentInfo.tsx index 94ba83f68..32d191b15 100644 --- a/frontend/mobile/app/(design-system)/components/ClubRecruitment/RecruitmentInfo/ClubRecruitmentInfo.tsx +++ b/frontend/mobile/app/(design-system)/components/ClubRecruitment/RecruitmentInfo/ClubRecruitmentInfo.tsx @@ -5,8 +5,9 @@ import { faComments } from '@fortawesome/free-solid-svg-icons/faComments'; import { faPenToSquare } from '@fortawesome/free-solid-svg-icons/faPenToSquare'; import { RecruitmentCycle, RecruitmentType } from '@generatesac/lib'; -import { Box, SACColors, createStyles } from '@/app/(design-system)'; - +import { SACColors } from '../../../shared/colors'; +import { createStyles } from '../../../theme'; +import { Box } from '../../Box/Box'; import { RecruitmentItem } from '../RecruitmentItem/ClubRecruitmentItem'; interface RecruitmentInfoProps { @@ -54,7 +55,6 @@ export const RecruitmentInfo = ({ const styles = createStyles({ recruitmentInfo: { - padding: 'l', flexDirection: 'row', gap: 'm', alignItems: 'stretch', diff --git a/frontend/mobile/app/(design-system)/components/ClubRecruitment/RecruitmentItem/ClubRecruitmentItem.tsx b/frontend/mobile/app/(design-system)/components/ClubRecruitment/RecruitmentItem/ClubRecruitmentItem.tsx index a7eba1029..26317f5b9 100644 --- a/frontend/mobile/app/(design-system)/components/ClubRecruitment/RecruitmentItem/ClubRecruitmentItem.tsx +++ b/frontend/mobile/app/(design-system)/components/ClubRecruitment/RecruitmentItem/ClubRecruitmentItem.tsx @@ -1,9 +1,12 @@ import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; -import { Box, SACColors, Text, createStyles } from '@/app/(design-system)'; import { firstLetterUppercase } from '@/utils/string'; +import { SACColors } from '../../../shared/colors'; +import { createStyles } from '../../../theme'; +import { Box } from '../../Box/Box'; import { Icon } from '../../Icon/Icon'; +import { Text } from '../../Text/Text'; interface RecruitmentItemProps { icon: IconDefinition; @@ -22,7 +25,7 @@ export const RecruitmentItem = ({ - + {firstLetterUppercase(title)} @@ -36,10 +39,9 @@ export const RecruitmentItem = ({ const styles = createStyles({ recruitmentItem: { alignItems: 'center', - borderWidth: 1, borderRadius: 'sm', - borderColor: 'gray', - width: '33%' + width: '33%', + padding: 's' }, recruitmentItemContent: { paddingVertical: 'l', diff --git a/frontend/mobile/app/(design-system)/components/EventCard/EventCardList.tsx b/frontend/mobile/app/(design-system)/components/EventCard/EventCardList.tsx new file mode 100644 index 000000000..0c98151f5 --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/EventCard/EventCardList.tsx @@ -0,0 +1,42 @@ +import { FlatList, Pressable } from 'react-native'; + +import { router } from 'expo-router'; + +import { Event } from '@generatesac/lib'; + +import { EventCard } from './EventCard'; + +interface EventCardListProps { + events: Event[]; +} + +export const EventCardList: React.FC = ({ events }) => { + const renderEventCard = ({ item }: { item: Event }) => { + return ( + router.push(`/event/${item.id}`)}> + + + ); + }; + return ( + item.id} + /> + ); +}; diff --git a/frontend/mobile/app/(design-system)/components/PointOfContactCard/PointOfContactCard.tsx b/frontend/mobile/app/(design-system)/components/PointOfContactCard/PointOfContactCard.tsx new file mode 100644 index 000000000..6f19e3f5f --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/PointOfContactCard/PointOfContactCard.tsx @@ -0,0 +1,40 @@ +import React from 'react'; + +import { Avatar } from '@rneui/themed'; + +import { Colors, SACColors } from '../../shared/colors'; +import { Box } from '../Box/Box'; +import { Text } from '../Text/Text'; + +interface PointOfContactProps { + imageUrl?: string; + name: string; + position: string; + color: SACColors; +} + +export const PointOfContact: React.FC = ({ + name, + position, + color +}) => { + return ( + + + + + {name} + + {position} + + + ); +}; diff --git a/frontend/mobile/app/(design-system)/components/PointOfContactCard/PointofContactsList.tsx b/frontend/mobile/app/(design-system)/components/PointOfContactCard/PointofContactsList.tsx new file mode 100644 index 000000000..7d7b0394f --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/PointOfContactCard/PointofContactsList.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { FlatList, Pressable } from 'react-native'; + +import { router } from 'expo-router'; + +import { PointOfContact } from '@generatesac/lib'; + +import { SACColors } from '../../shared/colors'; +import { PointOfContact as PointOfContactComponent } from './PointOfContactCard'; + +interface PointOfContactListProps { + contacts: PointOfContact[]; + color: SACColors; +} + +export const PointOfContactList: React.FC = ({ + contacts, + color +}) => { + const renderPointOfContact = ({ item }: { item: PointOfContact }) => { + return ( + router.push(`/contact/${item.id}`)}> + + + ); + }; + + return ( + item.id} + /> + ); +}; diff --git a/frontend/mobile/app/(design-system)/index.ts b/frontend/mobile/app/(design-system)/index.ts index e778b5290..c45d74daa 100644 --- a/frontend/mobile/app/(design-system)/index.ts +++ b/frontend/mobile/app/(design-system)/index.ts @@ -4,8 +4,11 @@ export * from './components/Box/Box'; export * from './theme'; export * from './shared/colors'; export * from './shared/spacing'; -export * from './components/Tag/Tag'; export * from './components/Arrow/Arrow'; +export * from './components/Button/Button'; +export * from './components/ClubRecruitment/RecruitmentInfo/ClubRecruitmentInfo'; export * from './components/Kebab/Kebab'; export * from './components/Dropdown/SelectOne'; export * from './components/Dropdown/Multiselect'; +export * from './components/PointOfContactCard/PointOfContactCard'; +export * from './components/Tag/Tag'; diff --git a/frontend/mobile/app/(design-system)/shared/spacing.ts b/frontend/mobile/app/(design-system)/shared/spacing.ts index 278816813..688f279f1 100644 --- a/frontend/mobile/app/(design-system)/shared/spacing.ts +++ b/frontend/mobile/app/(design-system)/shared/spacing.ts @@ -1,5 +1,7 @@ export const Spacing = { + negativeXl: -50, none: 0, + xxxs: 2, xxs: 4, xs: 8, s: 12, diff --git a/frontend/mobile/yarn.lock b/frontend/mobile/yarn.lock index 7d275ecfe..ac672100d 100644 --- a/frontend/mobile/yarn.lock +++ b/frontend/mobile/yarn.lock @@ -1107,7 +1107,7 @@ resolved "https://registry.npmjs.org/@expo/config-types/-/config-types-51.0.0.tgz" integrity sha512-acn03/u8mQvBhdTQtA7CNhevMltUhbSrpI01FYBJwpVntufkU++ncQujWKlgY/OwIajcfygk1AY4xcNZ5ImkRA== -"@expo/config@9.0.1", "@expo/config@~9.0.0", "@expo/config@~9.0.0-beta.0": +"@expo/config@9.0.1": version "9.0.1" resolved "https://registry.npmjs.org/@expo/config/-/config-9.0.1.tgz" integrity sha512-0tjaXBstTbXmD4z+UMFBkh2SZFwilizSQhW6DlaTMnPG5ezuw93zSFEWAuEC3YzkpVtNQTmYzxAYjxwh6seOGg== @@ -1124,6 +1124,23 @@ slugify "^1.3.4" sucrase "3.34.0" +"@expo/config@~9.0.0", "@expo/config@~9.0.0-beta.0": + version "9.0.2" + resolved "https://registry.yarnpkg.com/@expo/config/-/config-9.0.2.tgz#112b93436dbca8aa3da73a46329e5b58fdd435d2" + integrity sha512-BKQ4/qBf3OLT8hHp5kjObk2vxwoRQ1yYQBbG/OM9Jdz32yYtrU8opTbKRAxfZEWH5i3ZHdLrPdC1rO0I6WxtTw== + dependencies: + "@babel/code-frame" "~7.10.4" + "@expo/config-plugins" "~8.0.0" + "@expo/config-types" "^51.0.0-unreleased" + "@expo/json-file" "^8.3.0" + getenv "^1.0.0" + glob "7.1.6" + require-from-string "^2.0.2" + resolve-from "^5.0.0" + semver "^7.6.0" + slugify "^1.3.4" + sucrase "3.34.0" + "@expo/devcert@^1.0.0": version "1.1.2" resolved "https://registry.npmjs.org/@expo/devcert/-/devcert-1.1.2.tgz" @@ -1179,7 +1196,7 @@ json5 "^2.2.2" write-file-atomic "^2.3.0" -"@expo/metro-config@0.18.3", "@expo/metro-config@~0.18.0": +"@expo/metro-config@0.18.3": version "0.18.3" resolved "https://registry.npmjs.org/@expo/metro-config/-/metro-config-0.18.3.tgz" integrity sha512-E4iW+VT/xHPPv+t68dViOsW7egtGIr+sRElcym0iGpC4goLz9WBux/xGzWgxvgvvHEWa21uSZQPM0jWla0OZXg== @@ -1203,6 +1220,30 @@ postcss "~8.4.32" resolve-from "^5.0.0" +"@expo/metro-config@~0.18.0": + version "0.18.4" + resolved "https://registry.yarnpkg.com/@expo/metro-config/-/metro-config-0.18.4.tgz#bc298e21637a3007f3c31c238525d3bef17e823b" + integrity sha512-vh9WDf/SzE+NYCn6gqbzLKiXtENFlFZdAqyj9nI38RvQ4jw6TJIQ8+ExcdLDT3MOG36Ytg44XX9Zb3OWF6LVxw== + dependencies: + "@babel/core" "^7.20.0" + "@babel/generator" "^7.20.5" + "@babel/parser" "^7.20.0" + "@babel/types" "^7.20.0" + "@expo/config" "~9.0.0" + "@expo/env" "~0.3.0" + "@expo/json-file" "~8.3.0" + "@expo/spawn-async" "^1.7.2" + chalk "^4.1.0" + debug "^4.3.2" + find-yarn-workspace-root "~2.0.0" + fs-extra "^9.1.0" + getenv "^1.0.0" + glob "^7.2.3" + jsc-safe-url "^0.2.4" + lightningcss "~1.19.0" + postcss "~8.4.32" + resolve-from "^5.0.0" + "@expo/metro-runtime@3.2.1", "@expo/metro-runtime@~3.2.1": version "3.2.1" resolved "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-3.2.1.tgz" @@ -2783,7 +2824,14 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@^18.0.0": +"@types/node@*": + version "20.12.12" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.12.tgz#7cbecdf902085cec634fdb362172dfe12b8f2050" + integrity sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw== + dependencies: + undici-types "~5.26.4" + +"@types/node@^18.0.0": version "18.19.33" resolved "https://registry.npmjs.org/@types/node/-/node-18.19.33.tgz" integrity sha512-NR9+KrpSajr2qBVp/Yt5TU/rp+b5Mayi3+OlMlcg2cVCfRmcG5PWZ7S4+MG9PZ5gWBoc9Pd0BKSRViuBCRPu0A== @@ -5110,18 +5158,18 @@ expo@~51.0.2: integrity sha512-USckGusi4389X/mFy8X8tEA59vgsc5joOILQrTsDPdDJawkUidDAVqY4NIjJxOY7zEq1SfPv/LglJ4cOdtvfsg== dependencies: "@babel/runtime" "^7.20.0" - "@expo/cli" "0.18.14" - "@expo/config" "9.0.1" + "@expo/cli" "0.18.13" + "@expo/config" "9.0.2" "@expo/config-plugins" "8.0.4" - "@expo/metro-config" "0.18.3" + "@expo/metro-config" "0.18.4" "@expo/vector-icons" "^14.0.0" babel-preset-expo "~11.0.6" expo-asset "~10.0.6" expo-file-system "~17.0.1" - expo-font "~12.0.6" + expo-font "~12.0.5" expo-keep-awake "~13.0.2" expo-modules-autolinking "1.11.1" - expo-modules-core "1.12.12" + expo-modules-core "1.12.11" fbemitter "^3.0.0" whatwg-url-without-unicode "8.0.0-3" @@ -10265,7 +10313,16 @@ string-natural-compare@^3.0.1: resolved "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -10344,7 +10401,7 @@ stringify-entities@^4.0.0: character-entities-html4 "^2.0.0" character-entities-legacy "^3.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -10358,6 +10415,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" @@ -11372,7 +11436,7 @@ word-wrap@^1.2.5: resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -11390,6 +11454,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"