diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml
index fa822391f..4dd4ad7f8 100644
--- a/.github/workflows/backend.yml
+++ b/.github/workflows/backend.yml
@@ -6,11 +6,13 @@ on:
push:
paths:
- backend/**
+ - config/**
- .github/workflows/backend.yml
pull_request:
types: [opened]
paths:
- backend/**
+ - config/**
- .github/workflows/backend.yml
concurrency:
diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml
index db08cf75c..6911047dc 100644
--- a/.github/workflows/cli.yml
+++ b/.github/workflows/cli.yml
@@ -6,11 +6,13 @@ on:
push:
paths:
- cli/**
+ - config/**
- .github/workflows/cli.yml
pull_request:
types: [opened]
paths:
- cli/**
+ - config/**
- .github/workflows/cli.yml
concurrency:
diff --git a/.github/workflows/club_scraper.yml b/.github/workflows/mock_data.yml
similarity index 92%
rename from .github/workflows/club_scraper.yml
rename to .github/workflows/mock_data.yml
index e3b6877bb..0f1732add 100644
--- a/.github/workflows/club_scraper.yml
+++ b/.github/workflows/mock_data.yml
@@ -1,17 +1,17 @@
-name: Club Scraper
+name: Mock Data
permissions: read-all
on:
push:
paths:
- - scraper/club/**
- - .github/workflows/club_scraper.yml
+ - mock_data/**
+ - .github/workflows/mock_data.yml
pull_request:
types: [opened]
paths:
- - scraper/club/**
- - .github/workflows/club_scraper.yml
+ - mock_data/**
+ - .github/workflows/mock_data.yml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -19,7 +19,7 @@ concurrency:
env:
CARGO_TERM_COLOR: always
- MANIFEST_PATH: ./scraper/clubs/Cargo.toml
+ MANIFEST_PATH: ./mock_data/Cargo.toml
jobs:
test:
diff --git a/.gitignore b/.gitignore
index 2142bdd9d..c2fd14ed3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,7 @@
.DS_Store
# Cli
-sac-cli
+sac
# VSCode
.vscode
@@ -22,3 +22,5 @@ backend/tests/api/__debug_*
frontend/sac-mobile/ios/
frontend/sac-mobile/android/
tmp/
+ios
+android
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f935c8414..c5bbfbdd0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -131,7 +131,7 @@
### SAC CLI
- To install use `./install.sh` and then run `sac-cli` to see all commands.
+ To install use `./install.sh` and then run `sac` to see all commands.
# Git Flow
diff --git a/backend/go.mod b/backend/go.mod
index 21f721d1c..f867c3506 100644
--- a/backend/go.mod
+++ b/backend/go.mod
@@ -4,7 +4,7 @@ go 1.22.2
require (
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5
- github.com/aws/aws-sdk-go v1.50.5
+ github.com/aws/aws-sdk-go v1.51.21
github.com/garrettladley/mattress v0.4.0
github.com/go-playground/validator/v10 v10.19.0
github.com/goccy/go-json v0.10.2
diff --git a/backend/go.sum b/backend/go.sum
index a7976b02e..cdbd3120f 100644
--- a/backend/go.sum
+++ b/backend/go.sum
@@ -8,8 +8,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.50.5 h1:H2Aadcgwr7a2aqS6ZwcE+l1mA6ZrTseYCvjw2QLmxIA=
-github.com/aws/aws-sdk-go v1.50.5/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
+github.com/aws/aws-sdk-go v1.51.21 h1:UrT6JC9R9PkYYXDZBV0qDKTualMr+bfK2eboTknMgbs=
+github.com/aws/aws-sdk-go v1.51.21/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
diff --git a/backend/src/auth/password.go b/backend/src/auth/password.go
new file mode 100644
index 000000000..f1e6a9412
--- /dev/null
+++ b/backend/src/auth/password.go
@@ -0,0 +1,39 @@
+package auth
+
+import (
+ "regexp"
+ "strings"
+
+ "github.com/GenerateNU/sac/backend/src/constants"
+ "github.com/GenerateNU/sac/backend/src/errors"
+)
+
+func ValidatePassword(password string) *errors.Error {
+ if len(password) < 8 {
+ return &errors.InvalidPasswordNotLongEnough
+ }
+
+ if !hasDigit(password) {
+ return &errors.InvalidPasswordNoDigit
+ }
+
+ if !hasSpecialChar(password) {
+ return &errors.InvalidPasswordNoSpecialCharacter
+ }
+
+ 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/src/constants/auth.go b/backend/src/constants/auth.go
index 236691482..9cd14511f 100644
--- a/backend/src/constants/auth.go
+++ b/backend/src/constants/auth.go
@@ -3,6 +3,8 @@ package constants
import "time"
const (
- ACCESS_TOKEN_EXPIRY time.Duration = time.Hour * 24 * 30 // temporary TODO: change to 60 minutes
- REFRESH_TOKEN_EXPIRY time.Duration = time.Hour * 24 * 30
+ ACCESS_TOKEN_EXPIRY time.Duration = time.Minute * 24 * 30 // temporary TODO: change to 60 minutes
+ REFRESH_TOKEN_EXPIRY time.Duration = time.Minute * 24 * 30
)
+
+var SPECIAL_CHARACTERS = []rune{' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_', '`', '{', '|', '}', '~'} // see https://owasp.org/www-community/password-special-characters
diff --git a/backend/src/controllers/tag.go b/backend/src/controllers/tag.go
index 665a22aab..39c6cd843 100644
--- a/backend/src/controllers/tag.go
+++ b/backend/src/controllers/tag.go
@@ -1,7 +1,6 @@
package controllers
import (
- "github.com/GenerateNU/sac/backend/src/constants"
"github.com/GenerateNU/sac/backend/src/errors"
"github.com/GenerateNU/sac/backend/src/models"
"github.com/GenerateNU/sac/backend/src/services"
@@ -32,7 +31,7 @@ func NewTagController(tagService services.TagServiceInterface) *TagController {
// @Failure 500 {object} errors.Error
// @Router /tags [get]
func (t *TagController) GetTags(c *fiber.Ctx) error {
- tags, err := t.tagService.GetTags(c.Query("limit", constants.DEFAULT_LIMIT_STRING), c.Query("page", constants.DEFAULT_PAGE_STRING))
+ tags, err := t.tagService.GetTags()
if err != nil {
return err.FiberError(c)
}
diff --git a/backend/src/database/super.go b/backend/src/database/super.go
index 9d357ba24..7cc9c2b8d 100644
--- a/backend/src/database/super.go
+++ b/backend/src/database/super.go
@@ -22,6 +22,7 @@ func SuperUser(superUserSettings config.SuperUserSettings) (*models.User, *error
PasswordHash: *passwordHash,
FirstName: "SAC",
LastName: "Super",
+ Major0: models.ComputerScience,
College: models.KCCS,
GraduationCycle: models.May,
GraduationYear: 2025,
diff --git a/backend/src/errors/auth.go b/backend/src/errors/auth.go
index 57a6cc2d7..83b3c46c4 100644
--- a/backend/src/errors/auth.go
+++ b/backend/src/errors/auth.go
@@ -99,4 +99,16 @@ var (
StatusCode: fiber.StatusNotFound,
Message: "otp not found",
}
+ InvalidPasswordNotLongEnough = Error{
+ StatusCode: fiber.StatusBadRequest,
+ Message: "password must be at least 8 characters long",
+ }
+ InvalidPasswordNoDigit = Error{
+ StatusCode: fiber.StatusBadRequest,
+ Message: "password must contain at least one digit",
+ }
+ InvalidPasswordNoSpecialCharacter = Error{
+ StatusCode: fiber.StatusBadRequest,
+ Message: "password must contain at least one special character",
+ }
)
diff --git a/backend/src/file/file.go b/backend/src/file/file.go
index 9314ffce5..9e292dbe1 100644
--- a/backend/src/file/file.go
+++ b/backend/src/file/file.go
@@ -128,7 +128,6 @@ func (aw *AWSClient) UploadFile(folder string, fileHeader *multipart.FileHeader,
Body: bytes.NewReader(file),
})
if s3Err != nil {
- fmt.Printf("Failed to upload data to %s/%s, %v\n", bucket, key, err)
return nil, &errors.FailedToUploadFile
}
diff --git a/backend/src/models/category.go b/backend/src/models/category.go
index 6f9770557..a6d757d10 100644
--- a/backend/src/models/category.go
+++ b/backend/src/models/category.go
@@ -3,7 +3,7 @@ package models
type Category struct {
Model
- Name string `gorm:"type:varchar(255);unique" json:"name" validate:"required,max=255"`
+ Name string `gorm:"type:varchar(255);unique;not null" json:"name" validate:"required,max=255"`
Tag []Tag `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
}
diff --git a/backend/src/models/club.go b/backend/src/models/club.go
index 23c931720..2e4b31918 100644
--- a/backend/src/models/club.go
+++ b/backend/src/models/club.go
@@ -30,13 +30,13 @@ type Club struct {
SoftDeletedAt gorm.DeletedAt `gorm:"type:timestamptz;default:NULL" json:"-" validate:"-"`
- Name string `gorm:"type:varchar(255)" json:"name" validate:"required,max=255"`
- Preview string `gorm:"type:varchar(255)" json:"preview" validate:"required,max=255"`
- Description string `gorm:"type:text" json:"description" validate:"required,http_url,s3_url,max=255"` // S3 URL
- NumMembers int `gorm:"type:int" json:"num_members" validate:"required,min=1"`
- IsRecruiting bool `gorm:"type:bool;default:false" json:"is_recruiting" validate:"required"`
- RecruitmentCycle RecruitmentCycle `gorm:"type:varchar(255);default:always" json:"recruitment_cycle" validate:"required,max=255,oneof=fall spring fallSpring always"`
- RecruitmentType RecruitmentType `gorm:"type:varchar(255);default:unrestricted" json:"recruitment_type" validate:"required,max=255,oneof=unrestricted tryout application"`
+ Name string `gorm:"type:varchar(255);not null" json:"name" validate:"required,max=255"`
+ Preview string `gorm:"type:varchar(255);not null" json:"preview" validate:"required,max=255"`
+ Description string `gorm:"type:text;not null" json:"description" validate:"required,http_url,s3_url,max=255"` // S3 URL
+ NumMembers int `gorm:"type:int;not null" json:"num_members" validate:"required,min=1"`
+ IsRecruiting bool `gorm:"type:bool;default:false;not null" json:"is_recruiting" validate:"required"`
+ RecruitmentCycle RecruitmentCycle `gorm:"type:varchar(255);default:always;not null" json:"recruitment_cycle" validate:"required,max=255,oneof=fall spring fallSpring always"`
+ RecruitmentType RecruitmentType `gorm:"type:varchar(255);default:unrestricted;not null" json:"recruitment_type" validate:"required,max=255,oneof=unrestricted tryout application"`
ApplicationLink string `gorm:"type:varchar(255);default:NULL" json:"application_link" validate:"required,max=255,http_url"`
Logo string `gorm:"type:varchar(255);default:NULL" json:"logo" validate:"omitempty,http_url,s3_url,max=255"` // S3 URL
diff --git a/backend/src/models/comment.go b/backend/src/models/comment.go
index 9e2bb9eb1..30448d0fd 100644
--- a/backend/src/models/comment.go
+++ b/backend/src/models/comment.go
@@ -5,15 +5,15 @@ import "github.com/google/uuid"
type Comment struct {
Model
- Question string `gorm:"type:varchar(255)" json:"question" validate:"required,max=255"`
- Answer string `gorm:"type:varchar(255)" json:"answer" validate:",max=255"`
- NumFoundHelpful uint `gorm:"type:int;default:0" json:"num_found_helpful" validate:"min=0"`
+ Question string `gorm:"type:varchar(255);not null" json:"question" validate:"required,max=255"`
+ Answer string `gorm:"type:varchar(255);not null" json:"answer" validate:",max=255"`
+ NumFoundHelpful uint `gorm:"type:int;default:0;not null" json:"num_found_helpful" validate:"min=0"`
- AskedByID uuid.UUID `gorm:"type:uuid" json:"-" validate:"uuid4"`
- AskedBy User `gorm:"foreignKey:AskedByID" json:"-" validate:"-"`
+ AskedByID uuid.UUID `gorm:"type:uuid;not null" json:"-" validate:"uuid4"`
+ AskedBy User `gorm:"foreignKey:AskedByID;not null" json:"-" validate:"-"`
- ClubID uuid.UUID `gorm:"type:uuid" json:"-" validate:"uuid4"`
- Club Club `gorm:"foreignKey:ClubID" json:"-" validate:"-"`
+ ClubID uuid.UUID `gorm:"type:uuid;not null" json:"-" validate:"uuid4"`
+ Club Club `gorm:"foreignKey:ClubID;not null" json:"-" validate:"-"`
AnsweredByID *uuid.UUID `gorm:"type:uuid" json:"-" validate:"uuid4"`
AnsweredBy *User `gorm:"foreignKey:AnsweredBy" json:"-" validate:"-"`
diff --git a/backend/src/models/contact.go b/backend/src/models/contact.go
index 25993f43c..dc5280332 100644
--- a/backend/src/models/contact.go
+++ b/backend/src/models/contact.go
@@ -43,10 +43,10 @@ func GetContentPrefix(contactType ContactType) string {
type Contact struct {
Model
- Type ContactType `gorm:"type:varchar(255);uniqueIndex:idx_contact_type" json:"type" validate:"required,max=255,oneof=facebook instagram x linkedin youtube github slack discord email customSite"`
- Content string `gorm:"type:varchar(255)" json:"content" validate:"required,max=255"`
+ Type ContactType `gorm:"type:varchar(255);uniqueIndex:idx_contact_type;not null" json:"type" validate:"required,max=255,oneof=facebook instagram x linkedin youtube github slack discord email customSite"`
+ Content string `gorm:"type:varchar(255);not null" json:"content" validate:"required,max=255"`
- ClubID uuid.UUID `gorm:"foreignKey:ClubID;uniqueIndex:idx_contact_type" json:"-" validate:"uuid4"`
+ ClubID uuid.UUID `gorm:"foreignKey:ClubID;uniqueIndex:idx_contact_type;not null" json:"-" validate:"uuid4"`
}
type PutContactRequestBody struct {
diff --git a/backend/src/models/event.go b/backend/src/models/event.go
index 22538e154..3ec9c3db9 100644
--- a/backend/src/models/event.go
+++ b/backend/src/models/event.go
@@ -24,51 +24,46 @@ const (
type Event struct {
Model
- Name string `gorm:"type:varchar(255)" json:"name" validate:"required,max=255"`
- Preview string `gorm:"type:varchar(255)" json:"preview" validate:"required,max=255"`
- Content string `gorm:"type:varchar(255)" json:"content" validate:"required,max=255"`
- StartTime time.Time `gorm:"type:timestamptz" json:"start_time" validate:"required,ltecsfield=EndTime"`
- EndTime time.Time `gorm:"type:timestamptz" json:"end_time" validate:"required,gtecsfield=StartTime"`
- Location string `gorm:"type:varchar(255)" json:"location" validate:"required,max=255"`
- EventType EventType `gorm:"type:varchar(255);default:open" json:"event_type" validate:"required,max=255,oneof=open membersOnly"`
- IsRecurring bool `gorm:"not null;type:bool;default:false" json:"is_recurring" validate:"-"`
-
- Host *uuid.UUID `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"uuid4"`
+ Name string `gorm:"type:varchar(255);not null" json:"name" validate:"required,max=255"`
+ Preview string `gorm:"type:varchar(255);not null" json:"preview" validate:"required,max=255"`
+ Content string `gorm:"type:varchar(255);not null" json:"content" validate:"required,max=255"`
+ StartTime time.Time `gorm:"type:timestamptz;not null" json:"start_time" validate:"required,ltecsfield=EndTime"`
+ EndTime time.Time `gorm:"type:timestamptz;not null" json:"end_time" validate:"required,gtecsfield=StartTime"`
+ Location string `gorm:"type:varchar(255);not null" json:"location" validate:"required,max=255"`
+ EventType EventType `gorm:"type:varchar(255);default:open;not null" json:"event_type" validate:"required,max=255,oneof=open membersOnly"`
+ IsRecurring bool `gorm:"type:bool;default:false;not null" json:"is_recurring" validate:"-"`
+
+ Host *uuid.UUID `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;not null;" json:"-" validate:"uuid4"`
RSVP []User `gorm:"many2many:user_event_rsvps;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
Waitlist []User `gorm:"many2many:user_event_waitlists;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
Clubs []Club `gorm:"many2many:club_events;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
Tag []Tag `gorm:"many2many:event_tags;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
- Notification []Notification `gorm:"polymorphic:Reference;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;;" json:"-" validate:"-"`
+ Notification []Notification `gorm:"polymorphic:Reference;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
}
type Series struct {
Model
- RecurringType RecurringType `gorm:"type:varchar(255);default:open" json:"recurring_type" validate:"max=255"`
- SeparationCount int `gorm:"type:int" json:"separation_count" validate:"min=0"`
- MaxOccurrences int `gorm:"type:int" json:"max_occurrences" validate:"min=1"`
- DayOfWeek int `gorm:"type:int" json:"day_of_week" validate:"min=1,max=7"`
- WeekOfMonth int `gorm:"type:int" json:"week_of_month" validate:"min=1,max=5"`
- DayOfMonth int `gorm:"type:int" json:"day_of_month" validate:"min=1,max=31"`
- Events []Event `gorm:"many2many:event_series;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"events" validate:"-"`
+ RecurringType RecurringType `gorm:"type:varchar(255);default:open;not null" json:"recurring_type" validate:"max=255"`
+ MaxOccurrences int `gorm:"type:int;not null" json:"max_occurrences" validate:"min=1"`
+ Events []Event `gorm:"many2many:event_series;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"events" validate:"-"`
}
-// TODO: add not null to required fields on all gorm models
type EventSeries struct {
- EventID uuid.UUID `gorm:"not null; type:uuid;" json:"event_id" validate:"uuid4"`
+ EventID uuid.UUID `gorm:"type:uuid;not null" json:"event_id" validate:"uuid4"`
Event Event `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
- SeriesID uuid.UUID `gorm:"not null; type:uuid;" json:"series_id" validate:"uuid4"`
+ SeriesID uuid.UUID `gorm:"type:uuid;not null" json:"series_id" validate:"uuid4"`
Series Series `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
}
// Not needed for now, we will just update the events separately
type EventInstanceException struct {
Model
- EventID int `gorm:"not null; type:uuid" json:"event_id" validate:"required"`
+ EventID int `gorm:"type:uuid;not null" json:"event_id" validate:"required"`
Event Event
- IsRescheduled bool `gorm:"type:bool;default:true" json:"is_rescheduled" validate:"required"`
- IsCancelled bool `gorm:"type:bool;default:false" json:"is_cancelled" validate:"required"`
- StartTime time.Time `gorm:"type:timestamptz" json:"start_time" validate:"required,datetime,ltecsfield=EndTime"`
- EndTime time.Time `gorm:"type:timestamptz" json:"end_time" validate:"required,datetime,gtecsfield=StartTime"`
+ IsRescheduled bool `gorm:"type:bool;default:true;not null" json:"is_rescheduled" validate:"required"`
+ IsCancelled bool `gorm:"type:bool;default:false;not null" json:"is_cancelled" validate:"required"`
+ StartTime time.Time `gorm:"type:timestamptz;not null" json:"start_time" validate:"required,datetime,ltecsfield=EndTime"`
+ EndTime time.Time `gorm:"type:timestamptz;not null" json:"end_time" validate:"required,datetime,gtecsfield=StartTime"`
}
// TODO We will likely need to update the create and update structs to account for recurring series
@@ -83,7 +78,7 @@ type CreateEventRequestBody struct {
IsRecurring *bool `json:"is_recurring" validate:"required"`
// TODO club/tag/notification logic
- Host *uuid.UUID `json:"host" validate:"omitempty"`
+ Host *uuid.UUID `json:"host" validate:"required,uuid4"`
Clubs []Club `json:"-" validate:"omitempty"`
Tag []Tag `json:"-" validate:"omitempty"`
Notification []Notification `json:"-" validate:"omitempty"`
@@ -93,12 +88,8 @@ type CreateEventRequestBody struct {
}
type CreateSeriesRequestBody struct {
- RecurringType RecurringType `json:"recurring_type" validate:"required,max=255,oneof=daily weekly monthly"`
- SeparationCount int `json:"separation_count" validate:"required,min=0"`
- MaxOccurrences int `json:"max_occurrences" validate:"required,min=2"`
- DayOfWeek int `json:"day_of_week" validate:"required,min=1,max=7"`
- WeekOfMonth int `json:"week_of_month" validate:"required,min=1,max=5"`
- DayOfMonth int `json:"day_of_month" validate:"required,min=1,max=31"`
+ RecurringType RecurringType `json:"recurring_type" validate:"required,max=255,oneof=daily weekly monthly"`
+ MaxOccurrences int `json:"max_occurrences" validate:"required,min=2"`
}
type UpdateEventRequestBody struct {
@@ -108,8 +99,9 @@ type UpdateEventRequestBody struct {
StartTime time.Time `json:"start_time" validate:"omitempty,ltecsfield=EndTime"`
EndTime time.Time `json:"end_time" validate:"omitempty,gtecsfield=StartTime"`
Location string `json:"location" validate:"omitempty,max=255"`
- EventType EventType `gorm:"type:varchar(255);default:open" json:"event_type" validate:"omitempty,max=255,oneof=open membersOnly"`
+ EventType EventType `json:"event_type" validate:"omitempty,max=255,oneof=open membersOnly"`
+ Host *uuid.UUID `json:"host" validate:"omitempty"`
RSVP []User `json:"-" validate:"omitempty"`
Waitlist []User `json:"-" validate:"omitempty"`
Clubs []Club `json:"-" validate:"omitempty"`
@@ -119,12 +111,8 @@ type UpdateEventRequestBody struct {
// TODO: probably need to make changes to this to update the events as well
type UpdateSeriesRequestBody struct {
- RecurringType RecurringType `json:"recurring_type" validate:"omitempty,max=255,oneof=daily weekly monthly"`
- SeparationCount int `json:"separation_count" validate:"omitempty,min=0"`
- MaxOccurrences int `json:"max_occurrences" validate:"omitempty,min=2"`
- DayOfWeek int `json:"day_of_week" validate:"omitempty,min=1,max=7"`
- WeekOfMonth int `json:"week_of_month" validate:"omitempty,min=1,max=5"`
- DayOfMonth int `json:"day_of_month" validate:"omitempty,min=1,max=31"`
+ RecurringType RecurringType `json:"recurring_type" validate:"omitempty,max=255,oneof=daily weekly monthly"`
+ MaxOccurrences int `json:"max_occurrences" validate:"omitempty,min=2"`
EventDetails UpdateEventRequestBody `json:"event_details" validate:"omitempty"`
}
diff --git a/backend/src/models/file.go b/backend/src/models/file.go
index c64c95fc5..b33a5c68e 100644
--- a/backend/src/models/file.go
+++ b/backend/src/models/file.go
@@ -18,11 +18,11 @@ type File struct {
OwnerID uuid.UUID `gorm:"uniqueIndex:compositeindex;index;not null;type:uuid" json:"-" validate:"required,uuid4"`
OwnerType string `gorm:"uniqueIndex:compositeindex;index;not null;type:varchar(255)" json:"-" validate:"required,max=255"`
- FileName string `gorm:"type:varchar(255)" json:"file_name" validate:"required,max=255"`
- FileType string `gorm:"type:varchar(255)" json:"file_type" validate:"required,max=255"`
- FileSize int `gorm:"type:int" json:"file_size" validate:"required,min=1"`
- FileURL string `gorm:"type:varchar(255)" json:"file_url" validate:"required,max=255"`
- ObjectKey string `gorm:"type:varchar(255)" json:"object_key" validate:"required,max=255"`
+ FileName string `gorm:"type:varchar(255);not null" json:"file_name" validate:"required,max=255"`
+ FileType string `gorm:"type:varchar(255);not null" json:"file_type" validate:"required,max=255"`
+ FileSize int `gorm:"type:int;not null" json:"file_size" validate:"required,min=1"`
+ FileURL string `gorm:"type:varchar(255);not null" json:"file_url" validate:"required,max=255"`
+ ObjectKey string `gorm:"type:varchar(255);not null" json:"object_key" validate:"required,max=255"`
}
type CreateFileRequestBody struct {
diff --git a/backend/src/models/follower.go b/backend/src/models/follower.go
index 2fe61d680..8002aeb6d 100644
--- a/backend/src/models/follower.go
+++ b/backend/src/models/follower.go
@@ -9,8 +9,8 @@ func (Follower) TableName() string {
}
type Follower struct {
- UserID uuid.UUID `gorm:"type:uuid;not null;primaryKey" json:"user_id" validate:"required,uuid4"`
- ClubID uuid.UUID `gorm:"type:uuid;not null;primaryKey" json:"club_id" validate:"required,uuid4"`
+ UserID uuid.UUID `gorm:"type:uuid;primaryKey;not null" json:"user_id" validate:"required,uuid4"`
+ ClubID uuid.UUID `gorm:"type:uuid;primaryKey;not null" json:"club_id" validate:"required,uuid4"`
Club *Club `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
User *User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
diff --git a/backend/src/models/membership.go b/backend/src/models/membership.go
index f225acf72..ec28b2304 100644
--- a/backend/src/models/membership.go
+++ b/backend/src/models/membership.go
@@ -16,11 +16,11 @@ func (Membership) TableName() string {
}
type Membership struct {
- UserID uuid.UUID `gorm:"type:uuid;not null;primaryKey" json:"user_id" validate:"required,uuid4"`
- ClubID uuid.UUID `gorm:"type:uuid;not null;primaryKey" json:"club_id" validate:"required,uuid4"`
+ UserID uuid.UUID `gorm:"type:uuid;primaryKey;not null" json:"user_id" validate:"required,uuid4"`
+ ClubID uuid.UUID `gorm:"type:uuid;primaryKey;not null" json:"club_id" validate:"required,uuid4"`
Club *Club `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
User *User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
- MembershipType MembershipType `gorm:"type:varchar(255);not null;default:member" json:"membership_type" validate:"required,oneof=member admin"`
+ MembershipType MembershipType `gorm:"type:varchar(255);default:member;not null" json:"membership_type" validate:"required,oneof=member admin"`
}
diff --git a/backend/src/models/notification.go b/backend/src/models/notification.go
index fd07e4580..87d84a4ec 100644
--- a/backend/src/models/notification.go
+++ b/backend/src/models/notification.go
@@ -16,12 +16,12 @@ const (
type Notification struct {
Model
- SendAt time.Time `gorm:"type:timestamptz" json:"send_at" validate:"required"`
- Title string `gorm:"type:varchar(255)" json:"title" validate:"required,max=255"`
- Content string `gorm:"type:varchar(255)" json:"content" validate:"required,max=255"`
- DeepLink string `gorm:"type:varchar(255)" json:"deep_link" validate:"required,max=255"`
- Icon string `gorm:"type:varchar(255)" json:"icon" validate:"required,http_url,max=255"` // S3 URL
+ SendAt time.Time `gorm:"type:timestamptz;not null" json:"send_at" validate:"required"`
+ Title string `gorm:"type:varchar(255);not null" json:"title" validate:"required,max=255"`
+ Content string `gorm:"type:varchar(255);not null" json:"content" validate:"required,max=255"`
+ DeepLink string `gorm:"type:varchar(255);not null" json:"deep_link" validate:"required,max=255"`
+ Icon string `gorm:"type:varchar(255);not null" json:"icon" validate:"required,s3_url,http_url,max=255"` // S3 URL
- ReferenceID uuid.UUID `gorm:"type:int" json:"-" validate:"uuid4"`
- ReferenceType NotificationType `gorm:"type:varchar(255)" json:"-" validate:"max=255"`
+ ReferenceID uuid.UUID `gorm:"type:int;not null" json:"-" validate:"uuid4"`
+ ReferenceType NotificationType `gorm:"type:varchar(255);not null" json:"-" validate:"max=255"`
}
diff --git a/backend/src/models/poc.go b/backend/src/models/poc.go
index c2d424f1a..bfbf0a9c9 100644
--- a/backend/src/models/poc.go
+++ b/backend/src/models/poc.go
@@ -7,13 +7,13 @@ import (
type PointOfContact struct {
Model
- Name string `gorm:"type:varchar(255)" json:"name" validate:"required,max=255"`
+ Name string `gorm:"type:varchar(255);not null" json:"name" validate:"required,max=255"`
Email string `gorm:"uniqueIndex:compositeindex;index;not null;type:varchar(255)" json:"email" validate:"required,email,max=255"`
- Position string `gorm:"type:varchar(255);" json:"position" validate:"required,max=255"`
+ Position string `gorm:"type:varchar(255);not null" json:"position" validate:"required,max=255"`
- ClubID uuid.UUID `gorm:"uniqueIndex:compositeindex;index;not null;foreignKey:ClubID" json:"-" validate:"min=1"`
+ ClubID uuid.UUID `gorm:"uniqueIndex:compositeindex;index;foreignKey:ClubID;not null" json:"-" validate:"required,uuid4"`
- PhotoFile File `gorm:"polymorphic:Owner;" json:"photo_file"`
+ PhotoFile File `gorm:"polymorphic:Owner;not null" json:"photo_file"`
}
type CreatePointOfContactBody struct {
diff --git a/backend/src/models/root.go b/backend/src/models/root.go
index cf9a23f5e..b1f29154c 100644
--- a/backend/src/models/root.go
+++ b/backend/src/models/root.go
@@ -11,7 +11,7 @@ type Tabler interface {
}
type Model struct {
- ID uuid.UUID `gorm:"type:uuid;primaryKey;default:uuid_generate_v4()" json:"id" example:"123e4567-e89b-12d3-a456-426614174000"`
- CreatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP" json:"created_at" example:"2023-09-20T16:34:50Z"`
- UpdatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP" json:"updated_at" example:"2023-09-20T16:34:50Z"`
+ ID uuid.UUID `gorm:"type:uuid;primaryKey;default:uuid_generate_v4();not null" json:"id" example:"123e4567-e89b-12d3-a456-426614174000"`
+ CreatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP;not null" json:"created_at" example:"2023-09-20T16:34:50Z"`
+ UpdatedAt time.Time `gorm:"type:timestamp;default:CURRENT_TIMESTAMP;not null" json:"updated_at" example:"2023-09-20T16:34:50Z"`
}
diff --git a/backend/src/models/tag.go b/backend/src/models/tag.go
index 560e74d1b..42a2fea7b 100644
--- a/backend/src/models/tag.go
+++ b/backend/src/models/tag.go
@@ -5,9 +5,9 @@ import "github.com/google/uuid"
type Tag struct {
Model
- Name string `gorm:"type:varchar(255)" json:"name" validate:"required,max=255"`
+ Name string `gorm:"type:varchar(255);not null" json:"name" validate:"required,max=255"`
- CategoryID uuid.UUID `json:"category_id" validate:"required,uuid4"`
+ CategoryID uuid.UUID `gorm:"type:uuid;not null" json:"category_id" validate:"required,uuid4"`
User []User `gorm:"many2many:user_tags;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
Club []Club `gorm:"many2many:club_tags;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
diff --git a/backend/src/models/user.go b/backend/src/models/user.go
index a4b5d3632..e0c4015fc 100644
--- a/backend/src/models/user.go
+++ b/backend/src/models/user.go
@@ -26,6 +26,112 @@ const (
CSSH College = "CSSH" // College of Social Sciences and Humanities
)
+type Major string
+
+// see https://admissions.northeastern.edu/academics/areas-of-study/
+const (
+ AfricanaStudies Major = "africanaStudies"
+ AmericanSignLanguage Major = "americanSignLanguage"
+ AmericanSignLanguageEnglishInterpreting Major = "americanSignLanguage-EnglishInterpreting"
+ AppliedPhysics Major = "appliedPhysics"
+ ArchitecturalStudies Major = "architecturalStudies"
+ Architecture Major = "architecture"
+ ArtArtVisualStudies Major = "art:ArtVisualStudies"
+ BehavioralNeuroscience Major = "behavioralNeuroscience"
+ Biochemistry Major = "biochemistry"
+ Bioengineering Major = "bioengineering"
+ Biology Major = "biology"
+ BiomedicalPhysics Major = "biomedicalPhysics"
+ BusinessAdministration Major = "businessAdministration"
+ BusinessAdministrationAccounting Major = "businessAdministration:Accounting"
+ BusinessAdministrationAccountingAndAdvisoryServices Major = "businessAdministration:AccountingAndAdvisoryServices"
+ BusinessAdministrationBrandManagement Major = "businessAdministration:BrandManagement"
+ BusinessAdministrationBusinessAnalytics Major = "businessAdministration:BusinessAnalytics"
+ BusinessAdministrationCorporateInnovation Major = "businessAdministration:CorporateInnovation"
+ BusinessAdministrationEntrepreneurialStartups Major = "businessAdministration:EntrepreneurialStartups"
+ BusinessAdministrationFamilyBusiness Major = "businessAdministration:FamilyBusiness"
+ BusinessAdministrationFinance Major = "businessAdministration:Finance"
+ BusinessAdministrationFintech Major = "businessAdministration:Fintech"
+ BusinessAdministrationHealthcareManagementAndConsulting Major = "businessAdministration:HealthcareManagementAndConsulting"
+ BusinessAdministrationManagement Major = "businessAdministration:Management"
+ BusinessAdministrationManagementInformationSystems Major = "businessAdministration:ManagementInformationSystems"
+ BusinessAdministrationMarketing Major = "businessAdministration:Marketing"
+ BusinessAdministrationMarketingAnalytics Major = "businessAdministration:MarketingAnalytics"
+ BusinessAdministrationSocialInnovationAndEntrepreneurship Major = "businessAdministration:SocialInnovationAndEntrepreneurship"
+ BusinessAdministrationSupplyChainManagement Major = "businessAdministration:SupplyChainManagement"
+ CellAndMolecularBiology Major = "cellAndMolecularBiology"
+ ChemicalEngineering Major = "chemicalEngineering"
+ Chemistry Major = "chemistry"
+ CivilEngineering Major = "civilEngineering"
+ CommunicationStudies Major = "communicationStudies"
+ ComputerEngineering Major = "computerEngineering"
+ ComputerScience Major = "computerScience"
+ ComputingAndLaw Major = "computingAndLaw"
+ CriminologyAndCriminalJustice Major = "criminologyAndCriminalJustice"
+ CulturalAnthropology Major = "culturalAnthropology"
+ Cybersecurity Major = "cybersecurity"
+ DataScience Major = "dataScience"
+ Design Major = "design"
+ Economics Major = "economics"
+ ElectricalEngineering Major = "electricalEngineering"
+ English Major = "english"
+ EnvironmentalAndSustainabilityStudies Major = "environmentalAndSustainabilityStudies"
+ EnvironmentalEngineering Major = "environmentalEngineering"
+ EnvironmentalScience Major = "environmentalScience"
+ EnvironmentalStudies Major = "environmentalStudies"
+ GameArtAndAnimation Major = "gameArtAndAnimation"
+ GameDesign Major = "gameDesign"
+ GlobalAsianStudies Major = "globalAsianStudies"
+ HealthScience Major = "healthScience"
+ History Major = "history"
+ HistoryCultureAndLaw Major = "historyCultureAndLaw"
+ HumanServices Major = "humanServices"
+ IndustrialEngineering Major = "industrialEngineering"
+ InternationalAffairs Major = "internationalAffairs"
+ InternationalBusiness Major = "internationalBusiness"
+ InternationalBusinessAccounting Major = "internationalBusiness:Accounting"
+ InternationalBusinessAccountingAndAdvisoryServices Major = "internationalBusiness:AccountingAndAdvisoryServices"
+ InternationalBusinessBrandManagement Major = "internationalBusiness:BrandManagement"
+ InternationalBusinessBusinessAnalytics Major = "internationalBusiness:BusinessAnalytics"
+ InternationalBusinessCorporateInnovation Major = "internationalBusiness:CorporateInnovation"
+ InternationalBusinessEntrepreneurialStartups Major = "internationalBusiness:EntrepreneurialStartups"
+ InternationalBusinessFamilyBusiness Major = "internationalBusiness:FamilyBusiness"
+ InternationalBusinessFinance Major = "internationalBusiness:Finance"
+ InternationalBusinessFintech Major = "internationalBusiness:Fintech"
+ InternationalBusinessHealthcareManagementAndConsulting Major = "internationalBusiness:HealthcareManagementAndConsulting"
+ InternationalBusinessManagement Major = "internationalBusiness:Management"
+ InternationalBusinessManagementInformationSystems Major = "internationalBusiness:ManagementInformationSystems"
+ InternationalBusinessMarketing Major = "internationalBusiness:Marketing"
+ InternationalBusinessMarketingAnalytics Major = "internationalBusiness:MarketingAnalytics"
+ InternationalBusinessSocialInnovationAndEntrepreneurship Major = "internationalBusiness:SocialInnovationAndEntrepreneurship"
+ InternationalBusinessSupplyChainManagement Major = "internationalBusiness:SupplyChainManagement"
+ Journalism Major = "journalism"
+ LandscapeArchitecture Major = "landscapeArchitecture"
+ Linguistics Major = "linguistics"
+ MarineBiology Major = "marineBiology"
+ Mathematics Major = "mathematics"
+ MechanicalEngineering Major = "mechanicalEngineering"
+ MediaAndScreenStudies Major = "mediaAndScreenStudies"
+ MediaArts Major = "mediaArts"
+ Music Major = "music"
+ MusicTechnology Major = "musicTechnology"
+ Nursing Major = "nursing"
+ PharmaceuticalSciences Major = "pharmaceuticalSciences"
+ PharmacyPharmD Major = "pharmacy(PharmD)"
+ Philosophy Major = "philosophy"
+ Physics Major = "physics"
+ PoliticalScience Major = "politicalScience"
+ PoliticsPhilosophyEconomics Major = "politicsPhilosophyEconomics"
+ Psychology Major = "psychology"
+ PublicHealth Major = "publicHealth"
+ PublicRelations Major = "publicRelations"
+ ReligiousStudies Major = "religiousStudies"
+ Sociology Major = "sociology"
+ Spanish Major = "spanish"
+ SpeechLanguagePathologyAndAudiology Major = "speechLanguagePathologyAndAudiology"
+ Theatre Major = "theatre"
+)
+
type GraduationCycle string
const (
@@ -41,15 +147,18 @@ type Tokens struct {
type User struct {
Model
- Role UserRole `gorm:"type:varchar(255);default:'student'" json:"role" validate:"required,oneof=super student"`
- FirstName string `gorm:"type:varchar(255)" json:"first_name" validate:"required,max=255"`
- LastName string `gorm:"type:varchar(255)" json:"last_name" validate:"required,max=255"`
- Email string `gorm:"type:varchar(255);unique" json:"email" validate:"required,email,max=255"`
- PasswordHash string `gorm:"type:varchar(97)" json:"-" validate:"required,len=97"`
- College College `gorm:"type:varchar(255)" json:"college" validate:"required,max=255"`
- GraduationCycle GraduationCycle `gorm:"type:varchar(255)" json:"graduation_cycle" validate:"required,max=255,oneof=december may"`
- GraduationYear int16 `gorm:"type:smallint" json:"graduation_year" validate:"required"`
- IsVerified bool `gorm:"type:boolean;default:false" json:"is_verified"`
+ Role UserRole `gorm:"type:varchar(255);default:'student';not null" json:"role" validate:"required,oneof=super student"`
+ FirstName string `gorm:"type:varchar(255);not null" json:"first_name" validate:"required,max=255"`
+ LastName string `gorm:"type:varchar(255);not null" json:"last_name" validate:"required,max=255"`
+ Email string `gorm:"type:varchar(255);unique;not null" json:"email" validate:"required,email,max=255"`
+ PasswordHash string `gorm:"type:varchar(97);not null" json:"-" validate:"required,len=97"`
+ Major0 Major `gorm:"type:varchar(255)" json:"major0" validate:"not_equal_if_not_empty=Major1,not_equal_if_not_empty=Major2,required,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 Major `gorm:"type:varchar(255);" 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 Major `gorm:"type:varchar(255);" 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 College `gorm:"type:varchar(255);" json:"college" validate:"required,max=255"` // TODO: gorm not null?
+ GraduationCycle GraduationCycle `gorm:"type:varchar(255);" json:"graduation_cycle" validate:"required,max=255,oneof=december may"` // TODO: gorm not null?
+ GraduationYear int16 `gorm:"type:smallint;" json:"graduation_year" validate:"required"` // TODO: gorm not null?
+ IsVerified bool `gorm:"type:boolean;default:false;not null" json:"is_verified"`
Tag []Tag `gorm:"many2many:user_tags;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
Admin []Club `gorm:"many2many:user_club_admins;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" validate:"-"`
@@ -66,8 +175,11 @@ type CreateUserRequestBody struct {
FirstName string `json:"first_name" validate:"required,max=255"`
LastName string `json:"last_name" validate:"required,max=255"`
Email string `json:"email" validate:"required,email,neu_email,max=255"`
- Password string `json:"password" validate:"required,password,min=8,max=255"`
+ Password string `json:"password" validate:"required,max=255"` // MARK: must be validated manually
// Optional fields
+ Major0 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 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 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 College `json:"college" validate:"omitempty,oneof=CAMD DMSB KCCS CE BCHS SL CPS CS CSSH"`
GraduationCycle GraduationCycle `json:"graduation_cycle" validate:"omitempty,max=255,oneof=december may"`
GraduationYear int16 `json:"graduation_year" validate:"omitempty"`
@@ -76,6 +188,9 @@ type CreateUserRequestBody struct {
type UpdateUserRequestBody struct {
FirstName string `json:"first_name" validate:"omitempty,max=255"`
LastName string `json:"last_name" validate:"omitempty,max=255"`
+ Major0 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 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 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 College `json:"college" validate:"omitempty,oneof=CAMD DMSB KCCS CE BCHS SL CPS CS CSSH"`
GraduationCycle GraduationCycle `json:"graduation_cycle" validate:"omitempty,max=255,oneof=december may"`
GraduationYear int16 `json:"graduation_year" validate:"omitempty"`
@@ -83,12 +198,12 @@ type UpdateUserRequestBody struct {
type LoginUserResponseBody struct {
Email string `json:"email" validate:"required,email"`
- Password string `json:"password" validate:"required,password,min=8,max=255"`
+ Password string `json:"password" validate:"required,max=255"` // MARK: must be validated manually
}
type UpdatePasswordRequestBody struct {
- OldPassword string `json:"old_password" validate:"required,password,min=8,max=255"`
- NewPassword string `json:"new_password" validate:"required,password,nefield=OldPassword,min=8,max=255"`
+ OldPassword string `json:"old_password" validate:"required,max=255"` // MARK: must be validated manually
+ NewPassword string `json:"new_password" validate:"required,not_equal_if_not_empty=OldPassword,max=255"` // MARK: must be validated manually
}
type RefreshTokenRequestBody struct {
diff --git a/backend/src/models/verification.go b/backend/src/models/verification.go
index 434bb727e..db8fd37da 100644
--- a/backend/src/models/verification.go
+++ b/backend/src/models/verification.go
@@ -14,9 +14,9 @@ const (
)
type Verification struct {
- UserID uuid.UUID `gorm:"type:varchar(36);not null;primaryKey" json:"user_id" validate:"required,uuid4"`
+ UserID uuid.UUID `gorm:"type:varchar(36);primaryKey;not null" json:"user_id" validate:"required,uuid4"`
Token string `gorm:"type:varchar(255);unique" json:"token" validate:"required,max=255"`
- ExpiresAt time.Time `gorm:"type:timestamp;not null;primaryKey" json:"expires_at" validate:"required"`
+ ExpiresAt time.Time `gorm:"type:timestamp;primaryKey;not null" json:"expires_at" validate:"required"`
Type VerificationType `gorm:"type:varchar(255);not null" json:"type" validate:"required,oneof=email_verification password_reset"`
}
diff --git a/backend/src/services/auth.go b/backend/src/services/auth.go
index aa6d627a0..30079cd8f 100644
--- a/backend/src/services/auth.go
+++ b/backend/src/services/auth.go
@@ -40,6 +40,10 @@ func (a *AuthService) Login(loginBody models.LoginUserResponseBody) (*models.Use
return nil, nil, &errors.FailedToValidateUser
}
+ if pwordValErr := auth.ValidatePassword(loginBody.Password); pwordValErr != nil {
+ return nil, nil, pwordValErr
+ }
+
user, getUserByEmailErr := transactions.GetUserByEmail(a.DB, loginBody.Email)
if getUserByEmailErr != nil {
return nil, nil, getUserByEmailErr
diff --git a/backend/src/services/event.go b/backend/src/services/event.go
index e9c92e896..e544714b1 100644
--- a/backend/src/services/event.go
+++ b/backend/src/services/event.go
@@ -225,11 +225,11 @@ func createEventSlice(firstEvent *models.Event, series models.Series) []models.E
switch series.RecurringType {
case "daily":
- days = series.SeparationCount + 1
+ days = 1
case "weekly":
- days = 7 * (series.SeparationCount + 1)
+ days = 7
case "monthly":
- months = series.SeparationCount + 1
+ months = 1
}
for i := 1; i < series.MaxOccurrences; i++ {
@@ -252,6 +252,7 @@ func mapToEvent(eventBody models.UpdateEventRequestBody) *models.Event {
Location: eventBody.Location,
EventType: eventBody.EventType,
+ Host: eventBody.Host,
RSVP: eventBody.RSVP,
Waitlist: eventBody.Waitlist,
Clubs: eventBody.Clubs,
diff --git a/backend/src/services/tag.go b/backend/src/services/tag.go
index 2152b1717..38eb8b192 100644
--- a/backend/src/services/tag.go
+++ b/backend/src/services/tag.go
@@ -9,7 +9,7 @@ import (
)
type TagServiceInterface interface {
- GetTags(limit string, page string) ([]models.Tag, *errors.Error)
+ GetTags() ([]models.Tag, *errors.Error)
CreateTag(tagBody models.CreateTagRequestBody) (*models.Tag, *errors.Error)
GetTag(id string) (*models.Tag, *errors.Error)
UpdateTag(id string, tagBody models.UpdateTagRequestBody) (*models.Tag, *errors.Error)
@@ -37,18 +37,8 @@ func (t *TagService) CreateTag(tagBody models.CreateTagRequestBody) (*models.Tag
return transactions.CreateTag(t.DB, *tag)
}
-func (t *TagService) GetTags(limit string, page string) ([]models.Tag, *errors.Error) {
- limitAsInt, err := utilities.ValidateNonNegative(limit)
- if err != nil {
- return nil, &errors.FailedToValidateLimit
- }
-
- pageAsInt, err := utilities.ValidateNonNegative(page)
- if err != nil {
- return nil, &errors.FailedToValidatePage
- }
-
- return transactions.GetTags(t.DB, *limitAsInt, *pageAsInt)
+func (t *TagService) GetTags() ([]models.Tag, *errors.Error) {
+ return transactions.GetTags(t.DB)
}
func (t *TagService) GetTag(tagID string) (*models.Tag, *errors.Error) {
diff --git a/backend/src/services/user.go b/backend/src/services/user.go
index 71dbf22d9..e10b6fee4 100644
--- a/backend/src/services/user.go
+++ b/backend/src/services/user.go
@@ -35,6 +35,10 @@ func (u *UserService) CreateUser(userBody models.CreateUserRequestBody) (*models
return nil, nil, &errors.FailedToValidateUser
}
+ if pwordValErr := auth.ValidatePassword(userBody.Password); pwordValErr != nil {
+ return nil, nil, pwordValErr
+ }
+
user, err := utilities.MapRequestToModel(userBody, &models.User{})
if err != nil {
return nil, nil, &errors.FailedToMapRequestToModel
@@ -141,6 +145,14 @@ func (u *UserService) UpdatePassword(id string, passwordBody models.UpdatePasswo
return &errors.FailedToValidateUpdatePasswordBody
}
+ if pwordValErr := auth.ValidatePassword(passwordBody.OldPassword); pwordValErr != nil {
+ return pwordValErr
+ }
+
+ if pwordValErr := auth.ValidatePassword(passwordBody.NewPassword); pwordValErr != nil {
+ return pwordValErr
+ }
+
passwordHash, err := transactions.GetUserPasswordHash(u.DB, *idAsUUID)
if err != nil {
return err
diff --git a/backend/src/transactions/event.go b/backend/src/transactions/event.go
index f7525cbad..44b1a293f 100644
--- a/backend/src/transactions/event.go
+++ b/backend/src/transactions/event.go
@@ -106,15 +106,7 @@ func CreateEvent(db *gorm.DB, event models.Event) ([]models.Event, *errors.Error
}
func CreateEventSeries(db *gorm.DB, series models.Series) ([]models.Event, *errors.Error) {
- tx := db.Begin()
-
- if err := tx.Create(&series).Error; err != nil {
- tx.Rollback()
- return nil, &errors.FailedToCreateEventSeries
- }
-
- if err := tx.Commit().Error; err != nil {
- tx.Rollback()
+ if err := db.Create(&series).Error; err != nil {
return nil, &errors.FailedToCreateEventSeries
}
diff --git a/backend/src/transactions/tag.go b/backend/src/transactions/tag.go
index 282d06e76..caf6576ea 100644
--- a/backend/src/transactions/tag.go
+++ b/backend/src/transactions/tag.go
@@ -48,12 +48,10 @@ func GetTag(db *gorm.DB, tagID uuid.UUID) (*models.Tag, *errors.Error) {
return &tag, nil
}
-func GetTags(db *gorm.DB, limit int, page int) ([]models.Tag, *errors.Error) {
+func GetTags(db *gorm.DB) ([]models.Tag, *errors.Error) {
var tags []models.Tag
- offset := (page - 1) * limit
-
- if err := db.Limit(limit).Offset(offset).Find(&tags).Error; err != nil {
+ if err := db.Find(&tags).Error; err != nil {
return nil, &errors.FailedToGetTags
}
diff --git a/backend/src/utilities/validator.go b/backend/src/utilities/validator.go
index fe332be54..092165f60 100644
--- a/backend/src/utilities/validator.go
+++ b/backend/src/utilities/validator.go
@@ -2,7 +2,6 @@ package utilities
import (
"reflect"
- "regexp"
"strconv"
"strings"
@@ -22,10 +21,6 @@ func RegisterCustomValidators() (*validator.Validate, error) {
return nil, err
}
- if err := validate.RegisterValidation("password", validatePassword); err != nil {
- return nil, err
- }
-
if err := validate.RegisterValidation("s3_url", validateS3URL); err != nil {
return nil, err
}
@@ -36,6 +31,10 @@ func RegisterCustomValidators() (*validator.Validate, error) {
return nil, err
}
+ if err := validate.RegisterValidation("not_equal_if_not_empty", validateNotEqualIfNotEmpty); err != nil {
+ return nil, err
+ }
+
return validate, nil
}
@@ -56,16 +55,6 @@ func validateEmail(fl validator.FieldLevel) bool {
return true
}
-func validatePassword(fl validator.FieldLevel) bool {
- password := fl.Field().String()
-
- hasMinLength := len(password) >= 8
- hasDigit, _ := regexp.MatchString(`[0-9]`, password)
- hasSpecialChar, _ := regexp.MatchString(`[@#%&*+]`, password)
-
- return hasMinLength && hasDigit && hasSpecialChar
-}
-
func validateS3URL(fl validator.FieldLevel) bool {
return strings.HasPrefix(fl.Field().String(), "https://s3.amazonaws.com/")
}
@@ -97,6 +86,13 @@ func validateContactPointer(validate *validator.Validate, fl validator.FieldLeve
return validate.Var(contact.Content, rule) == nil && strings.HasPrefix(contact.Content, models.GetContentPrefix(contact.Type))
}
+func validateNotEqualIfNotEmpty(fl validator.FieldLevel) bool {
+ field := fl.Field().String()
+ otherField := fl.Parent().FieldByName(fl.Param()).String()
+
+ return field == "" || field != otherField
+}
+
func ValidateID(id string) (*uuid.UUID, *errors.Error) {
idAsUUID, err := uuid.Parse(id)
if err != nil {
diff --git a/backend/tests/api/club_test.go b/backend/tests/api/club_test.go
index e725eb1b3..d6ef0dcd6 100644
--- a/backend/tests/api/club_test.go
+++ b/backend/tests/api/club_test.go
@@ -42,8 +42,6 @@ func AssertClubBodyRespDB(eaa h.ExistingAppAssert, resp *http.Response, body *ma
eaa.Assert.NilError(err)
- eaa.Assert.Equal(2, len(dbClubs))
-
dbClub := dbClubs[0]
eaa.Assert.Equal(dbClub.ID, respClub.ID)
@@ -255,7 +253,7 @@ func TestCreateClubFailsOnInvalidDescription(t *testing.T) {
[]interface{}{
"Not an URL",
"@#139081#$Ad_Axf",
- // "https://google.com", <-- TODO fix once we handle mongo urls
+ "https://google.com",
},
)
}
@@ -300,7 +298,7 @@ func TestCreateClubFailsOnInvalidLogo(t *testing.T) {
[]interface{}{
"Not an URL",
"@#139081#$Ad_Axf",
- // "https://google.com", <-- TODO uncomment once we figure out s3 url validation
+ "https://google.com",
},
)
}
diff --git a/backend/tests/api/event_test.go b/backend/tests/api/event_test.go
index 0caf089f7..643140e5d 100644
--- a/backend/tests/api/event_test.go
+++ b/backend/tests/api/event_test.go
@@ -16,9 +16,9 @@ import (
"gorm.io/gorm"
)
-type EventFactory func() *map[string]interface{}
+type EventFactory func(hostID uuid.UUID) *map[string]interface{}
-func SampleEventFactory() *map[string]interface{} {
+func SampleEventFactory(hostID uuid.UUID) *map[string]interface{} {
return &map[string]interface{}{
"name": "Generate",
"preview": "Generate is Northeastern's premier student-led product development studio.",
@@ -28,23 +28,21 @@ func SampleEventFactory() *map[string]interface{} {
"location": "Carter Fields",
"event_type": "open",
"is_recurring": false,
+ "host": hostID,
}
}
-func SampleSeriesFactory() *map[string]interface{} {
+func SampleSeriesFactory(hostID uuid.UUID) *map[string]interface{} {
return CustomSampleSeriesFactory(
+ hostID,
models.CreateSeriesRequestBody{
- RecurringType: "daily",
- MaxOccurrences: 10,
- SeparationCount: 4,
- DayOfWeek: 3,
- WeekOfMonth: 2,
- DayOfMonth: 1,
+ RecurringType: "daily",
+ MaxOccurrences: 10,
},
)
}
-func CustomSampleSeriesFactory(series models.CreateSeriesRequestBody) *map[string]interface{} {
+func CustomSampleSeriesFactory(hostID uuid.UUID, series models.CreateSeriesRequestBody) *map[string]interface{} {
return &map[string]interface{}{
"name": "Software Development",
"preview": "CS4500 at northeastern",
@@ -54,6 +52,7 @@ func CustomSampleSeriesFactory(series models.CreateSeriesRequestBody) *map[strin
"location": "ISEC",
"event_type": "membersOnly",
"is_recurring": true,
+ "host": hostID,
"series": series,
}
}
@@ -142,25 +141,25 @@ func AssertEventBodyRespDB(eaa h.ExistingAppAssert, resp *http.Response, body *m
return dbEvent.ID
}
-func AssertSampleEventBodyRespDB(eaa h.ExistingAppAssert, resp *http.Response) []uuid.UUID {
- sampleEvent := SampleEventFactory()
+func AssertSampleEventBodyRespDB(eaa h.ExistingAppAssert, hostID uuid.UUID, resp *http.Response) []uuid.UUID {
+ sampleEvent := SampleEventFactory(hostID)
return AssertEventListBodyRespDB(eaa, resp, sampleEvent)
}
-func CreateSampleEvent(existingAppAssert h.ExistingAppAssert, factoryFunction EventFactory) (h.ExistingAppAssert, []uuid.UUID) {
+func CreateSampleEvent(existingAppAssert h.ExistingAppAssert, hostID uuid.UUID, factoryFunction EventFactory) (h.ExistingAppAssert, []uuid.UUID) {
var sampleEventUUIDs []uuid.UUID
newAppAssert := existingAppAssert.TestOnStatusAndTester(
h.TestRequest{
Method: fiber.MethodPost,
Path: "/api/v1/events/",
- Body: factoryFunction(),
+ Body: factoryFunction(hostID),
Role: &models.Super,
},
h.TesterWithStatus{
Status: fiber.StatusCreated,
Tester: func(eaa h.ExistingAppAssert, resp *http.Response) {
- sampleEventUUIDs = AssertSampleEventBodyRespDB(eaa, resp)
+ sampleEventUUIDs = AssertSampleEventBodyRespDB(eaa, hostID, resp)
},
},
)
@@ -189,18 +188,20 @@ func AssertNumSeriesRemainsAtN(eaa h.ExistingAppAssert, resp *http.Response, n i
}
func TestCreateEventWorks(t *testing.T) {
- existingAppAssert, _ := CreateSampleEvent(h.InitTest(t), SampleEventFactory)
- existingAppAssert.Close()
+ eaa, _, clubID := CreateSampleClub(h.InitTest(t))
+ eaa, _ = CreateSampleEvent(eaa, clubID, SampleEventFactory)
+ eaa.Close()
}
func TestCreateEventSeriesWorks(t *testing.T) {
- existingAppAssert, _ := CreateSampleEvent(h.InitTest(t), SampleSeriesFactory)
- existingAppAssert.Close()
+ eaa, _, clubID := CreateSampleClub(h.InitTest(t))
+ eaa, _ = CreateSampleEvent(eaa, clubID, SampleSeriesFactory)
+ eaa.Close()
}
func TestGetEventWorks(t *testing.T) {
- existingAppAssert, eventUUID := CreateSampleEvent(h.InitTest(t), SampleEventFactory)
-
+ existingAppAssert, _, clubID := CreateSampleClub(h.InitTest(t))
+ existingAppAssert, eventUUID := CreateSampleEvent(existingAppAssert, clubID, SampleEventFactory)
existingAppAssert.TestOnStatusAndTester(h.TestRequest{
Method: fiber.MethodGet,
Path: fmt.Sprintf("/api/v1/events/%s", eventUUID[0]),
@@ -209,14 +210,15 @@ func TestGetEventWorks(t *testing.T) {
h.TesterWithStatus{
Status: fiber.StatusOK,
Tester: func(eaa h.ExistingAppAssert, resp *http.Response) {
- AssertEventListBodyRespDB(eaa, resp, SampleEventFactory())
+ AssertEventListBodyRespDB(eaa, resp, SampleEventFactory(clubID))
},
},
).Close()
}
func TestGetEventsWorks(t *testing.T) {
- existingAppAssert, _ := CreateSampleEvent(h.InitTest(t), SampleEventFactory)
+ existingAppAssert, _, clubID := CreateSampleClub(h.InitTest(t))
+ existingAppAssert, _ = CreateSampleEvent(existingAppAssert, clubID, SampleEventFactory)
existingAppAssert.TestOnStatusAndTester(h.TestRequest{
Method: fiber.MethodGet,
@@ -239,7 +241,8 @@ func TestGetEventsWorks(t *testing.T) {
}
func TestGetSeriesByEventIDWorks(t *testing.T) {
- existingAppAssert, eventUUIDs := CreateSampleEvent(h.InitTest(t), SampleSeriesFactory)
+ existingAppAssert, _, clubID := CreateSampleClub(h.InitTest(t))
+ existingAppAssert, eventUUIDs := CreateSampleEvent(existingAppAssert, clubID, SampleSeriesFactory)
existingAppAssert.TestOnStatusAndTester(h.TestRequest{
Method: fiber.MethodGet,
@@ -258,9 +261,10 @@ func TestGetSeriesByEventIDWorks(t *testing.T) {
func AssertCreateBadEventDataFails(t *testing.T, jsonKey string, badValues []interface{}, expectedErr errors.Error) {
appAssert, _, _ := CreateSampleStudent(t, nil)
+ appAssert, _, clubID := CreateSampleClub(appAssert)
for _, badValue := range badValues {
- sampleEventPermutation := *SampleEventFactory()
+ sampleEventPermutation := *SampleEventFactory(clubID)
sampleEventPermutation[jsonKey] = badValue
appAssert.TestOnErrorAndTester(
@@ -324,8 +328,9 @@ func TestCreateEventFailsOnInvalidEventType(t *testing.T) {
func AssertCreateBadEventSeriesDataFails(t *testing.T, badSeries models.CreateSeriesRequestBody, expectedErr errors.Error) {
appAssert, _, _ := CreateSampleStudent(t, nil)
+ appAssert, _, clubID := CreateSampleClub(appAssert)
- sampleSeriesPermutation := CustomSampleSeriesFactory(badSeries)
+ sampleSeriesPermutation := CustomSampleSeriesFactory(clubID, badSeries)
appAssert.TestOnErrorAndTester(
h.TestRequest{
@@ -347,12 +352,8 @@ func AssertCreateBadEventSeriesDataFails(t *testing.T, badSeries models.CreateSe
func TestCreateSeriesFailsOnInvalidRecurringType(t *testing.T) {
AssertCreateBadEventSeriesDataFails(t,
models.CreateSeriesRequestBody{
- RecurringType: "annually",
- MaxOccurrences: 10,
- SeparationCount: 0,
- DayOfWeek: 3,
- WeekOfMonth: 2,
- DayOfMonth: 1,
+ RecurringType: "annually",
+ MaxOccurrences: 10,
},
errors.FailedToValidateEventSeries,
)
@@ -361,77 +362,18 @@ func TestCreateSeriesFailsOnInvalidRecurringType(t *testing.T) {
func TestCreateSeriesFailsOnInvalidMaxOccurrences(t *testing.T) {
AssertCreateBadEventSeriesDataFails(t,
models.CreateSeriesRequestBody{
- RecurringType: "weekly",
- MaxOccurrences: -1,
- SeparationCount: 0,
- DayOfWeek: 3,
- WeekOfMonth: 2,
- DayOfMonth: 1,
- },
- errors.FailedToValidateEventSeries,
- )
-}
-
-func TestCreateSeriesFailsOnInvalidSeparationCount(t *testing.T) {
- AssertCreateBadEventSeriesDataFails(t,
- models.CreateSeriesRequestBody{
- RecurringType: "weekly",
- MaxOccurrences: 10,
- SeparationCount: -1,
- DayOfWeek: 3,
- WeekOfMonth: 2,
- DayOfMonth: 1,
- },
- errors.FailedToValidateEventSeries,
- )
-}
-
-func TestCreateSeriesFailsOnInvalidDayOfWeek(t *testing.T) {
- AssertCreateBadEventSeriesDataFails(t,
- models.CreateSeriesRequestBody{
- RecurringType: "weekly",
- MaxOccurrences: 10,
- SeparationCount: 0,
- DayOfWeek: 8,
- WeekOfMonth: 2,
- DayOfMonth: 1,
- },
- errors.FailedToValidateEventSeries,
- )
-}
-
-func TestCreateSeriesFailsOnInvalidWeekOfMonth(t *testing.T) {
- AssertCreateBadEventSeriesDataFails(t,
- models.CreateSeriesRequestBody{
- RecurringType: "weekly",
- MaxOccurrences: 10,
- SeparationCount: 0,
- DayOfWeek: 5,
- WeekOfMonth: -5,
- DayOfMonth: 1,
- },
- errors.FailedToValidateEventSeries,
- )
-}
-
-func TestCreateSeriesFailsOnInvalidDayOfMonth(t *testing.T) {
- AssertCreateBadEventSeriesDataFails(t,
- models.CreateSeriesRequestBody{
- RecurringType: "weekly",
- MaxOccurrences: 10,
- SeparationCount: 0,
- DayOfWeek: 5,
- WeekOfMonth: 2,
- DayOfMonth: 42,
+ RecurringType: "weekly",
+ MaxOccurrences: -1,
},
errors.FailedToValidateEventSeries,
)
}
func TestUpdateEventWorks(t *testing.T) {
- appAssert, eventUUID := CreateSampleEvent(h.InitTest(t), SampleEventFactory)
+ appAssert, _, clubID := CreateSampleClub(h.InitTest(t))
+ appAssert, eventUUID := CreateSampleEvent(appAssert, clubID, SampleEventFactory)
- updatedEvent := SampleEventFactory()
+ updatedEvent := SampleEventFactory(clubID)
(*updatedEvent)["name"] = "Updated Name"
(*updatedEvent)["preview"] = "Updated Preview"
@@ -452,15 +394,12 @@ func TestUpdateEventWorks(t *testing.T) {
}
func TestUpdateEventSeriesWorks(t *testing.T) {
- appAssert, eventUUIDs := CreateSampleEvent(h.InitTest(t), SampleSeriesFactory)
+ appAssert, _, clubID := CreateSampleClub(h.InitTest(t))
+ appAssert, eventUUIDs := CreateSampleEvent(appAssert, clubID, SampleSeriesFactory)
updatedSeries := &map[string]interface{}{
- "recurring_type": "daily",
- "max_occurrences": 5,
- "separation_count": 4,
- "day_of_week": 3,
- "week_of_month": 2,
- "day_of_month": 1,
+ "recurring_type": "daily",
+ "max_occurrences": 5,
"event_details": &map[string]interface{}{
"name": "eece test",
"preview": "the best class ever",
@@ -470,6 +409,7 @@ func TestUpdateEventSeriesWorks(t *testing.T) {
"location": "Richards 224",
"event_type": "open",
"is_recurring": true,
+ "host": clubID,
},
}
@@ -490,9 +430,10 @@ func TestUpdateEventSeriesWorks(t *testing.T) {
}
func TestUpdateEventFailsOnInvalidBody(t *testing.T) {
- appAssert, eventUUID := CreateSampleEvent(h.InitTest(t), SampleEventFactory)
+ appAssert, _, clubID := CreateSampleClub(h.InitTest(t))
+ appAssert, eventUUID := CreateSampleEvent(appAssert, clubID, SampleEventFactory)
- body := SampleEventFactory()
+ body := SampleEventFactory(clubID)
for _, invalidData := range []map[string]interface{}{
{"start_time": "Not a datetime"},
@@ -538,7 +479,7 @@ func TestUpdateEventFailsOnInvalidBody(t *testing.T) {
}
func TestUpdateEventFailsBadRequest(t *testing.T) {
- appAssert := h.InitTest(t)
+ appAssert, _, clubID := CreateSampleClub(h.InitTest(t))
badRequests := []string{
"0",
@@ -553,7 +494,7 @@ func TestUpdateEventFailsBadRequest(t *testing.T) {
h.TestRequest{
Method: fiber.MethodPatch,
Path: fmt.Sprintf("/api/v1/events/%s", badRequest),
- Body: SampleEventFactory(),
+ Body: SampleEventFactory(clubID),
Role: &models.Super,
},
errors.FailedToValidateID,
@@ -564,12 +505,14 @@ func TestUpdateEventFailsBadRequest(t *testing.T) {
}
func TestUpdateEventFailsOnEventIdNotExist(t *testing.T) {
+ eaa, _, clubID := CreateSampleClub(h.InitTest(t))
+
uuid := uuid.New()
- h.InitTest(t).TestOnErrorAndTester(h.TestRequest{
+ eaa.TestOnErrorAndTester(h.TestRequest{
Method: fiber.MethodPatch,
Path: fmt.Sprintf("/api/v1/events/%s", uuid),
- Body: SampleEventFactory(),
+ Body: SampleEventFactory(clubID),
Role: &models.Super,
TestUserIDReplaces: h.StringToPointer("user_id"),
},
@@ -587,7 +530,7 @@ func TestUpdateEventFailsOnEventIdNotExist(t *testing.T) {
}
func TestUpdateSeriesFailsBadRequest(t *testing.T) {
- appAssert := h.InitTest(t)
+ appAssert, _, clubID := CreateSampleClub(h.InitTest(t))
badRequests := []string{
"0",
@@ -602,7 +545,7 @@ func TestUpdateSeriesFailsBadRequest(t *testing.T) {
h.TestRequest{
Method: fiber.MethodPatch,
Path: fmt.Sprintf("/api/v1/events/%s/series", badRequest),
- Body: SampleEventFactory(),
+ Body: SampleEventFactory(clubID),
Role: &models.Super,
},
errors.FailedToValidateID,
@@ -613,7 +556,8 @@ func TestUpdateSeriesFailsBadRequest(t *testing.T) {
}
func AssertDeleteWorks(t *testing.T, factoryFunction EventFactory, requestPath string, tester h.Tester) {
- appAssert, eventUUIDs := CreateSampleEvent(h.InitTest(t), factoryFunction)
+ appAssert, _, clubID := CreateSampleClub(h.InitTest(t))
+ appAssert, eventUUIDs := CreateSampleEvent(appAssert, clubID, factoryFunction)
appAssert.TestOnStatusAndTester(
h.TestRequest{
@@ -642,7 +586,8 @@ func TestDeleteSeriesByEventIDWorks(t *testing.T) {
}
func AssertDeleteNotExistFails(t *testing.T, factoryFunction EventFactory, requestPath string, tester h.Tester, badUUID uuid.UUID) {
- appAssert, _ := CreateSampleEvent(h.InitTest(t), factoryFunction)
+ appAssert, _, clubID := CreateSampleClub(h.InitTest(t))
+ appAssert, _ = CreateSampleEvent(appAssert, clubID, factoryFunction)
appAssert.TestOnErrorAndTester(
h.TestRequest{
diff --git a/backend/tests/api/helpers/auth.go b/backend/tests/api/helpers/auth.go
index ab21ac2d0..c36778cb9 100644
--- a/backend/tests/api/helpers/auth.go
+++ b/backend/tests/api/helpers/auth.go
@@ -151,6 +151,8 @@ func SampleStudentFactory() (models.User, string) {
LastName: "Doe",
Email: "doe.jane@northeastern.edu",
PasswordHash: *hashedPassword,
+ Major0: models.ComputerScience,
+ Major1: models.Economics,
College: models.KCCS,
GraduationCycle: models.May,
GraduationYear: 2025,
@@ -166,6 +168,9 @@ func SampleStudentJSONFactory(sampleStudent models.User, rawPassword string) *ma
"last_name": sampleStudent.LastName,
"email": sampleStudent.Email,
"password": rawPassword,
+ "major0": string(sampleStudent.Major0),
+ "major1": string(sampleStudent.Major1),
+ "major2": string(sampleStudent.Major2),
"college": string(sampleStudent.College),
"graduation_cycle": string(sampleStudent.GraduationCycle),
"graduation_year": int(sampleStudent.GraduationYear),
diff --git a/backend/tests/api/user_test.go b/backend/tests/api/user_test.go
index c9df6c605..2db88c551 100644
--- a/backend/tests/api/user_test.go
+++ b/backend/tests/api/user_test.go
@@ -42,7 +42,10 @@ func TestGetUsersWorksForSuper(t *testing.T) {
eaa.Assert.Equal("SAC", respUser.FirstName)
eaa.Assert.Equal("Super", respUser.LastName)
eaa.Assert.Equal("generatesac@gmail.com", respUser.Email)
- eaa.Assert.Equal(models.College("KCCS"), respUser.College)
+ eaa.Assert.Equal(models.ComputerScience, respUser.Major0)
+ eaa.Assert.Equal(models.Major(""), respUser.Major1)
+ eaa.Assert.Equal(models.Major(""), respUser.Major2)
+ eaa.Assert.Equal(models.KCCS, respUser.College)
eaa.Assert.Equal(models.May, respUser.GraduationCycle)
eaa.Assert.Equal(int16(2025), respUser.GraduationYear)
@@ -95,6 +98,9 @@ func TestGetUserWorks(t *testing.T) {
eaa.Assert.Equal(sampleUser["first_name"].(string), respUser.FirstName)
eaa.Assert.Equal(sampleUser["last_name"].(string), respUser.LastName)
eaa.Assert.Equal(sampleUser["email"].(string), respUser.Email)
+ eaa.Assert.Equal(models.Major(sampleUser["major0"].(string)), respUser.Major0)
+ eaa.Assert.Equal(models.Major(sampleUser["major1"].(string)), respUser.Major1)
+ eaa.Assert.Equal(models.Major(sampleUser["major2"].(string)), respUser.Major2)
eaa.Assert.Equal(models.College(sampleUser["college"].(string)), respUser.College)
eaa.Assert.Equal(models.GraduationCycle(sampleUser["graduation_cycle"].(string)), respUser.GraduationCycle)
eaa.Assert.Equal(int16(sampleUser["graduation_year"].(int)), respUser.GraduationYear)
@@ -187,6 +193,9 @@ func TestUpdateUserWorks(t *testing.T) {
eaa.Assert.Equal(newFirstName, respUser.FirstName)
eaa.Assert.Equal(newLastName, respUser.LastName)
eaa.Assert.Equal((sampleStudentJSON)["email"].(string), respUser.Email)
+ eaa.Assert.Equal(models.Major((sampleStudentJSON)["major0"].(string)), respUser.Major0)
+ eaa.Assert.Equal(models.Major((sampleStudentJSON)["major1"].(string)), respUser.Major1)
+ eaa.Assert.Equal(models.Major((sampleStudentJSON)["major2"].(string)), respUser.Major2)
eaa.Assert.Equal(models.College((sampleStudentJSON)["college"].(string)), respUser.College)
eaa.Assert.Equal(models.GraduationCycle(sampleStudentJSON["graduation_cycle"].(string)), respUser.GraduationCycle)
eaa.Assert.Equal(int16(sampleStudentJSON["graduation_year"].(int)), respUser.GraduationYear)
@@ -199,6 +208,9 @@ func TestUpdateUserWorks(t *testing.T) {
eaa.Assert.Equal(dbUser.FirstName, respUser.FirstName)
eaa.Assert.Equal(dbUser.LastName, respUser.LastName)
eaa.Assert.Equal(dbUser.Email, respUser.Email)
+ eaa.Assert.Equal(dbUser.Major0, respUser.Major0)
+ eaa.Assert.Equal(dbUser.Major1, respUser.Major1)
+ eaa.Assert.Equal(dbUser.Major2, respUser.Major2)
eaa.Assert.Equal(dbUser.College, respUser.College)
eaa.Assert.Equal(dbUser.GraduationCycle, respUser.GraduationCycle)
eaa.Assert.Equal(dbUser.GraduationYear, respUser.GraduationYear)
@@ -367,6 +379,9 @@ func AssertUserWithIDBodyRespDB(eaa h.ExistingAppAssert, resp *http.Response, bo
eaa.Assert.Equal(dbUser.FirstName, respUser.FirstName)
eaa.Assert.Equal(dbUser.LastName, respUser.LastName)
eaa.Assert.Equal(dbUser.Email, respUser.Email)
+ eaa.Assert.Equal(dbUser.Major0, respUser.Major0)
+ eaa.Assert.Equal(dbUser.Major1, respUser.Major1)
+ eaa.Assert.Equal(dbUser.Major2, respUser.Major2)
eaa.Assert.Equal(dbUser.College, respUser.College)
eaa.Assert.Equal(dbUser.GraduationCycle, respUser.GraduationCycle)
eaa.Assert.Equal(dbUser.GraduationYear, respUser.GraduationYear)
@@ -380,6 +395,9 @@ func AssertUserWithIDBodyRespDB(eaa h.ExistingAppAssert, resp *http.Response, bo
eaa.Assert.Equal((*body)["first_name"].(string), dbUser.FirstName)
eaa.Assert.Equal((*body)["last_name"].(string), dbUser.LastName)
eaa.Assert.Equal((*body)["email"].(string), dbUser.Email)
+ eaa.Assert.Equal(models.Major((*body)["major0"].(string)), dbUser.Major0)
+ eaa.Assert.Equal(models.Major((*body)["major1"].(string)), dbUser.Major1)
+ eaa.Assert.Equal(models.Major((*body)["major2"].(string)), dbUser.Major2)
eaa.Assert.Equal(models.College((*body)["college"].(string)), dbUser.College)
eaa.Assert.Equal(models.GraduationCycle((*body)["graduation_cycle"].(string)), dbUser.GraduationCycle)
eaa.Assert.Equal(int16((*body)["graduation_year"].(int)), dbUser.GraduationYear)
@@ -463,7 +481,7 @@ func TestCreateUserFailsIfUserWithEmailAlreadyExists(t *testing.T) {
}
func AssertCreateBadDataFails(t *testing.T, jsonKey string, badValues []interface{}) {
- appAssert, _, _ := CreateSampleStudent(t, nil)
+ appAssert := h.InitTest(t)
sampleStudent, rawPassword := h.SampleStudentFactory()
@@ -480,7 +498,7 @@ func AssertCreateBadDataFails(t *testing.T, jsonKey string, badValues []interfac
},
h.ErrorWithTester{
Error: errors.FailedToValidateUser,
- Tester: TestNumUsersRemainsAt2,
+ Tester: TestNumUsersRemainsAt1,
},
)
}
@@ -502,15 +520,40 @@ func TestCreateUserFailsOnInvalidEmail(t *testing.T) {
}
func TestCreateUserFailsOnInvalidPassword(t *testing.T) {
- AssertCreateBadDataFails(t,
- "password",
- []interface{}{
- "",
- "foo",
- "abcdefg",
- "abcdefg0",
- "abcdefg@",
- })
+ appAssert := h.InitTest(t)
+
+ sampleStudent, rawPassword := h.SampleStudentFactory()
+
+ inputsWithDesiredErr := []struct {
+ Password string
+ Error errors.Error
+ }{
+ {"0", errors.InvalidPasswordNotLongEnough},
+ {"foo", errors.InvalidPasswordNotLongEnough},
+ {"abcdefgh", errors.InvalidPasswordNoDigit},
+ {"abcdefg0", errors.InvalidPasswordNoSpecialCharacter},
+ {"abcdefg@", errors.InvalidPasswordNoDigit},
+ }
+
+ for _, inputWithDesiredErr := range inputsWithDesiredErr {
+ sampleUserPermutation := *h.SampleStudentJSONFactory(sampleStudent, rawPassword)
+ sampleUserPermutation["password"] = inputWithDesiredErr.Password
+
+ appAssert = appAssert.TestOnErrorAndTester(
+ h.TestRequest{
+ Method: fiber.MethodPost,
+ Path: "/api/v1/users/",
+ Body: &sampleUserPermutation,
+ Role: &models.Super,
+ },
+ h.ErrorWithTester{
+ Error: inputWithDesiredErr.Error,
+ Tester: TestNumUsersRemainsAt1,
+ },
+ )
+ }
+
+ appAssert.Close()
}
func TestCreateUserFailsOnMissingFields(t *testing.T) {
diff --git a/cli/commands/clean_tests.go b/cli/commands/clean_tests.go
index a3923ce53..edc7955bd 100644
--- a/cli/commands/clean_tests.go
+++ b/cli/commands/clean_tests.go
@@ -7,6 +7,7 @@ import (
"os/user"
"sync"
+ "github.com/GenerateNU/sac/backend/src/config"
_ "github.com/lib/pq"
"github.com/urfave/cli/v2"
)
@@ -32,7 +33,12 @@ func ClearDBCommand() *cli.Command {
func CleanTestDBs(ctx context.Context) error {
fmt.Println("Cleaning test databases")
- db, err := sql.Open("postgres", CONFIG.Database.WithDb())
+ config, err := config.GetConfiguration(CONFIG, false)
+ if err != nil {
+ return err
+ }
+
+ db, err := sql.Open("postgres", config.Database.WithDb())
if err != nil {
return err
}
@@ -45,7 +51,7 @@ func CleanTestDBs(ctx context.Context) error {
}
query := "SELECT datname FROM pg_database WHERE datistemplate = false AND datname != 'postgres' AND datname != $1 AND datname != $2 AND datname LIKE 'sac_test_%';"
- rows, err := db.Query(query, currentUser.Username, CONFIG.Database.DatabaseName)
+ rows, err := db.Query(query, currentUser.Username, config.Database.DatabaseName)
if err != nil {
return err
}
diff --git a/cli/commands/config.go b/cli/commands/config.go
index e33a7f4ab..b7bb45e1a 100644
--- a/cli/commands/config.go
+++ b/cli/commands/config.go
@@ -3,7 +3,6 @@ package commands
import (
"path/filepath"
- "github.com/GenerateNU/sac/backend/src/config"
"github.com/GenerateNU/sac/cli/utils"
)
@@ -12,6 +11,6 @@ var (
FRONTEND_DIR = filepath.Join(ROOT_DIR, "/frontend")
BACKEND_DIR = filepath.Join(ROOT_DIR, "/backend")
BACKEND_SRC_DIR = filepath.Join(BACKEND_DIR, "/src")
- CONFIG, _ = config.GetConfiguration(filepath.Join(ROOT_DIR, "/config"), false)
+ CONFIG = filepath.Join(ROOT_DIR, "/config")
MIGRATION_FILE = filepath.Join(BACKEND_SRC_DIR, "/migrations/data.sql")
)
diff --git a/cli/commands/drop.go b/cli/commands/drop.go
index 3a34f1ae1..37d90980e 100644
--- a/cli/commands/drop.go
+++ b/cli/commands/drop.go
@@ -5,6 +5,7 @@ import (
"fmt"
"sync"
+ "github.com/GenerateNU/sac/backend/src/config"
"github.com/urfave/cli/v2"
)
@@ -52,7 +53,12 @@ func DropData() error {
dbMutex.Lock()
defer dbMutex.Unlock()
- db, err := sql.Open("postgres", CONFIG.Database.WithDb())
+ config, err := config.GetConfiguration(CONFIG, false)
+ if err != nil {
+ return err
+ }
+
+ db, err := sql.Open("postgres", config.Database.WithDb())
if err != nil {
return err
}
@@ -95,7 +101,12 @@ func DropDB() error {
dbMutex.Lock()
defer dbMutex.Unlock()
- db, err := sql.Open("postgres", CONFIG.Database.WithDb())
+ config, err := config.GetConfiguration(CONFIG, false)
+ if err != nil {
+ return err
+ }
+
+ db, err := sql.Open("postgres", config.Database.WithDb())
if err != nil {
return err
}
diff --git a/cli/commands/insert.go b/cli/commands/insert.go
index afbf4dec8..b3d40eb1c 100644
--- a/cli/commands/insert.go
+++ b/cli/commands/insert.go
@@ -6,6 +6,7 @@ import (
"os"
"os/exec"
+ "github.com/GenerateNU/sac/backend/src/config"
"github.com/lib/pq"
"github.com/urfave/cli/v2"
)
@@ -34,7 +35,12 @@ func InsertCommand() *cli.Command {
}
func InsertDB() error {
- db, err := sql.Open("postgres", CONFIG.Database.WithDb())
+ config, err := config.GetConfiguration(CONFIG, false)
+ if err != nil {
+ return err
+ }
+
+ db, err := sql.Open("postgres", config.Database.WithDb())
if err != nil {
return err
}
diff --git a/cli/commands/migrate.go b/cli/commands/migrate.go
index 12076ec44..88bd04b12 100644
--- a/cli/commands/migrate.go
+++ b/cli/commands/migrate.go
@@ -32,7 +32,7 @@ func MigrateCommand() *cli.Command {
func Migrate() error {
fmt.Println("Migrating database")
- goCmd := exec.Command("go", "run", "main.go", "--only-migrate")
+ goCmd := exec.Command("go", "run", "main.go", "--only-migrate", "--use-dev-dot-env=false")
goCmd.Dir = BACKEND_SRC_DIR
output, err := goCmd.CombinedOutput()
diff --git a/cli/commands/reset.go b/cli/commands/reset.go
index 2ca80e1f7..f6801d730 100644
--- a/cli/commands/reset.go
+++ b/cli/commands/reset.go
@@ -87,7 +87,7 @@ func ResetMigration() error {
err := DropDB()
if err != nil {
- return cli.Exit(err.Error(), 1)
+ return cli.Exit(fmt.Sprintf("Error dropping database: %s", err.Error()), 1)
}
cmd := exec.Command("sleep", "1")
@@ -100,7 +100,7 @@ func ResetMigration() error {
err = Migrate()
if err != nil {
- return cli.Exit(err.Error(), 1)
+ return cli.Exit(fmt.Sprintf("Error migrating database: %s", err.Error()), 1)
}
fmt.Println("Migration reset successfully")
diff --git a/cli/main.go b/cli/main.go
index 880a7598a..a81e5667b 100755
--- a/cli/main.go
+++ b/cli/main.go
@@ -10,7 +10,7 @@ import (
func main() {
app := &cli.App{
- Name: "sac-cli",
+ Name: "sac",
Usage: "CLI for the GenerateNU SAC",
Commands: []*cli.Command{
commands.SwaggerCommand(),
diff --git a/cli/utils/path.go b/cli/utils/path.go
index 01b9a6a1a..7ff98bf94 100644
--- a/cli/utils/path.go
+++ b/cli/utils/path.go
@@ -13,7 +13,7 @@ func GetRootDir() (string, error) {
return "", err
}
- // Find the closest directory containing "sac-cli" (the root directory)
+ // Find the closest directory containing "sac" (the root directory)
rootDir, err := FindRootDir(currentDir)
if err != nil {
return "", err
@@ -23,19 +23,19 @@ func GetRootDir() (string, error) {
}
func FindRootDir(dir string) (string, error) {
- // Check if "sac-cli" exists in the current directory
- mainGoPath := filepath.Join(dir, "sac-cli")
+ // Check if "sac" exists in the current directory
+ mainGoPath := filepath.Join(dir, "sac")
_, err := os.Stat(mainGoPath)
if err == nil {
- // "sac-cli" found, this is the root directory
+ // "sac" found, this is the root directory
return dir, nil
}
// If not found, go up one level
parentDir := filepath.Dir(dir)
if parentDir == dir {
- // Reached the top without finding "sac-cli"
- return "", fmt.Errorf("could not find root directory containing sac-cli")
+ // Reached the top without finding "sac"
+ return "", fmt.Errorf("could not find root directory containing sac")
}
// Recursively search in the parent directory
diff --git a/config/.env.template b/config/.env.template
index 4ed05577b..64e07fbf4 100644
--- a/config/.env.template
+++ b/config/.env.template
@@ -6,4 +6,4 @@ SAC_RESEND_API_KEY="SAC_RESEND_API_KEY"
SAC_AWS_BUCKET_NAME="SAC_AWS_BUCKET_NAME"
SAC_AWS_ID="SAC_AWS_ID"
SAC_AWS_SECRET="SAC_AWS_SECRET"
-SAC_AWS_REGION="SAC_AWS_REGION"
+SAC_AWS_REGION="SAC_AWS_REGION"
\ No newline at end of file
diff --git a/config/local.yml b/config/local.yml
index 994ae216c..8a88af9b2 100644
--- a/config/local.yml
+++ b/config/local.yml
@@ -13,4 +13,4 @@ superuser:
password: Password#!1
auth:
accesskey: g(r|##*?>\Qp}h37e+,T2
- refreshkey: amk*2!gG}1i"8D9RwJS$p
+ refreshkey: amk*2!gG}1i"8D9RwJS$p
\ No newline at end of file
diff --git a/frontend/sac-mobile/app/(app)/(tabs)/_components/club-homepage-card.tsx b/frontend/sac-mobile/app/(app)/(tabs)/_components/club-homepage-card.tsx
new file mode 100644
index 000000000..8b44d5b83
--- /dev/null
+++ b/frontend/sac-mobile/app/(app)/(tabs)/_components/club-homepage-card.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+import { Pressable, Text, View } from 'react-native';
+
+import { router } from 'expo-router';
+
+import { Club } from '@/types/club';
+
+export const ClubHomePageCard = ({ club }: { club: Club }) => {
+ return (
+ router.push(`/club/${club.id}`)}
+ >
+
+
+
+
+ {club.name}
+
+
+ {club.description}
+
+
+
+
+ );
+};
diff --git a/frontend/sac-mobile/app/(app)/(tabs)/_components/event-homepage-card.tsx b/frontend/sac-mobile/app/(app)/(tabs)/_components/event-homepage-card.tsx
new file mode 100644
index 000000000..f81b9c89c
--- /dev/null
+++ b/frontend/sac-mobile/app/(app)/(tabs)/_components/event-homepage-card.tsx
@@ -0,0 +1,73 @@
+import React from 'react';
+import { Pressable, Text, View } from 'react-native';
+
+import { router } from 'expo-router';
+
+import Pin from '@/assets/images/svg/pin.svg';
+import Time from '@/assets/images/svg/time.svg';
+import { useEventHosts } from '@/hooks/use-event';
+import { Event } from '@/types/event';
+
+const EventHomePageCard = ({ event }: { event: Event }) => {
+ const { data: hosts, isLoading, error } = useEventHosts(event.id);
+
+ if (isLoading) {
+ return Loading...;
+ }
+
+ if (error) {
+ return Error: {error.message};
+ }
+
+ return (
+ router.push(`/event/${event.id}`)}
+ >
+
+
+
+
+
+
+ {hosts?.[0].name ?? 'No Hosts'}
+
+
+ {event.name}
+
+
+
+
+ {event.location}
+
+
+
+
+
+ {new Date(
+ event.start_time
+ ).toLocaleDateString('en-US', {
+ month: 'long',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: 'numeric'
+ })}
+
+
+
+
+
+ {event.preview}
+
+
+
+
+
+ );
+};
+
+export { EventHomePageCard };
diff --git a/frontend/sac-mobile/app/(app)/(tabs)/_components/faq-homepage-card.tsx b/frontend/sac-mobile/app/(app)/(tabs)/_components/faq-homepage-card.tsx
new file mode 100644
index 000000000..f84763413
--- /dev/null
+++ b/frontend/sac-mobile/app/(app)/(tabs)/_components/faq-homepage-card.tsx
@@ -0,0 +1,103 @@
+import React from 'react';
+import { Controller, useForm } from 'react-hook-form';
+import { Alert, Pressable, Text, View } from 'react-native';
+
+import { ZodError, z } from 'zod';
+
+import { Error } from '@/components/error';
+import { Input } from '@/components/input';
+import { FAQ } from '@/types/faq';
+
+type FAQData = {
+ question: string;
+};
+
+const FAQSchema = z.object({
+ question: z.string()
+});
+
+const FAQHomePageCard = ({ faq }: { faq: FAQ }) => {
+ const length = () => {
+ if (faq.club_name.length > 11) {
+ return faq.club_name.substring(0, 11) + '...';
+ } else {
+ return faq.club_name;
+ }
+ };
+
+ const {
+ control,
+ handleSubmit,
+ formState: { errors },
+ reset
+ } = useForm();
+
+ const onSubmit = ({ question }: FAQData) => {
+ try {
+ FAQSchema.parse({ question });
+ Alert.alert('Form Submitted', JSON.stringify(question));
+ reset();
+ } catch (error) {
+ if (error instanceof ZodError) {
+ Alert.alert('Validation Error', error.errors[0].message);
+ } else {
+ console.error('An unexpected error occurred:', error);
+ }
+ }
+ };
+
+ return (
+
+
+
+
+
+ {faq.club_name}
+
+
+ Frequently Asked
+
+
+ Questions
+
+
+
+
+ Question:
+ {faq.question}
+ Answer:
+
+ {faq.answer}
+
+
+ (
+
+ )}
+ name="question"
+ rules={{
+ required: 'Question is required'
+ }}
+ />
+ {errors.question && (
+
+
+
+ )}
+
+
+
+ );
+};
+
+export { FAQHomePageCard };
diff --git a/frontend/sac-mobile/app/(app)/(tabs)/_components/following-header.tsx b/frontend/sac-mobile/app/(app)/(tabs)/_components/following-header.tsx
new file mode 100644
index 000000000..31b6c04a8
--- /dev/null
+++ b/frontend/sac-mobile/app/(app)/(tabs)/_components/following-header.tsx
@@ -0,0 +1,93 @@
+import React from 'react';
+import {
+ FlatList,
+ Pressable,
+ Text,
+ TouchableOpacity,
+ View
+} from 'react-native';
+
+import { Stack, router } from 'expo-router';
+
+import { useUserFollowing } from '@/hooks/use-user';
+import { Club } from '@/types/club';
+
+const ViewAll = ({ isFollowingAny }: { isFollowingAny: boolean }) => {
+ return (
+ <>
+ {isFollowingAny && (
+
+ router.push('/(app)/following/')}
+ className="flex-row items-center"
+ >
+ View all
+
+
+ )}
+ >
+ );
+};
+
+const FollowingHeader = ({ id }: { id: string }) => {
+ const { data: userFollowingData, isLoading, error } = useUserFollowing(id);
+
+ if (isLoading) {
+ return Loading...;
+ }
+
+ if (error) {
+ return Error: {error.message};
+ }
+
+ const renderFollowing = ({ item }: { item: Club }) => {
+ return (
+ router.push(`/club/${item.id}`)}
+ >
+
+
+ {item.name}
+
+
+ );
+ };
+
+ if (!userFollowingData) {
+ return null;
+ }
+
+ return (
+ <>
+ {
+ return (
+ 0}
+ />
+ );
+ }
+ }}
+ />
+
+
+ item.id.toString()}
+ />
+
+ >
+ );
+};
+
+export { FollowingHeader };
diff --git a/frontend/sac-mobile/app/(app)/(tabs)/_components/homepage.tsx b/frontend/sac-mobile/app/(app)/(tabs)/_components/homepage.tsx
new file mode 100644
index 000000000..2185710ef
--- /dev/null
+++ b/frontend/sac-mobile/app/(app)/(tabs)/_components/homepage.tsx
@@ -0,0 +1,70 @@
+import React from 'react';
+import { FlatList, Text, View } from 'react-native';
+
+import { useClubs } from '@/hooks/use-club';
+import { useEvents } from '@/hooks/use-event';
+import { HomepageFAQ } from '@/lib/const';
+import { isClub, isEvent, isFAQ } from '@/lib/utils';
+import { Club } from '@/types/club';
+import { Event } from '@/types/event';
+import { FAQ } from '@/types/faq';
+
+import { ClubHomePageCard } from './club-homepage-card';
+import { EventHomePageCard } from './event-homepage-card';
+import { FAQHomePageCard } from './faq-homepage-card';
+
+export type HomepageItem = Event | Club | FAQ;
+
+const HomepageList = () => {
+ const { data: events, isLoading: eIsLoading, error: eError } = useEvents();
+ const { data: clubs, isLoading: cIsLoading, error: cError } = useClubs();
+
+ if (eIsLoading || cIsLoading) {
+ return Loading...;
+ }
+
+ if (eError || cError) {
+ return Error: {eError?.message || cError?.message};
+ }
+
+ const getHomepageItems = () => {
+ const allItems: HomepageItem[] = [];
+ if (events !== undefined) {
+ allItems.push(...events);
+ }
+ if (clubs !== undefined) {
+ allItems.push(...clubs);
+ }
+ allItems.push(...HomepageFAQ);
+ return allItems.sort(() => Math.random() - 0.5);
+ };
+
+ const renderItems = (items: HomepageItem[]) => {
+ return items.map((item: HomepageItem, index) => {
+ if (isClub(item)) {
+ const club = item as Club;
+ if (club.name === 'SAC') {
+ return null;
+ }
+ return ;
+ } else if (isEvent(item)) {
+ return ;
+ } else if (isFAQ(item)) {
+ return ;
+ }
+ });
+ };
+
+ return (
+
+ <>{item}>}
+ keyExtractor={(_, index) => index.toString()}
+ showsVerticalScrollIndicator={false}
+ />
+
+ );
+};
+
+export { HomepageList };
diff --git a/frontend/sac-mobile/app/(app)/(tabs)/_layout.tsx b/frontend/sac-mobile/app/(app)/(tabs)/_layout.tsx
index 1f1bdf094..c8ce473ab 100644
--- a/frontend/sac-mobile/app/(app)/(tabs)/_layout.tsx
+++ b/frontend/sac-mobile/app/(app)/(tabs)/_layout.tsx
@@ -1,24 +1,18 @@
import React, { useEffect } from 'react';
+import { View } from 'react-native';
import { Tabs } from 'expo-router';
-import { MaterialCommunityIcons } from '@expo/vector-icons';
-
+import HomeSelectedIcon from '@/assets/images/svg/home-selected.svg';
+import HomeIcon from '@/assets/images/svg/home.svg';
+import ProfileSelectedIcon from '@/assets/images/svg/profile-selected.svg';
+import ProfileIcon from '@/assets/images/svg/profile.svg';
+import SearchSelectedIcon from '@/assets/images/svg/search-selected.svg';
+import SearchIcon from '@/assets/images/svg/search.svg';
+import { Wordmark } from '@/components/wordmark';
import { useAuthStore } from '@/hooks/use-auth';
import { useAuth } from '@/context/AuthContext';
-const HomeTabBarIcon = ({ color }: { color: string }) => (
-
-);
-
-const ProfileTabBarIcon = ({ color }: { color: string }) => (
-
-);
-
-const SearchTabBarIcon = ({ color }: { color: string }) => (
-
-);
-
const Layout = () => {
// const { isLoggedIn, fetchUser } = useAuthStore();
const { initialized, getUser } = useAuth();
@@ -28,31 +22,40 @@ const Layout = () => {
}, [initialized]);
return (
-
+
(
+
+ ),
+ headerLeft: () => (
+
+
+
+ ),
+ headerShown: true,
+ tabBarIcon: ({ focused }) =>
+ focused ? :
}}
- redirect={!isLoggedIn}
/>
+ focused ? :
}}
- redirect={!isLoggedIn}
/>
+ focused ? :
}}
// redirect={!isLoggedIn}
/>
diff --git a/frontend/sac-mobile/app/(app)/(tabs)/index.tsx b/frontend/sac-mobile/app/(app)/(tabs)/index.tsx
index f79b705cf..805129222 100644
--- a/frontend/sac-mobile/app/(app)/(tabs)/index.tsx
+++ b/frontend/sac-mobile/app/(app)/(tabs)/index.tsx
@@ -1,79 +1,42 @@
import React from 'react';
-import { FlatList, Text } from 'react-native';
-import { SafeAreaView } from 'react-native-safe-area-context';
+import { SectionList, View } from 'react-native';
-import { Link } from 'expo-router';
+import { StatusBar } from 'expo-status-bar';
-import { clubs } from '@/data/clubs';
+import { HomepageList } from '@/app/(app)/(tabs)/_components/homepage';
import { useAuthStore } from '@/hooks/use-auth';
-import { useEvents } from '@/hooks/use-event';
-import { Club } from '@/types/club';
-import { Event } from '@/types/event';
-<<<<<<< Updated upstream
-const Home = () => {
- const { user } = useAuthStore();
- const { data: events, isLoading, error } = useEvents();
-
- if (isLoading) {
- return Loading...;
- }
-
- if (error) {
- return Error: {error.message};
- }
-
- const renderEvent = ({ item: event }: { item: Event }) => (
-
- {event.name}
-
- );
-
- const renderClub = ({ item: club }: { item: Club }) => (
-
- {club.name}
-
- );
-=======
import { FollowingHeader } from './_components/following-header';
import { useAuth } from '@/context/AuthContext';
+
const HomePage = () => {
- // const { user } = useAuthStore();
- const { user } = useAuth();
->>>>>>> Stashed changes
+ const { user } = useAuthStore();
return (
-
- Welcome {user?.first_name}
- item.id.toString()}
- />
- item.id.toString()}
+ <>
+
+
+ ]
+ },
+ {
+ data: []
+ }
+ ]}
+ keyExtractor={(_, index) => index.toString()}
+ renderItem={({ item }) => <>{item}>}
+ renderSectionHeader={() => }
+ stickySectionHeadersEnabled={false}
/>
-
+ >
);
};
-export default Home;
+export default HomePage;
diff --git a/frontend/sac-mobile/app/(app)/(tabs)/profile.tsx b/frontend/sac-mobile/app/(app)/(tabs)/profile.tsx
index 77ec40ddb..2bf332b4d 100644
--- a/frontend/sac-mobile/app/(app)/(tabs)/profile.tsx
+++ b/frontend/sac-mobile/app/(app)/(tabs)/profile.tsx
@@ -1,35 +1,24 @@
import React from 'react';
-import { View } from 'react-native';
+import { SafeAreaView, Text, View } from 'react-native';
import { Button } from '@/components/button';
import { useAuthStore } from '@/hooks/use-auth';
import { useAuth } from '@/context/AuthContext';
const Profile = () => {
-<<<<<<< Updated upstream
- const { signOut } = useAuthStore();
-=======
// const { signOut, user } = useAuthStore();
const { onLogout, user } = useAuth();
->>>>>>> Stashed changes
-
const handleSignOut = async () => {
onLogout!();
};
return (
-<<<<<<< Updated upstream
-
-
-
-=======
Welcome {user?.first_name}
->>>>>>> Stashed changes
);
};
diff --git a/frontend/sac-mobile/app/(app)/(tabs)/search.tsx b/frontend/sac-mobile/app/(app)/(tabs)/search.tsx
index 55f256958..89b989329 100644
--- a/frontend/sac-mobile/app/(app)/(tabs)/search.tsx
+++ b/frontend/sac-mobile/app/(app)/(tabs)/search.tsx
@@ -1,11 +1,23 @@
import React from 'react';
-import { Text, View } from 'react-native';
+import { SafeAreaView, ScrollView, Text, View } from 'react-native';
+import { TextInput } from 'react-native-gesture-handler';
+
+import { Button } from '@/components/button';
const Search = () => {
return (
-
- Search
-
+
+
+ Search
+
+
+
+
+
+
);
};
diff --git a/frontend/sac-mobile/app/(app)/_components/club-kebab.tsx b/frontend/sac-mobile/app/(app)/_components/club-kebab.tsx
new file mode 100644
index 000000000..70de0ac3f
--- /dev/null
+++ b/frontend/sac-mobile/app/(app)/_components/club-kebab.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import { Platform } from 'react-native';
+
+import { NativeActionEvent } from '@react-native-menu/menu';
+
+import { Kebab } from '@/components/kebab';
+
+const ClubKebab = () => {
+ const items = [
+ {
+ id: 'share',
+ title: 'Share Club',
+ image: Platform.select({
+ ios: 'square.and.arrow.up',
+ android: 'share-variant'
+ })
+ },
+ {
+ id: 'report',
+ title: 'Report Club',
+ image: Platform.select({
+ ios: 'person.crop.circle.badge.exclamationmark.fill',
+ android: 'person-circle-outline'
+ })
+ }
+ ];
+
+ const onPress = ({ nativeEvent }: NativeActionEvent) => {
+ console.warn(JSON.stringify(nativeEvent));
+ };
+
+ return ;
+};
+
+export { ClubKebab };
diff --git a/frontend/sac-mobile/app/(app)/_components/event-kebab.tsx b/frontend/sac-mobile/app/(app)/_components/event-kebab.tsx
new file mode 100644
index 000000000..05c8a295a
--- /dev/null
+++ b/frontend/sac-mobile/app/(app)/_components/event-kebab.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import { Platform } from 'react-native';
+
+import { NativeActionEvent } from '@react-native-menu/menu';
+
+import { Kebab } from '@/components/kebab';
+
+const EventKabab = () => {
+ const items = [
+ {
+ id: 'share',
+ title: 'Share Event',
+ image: Platform.select({
+ ios: 'square.and.arrow.up',
+ android: 'share-variant'
+ })
+ },
+ {
+ id: 'report',
+ title: 'Report Event',
+ image: Platform.select({
+ ios: 'person.crop.circle.badge.exclamationmark.fill',
+ android: 'person-circle-outline'
+ })
+ }
+ ];
+
+ const onPress = ({ nativeEvent }: NativeActionEvent) => {
+ console.warn(JSON.stringify(nativeEvent));
+ };
+
+ return ;
+};
+
+export { EventKabab };
diff --git a/frontend/sac-mobile/app/(app)/_layout.tsx b/frontend/sac-mobile/app/(app)/_layout.tsx
index 3071cb8e8..0f0a0e82f 100644
--- a/frontend/sac-mobile/app/(app)/_layout.tsx
+++ b/frontend/sac-mobile/app/(app)/_layout.tsx
@@ -1,13 +1,13 @@
import React from 'react';
-import { Platform, View } from 'react-native';
+import { View } from 'react-native';
import { Stack } from 'expo-router';
-import { MaterialCommunityIcons } from '@expo/vector-icons';
-import { MenuView } from '@react-native-menu/menu';
-
import { LeftArrow } from '@/components/left-arrow';
+import { ClubKebab } from './_components/club-kebab';
+import { EventKabab } from './_components/event-kebab';
+
const Layout = () => {
return (
@@ -15,8 +15,6 @@ const Layout = () => {
{
),
headerLeft: () => ,
- headerRight: () => {
- return (
- {
- console.warn(JSON.stringify(nativeEvent));
- }}
- actions={[
- {
- id: 'share',
- title: 'Share Event',
- image: Platform.select({
- ios: 'square.and.arrow.up',
- android: 'share-variant'
- })
- },
- {
- id: 'report',
- title: 'Report Event',
- image: Platform.select({
- ios: 'person.crop.circle.badge.exclamationmark.fill',
- android: 'person-circle-outline'
- })
- }
- ]}
- >
-
-
- );
- }
+ headerRight: () =>
}}
/>
{
),
headerLeft: () => ,
- headerRight: () => {
- return (
- {
- console.warn(JSON.stringify(nativeEvent));
- }}
- actions={[
- {
- id: 'share',
- title: 'Share Club',
- image: Platform.select({
- ios: 'square.and.arrow.up',
- android: 'share-variant'
- })
- },
- {
- id: 'report',
- title: 'Report Club',
- image: Platform.select({
- ios: 'person.crop.circle.badge.exclamationmark.fill',
- android: 'person-circle-outline'
- })
- }
- ]}
- >
-
-
- );
- }
+ headerRight: () =>
+ }}
+ />
+ (
+
+ ),
+ headerLeft: () =>
}}
/>
diff --git a/frontend/sac-mobile/app/(app)/club/[id].tsx b/frontend/sac-mobile/app/(app)/club/[id].tsx
index 5c2e6a67a..c1cc4b510 100644
--- a/frontend/sac-mobile/app/(app)/club/[id].tsx
+++ b/frontend/sac-mobile/app/(app)/club/[id].tsx
@@ -1,19 +1,40 @@
import React from 'react';
-import { SafeAreaView, Text } from 'react-native';
+import { Text, View } from 'react-native';
-import { Link, Stack, useLocalSearchParams } from 'expo-router';
+import { Stack, useLocalSearchParams } from 'expo-router';
+import { StatusBar } from 'expo-status-bar';
+
+import { useClub } from '@/hooks/use-club';
const ClubPage = () => {
const { id } = useLocalSearchParams<{ id: string }>();
+ const { data: club, isLoading, error } = useClub(id);
+
+ if (isLoading) {
+ return Loading...;
+ }
+
+ if (error) {
+ return Error: {error.message};
+ }
+
+ if (!club) {
+ return Club not found;
+ }
+
return (
-
-
- ClubPage
-
- FAQ
-
-
+ <>
+
+
+
+ {club.name}
+ {club.description}
+ {club.num_members}
+ {club.recruitment_cycle}
+ {club.application_link}
+
+ >
);
};
diff --git a/frontend/sac-mobile/app/(app)/club/_layout.tsx b/frontend/sac-mobile/app/(app)/club/_layout.tsx
index f16dcf3fe..beb0de35a 100644
--- a/frontend/sac-mobile/app/(app)/club/_layout.tsx
+++ b/frontend/sac-mobile/app/(app)/club/_layout.tsx
@@ -10,6 +10,12 @@ const Layout = () => {
name="faq/[id]"
options={{ headerShown: false, presentation: 'modal' }}
/>
+ {/* */}
);
};
diff --git a/frontend/sac-mobile/app/(app)/event/[id].tsx b/frontend/sac-mobile/app/(app)/event/[id].tsx
index 84e5a7fa8..3097c630d 100644
--- a/frontend/sac-mobile/app/(app)/event/[id].tsx
+++ b/frontend/sac-mobile/app/(app)/event/[id].tsx
@@ -1,29 +1,33 @@
-import React from 'react';
+import React, { useRef } from 'react';
import { ScrollView, Text, View } from 'react-native';
import { Stack, useLocalSearchParams } from 'expo-router';
+import { StatusBar } from 'expo-status-bar';
+
+import BottomSheet from '@gorhom/bottom-sheet';
import { AllHosts } from '@/app/(app)/event/_components/all-hosts';
import { TagList } from '@/components/all-tags';
import { Button } from '@/components/button';
import { Description } from '@/components/description';
-import { useAuthStore } from '@/hooks/use-auth';
+import { Title } from '@/components/title';
import { useEvent } from '@/hooks/use-event';
-import { Title } from '../_components/title';
import { EventHeader } from './_components/event-header';
import { EventLocation } from './_components/event-location';
import { EventTime } from './_components/event-time';
import { HostNames } from './_components/host-names';
import { LocationView } from './_components/location-view';
+import { RSVPBottomSheet } from './_components/rsvp-bottom-sheet';
// TODO: handle link OR location
const EventPage = () => {
const { id } = useLocalSearchParams<{ id: string }>();
- const { user } = useAuthStore();
const { data: event, isLoading, error } = useEvent(id);
+ const ref = useRef(null);
+
if (error) {
console.error(error);
return Error fetching event;
@@ -37,18 +41,15 @@ const EventPage = () => {
return Event not found;
}
- console.log('[event]', event);
-
return (
<>
-
+
+
-
+
@@ -84,6 +85,7 @@ const EventPage = () => {
+
>
);
};
diff --git a/frontend/sac-mobile/app/(app)/event/_components/event-header.tsx b/frontend/sac-mobile/app/(app)/event/_components/event-header.tsx
index 094661de5..6003a6d75 100644
--- a/frontend/sac-mobile/app/(app)/event/_components/event-header.tsx
+++ b/frontend/sac-mobile/app/(app)/event/_components/event-header.tsx
@@ -1,10 +1,21 @@
+import { forwardRef } from 'react';
import { Platform, View } from 'react-native';
+import BottomSheet from '@gorhom/bottom-sheet';
import { MenuView } from '@react-native-menu/menu';
import { Button } from '@/components/button';
-const EventHeader = () => {
+type Ref = BottomSheet;
+
+const EventHeader = forwardRef[((_, ref) => {
+ if (!ref) {
+ return null;
+ }
+
+ // @ts-ignore
+ const handleOpenPress = () => ref.current?.snapToIndex(0);
+
return (
@@ -51,9 +62,18 @@ const EventHeader = () => {
RSVP
+
+
);
-};
+});
export { EventHeader };
diff --git a/frontend/sac-mobile/app/(app)/event/_components/location-view.tsx b/frontend/sac-mobile/app/(app)/event/_components/location-view.tsx
index 647a6ef61..abc882879 100644
--- a/frontend/sac-mobile/app/(app)/event/_components/location-view.tsx
+++ b/frontend/sac-mobile/app/(app)/event/_components/location-view.tsx
@@ -1,9 +1,8 @@
import React from 'react';
-import { Image, Linking, Text, TouchableOpacity, View } from 'react-native';
+import { Linking, Text, TouchableOpacity, View } from 'react-native';
+import MapView from 'react-native-maps';
import { createOpenLink } from 'react-native-open-maps';
-import { useAssets } from 'expo-asset';
-
import { Button } from '@/components/button';
type LocationViewProps = {
@@ -12,16 +11,9 @@ type LocationViewProps = {
};
const LocationView = ({ location, meetingLink }: LocationViewProps) => {
- const [assets, error] = useAssets([
- require('@/assets/images/placeholder_location.png')
- ]);
const coordinates = { latitude: 42.3393326, longitude: -71.0869942 };
const openMap = createOpenLink({ ...coordinates });
- if (error) {
- console.error(error);
- }
-
return (
<>
@@ -52,13 +44,15 @@ const LocationView = ({ location, meetingLink }: LocationViewProps) => {
)}
- {assets ? (
-
- ) : null}
+
>
);
diff --git a/frontend/sac-mobile/app/(app)/event/_components/rsvp-bottom-sheet.tsx b/frontend/sac-mobile/app/(app)/event/_components/rsvp-bottom-sheet.tsx
new file mode 100644
index 000000000..c11fc4795
--- /dev/null
+++ b/frontend/sac-mobile/app/(app)/event/_components/rsvp-bottom-sheet.tsx
@@ -0,0 +1,56 @@
+import React, { forwardRef, useCallback } from 'react';
+import { Text, View } from 'react-native';
+
+import BottomSheet, { BottomSheetBackdrop } from '@gorhom/bottom-sheet';
+
+import { Button } from '@/components/button';
+
+type Ref = BottomSheet;
+
+const RSVPBottomSheet = forwardRef][((_, ref) => {
+ const snapPoints = ['35%'];
+
+ const renderBackdrop = useCallback(
+ (props: any) => (
+
+ ),
+ []
+ );
+
+ // @ts-ignore
+ const handleClosePress = () => ref.current?.close();
+
+ return (
+
+
+ Save Event
+
+ By saving this event, you are automatically signed up for
+ notifications and this event will be added to your calendar.
+
+
+
+
+
+
+ );
+});
+
+export { RSVPBottomSheet };
diff --git a/frontend/sac-mobile/app/(app)/tag/[id].tsx b/frontend/sac-mobile/app/(app)/tag/[id].tsx
new file mode 100644
index 000000000..426581a43
--- /dev/null
+++ b/frontend/sac-mobile/app/(app)/tag/[id].tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import { Text, View } from 'react-native';
+
+const TagPage = () => {
+ return (
+
+ TagPage
+
+ );
+};
+
+export default TagPage;
diff --git a/frontend/sac-mobile/app/(auth)/_components/code.tsx b/frontend/sac-mobile/app/(auth)/_components/code.tsx
new file mode 100644
index 000000000..142090c43
--- /dev/null
+++ b/frontend/sac-mobile/app/(auth)/_components/code.tsx
@@ -0,0 +1,67 @@
+import React from 'react';
+import { useState } from 'react';
+import { Text, View } from 'react-native';
+import {
+ CodeField,
+ Cursor,
+ useBlurOnFulfill,
+ useClearByFocusCell
+} from 'react-native-confirmation-code-field';
+
+import { Button } from '@/components/button';
+
+const CELL_COUNT = 6;
+
+type CodeProps = {
+ onCompleted: (code: string) => void;
+};
+
+const Code = ({ onCompleted }: CodeProps) => {
+ const [value, setValue] = useState('');
+ const ref = useBlurOnFulfill({ value, cellCount: CELL_COUNT });
+ const [props, getCellOnLayoutHandler] = useClearByFocusCell({
+ value,
+ setValue
+ });
+
+ return (
+
+ (
+
+
+ {symbol || (isFocused ? : null)}
+
+
+ )}
+ />
+
+
+ );
+};
+
+export default Code;
diff --git a/frontend/sac-mobile/app/(auth)/_components/login-form.tsx b/frontend/sac-mobile/app/(auth)/_components/login-form.tsx
index 116753cb3..970e9cf84 100644
--- a/frontend/sac-mobile/app/(auth)/_components/login-form.tsx
+++ b/frontend/sac-mobile/app/(auth)/_components/login-form.tsx
@@ -42,12 +42,7 @@ const LoginForm = () => {
try {
const validData = loginSchema.parse(loginData);
console.log({ validData });
-<<<<<<< Updated upstream
- await signIn(validData.email, validData.password);
- // router.replace('/(app)/(tabs)/');
-=======
await onLogin!(validData.email, validData.password);
->>>>>>> Stashed changes
} catch (err: any) {
if (err instanceof ZodError) {
Alert.alert(err.errors[0].message);
diff --git a/frontend/sac-mobile/app/(auth)/_components/registration-form.tsx b/frontend/sac-mobile/app/(auth)/_components/registration-form.tsx
index 9651c1f44..30657ebfa 100644
--- a/frontend/sac-mobile/app/(auth)/_components/registration-form.tsx
+++ b/frontend/sac-mobile/app/(auth)/_components/registration-form.tsx
@@ -53,13 +53,8 @@ const RegistrationForm = () => {
try {
registerSchema.parse({ password_confirmation, ...rest });
const updatedData = { ...rest };
-<<<<<<< Updated upstream
-
- await signUp(updatedData);
-=======
console.log(updatedData);
await onRegister!(updatedData);
->>>>>>> Stashed changes
} catch (error: any) {
if (error instanceof ZodError) {
Alert.alert(error.errors[0].message);
@@ -147,7 +142,7 @@ const RegistrationForm = () => {
{
render={({ field: { onChange, value } }) => (
{
>>>>>> Stashed changes
const verificationSchema = z.object({
code: z.string().min(6, {
@@ -29,29 +23,21 @@ const verificationSchema = z.object({
});
const VerificationForm = () => {
-<<<<<<< Updated upstream
const {
control,
handleSubmit,
formState: { errors }
} = useForm();
- const { user, completeVerification } = useAuthStore();
-=======
// const { user, completeVerification } = useAuthStore();
const { user, onVerify } = useAuth();
->>>>>>> Stashed changes
const [loading, setLoading] = useState(false);
useEffect(() => {
console.log('[verification-form]', { user });
}, [user]);
-<<<<<<< Updated upstream
- const onVerify = async ({ code }: VerificationFormData) => {
-=======
const onPressVerify = async (code: string) => {
->>>>>>> Stashed changes
setLoading(true);
try {
@@ -80,42 +66,19 @@ const VerificationForm = () => {
return (
<>
{loading && }
-<<<<<<< Updated upstream
-
- (
-
- )}
- name="code"
- rules={{
- required: 'Verification code is required'
- }}
- />
- {errors.code && }
-
-
-
-
-=======
]
->>>>>>> Stashed changes
- Did not receive a code?
+
+ Didn't receive the code?
+
- Resend
+
+ {' '}
+ Resend
+
>
diff --git a/frontend/sac-mobile/app/(auth)/_layout.tsx b/frontend/sac-mobile/app/(auth)/_layout.tsx
index 9dc36228e..70d3b7338 100644
--- a/frontend/sac-mobile/app/(auth)/_layout.tsx
+++ b/frontend/sac-mobile/app/(auth)/_layout.tsx
@@ -1,8 +1,12 @@
import React from 'react';
+import { View } from 'react-native';
-import { Stack } from 'expo-router';
+import { Stack, router } from 'expo-router';
-const AuthLayout = () => {
+import { Button } from '@/components/button';
+import { Wordmark } from '@/components/wordmark';
+
+const Layout = () => {
return (
{
options={{
title: '',
headerShown: true,
+ animationTypeForReplace: 'pop',
+ animation: 'slide_from_left',
+ statusBarColor: 'dark',
+ headerLeft: () => ,
+ headerBackground: () => (
+
+ )
+ }}
+ />
+ ,
+ headerBackground: () => (
+
+ ),
animationTypeForReplace: 'push',
- statusBarColor: 'dark'
+ animation: 'slide_from_right'
+ }}
+ />
+ ,
+ headerRight: () => (
+
+ ),
+ headerBackground: () => (
+
+ ),
+ animationTypeForReplace: 'pop',
+ animation: 'slide_from_left'
}}
/>
-
-
null,
+ headerBackground: () =>
+ }}
+ />
+ ,
+ headerBackground: () => (
+
+ )
+ }}
/>
,
+ headerBackground: () => (
+
+ )
+ }}
/>
);
};
-export default AuthLayout;
+export default Layout;
diff --git a/frontend/sac-mobile/app/(auth)/login.tsx b/frontend/sac-mobile/app/(auth)/login.tsx
index 8e106bab0..9671211fd 100644
--- a/frontend/sac-mobile/app/(auth)/login.tsx
+++ b/frontend/sac-mobile/app/(auth)/login.tsx
@@ -1,20 +1,17 @@
import React from 'react';
-import { Text, View } from 'react-native';
-import { SafeAreaView } from 'react-native-safe-area-context';
-
-import { Wordmark } from '@/components/wordmark';
+import { Keyboard, Pressable, Text, View } from 'react-native';
import { LoginForm } from './_components/login-form';
const Login = () => {
return (
-
+ Keyboard.dismiss()}
+ >
-
-
-
-
+
Let's go
@@ -36,7 +33,7 @@ const Login = () => {
-
+
);
};
diff --git a/frontend/sac-mobile/app/(auth)/register.tsx b/frontend/sac-mobile/app/(auth)/register.tsx
index db5ec52d5..ad215021a 100644
--- a/frontend/sac-mobile/app/(auth)/register.tsx
+++ b/frontend/sac-mobile/app/(auth)/register.tsx
@@ -1,44 +1,37 @@
import React from 'react';
-import { ScrollView, Text, View } from 'react-native';
-import { SafeAreaView } from 'react-native-safe-area-context';
-
-import { router } from 'expo-router';
-
-import { Button } from '@/components/button';
-import { Wordmark } from '@/components/wordmark';
+import {
+ KeyboardAvoidingView,
+ Platform,
+ ScrollView,
+ Text,
+ View
+} from 'react-native';
import { RegistrationForm } from './_components/registration-form';
const Register = () => {
return (
-
-
-
-
-
-
-
-
+
+
+
+
Sign up
-
+
Discover, follow, and join all the clubs & events
Northeastern has to offer
-
-
-
+
+
+
-
+
);
};
diff --git a/frontend/sac-mobile/app/(auth)/user-details.tsx b/frontend/sac-mobile/app/(auth)/user-details.tsx
index 40da8df67..db52f6881 100644
--- a/frontend/sac-mobile/app/(auth)/user-details.tsx
+++ b/frontend/sac-mobile/app/(auth)/user-details.tsx
@@ -1,24 +1,21 @@
-import { View } from 'react-native';
-import { SafeAreaView } from 'react-native-safe-area-context';
+import { SafeAreaView, ScrollView, Text, View } from 'react-native';
-import { router } from 'expo-router';
+import { StatusBar } from 'expo-status-bar';
-import { Button } from '@/components/button';
+import UserDetailForm from './_components/user-details-form';
const UserDetails = () => {
return (
-
-
-
+
+
+
+ Let's learn more about you
+
+
+
-
+
);
};
diff --git a/frontend/sac-mobile/app/(auth)/user-interests.tsx b/frontend/sac-mobile/app/(auth)/user-interests.tsx
new file mode 100644
index 000000000..387c6b440
--- /dev/null
+++ b/frontend/sac-mobile/app/(auth)/user-interests.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { Text, View } from 'react-native';
+
+import { StatusBar } from 'expo-status-bar';
+
+import UserInterestsForm from './_components/user-interest-form';
+
+const UserInterests = () => {
+ return (
+
+
+
+ What are you interested in?
+
+
+
+ );
+};
+
+export default UserInterests;
diff --git a/frontend/sac-mobile/app/(auth)/verification.tsx b/frontend/sac-mobile/app/(auth)/verification.tsx
index 258f16c60..0d7a5849d 100644
--- a/frontend/sac-mobile/app/(auth)/verification.tsx
+++ b/frontend/sac-mobile/app/(auth)/verification.tsx
@@ -1,33 +1,28 @@
import React from 'react';
-import { ScrollView, Text, View } from 'react-native';
-import { SafeAreaView } from 'react-native-safe-area-context';
+import { Text, View } from 'react-native';
-import { Wordmark } from '@/components/wordmark';
+import { StatusBar } from 'expo-status-bar';
import { VerificationForm } from './_components/verification-form';
const Verification = () => {
return (
-
-
-
-
-
-
-
-
- Verify your email
-
-
-
- Please enter the verification code sent to your email.
+
+
+
+
+
+ Verify your email
-
-
-
-
-
+
+ Please enter the verification code sent to your email.
+
+
+
+
+
+
);
};
diff --git a/frontend/sac-mobile/app/(auth)/welcome.tsx b/frontend/sac-mobile/app/(auth)/welcome.tsx
index e481455c0..3a116c7a7 100644
--- a/frontend/sac-mobile/app/(auth)/welcome.tsx
+++ b/frontend/sac-mobile/app/(auth)/welcome.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { SafeAreaView, Text, View } from 'react-native';
+import { Text, View } from 'react-native';
import { router } from 'expo-router';
@@ -7,10 +7,10 @@ import { Button } from '@/components/button';
const Welcome = () => {
return (
-
+
- Welcome to StudCal
+ Welcome to SAC
Discover, follow, and join all the clubs & events Northeastern
@@ -25,7 +25,7 @@ const Welcome = () => {
Get Started
-
+
);
};
diff --git a/frontend/sac-mobile/app/_layout.tsx b/frontend/sac-mobile/app/_layout.tsx
index 40c75999d..a4bc358cc 100644
--- a/frontend/sac-mobile/app/_layout.tsx
+++ b/frontend/sac-mobile/app/_layout.tsx
@@ -1,5 +1,6 @@
import { useEffect } from 'react';
import { View } from 'react-native';
+import { GestureHandlerRootView } from 'react-native-gesture-handler';
import Spinner from 'react-native-loading-spinner-overlay';
import { useFonts } from 'expo-font';
@@ -13,7 +14,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useAuthStore } from '@/hooks/use-auth';
import { AuthProvider, useAuth } from '@/context/AuthContext';
import * as SecureStore from 'expo-secure-store';
-import { GestureHandlerRootView } from 'react-native-gesture-handler';
export {
// Catch any errors thrown by the Layout component.
@@ -23,7 +23,7 @@ export {
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();
-const queryClient = new QueryClient({
+export const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
diff --git a/frontend/sac-mobile/assets/images/svg/home-selected.svg b/frontend/sac-mobile/assets/images/svg/home-selected.svg
new file mode 100644
index 000000000..44721e51d
--- /dev/null
+++ b/frontend/sac-mobile/assets/images/svg/home-selected.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/sac-mobile/assets/images/svg/home.svg b/frontend/sac-mobile/assets/images/svg/home.svg
new file mode 100644
index 000000000..cf07f3ef0
--- /dev/null
+++ b/frontend/sac-mobile/assets/images/svg/home.svg
@@ -0,0 +1,4 @@
+
diff --git a/frontend/sac-mobile/assets/images/svg/pin.svg b/frontend/sac-mobile/assets/images/svg/pin.svg
new file mode 100644
index 000000000..be2ec400d
--- /dev/null
+++ b/frontend/sac-mobile/assets/images/svg/pin.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/sac-mobile/assets/images/svg/profile-selected.svg b/frontend/sac-mobile/assets/images/svg/profile-selected.svg
new file mode 100644
index 000000000..78a5a2237
--- /dev/null
+++ b/frontend/sac-mobile/assets/images/svg/profile-selected.svg
@@ -0,0 +1,4 @@
+
diff --git a/frontend/sac-mobile/assets/images/svg/profile.svg b/frontend/sac-mobile/assets/images/svg/profile.svg
new file mode 100644
index 000000000..8f607b254
--- /dev/null
+++ b/frontend/sac-mobile/assets/images/svg/profile.svg
@@ -0,0 +1,4 @@
+
diff --git a/frontend/sac-mobile/assets/images/svg/search-selected.svg b/frontend/sac-mobile/assets/images/svg/search-selected.svg
new file mode 100644
index 000000000..725f63085
--- /dev/null
+++ b/frontend/sac-mobile/assets/images/svg/search-selected.svg
@@ -0,0 +1,4 @@
+
diff --git a/frontend/sac-mobile/assets/images/svg/search.svg b/frontend/sac-mobile/assets/images/svg/search.svg
new file mode 100644
index 000000000..90fbb18bb
--- /dev/null
+++ b/frontend/sac-mobile/assets/images/svg/search.svg
@@ -0,0 +1,5 @@
+
diff --git a/frontend/sac-mobile/assets/images/svg/time.svg b/frontend/sac-mobile/assets/images/svg/time.svg
new file mode 100644
index 000000000..9f137b948
--- /dev/null
+++ b/frontend/sac-mobile/assets/images/svg/time.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/sac-mobile/babel.config.js b/frontend/sac-mobile/babel.config.js
index 0464179ed..94046263e 100644
--- a/frontend/sac-mobile/babel.config.js
+++ b/frontend/sac-mobile/babel.config.js
@@ -2,6 +2,6 @@ module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
- plugins: ['nativewind/babel']
+ plugins: ['nativewind/babel', 'react-native-reanimated/plugin']
};
};
diff --git a/frontend/sac-mobile/components/button.tsx b/frontend/sac-mobile/components/button.tsx
index bb302a6bd..52af65559 100644
--- a/frontend/sac-mobile/components/button.tsx
+++ b/frontend/sac-mobile/components/button.tsx
@@ -11,7 +11,9 @@ const buttonVariants = {
secondary: ['bg-white', 'text-gray'],
outline: ['border border-gray-600 text-gray-500 font-medium'],
icon: ['bg-transparent'],
- pill: ['bg-gray-500', 'text-white', 'rounded-full']
+ underline: ['border-b font-bold border-gray-600 text-lg'],
+ pill: ['bg-gray-500', 'text-white', 'rounded-full'],
+ menu: ['text-black text-lg']
},
size: {
default: 'h-10 px-4 py-2',
@@ -46,7 +48,8 @@ const Button = ({ children, variant, size, ...props }: ButtonProps) => {
>
diff --git a/frontend/sac-mobile/components/dropdown.tsx b/frontend/sac-mobile/components/dropdown.tsx
index c2164c255..611a641e2 100644
--- a/frontend/sac-mobile/components/dropdown.tsx
+++ b/frontend/sac-mobile/components/dropdown.tsx
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
-import { DimensionValue, ScrollView, StyleSheet, Text } from 'react-native';
+import { DimensionValue, StyleSheet, Text, View } from 'react-native';
import { Dropdown } from 'react-native-element-dropdown';
import { Item } from '@/types/item';
@@ -14,7 +14,7 @@ interface DropdownProps {
onChangeText: (...event: any[]) => void;
value: Item;
onSubmitEditing: () => void;
- search?: boolean; // true for enable search
+ search?: boolean; // true to enable search
height?: DimensionValue;
error?: boolean;
}
@@ -26,11 +26,10 @@ const DropdownComponent = (props: DropdownProps) => {
const styles = StyleSheet.create({
container: {
- backgroundColor: 'white',
height: props.height || 78
},
dropdown: {
- height: '85%',
+ height: 50,
borderColor: borderColor,
borderWidth: borderWidth,
borderRadius: 12,
@@ -47,7 +46,8 @@ const DropdownComponent = (props: DropdownProps) => {
inputSearchStyle: {
height: 40,
fontSize: 14,
- borderRadius: 11
+ borderRadius: 11,
+ marginLeft: 8
},
containerStyle: {
borderRadius: 12,
@@ -70,7 +70,7 @@ const DropdownComponent = (props: DropdownProps) => {
});
return (
-
+
{props.title}
{
onChange={props.onChangeText}
value={props.value}
/>
-
+
);
};
diff --git a/frontend/sac-mobile/components/input.tsx b/frontend/sac-mobile/components/input.tsx
index ec09055ff..4a3120e0f 100644
--- a/frontend/sac-mobile/components/input.tsx
+++ b/frontend/sac-mobile/components/input.tsx
@@ -1,17 +1,80 @@
-import { Text, TextInput, TextInputProps, View } from 'react-native';
+import {
+ GestureResponderEvent,
+ Text,
+ TextInput,
+ TextInputProps,
+ View
+} from 'react-native';
-interface InputProps extends TextInputProps {
- title: string;
+import { VariantProps, cva } from 'class-variance-authority';
+
+import { Button } from '@/components/button';
+import { cn } from '@/lib/utils';
+
+const inputVariants = {
+ variant: {
+ default: ['pt-[4.5%]', 'pb-[4.5%]', 'pl-[5%]', 'border', 'rounded-xl'],
+ faq: [
+ 'bg-gray-100',
+ 'rounded-lg',
+ 'py-[2%]',
+ 'pl-[4.5%]',
+ 'pr-[2%]',
+ 'flex-row',
+ 'justify-between'
+ ]
+ }
+};
+
+const inputStyles = cva(['w-full'], {
+ variants: inputVariants,
+ defaultVariants: {
+ variant: 'default'
+ }
+});
+
+export interface InputProps
+ extends TextInputProps,
+ VariantProps {
+ title?: string;
error?: boolean;
}
-const Input = ({ title, error, ...props }: InputProps) => {
+const Input = ({ title, error, variant, ...props }: InputProps) => {
const borderColor = error ? 'border-red-600' : 'border-gray-500';
- return (
-
- {title}
+
+ let inputComponent = null;
+ if (variant === 'faq') {
+ inputComponent = (
+
+
+ {/* @ts-ignore */}
+
+ );
+ } else {
+ inputComponent = (
{
secureTextEntry={props.secureTextEntry || false}
onSubmitEditing={props.onSubmitEditing}
/>
+ );
+ }
+
+ return (
+
+ {title && {title}}
+ {inputComponent}
);
};
diff --git a/frontend/sac-mobile/components/kebab.tsx b/frontend/sac-mobile/components/kebab.tsx
new file mode 100644
index 000000000..9fa01eaf4
--- /dev/null
+++ b/frontend/sac-mobile/components/kebab.tsx
@@ -0,0 +1,25 @@
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+import {
+ MenuAction,
+ MenuView,
+ NativeActionEvent
+} from '@react-native-menu/menu';
+
+type KebabProps = {
+ items: MenuAction[];
+ onPressAction: (event: NativeActionEvent) => void;
+};
+
+const Kebab = ({ items, onPressAction }: KebabProps) => {
+ return (
+
+
+
+ );
+};
+
+export { Kebab };
diff --git a/frontend/sac-mobile/components/multiselect.tsx b/frontend/sac-mobile/components/multiselect.tsx
new file mode 100644
index 000000000..ce179dadc
--- /dev/null
+++ b/frontend/sac-mobile/components/multiselect.tsx
@@ -0,0 +1,99 @@
+import React, { useState } from 'react';
+import { StyleSheet, Text, View } from 'react-native';
+import { MultiSelect } from 'react-native-element-dropdown';
+
+import { Item } from '@/types/item';
+
+interface MultiSelectProps {
+ title: string;
+ item: Array- ;
+ placeholder: string;
+ onSubmitEditing: () => void;
+ search?: boolean;
+ error?: boolean;
+ onChange: (selectedItems: Item[]) => void;
+ maxSelect: number;
+}
+
+const MultiSelectComponent = (props: MultiSelectProps) => {
+ const borderColor = props.error ? 'red' : 'black';
+ const borderWidth = props.error ? 1 : 0.5;
+ const marginBottom = props.error ? '0.5%' : '2%';
+ const [selected, setSelected] = useState(Array
- );
+
+ const styles = StyleSheet.create({
+ container: {
+ padding: 0
+ },
+ itemContainerStyle: {
+ borderBottomWidth: 1,
+ borderColor: '#F0F0F0'
+ },
+ itemTextStyle: {
+ fontSize: 14,
+ paddingLeft: '2%'
+ },
+ containerStyle: {
+ borderRadius: 14,
+ marginTop: 6,
+ height: '88%'
+ },
+ dropdown: {
+ height: 50,
+ borderWidth: borderWidth,
+ borderRadius: 12,
+ paddingLeft: '5%',
+ paddingRight: '5%',
+ marginBottom: marginBottom,
+ borderColor: borderColor
+ },
+ placeholderStyle: {
+ fontSize: 14,
+ color: '#CDCBCB'
+ },
+ selectedTextStyle: {
+ fontSize: 14
+ },
+ inputSearchStyle: {
+ height: 45,
+ fontSize: 14,
+ borderRadius: 13,
+ paddingLeft: 7
+ },
+ selectedStyle: {
+ borderRadius: 10,
+ marginTop: '1%'
+ }
+ });
+
+ return (
+
+ {props.title}
+ {
+ setSelected(item);
+ props.onChange(item);
+ }}
+ selectedStyle={styles.selectedStyle}
+ />
+
+ );
+};
+
+export default MultiSelectComponent;
diff --git a/frontend/sac-mobile/components/tag.tsx b/frontend/sac-mobile/components/tag.tsx
index faea441ad..455cc24a8 100644
--- a/frontend/sac-mobile/components/tag.tsx
+++ b/frontend/sac-mobile/components/tag.tsx
@@ -1,5 +1,7 @@
import { Text } from 'react-native';
+import { router } from 'expo-router';
+
import { Button } from '@/components/button';
import { Tag as T } from '@/types/tag';
@@ -10,7 +12,7 @@ type TagProps = {
const Tag = ({ tag }: TagProps) => {
return (