diff --git a/backend/Dockerfile.redis b/backend/Dockerfile.redis new file mode 100644 index 000000000..253b44c28 --- /dev/null +++ b/backend/Dockerfile.redis @@ -0,0 +1,6 @@ +FROM redis:latest + +COPY redis_entrypoint.sh /usr/local/bin/redis_entrypoint.sh +RUN chmod +x /usr/local/bin/redis_entrypoint.sh + +ENTRYPOINT ["redis_entrypoint.sh"] \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile.server similarity index 100% rename from backend/Dockerfile rename to backend/Dockerfile.server diff --git a/backend/auth/locals.go b/backend/auth/locals.go deleted file mode 100644 index 8c1955655..000000000 --- a/backend/auth/locals.go +++ /dev/null @@ -1,52 +0,0 @@ -package auth - -import ( - "fmt" - - "github.com/GenerateNU/sac/backend/utilities" - "github.com/gofiber/fiber/v2" - "github.com/google/uuid" -) - -type localsKey byte - -const ( - claimsKey localsKey = 0 - userIDKey localsKey = 1 -) - -func CustomClaimsFrom(c *fiber.Ctx) (*CustomClaims, error) { - rawClaims := c.Locals(claimsKey) - if rawClaims == nil { - return nil, utilities.Forbidden() - } - - claims, ok := rawClaims.(*CustomClaims) - if !ok { - return nil, fmt.Errorf("claims are not of type CustomClaims. got: %T", rawClaims) - } - - return claims, nil -} - -func SetClaims(c *fiber.Ctx, claims *CustomClaims) { - c.Locals(claimsKey, claims) -} - -func UserIDFrom(c *fiber.Ctx) (*uuid.UUID, error) { - userID := c.Locals(userIDKey) - if userID == nil { - return nil, utilities.Forbidden() - } - - id, ok := userID.(*uuid.UUID) - if !ok { - return nil, fmt.Errorf("userID is not of type uuid.UUID. got: %T", userID) - } - - return id, nil -} - -func SetUserID(c *fiber.Ctx, id *uuid.UUID) { - c.Locals(userIDKey, id) -} diff --git a/backend/config/auth.go b/backend/config/auth.go index 0acf2019a..e359095c7 100644 --- a/backend/config/auth.go +++ b/backend/config/auth.go @@ -12,17 +12,17 @@ type AuthSettings struct { } type intermediateAuthSettings struct { - accessKey string `env:"ACCESS_KEY"` - refreshKey string `env:"REFRESH_KEY"` + AccessKey string `env:"ACCESS_KEY"` + RefreshKey string `env:"REFRESH_KEY"` } func (i *intermediateAuthSettings) into() (*AuthSettings, error) { - accessKey, err := m.NewSecret(i.accessKey) + accessKey, err := m.NewSecret(i.AccessKey) if err != nil { return nil, fmt.Errorf("failed to create secret from access key: %s", err.Error()) } - refreshKey, err := m.NewSecret(i.refreshKey) + refreshKey, err := m.NewSecret(i.RefreshKey) if err != nil { return nil, fmt.Errorf("failed to create secret from refresh key: %s", err.Error()) } diff --git a/backend/config/aws.go b/backend/config/aws.go index c6effb90b..bbdaaae00 100644 --- a/backend/config/aws.go +++ b/backend/config/aws.go @@ -14,29 +14,29 @@ type AWSSettings struct { } type intermediateAWSSettings struct { - bucketName string `env:"BUCKET_NAME"` - id string `env:"ID"` - secret string `env:"SECRET"` - region string `env:"REGION"` + BucketName string `env:"BUCKET_NAME"` + ID string `env:"ID"` + Secret string `env:"SECRET"` + Region string `env:"REGION"` } func (i *intermediateAWSSettings) into() (*AWSSettings, error) { - bucketName, err := m.NewSecret(i.bucketName) + bucketName, err := m.NewSecret(i.BucketName) if err != nil { return nil, fmt.Errorf("failed to create secret from bucket name: %s", err.Error()) } - id, err := m.NewSecret(i.id) + id, err := m.NewSecret(i.ID) if err != nil { return nil, fmt.Errorf("failed to create secret from ID: %s", err.Error()) } - secret, err := m.NewSecret(i.secret) + secret, err := m.NewSecret(i.Secret) if err != nil { return nil, fmt.Errorf("failed to create secret from secret: %s", err.Error()) } - region, err := m.NewSecret(i.region) + region, err := m.NewSecret(i.Region) if err != nil { return nil, fmt.Errorf("failed to create secret from region: %s", err.Error()) } diff --git a/backend/config/calendar.go b/backend/config/calendar.go index 1489aff70..41dfbb004 100644 --- a/backend/config/calendar.go +++ b/backend/config/calendar.go @@ -10,11 +10,11 @@ type CalendarSettings struct { } type intermediateCalendarSettings struct { - maxTerminationDate string `env:"MAX_TERMINATION_DATE"` + MaxTerminationDate string `env:"MAX_TERMINATION_DATE"` } func (i *intermediateCalendarSettings) into() (*CalendarSettings, error) { - maxTerminationDate, err := time.Parse("01-02-2006", i.maxTerminationDate) + maxTerminationDate, err := time.Parse("01-02-2006", i.MaxTerminationDate) if err != nil { return nil, fmt.Errorf("failed to parse max termination date: %s", err.Error()) } diff --git a/backend/config/config.go b/backend/config/config.go index 3298f0943..010b5a555 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -8,16 +8,14 @@ import ( ) func GetConfiguration(path string) (*Settings, error) { - err := godotenv.Load(path) - if err != nil { + if err := godotenv.Load(path); err != nil { return nil, fmt.Errorf("failed to load environment variables: %s", err.Error()) } - var intermediateSettings intermediateSettings - if err := env.Parse(&intermediateSettings); err != nil { + intSettings, err := env.ParseAs[intermediateSettings]() + if err != nil { return nil, fmt.Errorf("failed to parse environment variables: %s", err.Error()) } - settings, err := intermediateSettings.into() - return settings, err + return intSettings.into() } diff --git a/backend/config/database.go b/backend/config/database.go index b507a5e6a..2d0f367ea 100644 --- a/backend/config/database.go +++ b/backend/config/database.go @@ -43,26 +43,26 @@ func (s *DatabaseSettings) PostgresConn() string { } type intermediateDatabaseSettings struct { - username string `env:"USERNAME"` - password string `env:"PASSWORD"` - port uint `env:"PORT"` - host string `env:"HOST"` - databaseName string `env:"NAME"` - requireSSL bool `env:"REQUIRE_SSL"` + Username string `env:"USERNAME"` + Password string `env:"PASSWORD"` + Port uint `env:"PORT"` + Host string `env:"HOST"` + DatabaseName string `env:"NAME"` + RequireSSL bool `env:"REQUIRE_SSL"` } func (i *intermediateDatabaseSettings) into() (*DatabaseSettings, error) { - password, err := m.NewSecret(i.password) + password, err := m.NewSecret(i.Password) if err != nil { return nil, fmt.Errorf("failed to create secret from password: %s", err.Error()) } return &DatabaseSettings{ - Username: i.username, + Username: i.Username, Password: password, - Port: i.port, - Host: i.host, - DatabaseName: i.databaseName, - RequireSSL: i.requireSSL, + Port: i.Port, + Host: i.Host, + DatabaseName: i.DatabaseName, + RequireSSL: i.RequireSSL, }, nil } diff --git a/backend/config/redis.go b/backend/config/redis.go index 926d72a17..27a122312 100644 --- a/backend/config/redis.go +++ b/backend/config/redis.go @@ -16,26 +16,26 @@ type RedisSettings struct { } type intermediateRedisSettings struct { - username string `env:"USERNAME"` - password string `env:"PASSWORD"` - host string `env:"HOST"` - port uint `env:"PORT"` - db int `env:"DB"` + Username string `env:"USERNAME"` + Password string `env:"PASSWORD"` + Host string `env:"HOST"` + Port uint `env:"PORT"` + DB int `env:"DB"` // TLSConfig *intermediateTLSConfig `env:"TLS_CONFIG"` } func (i *intermediateRedisSettings) into() (*RedisSettings, error) { - password, err := m.NewSecret(i.password) + password, err := m.NewSecret(i.Password) if err != nil { return nil, fmt.Errorf("failed to create secret from password: %s", err.Error()) } return &RedisSettings{ - Username: i.username, + Username: i.Username, Password: password, - Host: i.host, - Port: i.port, - DB: i.db, + Host: i.Host, + Port: i.Port, + DB: i.DB, // TLSConfig: i.TLSConfig.into(), }, nil } diff --git a/backend/config/resend.go b/backend/config/resend.go index f2292d3ed..73c724d39 100644 --- a/backend/config/resend.go +++ b/backend/config/resend.go @@ -11,11 +11,11 @@ type ResendSettings struct { } type intermediateResendSettings struct { - apiKey string `env:"API_KEY"` + APIKey string `env:"API_KEY"` } func (i *intermediateResendSettings) into() (*ResendSettings, error) { - apiKey, err := m.NewSecret(i.apiKey) + apiKey, err := m.NewSecret(i.APIKey) if err != nil { return nil, fmt.Errorf("failed to create secret from API key: %s", err.Error()) } diff --git a/backend/config/settings.go b/backend/config/settings.go index 36c62f77c..40e05d218 100644 --- a/backend/config/settings.go +++ b/backend/config/settings.go @@ -21,79 +21,79 @@ type Integrations struct { } type intermediateSettings struct { - application ApplicationSettings `envPrefix:"SAC_APPLICATION_"` - database intermediateDatabaseSettings `envPrefix:"SAC_DB_"` - redisActiveTokens intermediateRedisSettings `envPrefix:"SAC_REDIS_ACTIVE_TOKENS_"` - redisBlacklist intermediateRedisSettings `envPrefix:"SAC_REDIS_BLACKLIST_"` - redisLimiter intermediateRedisSettings `envPrefix:"SAC_REDIS_LIMITER_"` - superUser intermediateSuperUserSettings `envPrefix:"SAC_SUDO_"` - auth intermediateAuthSettings `envPrefix:"SAC_AUTH_"` - aws intermediateAWSSettings `envPrefix:"SAC_AWS_"` - resend intermediateResendSettings `envPrefix:"SAC_RESEND_"` - calendar intermediateCalendarSettings `envPrefix:"SAC_CALENDAR_"` - googleSettings intermediateGoogleOAuthSettings `envPrefix:"SAC_GOOGLE_OAUTH"` - outlookSettings intermdeiateOutlookOAuthSettings `envPrefix:"SAC_OUTLOOK_OAUTH"` - search SearchSettings `env:"SAC_SEARCH"` + Application ApplicationSettings `envPrefix:"SAC_APPLICATION_"` + Database intermediateDatabaseSettings `envPrefix:"SAC_DB_"` + RedisActiveTokens intermediateRedisSettings `envPrefix:"SAC_REDIS_ACTIVE_TOKENS_"` + RedisBlacklist intermediateRedisSettings `envPrefix:"SAC_REDIS_BLACKLIST_"` + RedisLimiter intermediateRedisSettings `envPrefix:"SAC_REDIS_LIMITER_"` + SuperUser intermediateSuperUserSettings `envPrefix:"SAC_SUDO_"` + Auth intermediateAuthSettings `envPrefix:"SAC_AUTH_"` + AWS intermediateAWSSettings `envPrefix:"SAC_AWS_"` + Resend intermediateResendSettings `envPrefix:"SAC_RESEND_"` + Calendar intermediateCalendarSettings `envPrefix:"SAC_CALENDAR_"` + GoogleSettings intermediateGoogleOAuthSettings `envPrefix:"SAC_GOOGLE_OAUTH_"` + OutlookSettings intermdeiateOutlookOAuthSettings `envPrefix:"SAC_OUTLOOK_OAUTH_"` + Search SearchSettings `envPrefix:"SAC_SEARCH_"` } func (i *intermediateSettings) into() (*Settings, error) { - database, err := i.database.into() + database, err := i.Database.into() if err != nil { return nil, err } - redisActiveTokens, err := i.redisActiveTokens.into() + redisActiveTokens, err := i.RedisActiveTokens.into() if err != nil { return nil, err } - redisBlacklist, err := i.redisBlacklist.into() + redisBlacklist, err := i.RedisBlacklist.into() if err != nil { return nil, err } - redisLimiter, err := i.redisLimiter.into() + redisLimiter, err := i.RedisLimiter.into() if err != nil { return nil, err } - superUser, err := i.superUser.into() + superUser, err := i.SuperUser.into() if err != nil { return nil, err } - auth, err := i.auth.into() + auth, err := i.Auth.into() if err != nil { return nil, err } - aws, err := i.aws.into() + aws, err := i.AWS.into() if err != nil { return nil, err } - resend, err := i.resend.into() + resend, err := i.Resend.into() if err != nil { return nil, err } - calendar, err := i.calendar.into() + calendar, err := i.Calendar.into() if err != nil { return nil, err } - google, err := i.googleSettings.into() + google, err := i.GoogleSettings.into() if err != nil { return nil, err } - outlook, err := i.outlookSettings.into() + outlook, err := i.OutlookSettings.into() if err != nil { return nil, err } return &Settings{ - Application: i.application, + Application: i.Application, Database: *database, RedisActiveTokens: *redisActiveTokens, RedisBlacklist: *redisBlacklist, @@ -106,7 +106,7 @@ func (i *intermediateSettings) into() (*Settings, error) { OutlookOauth: *outlook, AWS: *aws, Resend: *resend, - Search: i.search, + Search: i.Search, }, }, nil } diff --git a/backend/config/sudo.go b/backend/config/sudo.go index d1e4d21ea..823f09cde 100644 --- a/backend/config/sudo.go +++ b/backend/config/sudo.go @@ -10,11 +10,11 @@ type SuperUserSettings struct { Password *m.Secret[string] } type intermediateSuperUserSettings struct { - password string `env:"PASSWORD"` + Password string `env:"PASSWORD"` } func (i *intermediateSuperUserSettings) into() (*SuperUserSettings, error) { - password, err := m.NewSecret(i.password) + password, err := m.NewSecret(i.Password) if err != nil { return nil, fmt.Errorf("failed to create secret from password: %s", err.Error()) } diff --git a/backend/constants/db.go b/backend/constants/db.go index 74f80d992..eb6d046ed 100644 --- a/backend/constants/db.go +++ b/backend/constants/db.go @@ -1,6 +1,9 @@ package constants +import "time" + const ( - MAX_IDLE_CONNECTIONS int = 10 - MAX_OPEN_CONNECTIONS int = 100 + MAX_IDLE_CONNECTIONS int = 10 + MAX_OPEN_CONNECTIONS int = 100 + DB_TIMEOUT time.Duration = 200 * time.Millisecond ) diff --git a/backend/database/store/redis.go b/backend/database/store/redis.go index c5e1c7c6c..e3bf3ec7a 100644 --- a/backend/database/store/redis.go +++ b/backend/database/store/redis.go @@ -25,9 +25,9 @@ func NewStores(limiter LimiterInterface, blacklist BlacklistInterface, activeTok func ConfigureRedis(settings config.Settings) *Stores { stores := NewStores( - NewLimiter(NewRedisClient(settings.RedisLimiter.Username, settings.RedisLimiter.Host, settings.RedisLimiter.Port, settings.RedisLimiter.Password, settings.RedisLimiter.DB)), - NewBlacklist(NewRedisClient(settings.RedisBlacklist.Username, settings.RedisBlacklist.Host, settings.RedisBlacklist.Port, settings.RedisBlacklist.Password, settings.RedisBlacklist.DB)), - NewActiveToken(NewRedisClient(settings.RedisActiveTokens.Username, settings.RedisActiveTokens.Host, settings.RedisActiveTokens.Port, settings.RedisActiveTokens.Password, settings.RedisActiveTokens.DB)), + NewLimiter(NewRedisClient(settings.RedisLimiter.Username, settings.RedisLimiter.Password, settings.RedisLimiter.Host, settings.RedisLimiter.Port, settings.RedisLimiter.DB)), + NewBlacklist(NewRedisClient(settings.RedisBlacklist.Username, settings.RedisBlacklist.Password, settings.RedisBlacklist.Host, settings.RedisBlacklist.Port, settings.RedisBlacklist.DB)), + NewActiveToken(NewRedisClient(settings.RedisActiveTokens.Username, settings.RedisActiveTokens.Password, settings.RedisActiveTokens.Host, settings.RedisActiveTokens.Port, settings.RedisActiveTokens.DB)), ) MustEstablishConn() diff --git a/backend/database/store/store.go b/backend/database/store/store.go index 074765484..82d224aec 100644 --- a/backend/database/store/store.go +++ b/backend/database/store/store.go @@ -26,15 +26,16 @@ type RedisClient struct { client *redis.Client } -func NewRedisClient(username, host string, port uint, password *m.Secret[string], db int) *RedisClient { +func NewRedisClient(username string, password *m.Secret[string], host string, port uint, db int) *RedisClient { client := redis.NewClient(&redis.Options{ - Username: username, - Addr: fmt.Sprintf("%s:%d", host, port), - Password: password.Expose(), - DB: db, - PoolSize: 10 * runtime.GOMAXPROCS(0), - MaxActiveConns: constants.REDIS_MAX_OPEN_CONNECTIONS, - MaxIdleConns: constants.REDIS_MAX_IDLE_CONNECTIONS, + Username: username, + Password: password.Expose(), + Addr: fmt.Sprintf("%s:%d", host, port), + DB: db, + PoolSize: 10 * runtime.GOMAXPROCS(0), + MaxActiveConns: constants.REDIS_MAX_OPEN_CONNECTIONS, + MaxIdleConns: constants.REDIS_MAX_IDLE_CONNECTIONS, + ContextTimeoutEnabled: true, }) return &RedisClient{ diff --git a/backend/database/super.go b/backend/database/super.go index 403f79ff6..6255b640c 100644 --- a/backend/database/super.go +++ b/backend/database/super.go @@ -32,14 +32,10 @@ func SuperUser(superUserSettings config.SuperUserSettings) (*models.User, error) func SuperClub() models.Club { return models.Club{ - Name: "SAC", - Preview: "SAC", - Description: "SAC", - NumMembers: 0, - IsRecruiting: true, - RecruitmentCycle: models.Always, - RecruitmentType: models.Application, - ApplicationLink: "https://generatenu.com/apply", - Logo: "https://aws.amazon.com/s3", + Name: "SAC", + Preview: "SAC", + Description: "SAC", + NumMembers: 0, + Logo: "https://aws.amazon.com/s3", } } diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index e46632407..4870e808b 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -1,35 +1,88 @@ services: redis-active-tokens: - image: redis/redis-stack-server:latest + build: + context: . + dockerfile: Dockerfile.redis container_name: redis_active_tokens ports: - 6379:6379 environment: - - REDIS_PASSWORD=redispassword!#1 + - REDIS_USERNAME=redis_active_tokens + - REDIS_PASSWORD=redis_active_tokens!#1 + - REDIS_DISABLE_DEFAULT_USER="true" volumes: - redis-active-data:/data redis-blacklist: - image: redis/redis-stack-server:latest + build: + context: . + dockerfile: Dockerfile.redis container_name: redis_blacklist ports: - 6380:6379 environment: - - REDIS_PASSWORD=redispassword!#2 + - REDIS_USERNAME=redis_blacklist + - REDIS_PASSWORD=redis_blacklist!#2 + - REDIS_DISABLE_DEFAULT_USER="true" volumes: - redis-blacklist-data:/data redis-limiter: - image: redis/redis-stack-server:latest + build: + context: . + dockerfile: Dockerfile.redis container_name: redis_limiter ports: - 6381:6379 environment: - - REDIS_PASSWORD=redispassword!#3 + - REDIS_USERNAME=redis_limiter + - REDIS_PASSWORD=redis_limiter!#3 + - REDIS_DISABLE_DEFAULT_USER="true" volumes: - redis-limiter-data:/data + opensearch-node1: + image: opensearchproject/opensearch:latest + container_name: opensearch-node1 + environment: + - cluster.name=opensearch-cluster + - node.name=opensearch-node1 + - discovery.type=single-node + - bootstrap.memory_lock=true # along with the memlock settings below, disables swapping + - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" # minimum and maximum Java heap size, recommend setting both to 50% of system RAM + - DISABLE_SECURITY_PLUGIN=true # + ulimits: + memlock: + soft: -1 + hard: -1 + nofile: + soft: 65536 # maximum number of open files for the OpenSearch user, set to at least 65536 on modern systems + hard: 65536 + volumes: + - opensearch-data1:/usr/share/opensearch/data + ports: + - 9200:9200 + - 9600:9600 # required for Performance Analyzer + networks: + - opensearch-net + opensearch-dashboards: + image: opensearchproject/opensearch-dashboards:latest + container_name: opensearch-dashboards + ports: + - 5601:5601 + expose: + - "5601" + environment: + OPENSEARCH_HOSTS: '["http://opensearch-node1:9200"]' + DISABLE_SECURITY_DASHBOARDS_PLUGIN: true + networks: + - opensearch-net + volumes: redis-active-data: redis-blacklist-data: redis-limiter-data: + opensearch-data1: + +networks: + opensearch-net: diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 38a673141..598efc4e3 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -180,7 +180,7 @@ const docTemplate = `{ }, "/auth/refresh": { "post": { - "description": "Refreshes a user's access token", + "description": "Refreshes a user's access token and returns a new pair of tokens", "consumes": [ "application/json" ], @@ -190,7 +190,7 @@ const docTemplate = `{ "tags": [ "auth" ], - "summary": "Refreshes a user's access token", + "summary": "Refreshes a user's access token and returns a new pair of tokens", "operationId": "refresh-user", "responses": { "200": { @@ -1033,17 +1033,17 @@ const docTemplate = `{ } } }, - "/clubs/{clubID}/contacts/": { + "/clubs/{clubID}/events/": { "get": { - "description": "Retrieves all contacts associated with a club", + "description": "Retrieves all events associated with a club", "produces": [ "application/json" ], "tags": [ - "club-contact" + "club-event" ], - "summary": "Retrieve all contacts for a club", - "operationId": "get-contacts-by-club", + "summary": "Retrieve all events for a club", + "operationId": "get-events-by-club", "parameters": [ { "type": "string", @@ -1051,6 +1051,18 @@ const docTemplate = `{ "name": "clubID", "in": "path", "required": true + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Page", + "name": "page", + "in": "query" } ], "responses": { @@ -1059,7 +1071,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/models.Contact" + "$ref": "#/definitions/models.Event" } } }, @@ -1076,20 +1088,19 @@ const docTemplate = `{ "schema": {} } } - }, - "put": { - "description": "Creates a contact", - "consumes": [ - "application/json" - ], + } + }, + "/clubs/{clubID}/followers/": { + "get": { + "description": "Retrieves all followers associated with a club", "produces": [ "application/json" ], "tags": [ - "club-contact" + "club-follower" ], - "summary": "Creates a contact", - "operationId": "put-contact", + "summary": "Retrieve all followers for a club", + "operationId": "get-followers-by-club", "parameters": [ { "type": "string", @@ -1099,30 +1110,32 @@ const docTemplate = `{ "required": true }, { - "description": "Contact Body", - "name": "contactBody", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/contacts.PutContactRequestBody" - } + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Page", + "name": "page", + "in": "query" } ], "responses": { - "201": { - "description": "Created", + "200": { + "description": "OK", "schema": { - "$ref": "#/definitions/models.Contact" + "type": "array", + "items": { + "$ref": "#/definitions/models.User" + } } }, "400": { "description": "Bad Request", "schema": {} }, - "401": { - "description": "Unauthorized", - "schema": {} - }, "404": { "description": "Not Found", "schema": {} @@ -1134,17 +1147,17 @@ const docTemplate = `{ } } }, - "/clubs/{clubID}/events/": { + "/clubs/{clubID}/leadership/": { "get": { - "description": "Retrieves all events associated with a club", + "description": "Retrieves all leadership associated with a club", "produces": [ "application/json" ], "tags": [ - "club-event" + "club-leader" ], - "summary": "Retrieve all events for a club", - "operationId": "get-events-by-club", + "summary": "Retrieve all leadership for a club", + "operationId": "get-leadership-by-club", "parameters": [ { "type": "string", @@ -1152,18 +1165,6 @@ const docTemplate = `{ "name": "clubID", "in": "path", "required": true - }, - { - "type": "integer", - "description": "Limit", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "description": "Page", - "name": "page", - "in": "query" } ], "responses": { @@ -1172,7 +1173,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/models.Event" + "$ref": "#/definitions/models.Leader" } } }, @@ -1189,19 +1190,20 @@ const docTemplate = `{ "schema": {} } } - } - }, - "/clubs/{clubID}/followers/": { - "get": { - "description": "Retrieves all followers associated with a club", + }, + "post": { + "description": "Creates a leader associated with a club", + "consumes": [ + "multipart/form-data" + ], "produces": [ "application/json" ], "tags": [ - "club-follower" + "club-leader" ], - "summary": "Retrieve all followers for a club", - "operationId": "get-followers-by-club", + "summary": "Create a leader for a club", + "operationId": "create-leader-by-club", "parameters": [ { "type": "string", @@ -1209,34 +1211,23 @@ const docTemplate = `{ "name": "clubID", "in": "path", "required": true - }, - { - "type": "integer", - "description": "Limit", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "description": "Page", - "name": "page", - "in": "query" } ], "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.User" - } + "$ref": "#/definitions/models.Leader" } }, "400": { "description": "Bad Request", "schema": {} }, + "401": { + "description": "Unauthorized", + "schema": {} + }, "404": { "description": "Not Found", "schema": {} @@ -1248,17 +1239,17 @@ const docTemplate = `{ } } }, - "/clubs/{clubID}/members/": { + "/clubs/{clubID}/leadership/{leaderID}": { "get": { - "description": "Retrieves all members associated with a club", + "description": "Retrieves a leader associated with a club", "produces": [ "application/json" ], "tags": [ - "club-member" + "club-leader" ], - "summary": "Retrieve all members for a club", - "operationId": "get-members-by-club", + "summary": "Retrieve a leader for a club", + "operationId": "get-leader-by-club", "parameters": [ { "type": "string", @@ -1268,36 +1259,24 @@ const docTemplate = `{ "required": true }, { - "type": "integer", - "description": "Limit", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "description": "Page", - "name": "page", - "in": "query" + "type": "string", + "description": "leader ID", + "name": "leaderID", + "in": "path", + "required": true } ], "responses": { "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.User" - } + "$ref": "#/definitions/models.Leader" } }, "400": { "description": "Bad Request", "schema": {} }, - "401": { - "description": "Unauthorized", - "schema": {} - }, "404": { "description": "Not Found", "schema": {} @@ -1308,8 +1287,8 @@ const docTemplate = `{ } } }, - "post": { - "description": "Creates a new member associated with a club", + "put": { + "description": "Updates a leader associated with a club", "consumes": [ "application/json" ], @@ -1317,10 +1296,10 @@ const docTemplate = `{ "application/json" ], "tags": [ - "club-member" + "club-leader" ], - "summary": "Create a new member for a club", - "operationId": "create-member-for-club", + "summary": "Update a leader for a club", + "operationId": "update-leader-by-club", "parameters": [ { "type": "string", @@ -1331,17 +1310,17 @@ const docTemplate = `{ }, { "type": "string", - "description": "User ID", - "name": "userID", + "description": "leader ID", + "name": "leaderID", "in": "path", "required": true } ], "responses": { - "201": { - "description": "Created", + "200": { + "description": "OK", "schema": { - "$ref": "#/definitions/models.User" + "$ref": "#/definitions/models.Leader" } }, "400": { @@ -1363,15 +1342,15 @@ const docTemplate = `{ } }, "delete": { - "description": "Deletes a member associated with a club", + "description": "Delete a leader associated with a club", "produces": [ "application/json" ], "tags": [ - "club-member" + "club-leader" ], - "summary": "Delete a member from a club", - "operationId": "delete-member-from-club", + "summary": "Delete a leader for a club", + "operationId": "delete-leader-by-club", "parameters": [ { "type": "string", @@ -1382,27 +1361,20 @@ const docTemplate = `{ }, { "type": "string", - "description": "User ID", - "name": "userID", + "description": "leader ID", + "name": "leaderID", "in": "path", "required": true } ], "responses": { "204": { - "description": "No Content", - "schema": { - "$ref": "#/definitions/models.User" - } + "description": "No Content" }, "400": { "description": "Bad Request", "schema": {} }, - "401": { - "description": "Unauthorized", - "schema": {} - }, "404": { "description": "Not Found", "schema": {} @@ -1412,11 +1384,9 @@ const docTemplate = `{ "schema": {} } } - } - }, - "/clubs/{clubID}/poc/": { - "post": { - "description": "Creates a point of contact associated with a club", + }, + "patch": { + "description": "Updates a leader photo associated with a club", "consumes": [ "multipart/form-data" ], @@ -1424,10 +1394,10 @@ const docTemplate = `{ "application/json" ], "tags": [ - "club-point-of-contact" + "club-leader" ], - "summary": "Create a point of contact for a club", - "operationId": "create-point-of-contact-by-club", + "summary": "Update a leader photo for a club", + "operationId": "update-leader-photo-by-club", "parameters": [ { "type": "string", @@ -1435,13 +1405,20 @@ const docTemplate = `{ "name": "clubID", "in": "path", "required": true + }, + { + "type": "string", + "description": "leader ID", + "name": "leaderID", + "in": "path", + "required": true } ], "responses": { - "201": { - "description": "Created", + "200": { + "description": "OK", "schema": { - "$ref": "#/definitions/models.PointOfContact" + "$ref": "#/definitions/models.Leader" } }, "400": { @@ -1463,20 +1440,17 @@ const docTemplate = `{ } } }, - "/clubs/{clubID}/poc/{pocID}": { - "put": { - "description": "Updates a point of contact associated with a club", - "consumes": [ - "application/json" - ], + "/clubs/{clubID}/members/": { + "get": { + "description": "Retrieves all members associated with a club", "produces": [ "application/json" ], "tags": [ - "club-point-of-contact" + "club-member" ], - "summary": "Update a point of contact for a club", - "operationId": "update-point-of-contact-by-club", + "summary": "Retrieve all members for a club", + "operationId": "get-members-by-club", "parameters": [ { "type": "string", @@ -1486,18 +1460,26 @@ const docTemplate = `{ "required": true }, { - "type": "string", - "description": "Point of Contact ID", - "name": "pocID", - "in": "path", - "required": true + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Page", + "name": "page", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/models.PointOfContact" + "type": "array", + "items": { + "$ref": "#/definitions/models.User" + } } }, "400": { @@ -1518,16 +1500,19 @@ const docTemplate = `{ } } }, - "delete": { - "description": "Delete a point of contact associated with a club", + "post": { + "description": "Creates a new member associated with a club", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "club-point-of-contact" + "club-member" ], - "summary": "Delete a point of contact for a club", - "operationId": "delete-point-of-contact-by-club", + "summary": "Create a new member for a club", + "operationId": "create-member-for-club", "parameters": [ { "type": "string", @@ -1538,20 +1523,27 @@ const docTemplate = `{ }, { "type": "string", - "description": "Point of Contact ID", - "name": "pocID", + "description": "User ID", + "name": "userID", "in": "path", "required": true } ], "responses": { - "204": { - "description": "No Content" + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.User" + } }, "400": { "description": "Bad Request", "schema": {} }, + "401": { + "description": "Unauthorized", + "schema": {} + }, "404": { "description": "Not Found", "schema": {} @@ -1562,19 +1554,16 @@ const docTemplate = `{ } } }, - "patch": { - "description": "Updates a point of contact photo associated with a club", - "consumes": [ - "multipart/form-data" - ], + "delete": { + "description": "Deletes a member associated with a club", "produces": [ "application/json" ], "tags": [ - "club-point-of-contact" + "club-member" ], - "summary": "Update a point of contact photo for a club", - "operationId": "update-point-of-contact-photo-by-club", + "summary": "Delete a member from a club", + "operationId": "delete-member-from-club", "parameters": [ { "type": "string", @@ -1585,17 +1574,17 @@ const docTemplate = `{ }, { "type": "string", - "description": "Point of Contact ID", - "name": "pocID", + "description": "User ID", + "name": "userID", "in": "path", "required": true } ], "responses": { - "200": { - "description": "OK", + "204": { + "description": "No Content", "schema": { - "$ref": "#/definitions/models.PointOfContact" + "$ref": "#/definitions/models.User" } }, "400": { @@ -1617,17 +1606,17 @@ const docTemplate = `{ } } }, - "/clubs/{clubID}/pocs/": { + "/clubs/{clubID}/socials/": { "get": { - "description": "Retrieves all point of contacts associated with a club", + "description": "Retrieves all socials associated with a club", "produces": [ "application/json" ], "tags": [ - "club-point-of-contact" + "club-social" ], - "summary": "Retrieve all point of contacts for a club", - "operationId": "get-point-of-contacts-by-club", + "summary": "Retrieve all socials for a club", + "operationId": "get-socials-by-club", "parameters": [ { "type": "string", @@ -1643,7 +1632,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/models.PointOfContact" + "$ref": "#/definitions/models.Social" } } }, @@ -1660,19 +1649,20 @@ const docTemplate = `{ "schema": {} } } - } - }, - "/clubs/{clubID}/pocs/{pocID}": { - "get": { - "description": "Retrieves a point of contact associated with a club", + }, + "put": { + "description": "Creates a social", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "club-point-of-contact" + "club-social" ], - "summary": "Retrieve a point of contact for a club", - "operationId": "get-point-of-contact-by-club", + "summary": "Creates a social", + "operationId": "put-social", "parameters": [ { "type": "string", @@ -1682,24 +1672,30 @@ const docTemplate = `{ "required": true }, { - "type": "string", - "description": "Point of Contact ID", - "name": "pocID", - "in": "path", - "required": true + "description": "Social Body", + "name": "socialBody", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/socials.PutSocialRequestBody" + } } ], "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { - "$ref": "#/definitions/models.PointOfContact" + "$ref": "#/definitions/models.Social" } }, "400": { "description": "Bad Request", "schema": {} }, + "401": { + "description": "Unauthorized", + "schema": {} + }, "404": { "description": "Not Found", "schema": {} @@ -1868,162 +1864,6 @@ const docTemplate = `{ } } }, - "/contacts/": { - "get": { - "description": "Retrieves all contacts", - "produces": [ - "application/json" - ], - "tags": [ - "contact" - ], - "summary": "Retrieve all contacts", - "operationId": "get-contacts", - "parameters": [ - { - "type": "integer", - "description": "Limit", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "description": "Page", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Contact" - } - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "string" - } - }, - "404": { - "description": "Not Found", - "schema": { - "type": "string" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "string" - } - } - } - } - }, - "/contacts/{contactID}/": { - "get": { - "description": "Retrieves a contact by id", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "contact" - ], - "summary": "Retrieves a contact", - "operationId": "get-contact", - "parameters": [ - { - "type": "string", - "description": "Contact ID", - "name": "contactID", - "in": "path", - "required": true - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/models.Contact" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "string" - } - }, - "404": { - "description": "Not Found", - "schema": { - "type": "string" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "string" - } - } - } - }, - "delete": { - "description": "Deletes a contact", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "contact" - ], - "summary": "Deletes a contact", - "operationId": "delete-contact", - "parameters": [ - { - "type": "string", - "description": "Contact ID", - "name": "contactID", - "in": "path", - "required": true - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/models.Contact" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "string" - } - }, - "404": { - "description": "Not Found", - "schema": { - "type": "string" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "string" - } - } - } - } - }, "/events/": { "get": { "description": "Retrieves all events", @@ -2309,7 +2149,7 @@ const docTemplate = `{ } } }, - "/pocs/": { + "/leader/": { "get": { "description": "Retrieves all point of contacts", "produces": [ @@ -2340,7 +2180,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/models.PointOfContact" + "$ref": "#/definitions/models.Leader" } } }, @@ -2365,7 +2205,7 @@ const docTemplate = `{ } } }, - "/pocs/{pocID}/": { + "/leader/{leaderID}/": { "get": { "description": "Retrieves a point of contact by id", "produces": [ @@ -2379,17 +2219,318 @@ const docTemplate = `{ "parameters": [ { "type": "string", - "description": "Point of Contact ID", - "name": "pocID", + "description": "Point of Contact ID", + "name": "leaderID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Leader" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/search/clubs": { + "get": { + "description": "Searches through clubs", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "search" + ], + "summary": "Searches through clubs", + "operationId": "search-clubs", + "parameters": [ + { + "type": "integer", + "name": "max_members", + "in": "query" + }, + { + "type": "integer", + "name": "min_members", + "in": "query" + }, + { + "type": "string", + "name": "search", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "name": "tags", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/types.SearchResult-models_Club" + } + } + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/search/events": { + "get": { + "description": "Searches through events", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "search" + ], + "summary": "Searches through events", + "operationId": "search-event", + "parameters": [ + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "name": "clubs", + "in": "query" + }, + { + "type": "string", + "name": "end_time", + "in": "query" + }, + { + "type": "array", + "items": { + "enum": [ + "hybrid", + "in_person", + "virtual" + ], + "type": "string" + }, + "collectionFormat": "csv", + "name": "event_type", + "in": "query" + }, + { + "type": "string", + "name": "search", + "in": "query" + }, + { + "type": "string", + "name": "start_time", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "name": "tags", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/types.SearchResult-models_Event" + } + } + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/socials/": { + "get": { + "description": "Retrieves all socials", + "produces": [ + "application/json" + ], + "tags": [ + "social" + ], + "summary": "Retrieve all socials", + "operationId": "get-socials", + "parameters": [ + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Page", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Social" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/socials/{socialID}/": { + "get": { + "description": "Retrieves a social by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "social" + ], + "summary": "Retrieves a social", + "operationId": "get-social", + "parameters": [ + { + "type": "string", + "description": "Social ID", + "name": "socialID", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.Social" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "description": "Deletes a social", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "social" + ], + "summary": "Deletes a social", + "operationId": "delete-social", + "parameters": [ + { + "type": "string", + "description": "Social ID", + "name": "socialID", "in": "path", "required": true } ], "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { - "$ref": "#/definitions/models.PointOfContact" + "$ref": "#/definitions/models.Social" } }, "400": { @@ -3276,8 +3417,7 @@ const docTemplate = `{ }, "password": { "description": "MARK: must be validated manually", - "type": "string", - "maxLength": 255 + "type": "string" } } }, @@ -3290,13 +3430,11 @@ const docTemplate = `{ "properties": { "new_password": { "description": "MARK: must be validated manually", - "type": "string", - "maxLength": 255 + "type": "string" }, "old_password": { "description": "MARK: must be validated manually", - "type": "string", - "maxLength": 255 + "type": "string" } } }, @@ -4280,15 +4418,15 @@ const docTemplate = `{ ], "properties": { "new_password": { - "type": "string", - "minLength": 8 + "description": "MARK: must be validated manually", + "type": "string" }, "token": { "type": "string" }, "verify_new_password": { - "type": "string", - "minLength": 8 + "description": "MARK: must be validated manually", + "type": "string" } } }, @@ -4304,36 +4442,14 @@ const docTemplate = `{ } } }, - "contacts.PutContactRequestBody": { + "models.Application": { "type": "object", - "required": [ - "content", - "type" - ], "properties": { - "content": { - "type": "string", - "maxLength": 255 + "description": { + "type": "string" }, - "type": { - "maxLength": 255, - "enum": [ - "facebook", - "instagram", - "x", - "linkedin", - "youtube", - "github", - "slack", - "discord", - "email", - "customSite" - ], - "allOf": [ - { - "$ref": "#/definitions/models.ContactType" - } - ] + "title": { + "type": "string" } } }, @@ -4360,9 +4476,6 @@ const docTemplate = `{ "models.Club": { "type": "object", "properties": { - "application_link": { - "type": "string" - }, "created_at": { "type": "string", "example": "2023-09-20T16:34:50Z" @@ -4374,9 +4487,6 @@ const docTemplate = `{ "type": "string", "example": "123e4567-e89b-12d3-a456-426614174000" }, - "is_recruiting": { - "type": "boolean" - }, "logo": { "type": "string" }, @@ -4392,11 +4502,8 @@ const docTemplate = `{ "preview": { "type": "string" }, - "recruitment_cycle": { - "$ref": "#/definitions/models.RecruitmentCycle" - }, - "recruitment_type": { - "$ref": "#/definitions/models.RecruitmentType" + "recruitment": { + "$ref": "#/definitions/models.Recruitment" }, "updated_at": { "type": "string", @@ -4443,56 +4550,6 @@ const docTemplate = `{ "CSSH" ] }, - "models.Contact": { - "type": "object", - "properties": { - "content": { - "type": "string" - }, - "created_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - }, - "id": { - "type": "string", - "example": "123e4567-e89b-12d3-a456-426614174000" - }, - "type": { - "$ref": "#/definitions/models.ContactType" - }, - "updated_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - } - } - }, - "models.ContactType": { - "type": "string", - "enum": [ - "facebook", - "instagram", - "x", - "linkedin", - "youtube", - "github", - "slack", - "discord", - "email", - "customSite" - ], - "x-enum-varnames": [ - "Facebook", - "Instagram", - "X", - "LinkedIn", - "YouTube", - "GitHub", - "Slack", - "Discord", - "Email", - "CustomSite" - ] - }, "models.Event": { "type": "object", "properties": { @@ -4610,6 +4667,35 @@ const docTemplate = `{ "May" ] }, + "models.Leader": { + "type": "object", + "properties": { + "created_at": { + "type": "string", + "example": "2023-09-20T16:34:50Z" + }, + "email": { + "type": "string" + }, + "id": { + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000" + }, + "name": { + "type": "string" + }, + "photo_file": { + "$ref": "#/definitions/models.File" + }, + "position": { + "type": "string" + }, + "updated_at": { + "type": "string", + "example": "2023-09-20T16:34:50Z" + } + } + }, "models.Major": { "type": "string", "enum": [ @@ -4817,32 +4903,23 @@ const docTemplate = `{ "Theatre" ] }, - "models.PointOfContact": { + "models.Recruitment": { "type": "object", "properties": { - "created_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - }, - "email": { - "type": "string" - }, - "id": { - "type": "string", - "example": "123e4567-e89b-12d3-a456-426614174000" - }, - "name": { - "type": "string" + "applications": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Application" + } }, - "photo_file": { - "$ref": "#/definitions/models.File" + "cycle": { + "$ref": "#/definitions/models.RecruitmentCycle" }, - "position": { - "type": "string" + "is_recruiting": { + "type": "boolean" }, - "updated_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" + "type": { + "$ref": "#/definitions/models.RecruitmentType" } } }, @@ -4869,9 +4946,59 @@ const docTemplate = `{ "application" ], "x-enum-varnames": [ - "Unrestricted", - "Tryout", - "Application" + "RecruitmentTypeUnrestricted", + "RecruitmentTypeTryout", + "RecruitmentTypeApplication" + ] + }, + "models.Social": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "created_at": { + "type": "string", + "example": "2023-09-20T16:34:50Z" + }, + "id": { + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000" + }, + "type": { + "$ref": "#/definitions/models.SocialType" + }, + "updated_at": { + "type": "string", + "example": "2023-09-20T16:34:50Z" + } + } + }, + "models.SocialType": { + "type": "string", + "enum": [ + "facebook", + "instagram", + "x", + "linkedin", + "youtube", + "github", + "slack", + "discord", + "email", + "customSite" + ], + "x-enum-varnames": [ + "Facebook", + "Instagram", + "X", + "LinkedIn", + "YouTube", + "GitHub", + "Slack", + "Discord", + "Email", + "CustomSite" ] }, "models.Tag": { @@ -4947,6 +5074,39 @@ const docTemplate = `{ } } }, + "socials.PutSocialRequestBody": { + "type": "object", + "required": [ + "content", + "type" + ], + "properties": { + "content": { + "type": "string", + "maxLength": 255 + }, + "type": { + "maxLength": 255, + "enum": [ + "facebook", + "instagram", + "x", + "linkedin", + "youtube", + "github", + "slack", + "discord", + "email", + "customSite" + ], + "allOf": [ + { + "$ref": "#/definitions/models.SocialType" + } + ] + } + } + }, "tags.CreateClubTagsRequestBody": { "type": "object", "required": [ @@ -4975,6 +5135,28 @@ const docTemplate = `{ } } }, + "types.SearchResult-models_Club": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Club" + } + } + } + }, + "types.SearchResult-models_Event": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Event" + } + } + } + }, "utilities.SuccessResponse": { "type": "object", "properties": { diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index bbfe208db..ff93b92eb 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -169,7 +169,7 @@ }, "/auth/refresh": { "post": { - "description": "Refreshes a user's access token", + "description": "Refreshes a user's access token and returns a new pair of tokens", "consumes": [ "application/json" ], @@ -179,7 +179,7 @@ "tags": [ "auth" ], - "summary": "Refreshes a user's access token", + "summary": "Refreshes a user's access token and returns a new pair of tokens", "operationId": "refresh-user", "responses": { "200": { @@ -1022,17 +1022,17 @@ } } }, - "/clubs/{clubID}/contacts/": { + "/clubs/{clubID}/events/": { "get": { - "description": "Retrieves all contacts associated with a club", + "description": "Retrieves all events associated with a club", "produces": [ "application/json" ], "tags": [ - "club-contact" + "club-event" ], - "summary": "Retrieve all contacts for a club", - "operationId": "get-contacts-by-club", + "summary": "Retrieve all events for a club", + "operationId": "get-events-by-club", "parameters": [ { "type": "string", @@ -1040,6 +1040,18 @@ "name": "clubID", "in": "path", "required": true + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Page", + "name": "page", + "in": "query" } ], "responses": { @@ -1048,7 +1060,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/models.Contact" + "$ref": "#/definitions/models.Event" } } }, @@ -1065,20 +1077,19 @@ "schema": {} } } - }, - "put": { - "description": "Creates a contact", - "consumes": [ - "application/json" - ], + } + }, + "/clubs/{clubID}/followers/": { + "get": { + "description": "Retrieves all followers associated with a club", "produces": [ "application/json" ], "tags": [ - "club-contact" + "club-follower" ], - "summary": "Creates a contact", - "operationId": "put-contact", + "summary": "Retrieve all followers for a club", + "operationId": "get-followers-by-club", "parameters": [ { "type": "string", @@ -1088,30 +1099,32 @@ "required": true }, { - "description": "Contact Body", - "name": "contactBody", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/contacts.PutContactRequestBody" - } + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Page", + "name": "page", + "in": "query" } ], "responses": { - "201": { - "description": "Created", + "200": { + "description": "OK", "schema": { - "$ref": "#/definitions/models.Contact" + "type": "array", + "items": { + "$ref": "#/definitions/models.User" + } } }, "400": { "description": "Bad Request", "schema": {} }, - "401": { - "description": "Unauthorized", - "schema": {} - }, "404": { "description": "Not Found", "schema": {} @@ -1123,17 +1136,17 @@ } } }, - "/clubs/{clubID}/events/": { + "/clubs/{clubID}/leadership/": { "get": { - "description": "Retrieves all events associated with a club", + "description": "Retrieves all leadership associated with a club", "produces": [ "application/json" ], "tags": [ - "club-event" + "club-leader" ], - "summary": "Retrieve all events for a club", - "operationId": "get-events-by-club", + "summary": "Retrieve all leadership for a club", + "operationId": "get-leadership-by-club", "parameters": [ { "type": "string", @@ -1141,18 +1154,6 @@ "name": "clubID", "in": "path", "required": true - }, - { - "type": "integer", - "description": "Limit", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "description": "Page", - "name": "page", - "in": "query" } ], "responses": { @@ -1161,7 +1162,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/models.Event" + "$ref": "#/definitions/models.Leader" } } }, @@ -1178,19 +1179,20 @@ "schema": {} } } - } - }, - "/clubs/{clubID}/followers/": { - "get": { - "description": "Retrieves all followers associated with a club", + }, + "post": { + "description": "Creates a leader associated with a club", + "consumes": [ + "multipart/form-data" + ], "produces": [ "application/json" ], "tags": [ - "club-follower" + "club-leader" ], - "summary": "Retrieve all followers for a club", - "operationId": "get-followers-by-club", + "summary": "Create a leader for a club", + "operationId": "create-leader-by-club", "parameters": [ { "type": "string", @@ -1198,34 +1200,23 @@ "name": "clubID", "in": "path", "required": true - }, - { - "type": "integer", - "description": "Limit", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "description": "Page", - "name": "page", - "in": "query" } ], "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.User" - } + "$ref": "#/definitions/models.Leader" } }, "400": { "description": "Bad Request", "schema": {} }, + "401": { + "description": "Unauthorized", + "schema": {} + }, "404": { "description": "Not Found", "schema": {} @@ -1237,17 +1228,17 @@ } } }, - "/clubs/{clubID}/members/": { + "/clubs/{clubID}/leadership/{leaderID}": { "get": { - "description": "Retrieves all members associated with a club", + "description": "Retrieves a leader associated with a club", "produces": [ "application/json" ], "tags": [ - "club-member" + "club-leader" ], - "summary": "Retrieve all members for a club", - "operationId": "get-members-by-club", + "summary": "Retrieve a leader for a club", + "operationId": "get-leader-by-club", "parameters": [ { "type": "string", @@ -1257,36 +1248,24 @@ "required": true }, { - "type": "integer", - "description": "Limit", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "description": "Page", - "name": "page", - "in": "query" + "type": "string", + "description": "leader ID", + "name": "leaderID", + "in": "path", + "required": true } ], "responses": { "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.User" - } + "$ref": "#/definitions/models.Leader" } }, "400": { "description": "Bad Request", "schema": {} }, - "401": { - "description": "Unauthorized", - "schema": {} - }, "404": { "description": "Not Found", "schema": {} @@ -1297,8 +1276,8 @@ } } }, - "post": { - "description": "Creates a new member associated with a club", + "put": { + "description": "Updates a leader associated with a club", "consumes": [ "application/json" ], @@ -1306,10 +1285,10 @@ "application/json" ], "tags": [ - "club-member" + "club-leader" ], - "summary": "Create a new member for a club", - "operationId": "create-member-for-club", + "summary": "Update a leader for a club", + "operationId": "update-leader-by-club", "parameters": [ { "type": "string", @@ -1320,17 +1299,17 @@ }, { "type": "string", - "description": "User ID", - "name": "userID", + "description": "leader ID", + "name": "leaderID", "in": "path", "required": true } ], "responses": { - "201": { - "description": "Created", + "200": { + "description": "OK", "schema": { - "$ref": "#/definitions/models.User" + "$ref": "#/definitions/models.Leader" } }, "400": { @@ -1352,15 +1331,15 @@ } }, "delete": { - "description": "Deletes a member associated with a club", + "description": "Delete a leader associated with a club", "produces": [ "application/json" ], "tags": [ - "club-member" + "club-leader" ], - "summary": "Delete a member from a club", - "operationId": "delete-member-from-club", + "summary": "Delete a leader for a club", + "operationId": "delete-leader-by-club", "parameters": [ { "type": "string", @@ -1371,27 +1350,20 @@ }, { "type": "string", - "description": "User ID", - "name": "userID", + "description": "leader ID", + "name": "leaderID", "in": "path", "required": true } ], "responses": { "204": { - "description": "No Content", - "schema": { - "$ref": "#/definitions/models.User" - } + "description": "No Content" }, "400": { "description": "Bad Request", "schema": {} }, - "401": { - "description": "Unauthorized", - "schema": {} - }, "404": { "description": "Not Found", "schema": {} @@ -1401,11 +1373,9 @@ "schema": {} } } - } - }, - "/clubs/{clubID}/poc/": { - "post": { - "description": "Creates a point of contact associated with a club", + }, + "patch": { + "description": "Updates a leader photo associated with a club", "consumes": [ "multipart/form-data" ], @@ -1413,10 +1383,10 @@ "application/json" ], "tags": [ - "club-point-of-contact" + "club-leader" ], - "summary": "Create a point of contact for a club", - "operationId": "create-point-of-contact-by-club", + "summary": "Update a leader photo for a club", + "operationId": "update-leader-photo-by-club", "parameters": [ { "type": "string", @@ -1424,13 +1394,20 @@ "name": "clubID", "in": "path", "required": true + }, + { + "type": "string", + "description": "leader ID", + "name": "leaderID", + "in": "path", + "required": true } ], "responses": { - "201": { - "description": "Created", + "200": { + "description": "OK", "schema": { - "$ref": "#/definitions/models.PointOfContact" + "$ref": "#/definitions/models.Leader" } }, "400": { @@ -1452,20 +1429,17 @@ } } }, - "/clubs/{clubID}/poc/{pocID}": { - "put": { - "description": "Updates a point of contact associated with a club", - "consumes": [ - "application/json" - ], + "/clubs/{clubID}/members/": { + "get": { + "description": "Retrieves all members associated with a club", "produces": [ "application/json" ], "tags": [ - "club-point-of-contact" + "club-member" ], - "summary": "Update a point of contact for a club", - "operationId": "update-point-of-contact-by-club", + "summary": "Retrieve all members for a club", + "operationId": "get-members-by-club", "parameters": [ { "type": "string", @@ -1475,18 +1449,26 @@ "required": true }, { - "type": "string", - "description": "Point of Contact ID", - "name": "pocID", - "in": "path", - "required": true + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Page", + "name": "page", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/models.PointOfContact" + "type": "array", + "items": { + "$ref": "#/definitions/models.User" + } } }, "400": { @@ -1507,16 +1489,19 @@ } } }, - "delete": { - "description": "Delete a point of contact associated with a club", + "post": { + "description": "Creates a new member associated with a club", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "club-point-of-contact" + "club-member" ], - "summary": "Delete a point of contact for a club", - "operationId": "delete-point-of-contact-by-club", + "summary": "Create a new member for a club", + "operationId": "create-member-for-club", "parameters": [ { "type": "string", @@ -1527,20 +1512,27 @@ }, { "type": "string", - "description": "Point of Contact ID", - "name": "pocID", + "description": "User ID", + "name": "userID", "in": "path", "required": true } ], "responses": { - "204": { - "description": "No Content" + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.User" + } }, "400": { "description": "Bad Request", "schema": {} }, + "401": { + "description": "Unauthorized", + "schema": {} + }, "404": { "description": "Not Found", "schema": {} @@ -1551,19 +1543,16 @@ } } }, - "patch": { - "description": "Updates a point of contact photo associated with a club", - "consumes": [ - "multipart/form-data" - ], + "delete": { + "description": "Deletes a member associated with a club", "produces": [ "application/json" ], "tags": [ - "club-point-of-contact" + "club-member" ], - "summary": "Update a point of contact photo for a club", - "operationId": "update-point-of-contact-photo-by-club", + "summary": "Delete a member from a club", + "operationId": "delete-member-from-club", "parameters": [ { "type": "string", @@ -1574,17 +1563,17 @@ }, { "type": "string", - "description": "Point of Contact ID", - "name": "pocID", + "description": "User ID", + "name": "userID", "in": "path", "required": true } ], "responses": { - "200": { - "description": "OK", + "204": { + "description": "No Content", "schema": { - "$ref": "#/definitions/models.PointOfContact" + "$ref": "#/definitions/models.User" } }, "400": { @@ -1606,17 +1595,17 @@ } } }, - "/clubs/{clubID}/pocs/": { + "/clubs/{clubID}/socials/": { "get": { - "description": "Retrieves all point of contacts associated with a club", + "description": "Retrieves all socials associated with a club", "produces": [ "application/json" ], "tags": [ - "club-point-of-contact" + "club-social" ], - "summary": "Retrieve all point of contacts for a club", - "operationId": "get-point-of-contacts-by-club", + "summary": "Retrieve all socials for a club", + "operationId": "get-socials-by-club", "parameters": [ { "type": "string", @@ -1632,7 +1621,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/models.PointOfContact" + "$ref": "#/definitions/models.Social" } } }, @@ -1649,19 +1638,20 @@ "schema": {} } } - } - }, - "/clubs/{clubID}/pocs/{pocID}": { - "get": { - "description": "Retrieves a point of contact associated with a club", + }, + "put": { + "description": "Creates a social", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "club-point-of-contact" + "club-social" ], - "summary": "Retrieve a point of contact for a club", - "operationId": "get-point-of-contact-by-club", + "summary": "Creates a social", + "operationId": "put-social", "parameters": [ { "type": "string", @@ -1671,24 +1661,30 @@ "required": true }, { - "type": "string", - "description": "Point of Contact ID", - "name": "pocID", - "in": "path", - "required": true + "description": "Social Body", + "name": "socialBody", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/socials.PutSocialRequestBody" + } } ], "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { - "$ref": "#/definitions/models.PointOfContact" + "$ref": "#/definitions/models.Social" } }, "400": { "description": "Bad Request", "schema": {} }, + "401": { + "description": "Unauthorized", + "schema": {} + }, "404": { "description": "Not Found", "schema": {} @@ -1857,162 +1853,6 @@ } } }, - "/contacts/": { - "get": { - "description": "Retrieves all contacts", - "produces": [ - "application/json" - ], - "tags": [ - "contact" - ], - "summary": "Retrieve all contacts", - "operationId": "get-contacts", - "parameters": [ - { - "type": "integer", - "description": "Limit", - "name": "limit", - "in": "query" - }, - { - "type": "integer", - "description": "Page", - "name": "page", - "in": "query" - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Contact" - } - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "string" - } - }, - "404": { - "description": "Not Found", - "schema": { - "type": "string" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "string" - } - } - } - } - }, - "/contacts/{contactID}/": { - "get": { - "description": "Retrieves a contact by id", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "contact" - ], - "summary": "Retrieves a contact", - "operationId": "get-contact", - "parameters": [ - { - "type": "string", - "description": "Contact ID", - "name": "contactID", - "in": "path", - "required": true - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/models.Contact" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "string" - } - }, - "404": { - "description": "Not Found", - "schema": { - "type": "string" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "string" - } - } - } - }, - "delete": { - "description": "Deletes a contact", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "contact" - ], - "summary": "Deletes a contact", - "operationId": "delete-contact", - "parameters": [ - { - "type": "string", - "description": "Contact ID", - "name": "contactID", - "in": "path", - "required": true - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/models.Contact" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "type": "string" - } - }, - "404": { - "description": "Not Found", - "schema": { - "type": "string" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "type": "string" - } - } - } - } - }, "/events/": { "get": { "description": "Retrieves all events", @@ -2298,7 +2138,7 @@ } } }, - "/pocs/": { + "/leader/": { "get": { "description": "Retrieves all point of contacts", "produces": [ @@ -2329,7 +2169,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/models.PointOfContact" + "$ref": "#/definitions/models.Leader" } } }, @@ -2354,7 +2194,7 @@ } } }, - "/pocs/{pocID}/": { + "/leader/{leaderID}/": { "get": { "description": "Retrieves a point of contact by id", "produces": [ @@ -2368,17 +2208,318 @@ "parameters": [ { "type": "string", - "description": "Point of Contact ID", - "name": "pocID", + "description": "Point of Contact ID", + "name": "leaderID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Leader" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/search/clubs": { + "get": { + "description": "Searches through clubs", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "search" + ], + "summary": "Searches through clubs", + "operationId": "search-clubs", + "parameters": [ + { + "type": "integer", + "name": "max_members", + "in": "query" + }, + { + "type": "integer", + "name": "min_members", + "in": "query" + }, + { + "type": "string", + "name": "search", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "name": "tags", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/types.SearchResult-models_Club" + } + } + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/search/events": { + "get": { + "description": "Searches through events", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "search" + ], + "summary": "Searches through events", + "operationId": "search-event", + "parameters": [ + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "name": "clubs", + "in": "query" + }, + { + "type": "string", + "name": "end_time", + "in": "query" + }, + { + "type": "array", + "items": { + "enum": [ + "hybrid", + "in_person", + "virtual" + ], + "type": "string" + }, + "collectionFormat": "csv", + "name": "event_type", + "in": "query" + }, + { + "type": "string", + "name": "search", + "in": "query" + }, + { + "type": "string", + "name": "start_time", + "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "name": "tags", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/types.SearchResult-models_Event" + } + } + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/socials/": { + "get": { + "description": "Retrieves all socials", + "produces": [ + "application/json" + ], + "tags": [ + "social" + ], + "summary": "Retrieve all socials", + "operationId": "get-socials", + "parameters": [ + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Page", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Social" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + } + }, + "/socials/{socialID}/": { + "get": { + "description": "Retrieves a social by id", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "social" + ], + "summary": "Retrieves a social", + "operationId": "get-social", + "parameters": [ + { + "type": "string", + "description": "Social ID", + "name": "socialID", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.Social" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "description": "Deletes a social", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "social" + ], + "summary": "Deletes a social", + "operationId": "delete-social", + "parameters": [ + { + "type": "string", + "description": "Social ID", + "name": "socialID", "in": "path", "required": true } ], "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { - "$ref": "#/definitions/models.PointOfContact" + "$ref": "#/definitions/models.Social" } }, "400": { @@ -3265,8 +3406,7 @@ }, "password": { "description": "MARK: must be validated manually", - "type": "string", - "maxLength": 255 + "type": "string" } } }, @@ -3279,13 +3419,11 @@ "properties": { "new_password": { "description": "MARK: must be validated manually", - "type": "string", - "maxLength": 255 + "type": "string" }, "old_password": { "description": "MARK: must be validated manually", - "type": "string", - "maxLength": 255 + "type": "string" } } }, @@ -4269,15 +4407,15 @@ ], "properties": { "new_password": { - "type": "string", - "minLength": 8 + "description": "MARK: must be validated manually", + "type": "string" }, "token": { "type": "string" }, "verify_new_password": { - "type": "string", - "minLength": 8 + "description": "MARK: must be validated manually", + "type": "string" } } }, @@ -4293,36 +4431,14 @@ } } }, - "contacts.PutContactRequestBody": { + "models.Application": { "type": "object", - "required": [ - "content", - "type" - ], "properties": { - "content": { - "type": "string", - "maxLength": 255 + "description": { + "type": "string" }, - "type": { - "maxLength": 255, - "enum": [ - "facebook", - "instagram", - "x", - "linkedin", - "youtube", - "github", - "slack", - "discord", - "email", - "customSite" - ], - "allOf": [ - { - "$ref": "#/definitions/models.ContactType" - } - ] + "title": { + "type": "string" } } }, @@ -4349,9 +4465,6 @@ "models.Club": { "type": "object", "properties": { - "application_link": { - "type": "string" - }, "created_at": { "type": "string", "example": "2023-09-20T16:34:50Z" @@ -4363,9 +4476,6 @@ "type": "string", "example": "123e4567-e89b-12d3-a456-426614174000" }, - "is_recruiting": { - "type": "boolean" - }, "logo": { "type": "string" }, @@ -4381,11 +4491,8 @@ "preview": { "type": "string" }, - "recruitment_cycle": { - "$ref": "#/definitions/models.RecruitmentCycle" - }, - "recruitment_type": { - "$ref": "#/definitions/models.RecruitmentType" + "recruitment": { + "$ref": "#/definitions/models.Recruitment" }, "updated_at": { "type": "string", @@ -4432,56 +4539,6 @@ "CSSH" ] }, - "models.Contact": { - "type": "object", - "properties": { - "content": { - "type": "string" - }, - "created_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - }, - "id": { - "type": "string", - "example": "123e4567-e89b-12d3-a456-426614174000" - }, - "type": { - "$ref": "#/definitions/models.ContactType" - }, - "updated_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - } - } - }, - "models.ContactType": { - "type": "string", - "enum": [ - "facebook", - "instagram", - "x", - "linkedin", - "youtube", - "github", - "slack", - "discord", - "email", - "customSite" - ], - "x-enum-varnames": [ - "Facebook", - "Instagram", - "X", - "LinkedIn", - "YouTube", - "GitHub", - "Slack", - "Discord", - "Email", - "CustomSite" - ] - }, "models.Event": { "type": "object", "properties": { @@ -4599,6 +4656,35 @@ "May" ] }, + "models.Leader": { + "type": "object", + "properties": { + "created_at": { + "type": "string", + "example": "2023-09-20T16:34:50Z" + }, + "email": { + "type": "string" + }, + "id": { + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000" + }, + "name": { + "type": "string" + }, + "photo_file": { + "$ref": "#/definitions/models.File" + }, + "position": { + "type": "string" + }, + "updated_at": { + "type": "string", + "example": "2023-09-20T16:34:50Z" + } + } + }, "models.Major": { "type": "string", "enum": [ @@ -4806,32 +4892,23 @@ "Theatre" ] }, - "models.PointOfContact": { + "models.Recruitment": { "type": "object", "properties": { - "created_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" - }, - "email": { - "type": "string" - }, - "id": { - "type": "string", - "example": "123e4567-e89b-12d3-a456-426614174000" - }, - "name": { - "type": "string" + "applications": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Application" + } }, - "photo_file": { - "$ref": "#/definitions/models.File" + "cycle": { + "$ref": "#/definitions/models.RecruitmentCycle" }, - "position": { - "type": "string" + "is_recruiting": { + "type": "boolean" }, - "updated_at": { - "type": "string", - "example": "2023-09-20T16:34:50Z" + "type": { + "$ref": "#/definitions/models.RecruitmentType" } } }, @@ -4858,9 +4935,59 @@ "application" ], "x-enum-varnames": [ - "Unrestricted", - "Tryout", - "Application" + "RecruitmentTypeUnrestricted", + "RecruitmentTypeTryout", + "RecruitmentTypeApplication" + ] + }, + "models.Social": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "created_at": { + "type": "string", + "example": "2023-09-20T16:34:50Z" + }, + "id": { + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000" + }, + "type": { + "$ref": "#/definitions/models.SocialType" + }, + "updated_at": { + "type": "string", + "example": "2023-09-20T16:34:50Z" + } + } + }, + "models.SocialType": { + "type": "string", + "enum": [ + "facebook", + "instagram", + "x", + "linkedin", + "youtube", + "github", + "slack", + "discord", + "email", + "customSite" + ], + "x-enum-varnames": [ + "Facebook", + "Instagram", + "X", + "LinkedIn", + "YouTube", + "GitHub", + "Slack", + "Discord", + "Email", + "CustomSite" ] }, "models.Tag": { @@ -4936,6 +5063,39 @@ } } }, + "socials.PutSocialRequestBody": { + "type": "object", + "required": [ + "content", + "type" + ], + "properties": { + "content": { + "type": "string", + "maxLength": 255 + }, + "type": { + "maxLength": 255, + "enum": [ + "facebook", + "instagram", + "x", + "linkedin", + "youtube", + "github", + "slack", + "discord", + "email", + "customSite" + ], + "allOf": [ + { + "$ref": "#/definitions/models.SocialType" + } + ] + } + } + }, "tags.CreateClubTagsRequestBody": { "type": "object", "required": [ @@ -4964,6 +5124,28 @@ } } }, + "types.SearchResult-models_Club": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Club" + } + } + } + }, + "types.SearchResult-models_Event": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Event" + } + } + } + }, "utilities.SuccessResponse": { "type": "object", "properties": { diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 93f68a29c..34a776f79 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -5,7 +5,6 @@ definitions: type: string password: description: 'MARK: must be validated manually' - maxLength: 255 type: string required: - email @@ -15,11 +14,9 @@ definitions: properties: new_password: description: 'MARK: must be validated manually' - maxLength: 255 type: string old_password: description: 'MARK: must be validated manually' - maxLength: 255 type: string required: - new_password @@ -874,12 +871,12 @@ definitions: base.VerifyPasswordResetTokenRequestBody: properties: new_password: - minLength: 8 + description: 'MARK: must be validated manually' type: string token: type: string verify_new_password: - minLength: 8 + description: 'MARK: must be validated manually' type: string required: - new_password @@ -894,29 +891,12 @@ definitions: required: - name type: object - contacts.PutContactRequestBody: + models.Application: properties: - content: - maxLength: 255 + description: + type: string + title: type: string - type: - allOf: - - $ref: '#/definitions/models.ContactType' - enum: - - facebook - - instagram - - x - - linkedin - - youtube - - github - - slack - - discord - - email - - customSite - maxLength: 255 - required: - - content - - type type: object models.Category: properties: @@ -934,8 +914,6 @@ definitions: type: object models.Club: properties: - application_link: - type: string created_at: example: "2023-09-20T16:34:50Z" type: string @@ -944,8 +922,6 @@ definitions: id: example: 123e4567-e89b-12d3-a456-426614174000 type: string - is_recruiting: - type: boolean logo: type: string name: @@ -956,10 +932,8 @@ definitions: type: string preview: type: string - recruitment_cycle: - $ref: '#/definitions/models.RecruitmentCycle' - recruitment_type: - $ref: '#/definitions/models.RecruitmentType' + recruitment: + $ref: '#/definitions/models.Recruitment' updated_at: example: "2023-09-20T16:34:50Z" type: string @@ -998,46 +972,6 @@ definitions: - CPS - CS - CSSH - models.Contact: - properties: - content: - type: string - created_at: - example: "2023-09-20T16:34:50Z" - type: string - id: - example: 123e4567-e89b-12d3-a456-426614174000 - type: string - type: - $ref: '#/definitions/models.ContactType' - updated_at: - example: "2023-09-20T16:34:50Z" - type: string - type: object - models.ContactType: - enum: - - facebook - - instagram - - x - - linkedin - - youtube - - github - - slack - - discord - - email - - customSite - type: string - x-enum-varnames: - - Facebook - - Instagram - - X - - LinkedIn - - YouTube - - GitHub - - Slack - - Discord - - Email - - CustomSite models.Event: properties: created_at: @@ -1119,6 +1053,26 @@ definitions: x-enum-varnames: - December - May + models.Leader: + properties: + created_at: + example: "2023-09-20T16:34:50Z" + type: string + email: + type: string + id: + example: 123e4567-e89b-12d3-a456-426614174000 + type: string + name: + type: string + photo_file: + $ref: '#/definitions/models.File' + position: + type: string + updated_at: + example: "2023-09-20T16:34:50Z" + type: string + type: object models.Major: enum: - africanaStudies @@ -1323,25 +1277,18 @@ definitions: - Spanish - SpeechLanguagePathologyAndAudiology - Theatre - models.PointOfContact: + models.Recruitment: properties: - created_at: - example: "2023-09-20T16:34:50Z" - type: string - email: - type: string - id: - example: 123e4567-e89b-12d3-a456-426614174000 - type: string - name: - type: string - photo_file: - $ref: '#/definitions/models.File' - position: - type: string - updated_at: - example: "2023-09-20T16:34:50Z" - type: string + applications: + items: + $ref: '#/definitions/models.Application' + type: array + cycle: + $ref: '#/definitions/models.RecruitmentCycle' + is_recruiting: + type: boolean + type: + $ref: '#/definitions/models.RecruitmentType' type: object models.RecruitmentCycle: enum: @@ -1362,9 +1309,49 @@ definitions: - application type: string x-enum-varnames: - - Unrestricted - - Tryout - - Application + - RecruitmentTypeUnrestricted + - RecruitmentTypeTryout + - RecruitmentTypeApplication + models.Social: + properties: + content: + type: string + created_at: + example: "2023-09-20T16:34:50Z" + type: string + id: + example: 123e4567-e89b-12d3-a456-426614174000 + type: string + type: + $ref: '#/definitions/models.SocialType' + updated_at: + example: "2023-09-20T16:34:50Z" + type: string + type: object + models.SocialType: + enum: + - facebook + - instagram + - x + - linkedin + - youtube + - github + - slack + - discord + - email + - customSite + type: string + x-enum-varnames: + - Facebook + - Instagram + - X + - LinkedIn + - YouTube + - GitHub + - Slack + - Discord + - Email + - CustomSite models.Tag: properties: category_id: @@ -1415,6 +1402,30 @@ definitions: example: "2023-09-20T16:34:50Z" type: string type: object + socials.PutSocialRequestBody: + properties: + content: + maxLength: 255 + type: string + type: + allOf: + - $ref: '#/definitions/models.SocialType' + enum: + - facebook + - instagram + - x + - linkedin + - youtube + - github + - slack + - discord + - email + - customSite + maxLength: 255 + required: + - content + - type + type: object tags.CreateClubTagsRequestBody: properties: tags: @@ -1433,6 +1444,20 @@ definitions: required: - tags type: object + types.SearchResult-models_Club: + properties: + results: + items: + $ref: '#/definitions/models.Club' + type: array + type: object + types.SearchResult-models_Event: + properties: + results: + items: + $ref: '#/definitions/models.Event' + type: array + type: object utilities.SuccessResponse: properties: message: @@ -1555,7 +1580,7 @@ paths: post: consumes: - application/json - description: Refreshes a user's access token + description: Refreshes a user's access token and returns a new pair of tokens operationId: refresh-user produces: - application/json @@ -1573,7 +1598,7 @@ paths: "500": description: Internal Server Error schema: {} - summary: Refreshes a user's access token + summary: Refreshes a user's access token and returns a new pair of tokens tags: - auth /auth/register: @@ -2132,76 +2157,6 @@ paths: summary: Update a club tags: - club - /clubs/{clubID}/contacts/: - get: - description: Retrieves all contacts associated with a club - operationId: get-contacts-by-club - parameters: - - description: Club ID - in: path - name: clubID - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - items: - $ref: '#/definitions/models.Contact' - type: array - "400": - description: Bad Request - schema: {} - "404": - description: Not Found - schema: {} - "500": - description: Internal Server Error - schema: {} - summary: Retrieve all contacts for a club - tags: - - club-contact - put: - consumes: - - application/json - description: Creates a contact - operationId: put-contact - parameters: - - description: Club ID - in: path - name: clubID - required: true - type: string - - description: Contact Body - in: body - name: contactBody - required: true - schema: - $ref: '#/definitions/contacts.PutContactRequestBody' - produces: - - application/json - responses: - "201": - description: Created - schema: - $ref: '#/definitions/models.Contact' - "400": - description: Bad Request - schema: {} - "401": - description: Unauthorized - schema: {} - "404": - description: Not Found - schema: {} - "500": - description: Internal Server Error - schema: {} - summary: Creates a contact - tags: - - club-contact /clubs/{clubID}/events/: get: description: Retrieves all events associated with a club @@ -2280,33 +2235,60 @@ paths: summary: Retrieve all followers for a club tags: - club-follower - /clubs/{clubID}/members/: - delete: - description: Deletes a member associated with a club - operationId: delete-member-from-club + /clubs/{clubID}/leadership/: + get: + description: Retrieves all leadership associated with a club + operationId: get-leadership-by-club parameters: - description: Club ID in: path name: clubID required: true type: string - - description: User ID - in: path - name: userID - required: true - type: string produces: - application/json responses: - "204": - description: No Content + "200": + description: OK schema: - $ref: '#/definitions/models.User' + items: + $ref: '#/definitions/models.Leader' + type: array "400": description: Bad Request schema: {} - "401": - description: Unauthorized + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Retrieve all leadership for a club + tags: + - club-leader + post: + consumes: + - multipart/form-data + description: Creates a leader associated with a club + operationId: create-leader-by-club + parameters: + - description: Club ID + in: path + name: clubID + required: true + type: string + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/models.Leader' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized schema: {} "404": description: Not Found @@ -2314,73 +2296,97 @@ paths: "500": description: Internal Server Error schema: {} - summary: Delete a member from a club + summary: Create a leader for a club tags: - - club-member + - club-leader + /clubs/{clubID}/leadership/{leaderID}: + delete: + description: Delete a leader associated with a club + operationId: delete-leader-by-club + parameters: + - description: Club ID + in: path + name: clubID + required: true + type: string + - description: leader ID + in: path + name: leaderID + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + "400": + description: Bad Request + schema: {} + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Delete a leader for a club + tags: + - club-leader get: - description: Retrieves all members associated with a club - operationId: get-members-by-club + description: Retrieves a leader associated with a club + operationId: get-leader-by-club parameters: - description: Club ID in: path name: clubID required: true type: string - - description: Limit - in: query - name: limit - type: integer - - description: Page - in: query - name: page - type: integer + - description: leader ID + in: path + name: leaderID + required: true + type: string produces: - application/json responses: "200": description: OK schema: - items: - $ref: '#/definitions/models.User' - type: array + $ref: '#/definitions/models.Leader' "400": description: Bad Request schema: {} - "401": - description: Unauthorized - schema: {} "404": description: Not Found schema: {} "500": description: Internal Server Error schema: {} - summary: Retrieve all members for a club + summary: Retrieve a leader for a club tags: - - club-member - post: + - club-leader + patch: consumes: - - application/json - description: Creates a new member associated with a club - operationId: create-member-for-club + - multipart/form-data + description: Updates a leader photo associated with a club + operationId: update-leader-photo-by-club parameters: - description: Club ID in: path name: clubID required: true type: string - - description: User ID + - description: leader ID in: path - name: userID + name: leaderID required: true type: string produces: - application/json responses: - "201": - description: Created + "200": + description: OK schema: - $ref: '#/definitions/models.User' + $ref: '#/definitions/models.Leader' "400": description: Bad Request schema: {} @@ -2393,28 +2399,32 @@ paths: "500": description: Internal Server Error schema: {} - summary: Create a new member for a club + summary: Update a leader photo for a club tags: - - club-member - /clubs/{clubID}/poc/: - post: + - club-leader + put: consumes: - - multipart/form-data - description: Creates a point of contact associated with a club - operationId: create-point-of-contact-by-club + - application/json + description: Updates a leader associated with a club + operationId: update-leader-by-club parameters: - description: Club ID in: path name: clubID required: true type: string + - description: leader ID + in: path + name: leaderID + required: true + type: string produces: - application/json responses: - "201": - description: Created + "200": + description: OK schema: - $ref: '#/definitions/models.PointOfContact' + $ref: '#/definitions/models.Leader' "400": description: Bad Request schema: {} @@ -2427,22 +2437,22 @@ paths: "500": description: Internal Server Error schema: {} - summary: Create a point of contact for a club + summary: Update a leader for a club tags: - - club-point-of-contact - /clubs/{clubID}/poc/{pocID}: + - club-leader + /clubs/{clubID}/members/: delete: - description: Delete a point of contact associated with a club - operationId: delete-point-of-contact-by-club + description: Deletes a member associated with a club + operationId: delete-member-from-club parameters: - description: Club ID in: path name: clubID required: true type: string - - description: Point of Contact ID + - description: User ID in: path - name: pocID + name: userID required: true type: string produces: @@ -2450,41 +2460,49 @@ paths: responses: "204": description: No Content + schema: + $ref: '#/definitions/models.User' "400": description: Bad Request schema: {} + "401": + description: Unauthorized + schema: {} "404": description: Not Found schema: {} "500": description: Internal Server Error schema: {} - summary: Delete a point of contact for a club + summary: Delete a member from a club tags: - - club-point-of-contact - patch: - consumes: - - multipart/form-data - description: Updates a point of contact photo associated with a club - operationId: update-point-of-contact-photo-by-club + - club-member + get: + description: Retrieves all members associated with a club + operationId: get-members-by-club parameters: - description: Club ID in: path name: clubID required: true type: string - - description: Point of Contact ID - in: path - name: pocID - required: true - type: string + - description: Limit + in: query + name: limit + type: integer + - description: Page + in: query + name: page + type: integer produces: - application/json responses: "200": description: OK schema: - $ref: '#/definitions/models.PointOfContact' + items: + $ref: '#/definitions/models.User' + type: array "400": description: Bad Request schema: {} @@ -2497,32 +2515,32 @@ paths: "500": description: Internal Server Error schema: {} - summary: Update a point of contact photo for a club + summary: Retrieve all members for a club tags: - - club-point-of-contact - put: + - club-member + post: consumes: - application/json - description: Updates a point of contact associated with a club - operationId: update-point-of-contact-by-club + description: Creates a new member associated with a club + operationId: create-member-for-club parameters: - description: Club ID in: path name: clubID required: true type: string - - description: Point of Contact ID + - description: User ID in: path - name: pocID + name: userID required: true type: string produces: - application/json responses: - "200": - description: OK + "201": + description: Created schema: - $ref: '#/definitions/models.PointOfContact' + $ref: '#/definitions/models.User' "400": description: Bad Request schema: {} @@ -2535,13 +2553,13 @@ paths: "500": description: Internal Server Error schema: {} - summary: Update a point of contact for a club + summary: Create a new member for a club tags: - - club-point-of-contact - /clubs/{clubID}/pocs/: + - club-member + /clubs/{clubID}/socials/: get: - description: Retrieves all point of contacts associated with a club - operationId: get-point-of-contacts-by-club + description: Retrieves all socials associated with a club + operationId: get-socials-by-club parameters: - description: Club ID in: path @@ -2555,7 +2573,7 @@ paths: description: OK schema: items: - $ref: '#/definitions/models.PointOfContact' + $ref: '#/definitions/models.Social' type: array "400": description: Bad Request @@ -2566,43 +2584,48 @@ paths: "500": description: Internal Server Error schema: {} - summary: Retrieve all point of contacts for a club + summary: Retrieve all socials for a club tags: - - club-point-of-contact - /clubs/{clubID}/pocs/{pocID}: - get: - description: Retrieves a point of contact associated with a club - operationId: get-point-of-contact-by-club + - club-social + put: + consumes: + - application/json + description: Creates a social + operationId: put-social parameters: - description: Club ID in: path name: clubID required: true type: string - - description: Point of Contact ID - in: path - name: pocID + - description: Social Body + in: body + name: socialBody required: true - type: string + schema: + $ref: '#/definitions/socials.PutSocialRequestBody' produces: - application/json responses: - "200": - description: OK + "201": + description: Created schema: - $ref: '#/definitions/models.PointOfContact' + $ref: '#/definitions/models.Social' "400": description: Bad Request schema: {} + "401": + description: Unauthorized + schema: {} "404": description: Not Found schema: {} "500": description: Internal Server Error schema: {} - summary: Retrieve a point of contact for a club + summary: Creates a social tags: - - club-point-of-contact + - club-social /clubs/{clubID}/tags/: get: description: Retrieves all tags associated with a club @@ -2712,110 +2735,6 @@ paths: summary: Delete a tag for a club tags: - club-tag - /contacts/: - get: - description: Retrieves all contacts - operationId: get-contacts - parameters: - - description: Limit - in: query - name: limit - type: integer - - description: Page - in: query - name: page - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - items: - $ref: '#/definitions/models.Contact' - type: array - "400": - description: Bad Request - schema: - type: string - "404": - description: Not Found - schema: - type: string - "500": - description: Internal Server Error - schema: - type: string - summary: Retrieve all contacts - tags: - - contact - /contacts/{contactID}/: - delete: - consumes: - - application/json - description: Deletes a contact - operationId: delete-contact - parameters: - - description: Contact ID - in: path - name: contactID - required: true - type: string - produces: - - application/json - responses: - "201": - description: Created - schema: - $ref: '#/definitions/models.Contact' - "400": - description: Bad Request - schema: - type: string - "404": - description: Not Found - schema: - type: string - "500": - description: Internal Server Error - schema: - type: string - summary: Deletes a contact - tags: - - contact - get: - consumes: - - application/json - description: Retrieves a contact by id - operationId: get-contact - parameters: - - description: Contact ID - in: path - name: contactID - required: true - type: string - produces: - - application/json - responses: - "201": - description: Created - schema: - $ref: '#/definitions/models.Contact' - "400": - description: Bad Request - schema: - type: string - "404": - description: Not Found - schema: - type: string - "500": - description: Internal Server Error - schema: - type: string - summary: Retrieves a contact - tags: - - contact /events/: get: description: Retrieves all events @@ -3013,7 +2932,7 @@ paths: summary: Retrieve a file tags: - file - /pocs/: + /leader/: get: description: Retrieves all point of contacts operationId: get-point-of-contacts @@ -3033,7 +2952,7 @@ paths: description: OK schema: items: - $ref: '#/definitions/models.PointOfContact' + $ref: '#/definitions/models.Leader' type: array "400": description: Bad Request @@ -3050,14 +2969,14 @@ paths: summary: Retrieve all point of contacts tags: - point of contact - /pocs/{pocID}/: + /leader/{leaderID}/: get: description: Retrieves a point of contact by id operationId: get-point-of-contact parameters: - description: Point of Contact ID in: path - name: pocID + name: leaderID required: true type: string produces: @@ -3066,7 +2985,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/models.PointOfContact' + $ref: '#/definitions/models.Leader' "400": description: Bad Request schema: @@ -3082,6 +3001,206 @@ paths: summary: Retrieves a point of contact tags: - point of contact + /search/clubs: + get: + consumes: + - application/json + description: Searches through clubs + operationId: search-clubs + parameters: + - in: query + name: max_members + type: integer + - in: query + name: min_members + type: integer + - in: query + name: search + type: string + - collectionFormat: csv + in: query + items: + type: string + name: tags + type: array + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/types.SearchResult-models_Club' + type: array + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Searches through clubs + tags: + - search + /search/events: + get: + consumes: + - application/json + description: Searches through events + operationId: search-event + parameters: + - collectionFormat: csv + in: query + items: + type: string + name: clubs + type: array + - in: query + name: end_time + type: string + - collectionFormat: csv + in: query + items: + enum: + - hybrid + - in_person + - virtual + type: string + name: event_type + type: array + - in: query + name: search + type: string + - in: query + name: start_time + type: string + - collectionFormat: csv + in: query + items: + type: string + name: tags + type: array + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/types.SearchResult-models_Event' + type: array + "404": + description: Not Found + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Searches through events + tags: + - search + /socials/: + get: + description: Retrieves all socials + operationId: get-socials + parameters: + - description: Limit + in: query + name: limit + type: integer + - description: Page + in: query + name: page + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.Social' + type: array + "400": + description: Bad Request + schema: + type: string + "404": + description: Not Found + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Retrieve all socials + tags: + - social + /socials/{socialID}/: + delete: + consumes: + - application/json + description: Deletes a social + operationId: delete-social + parameters: + - description: Social ID + in: path + name: socialID + required: true + type: string + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/models.Social' + "400": + description: Bad Request + schema: + type: string + "404": + description: Not Found + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Deletes a social + tags: + - social + get: + consumes: + - application/json + description: Retrieves a social by id + operationId: get-social + parameters: + - description: Social ID + in: path + name: socialID + required: true + type: string + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/models.Social' + "400": + description: Bad Request + schema: + type: string + "404": + description: Not Found + schema: + type: string + "500": + description: Internal Server Error + schema: + type: string + summary: Retrieves a social + tags: + - social /tags: get: description: Retrieves all tags diff --git a/backend/entities/categories/base/controller.go b/backend/entities/categories/base/controller.go index 2ec179336..5732bb2b4 100644 --- a/backend/entities/categories/base/controller.go +++ b/backend/entities/categories/base/controller.go @@ -64,12 +64,12 @@ func (cat *CategoryController) CreateCategory(c *fiber.Ctx) error { // @Failure 500 {string} error // @Router /categories/ [get] func (cat *CategoryController) GetCategories(c *fiber.Ctx) error { - pagination, ok := fiberpaginate.FromContext(c) + pageInfo, ok := fiberpaginate.FromContext(c) if !ok { - return utilities.ErrExpectedPagination + return utilities.ErrExpectedPageInfo } - categories, err := cat.categoryService.GetCategories(*pagination) + categories, err := cat.categoryService.GetCategories(*pageInfo) if err != nil { return err } diff --git a/backend/entities/categories/tags/controller.go b/backend/entities/categories/tags/controller.go index 8fb211b12..efaba43a8 100644 --- a/backend/entities/categories/tags/controller.go +++ b/backend/entities/categories/tags/controller.go @@ -32,12 +32,12 @@ func NewCategoryTagController(categoryTagService CategoryTagServiceInterface) *C // @Failure 500 {object} error // @Router /categories/{categoryID}/tags/ [get] func (ct *CategoryTagController) GetTagsByCategory(c *fiber.Ctx) error { - pagination, ok := fiberpaginate.FromContext(c) + pageInfo, ok := fiberpaginate.FromContext(c) if !ok { - return utilities.ErrExpectedPagination + return utilities.ErrExpectedPageInfo } - tags, err := ct.categoryTagService.GetTagsByCategory(c.Params("categoryID"), *pagination) + tags, err := ct.categoryTagService.GetTagsByCategory(c.Params("categoryID"), *pageInfo) if err != nil { return err } diff --git a/backend/entities/clubs/base/controller.go b/backend/entities/clubs/base/controller.go index 0d665fa11..ae6e52bcc 100644 --- a/backend/entities/clubs/base/controller.go +++ b/backend/entities/clubs/base/controller.go @@ -3,6 +3,7 @@ package base import ( "net/http" + "github.com/GenerateNU/sac/backend/locals" "github.com/GenerateNU/sac/backend/utilities" "github.com/garrettladley/fiberpaginate" "github.com/gofiber/fiber/v2" @@ -30,12 +31,12 @@ func NewClubController(clubService ClubServiceInterface) *ClubController { // @Failure 500 {object} error // @Router /clubs/ [get] func (cl *ClubController) GetClubs(c *fiber.Ctx) error { - pagination, ok := fiberpaginate.FromContext(c) + pageInfo, ok := fiberpaginate.FromContext(c) if !ok { - return utilities.ErrExpectedPagination + return utilities.ErrExpectedPageInfo } - clubs, err := cl.clubService.GetClubs(*pagination) + clubs, err := cl.clubService.GetClubs(*pageInfo) if err != nil { return err } @@ -65,7 +66,12 @@ func (cl *ClubController) CreateClub(c *fiber.Ctx) error { return utilities.InvalidJSON() } - club, err := cl.clubService.CreateClub(clubBody) + userID, err := locals.UserIDFrom(c) + if err != nil { + return err + } + + club, err := cl.clubService.CreateClub(*userID, clubBody) if err != nil { return err } diff --git a/backend/entities/clubs/base/models.go b/backend/entities/clubs/base/models.go index e0a6b63f1..a547908c2 100644 --- a/backend/entities/clubs/base/models.go +++ b/backend/entities/clubs/base/models.go @@ -2,11 +2,9 @@ package base import ( "github.com/GenerateNU/sac/backend/entities/models" - "github.com/google/uuid" ) type CreateClubRequestBody struct { - UserID uuid.UUID `json:"user_id" validate:"required,uuid4"` Name string `json:"name" validate:"required,max=255"` Preview string `json:"preview" validate:"required,max=255"` Description string `json:"description" validate:"required,http_url,s3_url,max=255"` diff --git a/backend/entities/clubs/base/routes.go b/backend/entities/clubs/base/routes.go index 7633698b2..d90ffd0e5 100644 --- a/backend/entities/clubs/base/routes.go +++ b/backend/entities/clubs/base/routes.go @@ -2,11 +2,12 @@ package base import ( p "github.com/GenerateNU/sac/backend/auth" - "github.com/GenerateNU/sac/backend/entities/clubs/contacts" "github.com/GenerateNU/sac/backend/entities/clubs/events" "github.com/GenerateNU/sac/backend/entities/clubs/followers" + "github.com/GenerateNU/sac/backend/entities/clubs/leadership" "github.com/GenerateNU/sac/backend/entities/clubs/members" - "github.com/GenerateNU/sac/backend/entities/clubs/pocs" + "github.com/GenerateNU/sac/backend/entities/clubs/recruitment" + "github.com/GenerateNU/sac/backend/entities/clubs/socials" "github.com/GenerateNU/sac/backend/entities/clubs/tags" authMiddleware "github.com/GenerateNU/sac/backend/middleware/auth" @@ -20,9 +21,10 @@ func ClubRoutes(clubParams types.RouteParams) { tags.ClubTag(clubParams) followers.ClubFollower(clubParams) members.ClubMember(clubParams) - contacts.ClubContact(clubParams) + leadership.ClubLeader(clubParams) events.ClubEvent(clubParams) - pocs.ClubPointOfContact(clubParams) + socials.ClubSocial(clubParams) + recruitment.ClubRecruitment(clubParams) } func ClubRouter(clubParams types.RouteParams) fiber.Router { diff --git a/backend/entities/clubs/base/service.go b/backend/entities/clubs/base/service.go index 3bfebe917..495727065 100644 --- a/backend/entities/clubs/base/service.go +++ b/backend/entities/clubs/base/service.go @@ -1,19 +1,19 @@ package base import ( - "fmt" - "github.com/GenerateNU/sac/backend/entities/clubs" "github.com/GenerateNU/sac/backend/entities/models" + "github.com/GenerateNU/sac/backend/errs" "github.com/GenerateNU/sac/backend/types" "github.com/GenerateNU/sac/backend/utilities" "github.com/garrettladley/fiberpaginate" + "github.com/google/uuid" ) type ClubServiceInterface interface { GetClubs(pageInfo fiberpaginate.PageInfo) ([]models.Club, error) GetClub(id string) (*models.Club, error) - CreateClub(clubBody CreateClubRequestBody) (*models.Club, error) + CreateClub(userID uuid.UUID, clubBody CreateClubRequestBody) (*models.Club, error) UpdateClub(id string, clubBody UpdateClubRequestBody) (*models.Club, error) DeleteClub(id string) error } @@ -30,7 +30,7 @@ func (c *ClubService) GetClubs(pagination fiberpaginate.PageInfo) ([]models.Club return GetClubs(c.DB, pagination) } -func (c *ClubService) CreateClub(clubBody CreateClubRequestBody) (*models.Club, error) { +func (c *ClubService) CreateClub(userID uuid.UUID, clubBody CreateClubRequestBody) (*models.Club, error) { if err := utilities.Validate(c.Validate, clubBody); err != nil { return nil, err } @@ -40,7 +40,7 @@ func (c *ClubService) CreateClub(clubBody CreateClubRequestBody) (*models.Club, return nil, err } - return CreateClub(c.DB, clubBody.UserID, *club) + return CreateClub(c.DB, userID, *club) } func (c *ClubService) GetClub(id string) (*models.Club, error) { @@ -59,7 +59,7 @@ func (c *ClubService) UpdateClub(id string, clubBody UpdateClubRequestBody) (*mo } if utilities.AtLeastOne(clubBody, UpdateClubRequestBody{}) { - return nil, fmt.Errorf("at least one field must be present") + return nil, errs.ErrAtLeastOne } if err := utilities.Validate(c.Validate, clubBody); err != nil { diff --git a/backend/entities/clubs/base/transactions.go b/backend/entities/clubs/base/transactions.go index 8399b7c24..65c924e07 100644 --- a/backend/entities/clubs/base/transactions.go +++ b/backend/entities/clubs/base/transactions.go @@ -2,6 +2,7 @@ package base import ( "errors" + "log/slog" "github.com/GenerateNU/sac/backend/constants" "github.com/GenerateNU/sac/backend/utilities" @@ -47,6 +48,7 @@ func CreateClub(db *gorm.DB, userId uuid.UUID, club models.Club) (*models.Club, if err := tx.Create(&club).Error; err != nil { tx.Rollback() + slog.Info("err in create club", "err", err) return nil, err } @@ -58,6 +60,7 @@ func CreateClub(db *gorm.DB, userId uuid.UUID, club models.Club) (*models.Club, if err := tx.Create(&membership).Error; err != nil { tx.Rollback() + slog.Info("err in create membership", "err", err) return nil, err } @@ -71,8 +74,7 @@ func CreateClub(db *gorm.DB, userId uuid.UUID, club models.Club) (*models.Club, return nil, err } - err = search.Upsert[models.Club](db, constants.CLUBS_INDEX, club.ID.String(), &club) - if err != nil { + if err := search.Upsert[models.Club](db, constants.CLUBS_INDEX, club.ID.String(), &club); err != nil { return nil, err } diff --git a/backend/entities/clubs/contacts/controller.go b/backend/entities/clubs/contacts/controller.go deleted file mode 100644 index ac0bb51b9..000000000 --- a/backend/entities/clubs/contacts/controller.go +++ /dev/null @@ -1,69 +0,0 @@ -package contacts - -import ( - "net/http" - - "github.com/GenerateNU/sac/backend/utilities" - "github.com/gofiber/fiber/v2" -) - -type ClubContactController struct { - clubContactService ClubContactServiceInterface -} - -func NewClubContactController(clubContactService ClubContactServiceInterface) *ClubContactController { - return &ClubContactController{clubContactService: clubContactService} -} - -// GetClubContacts godoc -// -// @Summary Retrieve all contacts for a club -// @Description Retrieves all contacts associated with a club -// @ID get-contacts-by-club -// @Tags club-contact -// @Produce json -// @Param clubID path string true "Club ID" -// @Success 200 {object} []models.Contact -// @Failure 400 {object} error -// @Failure 404 {object} error -// @Failure 500 {object} error -// @Router /clubs/{clubID}/contacts/ [get] -func (cc *ClubContactController) GetClubContacts(c *fiber.Ctx) error { - contacts, err := cc.clubContactService.GetClubContacts(c.Params("clubID")) - if err != nil { - return err - } - - return c.Status(http.StatusOK).JSON(contacts) -} - -// PutContact godoc -// -// @Summary Creates a contact -// @Description Creates a contact -// @ID put-contact -// @Tags club-contact -// @Accept json -// @Produce json -// @Param clubID path string true "Club ID" -// @Param contactBody body PutContactRequestBody true "Contact Body" -// @Success 201 {object} models.Contact -// @Failure 400 {object} error -// @Failure 401 {object} error -// @Failure 404 {object} error -// @Failure 500 {object} error -// @Router /clubs/{clubID}/contacts/ [put] -func (cc *ClubContactController) PutContact(c *fiber.Ctx) error { - var contactBody PutContactRequestBody - - if err := c.BodyParser(&contactBody); err != nil { - return utilities.InvalidJSON() - } - - contact, err := cc.clubContactService.PutClubContact(c.Params("clubID"), contactBody) - if err != nil { - return err - } - - return c.Status(http.StatusOK).JSON(contact) -} diff --git a/backend/entities/clubs/contacts/models.go b/backend/entities/clubs/contacts/models.go deleted file mode 100644 index 26c5cf1dd..000000000 --- a/backend/entities/clubs/contacts/models.go +++ /dev/null @@ -1,8 +0,0 @@ -package contacts - -import "github.com/GenerateNU/sac/backend/entities/models" - -type PutContactRequestBody struct { - Type models.ContactType `json:"type" validate:"required,max=255,oneof=facebook instagram x linkedin youtube github slack discord email customSite"` - Content string `json:"content" validate:"required,contact_pointer,max=255"` -} diff --git a/backend/entities/clubs/contacts/routes.go b/backend/entities/clubs/contacts/routes.go deleted file mode 100644 index 252fcfe52..000000000 --- a/backend/entities/clubs/contacts/routes.go +++ /dev/null @@ -1,20 +0,0 @@ -package contacts - -import ( - authMiddleware "github.com/GenerateNU/sac/backend/middleware/auth" - "github.com/GenerateNU/sac/backend/types" -) - -func ClubContact(clubParams types.RouteParams) { - clubContactController := NewClubContactController(NewClubContactService(clubParams.ServiceParams)) - - clubContacts := clubParams.Router.Group("/contacts") - - // api/v1/clubs/:clubID/contacts/* - clubContacts.Get("/", clubContactController.GetClubContacts) - clubContacts.Put( - "/", - authMiddleware.AttachExtractor(clubParams.AuthMiddleware.ClubAuthorizeById, authMiddleware.ExtractFromParams("clubID")), - clubContactController.PutContact, - ) -} diff --git a/backend/entities/clubs/contacts/service.go b/backend/entities/clubs/contacts/service.go deleted file mode 100644 index 5c12bd409..000000000 --- a/backend/entities/clubs/contacts/service.go +++ /dev/null @@ -1,49 +0,0 @@ -package contacts - -import ( - "github.com/GenerateNU/sac/backend/entities/models" - "github.com/GenerateNU/sac/backend/types" - "github.com/GenerateNU/sac/backend/utilities" -) - -type ClubContactServiceInterface interface { - GetClubContacts(clubID string) ([]models.Contact, error) - PutClubContact(clubID string, contactBody PutContactRequestBody) (*models.Contact, error) -} - -type ClubContactService struct { - types.ServiceParams -} - -func NewClubContactService(params types.ServiceParams) ClubContactServiceInterface { - return &ClubContactService{params} -} - -func (c *ClubContactService) GetClubContacts(clubID string) ([]models.Contact, error) { - idAsUUID, err := utilities.ValidateID(clubID) - if err != nil { - return nil, err - } - - return GetClubContacts(c.DB, *idAsUUID) -} - -func (c *ClubContactService) PutClubContact(clubID string, contactBody PutContactRequestBody) (*models.Contact, error) { - idAsUUID, err := utilities.ValidateID(clubID) - if err != nil { - return nil, err - } - - if err := utilities.Validate(c.Validate, contactBody); err != nil { - return nil, err - } - - contact, err := utilities.MapJsonTags(contactBody, &models.Contact{}) - if err != nil { - return nil, err - } - - contact.ClubID = *idAsUUID - - return PutClubContact(c.DB, *contact) -} diff --git a/backend/entities/clubs/events/controller.go b/backend/entities/clubs/events/controller.go index 9f0235657..9726078b5 100644 --- a/backend/entities/clubs/events/controller.go +++ b/backend/entities/clubs/events/controller.go @@ -32,12 +32,12 @@ func NewClubEventController(clubEventService ClubEventServiceInterface) *ClubEve // @Failure 500 {object} error // @Router /clubs/{clubID}/events/ [get] func (cl *ClubEventController) GetClubEvents(c *fiber.Ctx) error { - pagination, ok := fiberpaginate.FromContext(c) + pageInfo, ok := fiberpaginate.FromContext(c) if !ok { - return utilities.ErrExpectedPagination + return utilities.ErrExpectedPageInfo } - if events, err := cl.clubEventService.GetClubEvents(c.Params("clubID"), *pagination); err != nil { + if events, err := cl.clubEventService.GetClubEvents(c.Params("clubID"), *pageInfo); err != nil { return err } else { return c.Status(http.StatusOK).JSON(events) diff --git a/backend/entities/clubs/followers/controller.go b/backend/entities/clubs/followers/controller.go index 45f8b9217..24e4c4b6e 100644 --- a/backend/entities/clubs/followers/controller.go +++ b/backend/entities/clubs/followers/controller.go @@ -32,15 +32,31 @@ func NewClubFollowerController(clubFollowerService ClubFollowerServiceInterface) // @Failure 500 {object} error // @Router /clubs/{clubID}/followers/ [get] func (cf *ClubFollowerController) GetClubFollowers(c *fiber.Ctx) error { - pagination, ok := fiberpaginate.FromContext(c) + pageInfo, ok := fiberpaginate.FromContext(c) if !ok { - return utilities.ErrExpectedPagination + return utilities.ErrExpectedPageInfo } - followers, err := cf.clubFollowerService.GetClubFollowers(c.Params("clubID"), *pagination) + followers, err := cf.clubFollowerService.GetClubFollowers(c.Params("clubID"), *pageInfo) if err != nil { return err } return c.Status(http.StatusOK).JSON(followers) } + +func (cf *ClubFollowerController) CreateClubFollowing(c *fiber.Ctx) error { + if err := cf.clubFollowerService.CreateClubFollower(c.Params("clubID"), c.Params("userID")); err != nil { + return err + } + + return c.SendStatus(http.StatusCreated) +} + +func (cf *ClubFollowerController) DeleteClubFollowing(c *fiber.Ctx) error { + if err := cf.clubFollowerService.DeleteClubFollower(c.Params("clubID"), c.Params("userID")); err != nil { + return err + } + + return c.SendStatus(http.StatusNoContent) +} diff --git a/backend/entities/clubs/followers/routes.go b/backend/entities/clubs/followers/routes.go index 81f9e3a61..a89ee1f66 100644 --- a/backend/entities/clubs/followers/routes.go +++ b/backend/entities/clubs/followers/routes.go @@ -1,6 +1,8 @@ package followers import ( + authMiddleware "github.com/GenerateNU/sac/backend/middleware/auth" + "github.com/GenerateNU/sac/backend/types" ) @@ -11,4 +13,14 @@ func ClubFollower(clubParams types.RouteParams) { // api/clubs/:clubID/followers/* clubFollowers.Get("/", clubParams.UtilityMiddleware.Paginator, clubFollowerController.GetClubFollowers) + clubFollowers.Post( + "/:userID", + authMiddleware.AttachExtractor(clubParams.AuthMiddleware.ClubAuthorizeById, authMiddleware.ExtractFromParams("clubID")), + clubFollowerController.CreateClubFollowing, + ) + clubFollowers.Delete( + "/:userID", + authMiddleware.AttachExtractor(clubParams.AuthMiddleware.ClubAuthorizeById, authMiddleware.ExtractFromParams("clubID")), + clubFollowerController.DeleteClubFollowing, + ) } diff --git a/backend/entities/clubs/followers/service.go b/backend/entities/clubs/followers/service.go index e1fced5c2..5568295a8 100644 --- a/backend/entities/clubs/followers/service.go +++ b/backend/entities/clubs/followers/service.go @@ -9,6 +9,8 @@ import ( type ClubFollowerServiceInterface interface { GetClubFollowers(clubID string, pageInfo fiberpaginate.PageInfo) ([]models.User, error) + CreateClubFollower(clubID string, userID string) error + DeleteClubFollower(clubID string, userID string) error } type ClubFollowerService struct { @@ -27,3 +29,31 @@ func (cf *ClubFollowerService) GetClubFollowers(clubID string, pageInfo fiberpag return GetClubFollowers(cf.DB, *idAsUUID, pageInfo) } + +func (cf *ClubFollowerService) CreateClubFollower(clubID string, userID string) error { + clubIDAsUUID, err := utilities.ValidateID(clubID) + if err != nil { + return err + } + + userIDAsUUID, err := utilities.ValidateID(userID) + if err != nil { + return err + } + + return CreateClubFollower(cf.DB, *clubIDAsUUID, *userIDAsUUID) +} + +func (cf *ClubFollowerService) DeleteClubFollower(clubID string, userID string) error { + clubIDAsUUID, err := utilities.ValidateID(clubID) + if err != nil { + return err + } + + userIDAsUUID, err := utilities.ValidateID(userID) + if err != nil { + return err + } + + return DeleteClubFollower(cf.DB, *clubIDAsUUID, *userIDAsUUID) +} diff --git a/backend/entities/clubs/followers/transactions.go b/backend/entities/clubs/followers/transactions.go index 8a98f8d2e..1595c19c5 100644 --- a/backend/entities/clubs/followers/transactions.go +++ b/backend/entities/clubs/followers/transactions.go @@ -3,6 +3,7 @@ package followers import ( "github.com/GenerateNU/sac/backend/entities/clubs" "github.com/GenerateNU/sac/backend/entities/models" + "github.com/GenerateNU/sac/backend/entities/users" "github.com/GenerateNU/sac/backend/utilities" "github.com/garrettladley/fiberpaginate" "github.com/google/uuid" @@ -22,3 +23,39 @@ func GetClubFollowers(db *gorm.DB, clubID uuid.UUID, pageInfo fiberpaginate.Page return users, nil } + +func CreateClubFollower(db *gorm.DB, clubID uuid.UUID, userID uuid.UUID) error { + user, err := users.GetUser(db, userID) + if err != nil { + return err + } + + club, err := clubs.GetClub(db, clubID) + if err != nil { + return err + } + + if err := db.Model(&user).Association("Follower").Append(club); err != nil { + return err + } + + return nil +} + +func DeleteClubFollower(db *gorm.DB, clubID uuid.UUID, userID uuid.UUID) error { + user, err := users.GetUser(db, userID) + if err != nil { + return err + } + + club, err := clubs.GetClub(db, clubID) + if err != nil { + return err + } + + if err := db.Model(&user).Association("Follower").Delete(club); err != nil { + return err + } + + return nil +} diff --git a/backend/entities/clubs/leadership/controller.go b/backend/entities/clubs/leadership/controller.go new file mode 100644 index 000000000..1f59be18e --- /dev/null +++ b/backend/entities/clubs/leadership/controller.go @@ -0,0 +1,180 @@ +package leadership + +import ( + "net/http" + + "github.com/GenerateNU/sac/backend/utilities" + "github.com/gofiber/fiber/v2" +) + +type ClubLeaderController struct { + clubLeaderService ClubLeaderServiceInterface +} + +func NewClubLeaderController(clubLeaderService ClubLeaderServiceInterface) *ClubLeaderController { + return &ClubLeaderController{clubLeaderService: clubLeaderService} +} + +// GetClubLeadership godoc +// +// @Summary Retrieve all leadership for a club +// @Description Retrieves all leadership associated with a club +// @ID get-leadership-by-club +// @Tags club-leader +// @Produce json +// @Param clubID path string true "Club ID" +// @Success 200 {object} []models.Leader +// @Failure 400 {object} error +// @Failure 404 {object} error +// @Failure 500 {object} error +// @Router /clubs/{clubID}/leadership/ [get] +func (l *ClubLeaderController) GetClubLeadership(c *fiber.Ctx) error { + leadership, err := l.clubLeaderService.GetClubLeadership(c.Params("clubID")) + if err != nil { + return err + } + + return c.Status(http.StatusOK).JSON(leadership) +} + +// GetClubLeader godoc +// +// @Summary Retrieve a leader for a club +// @Description Retrieves a leader associated with a club +// @ID get-leader-by-club +// @Tags club-leader +// @Produce json +// @Param clubID path string true "Club ID" +// @Param leaderID path string true "leader ID" +// @Success 200 {object} models.Leader +// @Failure 400 {object} error +// @Failure 404 {object} error +// @Failure 500 {object} error +// @Router /clubs/{clubID}/leadership/{leaderID} [get] +func (l *ClubLeaderController) GetClubLeader(c *fiber.Ctx) error { + leader, err := l.clubLeaderService.GetClubLeader(c.Params("clubID"), c.Params("leaderID")) + if err != nil { + return err + } + + return c.Status(http.StatusOK).JSON(leader) +} + +// UpdateClubLeaderPhoto godoc +// +// @Summary Update a leader photo for a club +// @Description Updates a leader photo associated with a club +// @ID update-leader-photo-by-club +// @Tags club-leader +// @Accept multipart/form-data +// @Produce json +// @Param clubID path string true "Club ID" +// @Param leaderID path string true "leader ID" +// @Success 200 {object} models.Leader +// @Failure 400 {object} error +// @Failure 401 {object} error +// @Failure 404 {object} error +// @Failure 500 {object} error +// @Router /clubs/{clubID}/leadership/{leaderID} [patch] +func (l *ClubLeaderController) UpdateClubLeaderPhoto(c *fiber.Ctx) error { + formFile, err := c.FormFile("file") + if err != nil { + return err + } + + leader, err := l.clubLeaderService.UpdateClubLeaderPhoto(c.Params("clubID"), c.Params("leaderID"), formFile) + if err != nil { + return err + } + + return c.Status(http.StatusOK).JSON(leader) +} + +// UpdateClubLeader godoc +// +// @Summary Update a leader for a club +// @Description Updates a leader associated with a club +// @ID update-leader-by-club +// @Tags club-leader +// @Accept json +// @Produce json +// @Param clubID path string true "Club ID" +// @Param leaderID path string true "leader ID" +// @Success 200 {object} models.Leader +// @Failure 400 {object} error +// @Failure 401 {object} error +// @Failure 404 {object} error +// @Failure 500 {object} error +// @Router /clubs/{clubID}/leadership/{leaderID} [put] +func (l *ClubLeaderController) UpdateClubLeader(c *fiber.Ctx) error { + var leaderBody UpdateLeaderBody + + if err := c.BodyParser(&leaderBody); err != nil { + return utilities.InvalidJSON() + } + + leader, err := l.clubLeaderService.UpdateClubLeader(c.Params("clubID"), c.Params("leaderID"), leaderBody) + if err != nil { + return err + } + + return c.Status(http.StatusOK).JSON(leader) +} + +// CreateClubLeader godoc +// +// @Summary Create a leader for a club +// @Description Creates a leader associated with a club +// @ID create-leader-by-club +// @Tags club-leader +// @Accept multipart/form-data +// @Produce json +// @Param clubID path string true "Club ID" +// @Success 201 {object} models.Leader +// @Failure 400 {object} error +// @Failure 401 {object} error +// @Failure 404 {object} error +// @Failure 500 {object} error +// @Router /clubs/{clubID}/leadership/ [post] +func (l *ClubLeaderController) CreateClubLeader(c *fiber.Ctx) error { + var leaderBody CreateLeaderBody + + if err := c.BodyParser(&leaderBody); err != nil { + return utilities.InvalidJSON() + } + + formFile, err := c.FormFile("file") + if err != nil { + return err + } + + leader, err := l.clubLeaderService.CreateClubLeader(c.Params("clubID"), leaderBody, formFile) + if err != nil { + return err + } + + return c.Status(http.StatusCreated).JSON(leader) +} + +// DeleteClubLeader godoc +// +// @Summary Delete a leader for a club +// @Description Delete a leader associated with a club +// @ID delete-leader-by-club +// @Tags club-leader +// @Produce json +// @Param clubID path string true "Club ID" +// @Param leaderID path string true "leader ID" +// @Success 204 {object} nil +// @Failure 400 {object} error +// @Failure 404 {object} error +// @Failure 500 {object} error +// @Router /clubs/{clubID}/leadership/{leaderID} [delete] +func (l *ClubLeaderController) DeleteClubLeader(c *fiber.Ctx) error { + err := l.clubLeaderService.DeleteClubLeader(c.Params("clubID"), c.Params("leaderID")) + if err != nil { + return err + } + + return c.SendStatus(http.StatusNoContent) +} diff --git a/backend/entities/clubs/pocs/models.go b/backend/entities/clubs/leadership/models.go similarity index 80% rename from backend/entities/clubs/pocs/models.go rename to backend/entities/clubs/leadership/models.go index 85fa0a9dd..fe71a997e 100644 --- a/backend/entities/clubs/pocs/models.go +++ b/backend/entities/clubs/leadership/models.go @@ -1,12 +1,12 @@ -package pocs +package leadership -type CreatePointOfContactBody struct { +type CreateLeaderBody struct { Name string `json:"name" validate:"required,max=255"` Email string `json:"email" validate:"required,email,max=255"` Position string `json:"position" validate:"required,max=255"` } -type UpdatePointOfContactBody struct { +type UpdateLeaderBody struct { Name string `json:"name" validate:"omitempty,max=255"` Email string `json:"email" validate:"omitempty,email,max=255"` Position string `json:"position" validate:"omitempty,max=255"` diff --git a/backend/entities/clubs/leadership/routes.go b/backend/entities/clubs/leadership/routes.go new file mode 100644 index 000000000..d56b868c2 --- /dev/null +++ b/backend/entities/clubs/leadership/routes.go @@ -0,0 +1,36 @@ +package leadership + +import ( + authMiddleware "github.com/GenerateNU/sac/backend/middleware/auth" + "github.com/GenerateNU/sac/backend/types" +) + +func ClubLeader(leaderParams types.RouteParams) { + clubLeaderController := NewClubLeaderController(NewClubLeaderService(leaderParams.ServiceParams)) + + clubLeaders := leaderParams.Router.Group("/leadership") + + // api/v1/clubs/:clubID/leadership/* + clubLeaders.Get("/", clubLeaderController.GetClubLeadership) + clubLeaders.Get("/:leaderID", clubLeaderController.GetClubLeader) + clubLeaders.Post( + "/", + authMiddleware.AttachExtractor(leaderParams.AuthMiddleware.ClubAuthorizeById, authMiddleware.ExtractFromParams("clubID")), + clubLeaderController.CreateClubLeader, + ) + clubLeaders.Patch( + "/:leaderID", + authMiddleware.AttachExtractor(leaderParams.AuthMiddleware.ClubAuthorizeById, authMiddleware.ExtractFromParams("clubID")), + clubLeaderController.UpdateClubLeader, + ) + clubLeaders.Patch( + "/:leaderID/photo", + authMiddleware.AttachExtractor(leaderParams.AuthMiddleware.ClubAuthorizeById, authMiddleware.ExtractFromParams("clubID")), + clubLeaderController.UpdateClubLeaderPhoto, + ) + clubLeaders.Delete( + "/:leaderID", + authMiddleware.AttachExtractor(leaderParams.AuthMiddleware.ClubAuthorizeById, authMiddleware.ExtractFromParams("clubID")), + clubLeaderController.DeleteClubLeader, + ) +} diff --git a/backend/entities/clubs/leadership/service.go b/backend/entities/clubs/leadership/service.go new file mode 100644 index 000000000..1c5c4c14f --- /dev/null +++ b/backend/entities/clubs/leadership/service.go @@ -0,0 +1,154 @@ +package leadership + +import ( + "mime/multipart" + + "github.com/GenerateNU/sac/backend/entities/files/base" + "github.com/GenerateNU/sac/backend/entities/models" + + "github.com/GenerateNU/sac/backend/integrations/file" + "github.com/GenerateNU/sac/backend/types" + "github.com/GenerateNU/sac/backend/utilities" +) + +type ClubLeaderServiceInterface interface { + GetClubLeadership(clubID string) ([]models.Leader, error) + GetClubLeader(clubID, leaderID string) (*models.Leader, error) + CreateClubLeader(clubID string, leaderBody CreateLeaderBody, fileHeader *multipart.FileHeader) (*models.Leader, error) + UpdateClubLeaderPhoto(clubID, leaderID string, fileHeader *multipart.FileHeader) (*models.Leader, error) + UpdateClubLeader(clubID, leaderID string, leaderBody UpdateLeaderBody) (*models.Leader, error) + DeleteClubLeader(clubID, leaderID string) error +} + +type ClubLeaderService struct { + types.ServiceParams +} + +func NewClubLeaderService(serviceParams types.ServiceParams) ClubLeaderServiceInterface { + return &ClubLeaderService{serviceParams} +} + +func (cl *ClubLeaderService) GetClubLeadership(clubID string) ([]models.Leader, error) { + clubIdAsUUID, err := utilities.ValidateID(clubID) + if err != nil { + return nil, err + } + + return GetClubLeadership(cl.DB, *clubIdAsUUID) +} + +func (cl *ClubLeaderService) GetClubLeader(clubID, leaderID string) (*models.Leader, error) { + clubIdAsUUID, err := utilities.ValidateID(clubID) + if err != nil { + return nil, err + } + + leaderIDAsUUID, err := utilities.ValidateID(leaderID) + if err != nil { + return nil, err + } + + return GetClubLeader(cl.DB, *clubIdAsUUID, *leaderIDAsUUID) +} + +// MOVE THIS TO TRANSACTIOPNS +func (cl *ClubLeaderService) CreateClubLeader(clubID string, leaderBody CreateLeaderBody, fileHeader *multipart.FileHeader) (*models.Leader, error) { + if err := utilities.Validate(cl.Validate, leaderBody); err != nil { + return nil, err + } + + clubIdAsUUID, err := utilities.ValidateID(clubID) + if err != nil { + return nil, err + } + + leader, err := GetClubLeaderByClubIDAndEmail(cl.DB, *clubIdAsUUID, leaderBody.Email) + if err == nil { + return leader, nil + } + + return CreateClubLeader(cl.DB, + models.Leader{ + Name: leaderBody.Name, + Email: leaderBody.Email, + Position: leaderBody.Position, + ClubID: *clubIdAsUUID, + }, + func() (*models.FileInfo, error) { + return cl.Integrations.File.UploadFile("leadership", fileHeader, []file.FileType{file.IMAGE}) + }, + cl.Integrations.File.DeleteFile, + ) +} + +func (cl *ClubLeaderService) UpdateClubLeaderPhoto(clubID, leaderID string, fileHeader *multipart.FileHeader) (*models.Leader, error) { + clubIdAsUUID, err := utilities.ValidateID(clubID) + if err != nil { + return nil, err + } + + leaderIDAsUUID, err := utilities.ValidateID(leaderID) + if err != nil { + return nil, err + } + + leader, err := GetClubLeader(cl.DB, *clubIdAsUUID, *leaderIDAsUUID) + if err != nil { + return nil, err + } + + if err := cl.Integrations.File.DeleteFile(leader.PhotoFile.FileURL); err != nil { + return nil, err + } + + fileInfo, err := cl.Integrations.File.UploadFile("point_of_contacts", fileHeader, []file.FileType{file.IMAGE}) + if err != nil { + return nil, err + } + + file, err := base.UpdateFile(cl.DB, leader.PhotoFile.ID, *fileInfo) + if err != nil { + return nil, err + } + + leader.PhotoFile = *file + + return leader, nil +} + +func (cl *ClubLeaderService) UpdateClubLeader(clubID, leaderID string, leaderBody UpdateLeaderBody) (*models.Leader, error) { + if err := utilities.Validate(cl.Validate, leaderBody); err != nil { + return nil, err + } + + clubIdAsUUID, err := utilities.ValidateID(clubID) + if err != nil { + return nil, err + } + + leaderIDAsUUID, err := utilities.ValidateID(leaderID) + if err != nil { + return nil, err + } + + leader, err := utilities.MapJsonTags(leaderBody, &models.Leader{}) + if err != nil { + return nil, err + } + + return UpdateClubLeader(cl.DB, *clubIdAsUUID, *leaderIDAsUUID, *leader) +} + +func (cl *ClubLeaderService) DeleteClubLeader(clubID, leaderID string) error { + clubIdAsUUID, err := utilities.ValidateID(clubID) + if err != nil { + return err + } + + leaderIDAsUUID, err := utilities.ValidateID(leaderID) + if err != nil { + return err + } + + return DeleteClubLeader(cl.DB, *clubIdAsUUID, *leaderIDAsUUID, cl.Integrations.File.DeleteFile) +} diff --git a/backend/entities/clubs/leadership/transactions.go b/backend/entities/clubs/leadership/transactions.go new file mode 100644 index 000000000..967659779 --- /dev/null +++ b/backend/entities/clubs/leadership/transactions.go @@ -0,0 +1,138 @@ +package leadership + +import ( + "errors" + + "github.com/GenerateNU/sac/backend/entities/files/base" + "github.com/GenerateNU/sac/backend/entities/models" + + "github.com/GenerateNU/sac/backend/utilities" + "github.com/google/uuid" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +func GetClubLeadership(db *gorm.DB, clubID uuid.UUID) ([]models.Leader, error) { + var leadership []models.Leader + + result := db.Preload("PhotoFile").Where("club_id = ?", clubID).Find(&leadership) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, utilities.ErrNotFound + } + return nil, result.Error + } + + return leadership, nil +} + +func GetClubLeader(db *gorm.DB, clubID uuid.UUID, leaderID uuid.UUID) (*models.Leader, error) { + var leader models.Leader + + if err := db.Preload("PhotoFile").First(&leader, "id = ? AND club_id = ?", leaderID, clubID).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, utilities.ErrNotFound + } + return nil, err + } + + return &leader, nil +} + +func GetClubLeaderByClubIDAndEmail(db *gorm.DB, clubID uuid.UUID, email string) (*models.Leader, error) { + var leader models.Leader + + if err := db.First(&leader, "email = ? AND club_id = ?", email, clubID).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, utilities.ErrNotFound + } + return nil, err + } + + return &leader, nil +} + +func CreateClubLeader(db *gorm.DB, leader models.Leader, fileIntegrationUpload func() (*models.FileInfo, error), fileIntegrationDelete func(string) error) (*models.Leader, error) { + fileInfo, err := fileIntegrationUpload() + if err != nil { + return nil, err + } + + tx := db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + if err := db.Create(&leader).Error; err != nil { + return nil, err + } + + file, err := base.CreateFile(tx, leader.ID, "leadership", *fileInfo) + if err != nil { + tx.Rollback() + return nil, err + } + + if err := tx.Commit().Error; err != nil { + if err := fileIntegrationDelete(fileInfo.FileURL); err != nil { + return nil, err + } + + return nil, err + } + + leader.PhotoFile = *file + + return &leader, nil +} + +func UpdateClubLeader(db *gorm.DB, clubID uuid.UUID, leaderID uuid.UUID, updatedLeader models.Leader) (*models.Leader, error) { + leader, err := GetClubLeader(db, clubID, leaderID) + if err != nil { + return nil, err + } + if err := db.Model(&leader).Clauses(clause.Returning{}).Updates(&updatedLeader).Error; err != nil { + return nil, err + } + + return leader, nil +} + +func DeleteClubLeader(db *gorm.DB, clubID uuid.UUID, leaderID uuid.UUID, fileIntegrationDelete func(string) error) error { + leader, err := GetClubLeader(db, clubID, leaderID) + if err != nil { + return err + } + + if err := fileIntegrationDelete(leader.PhotoFile.FileURL); err != nil { + return err + } + + tx := db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + if err := tx.Error; err != nil { + return err + } + + err = base.DeleteFile(tx, leader.PhotoFile.ID) + if err != nil { + tx.Rollback() + return err + } + + if result := db.Delete(&models.Leader{}, "id = ? AND club_id = ?", leaderID, clubID); result.RowsAffected == 0 { + tx.Rollback() + if result.Error == nil { + return utilities.ErrNotFound + } + return result.Error + } + + return tx.Commit().Error +} diff --git a/backend/entities/clubs/members/controller.go b/backend/entities/clubs/members/controller.go index b5c899bee..d9b9b4eef 100644 --- a/backend/entities/clubs/members/controller.go +++ b/backend/entities/clubs/members/controller.go @@ -16,29 +16,13 @@ func NewClubMemberController(clubMemberService ClubMemberServiceInterface) *Club return &ClubMemberController{clubMemberService: clubMemberService} } -// GetClubMembers godoc -// -// @Summary Retrieve all members for a club -// @Description Retrieves all members associated with a club -// @ID get-members-by-club -// @Tags club-member -// @Produce json -// @Param clubID path string true "Club ID" -// @Param limit query int false "Limit" -// @Param page query int false "Page" -// @Success 200 {object} []models.User -// @Failure 400 {object} error -// @Failure 401 {object} error -// @Failure 404 {object} error -// @Failure 500 {object} error -// @Router /clubs/{clubID}/members/ [get] func (cm *ClubMemberController) GetClubMembers(c *fiber.Ctx) error { - pagination, ok := fiberpaginate.FromContext(c) + pageInfo, ok := fiberpaginate.FromContext(c) if !ok { - return utilities.ErrExpectedPagination + return utilities.ErrExpectedPageInfo } - followers, err := cm.clubMemberService.GetClubMembers(c.Params("clubID"), *pagination) + followers, err := cm.clubMemberService.GetClubMembers(c.Params("clubID"), *pageInfo) if err != nil { return err } @@ -46,49 +30,16 @@ func (cm *ClubMemberController) GetClubMembers(c *fiber.Ctx) error { return c.Status(http.StatusOK).JSON(followers) } -// CreateClubMember godoc -// -// @Summary Create a new member for a club -// @Description Creates a new member associated with a club -// @ID create-member-for-club -// @Tags club-member -// @Accept json -// @Produce json -// @Param clubID path string true "Club ID" -// @Param userID path string true "User ID" -// @Success 201 {object} models.User -// @Failure 400 {object} error -// @Failure 401 {object} error -// @Failure 404 {object} error -// @Failure 500 {object} error -// @Router /clubs/{clubID}/members/ [post] func (cm *ClubMemberController) CreateClubMember(c *fiber.Ctx) error { - err := cm.clubMemberService.CreateClubMember(c.Params("clubID"), c.Params("userID")) - if err != nil { + if err := cm.clubMemberService.CreateClubMember(c.Params("clubID"), c.Params("userID")); err != nil { return err } return c.SendStatus(http.StatusCreated) } -// DeleteClubMember godoc -// -// @Summary Delete a member from a club -// @Description Deletes a member associated with a club -// @ID delete-member-from-club -// @Tags club-member -// @Produce json -// @Param clubID path string true "Club ID" -// @Param userID path string true "User ID" -// @Success 204 {object} models.User -// @Failure 400 {object} error -// @Failure 401 {object} error -// @Failure 404 {object} error -// @Failure 500 {object} error -// @Router /clubs/{clubID}/members/ [delete] func (cm *ClubMemberController) DeleteClubMember(c *fiber.Ctx) error { - err := cm.clubMemberService.DeleteClubMember(c.Params("clubID"), c.Params("userID")) - if err != nil { + if err := cm.clubMemberService.DeleteClubMember(c.Params("clubID"), c.Params("userID")); err != nil { return err } diff --git a/backend/entities/clubs/members/transactions.go b/backend/entities/clubs/members/transactions.go index 8a0fd1f62..ef2d78865 100644 --- a/backend/entities/clubs/members/transactions.go +++ b/backend/entities/clubs/members/transactions.go @@ -2,9 +2,9 @@ package members import ( "github.com/GenerateNU/sac/backend/entities/clubs" + "github.com/GenerateNU/sac/backend/entities/clubs/followers" "github.com/GenerateNU/sac/backend/entities/models" "github.com/GenerateNU/sac/backend/entities/users" - "github.com/GenerateNU/sac/backend/entities/users/followers" "github.com/GenerateNU/sac/backend/utilities" "github.com/garrettladley/fiberpaginate" @@ -58,7 +58,7 @@ func CreateClubMember(db *gorm.DB, clubID uuid.UUID, userID uuid.UUID) error { return err } - if err := followers.CreateFollowing(tx, userID, clubID); err != nil { + if err := followers.CreateClubFollower(tx, userID, clubID); err != nil { tx.Rollback() return err } @@ -94,7 +94,7 @@ func DeleteClubMember(db *gorm.DB, clubID uuid.UUID, userID uuid.UUID) error { return err } - if err := followers.DeleteFollowing(tx, userID, clubID); err != nil { + if err := followers.DeleteClubFollower(tx, userID, clubID); err != nil { tx.Rollback() return err } diff --git a/backend/entities/clubs/pocs/controller.go b/backend/entities/clubs/pocs/controller.go deleted file mode 100644 index 8d9de7e6f..000000000 --- a/backend/entities/clubs/pocs/controller.go +++ /dev/null @@ -1,180 +0,0 @@ -package pocs - -import ( - "net/http" - - "github.com/GenerateNU/sac/backend/utilities" - "github.com/gofiber/fiber/v2" -) - -type ClubPointOfContactController struct { - clubPointOfContactService ClubPointOfContactServiceInterface -} - -func NewClubPointOfContactController(clubPointOfContactService ClubPointOfContactServiceInterface) *ClubPointOfContactController { - return &ClubPointOfContactController{clubPointOfContactService: clubPointOfContactService} -} - -// GetClubPointOfContacts godoc -// -// @Summary Retrieve all point of contacts for a club -// @Description Retrieves all point of contacts associated with a club -// @ID get-point-of-contacts-by-club -// @Tags club-point-of-contact -// @Produce json -// @Param clubID path string true "Club ID" -// @Success 200 {object} []models.PointOfContact -// @Failure 400 {object} error -// @Failure 404 {object} error -// @Failure 500 {object} error -// @Router /clubs/{clubID}/pocs/ [get] -func (cpoc *ClubPointOfContactController) GetClubPointOfContacts(c *fiber.Ctx) error { - pointOfContact, err := cpoc.clubPointOfContactService.GetClubPointOfContacts(c.Params("clubID")) - if err != nil { - return err - } - - return c.Status(http.StatusOK).JSON(pointOfContact) -} - -// GetClubPointOfContact godoc -// -// @Summary Retrieve a point of contact for a club -// @Description Retrieves a point of contact associated with a club -// @ID get-point-of-contact-by-club -// @Tags club-point-of-contact -// @Produce json -// @Param clubID path string true "Club ID" -// @Param pocID path string true "Point of Contact ID" -// @Success 200 {object} models.PointOfContact -// @Failure 400 {object} error -// @Failure 404 {object} error -// @Failure 500 {object} error -// @Router /clubs/{clubID}/pocs/{pocID} [get] -func (cpoc *ClubPointOfContactController) GetClubPointOfContact(c *fiber.Ctx) error { - pointOfContact, err := cpoc.clubPointOfContactService.GetClubPointOfContact(c.Params("clubID"), c.Params("pocID")) - if err != nil { - return err - } - - return c.Status(http.StatusOK).JSON(pointOfContact) -} - -// UpdateClubPointOfContactPhoto godoc -// -// @Summary Update a point of contact photo for a club -// @Description Updates a point of contact photo associated with a club -// @ID update-point-of-contact-photo-by-club -// @Tags club-point-of-contact -// @Accept multipart/form-data -// @Produce json -// @Param clubID path string true "Club ID" -// @Param pocID path string true "Point of Contact ID" -// @Success 200 {object} models.PointOfContact -// @Failure 400 {object} error -// @Failure 401 {object} error -// @Failure 404 {object} error -// @Failure 500 {object} error -// @Router /clubs/{clubID}/poc/{pocID} [patch] -func (cpoc *ClubPointOfContactController) UpdateClubPointOfContactPhoto(c *fiber.Ctx) error { - formFile, err := c.FormFile("file") - if err != nil { - return err - } - - pointOfContact, err := cpoc.clubPointOfContactService.UpdateClubPointOfContactPhoto(c.Params("clubID"), c.Params("pocID"), formFile) - if err != nil { - return err - } - - return c.Status(http.StatusOK).JSON(pointOfContact) -} - -// UpdateClubPointOfContact godoc -// -// @Summary Update a point of contact for a club -// @Description Updates a point of contact associated with a club -// @ID update-point-of-contact-by-club -// @Tags club-point-of-contact -// @Accept json -// @Produce json -// @Param clubID path string true "Club ID" -// @Param pocID path string true "Point of Contact ID" -// @Success 200 {object} models.PointOfContact -// @Failure 400 {object} error -// @Failure 401 {object} error -// @Failure 404 {object} error -// @Failure 500 {object} error -// @Router /clubs/{clubID}/poc/{pocID} [put] -func (cpoc *ClubPointOfContactController) UpdateClubPointOfContact(c *fiber.Ctx) error { - var pointOfContactBody UpdatePointOfContactBody - - if err := c.BodyParser(&pointOfContactBody); err != nil { - return utilities.InvalidJSON() - } - - pointOfContact, err := cpoc.clubPointOfContactService.UpdateClubPointOfContact(c.Params("clubID"), c.Params("pocID"), pointOfContactBody) - if err != nil { - return err - } - - return c.Status(http.StatusOK).JSON(pointOfContact) -} - -// CreateClubPointOfContact godoc -// -// @Summary Create a point of contact for a club -// @Description Creates a point of contact associated with a club -// @ID create-point-of-contact-by-club -// @Tags club-point-of-contact -// @Accept multipart/form-data -// @Produce json -// @Param clubID path string true "Club ID" -// @Success 201 {object} models.PointOfContact -// @Failure 400 {object} error -// @Failure 401 {object} error -// @Failure 404 {object} error -// @Failure 500 {object} error -// @Router /clubs/{clubID}/poc/ [post] -func (cpoc *ClubPointOfContactController) CreateClubPointOfContact(c *fiber.Ctx) error { - var pointOfContactBody CreatePointOfContactBody - - if err := c.BodyParser(&pointOfContactBody); err != nil { - return utilities.InvalidJSON() - } - - formFile, err := c.FormFile("file") - if err != nil { - return err - } - - pointOfContact, err := cpoc.clubPointOfContactService.CreateClubPointOfContact(c.Params("clubID"), pointOfContactBody, formFile) - if err != nil { - return err - } - - return c.Status(http.StatusCreated).JSON(pointOfContact) -} - -// DeleteClubPointOfContact godoc -// -// @Summary Delete a point of contact for a club -// @Description Delete a point of contact associated with a club -// @ID delete-point-of-contact-by-club -// @Tags club-point-of-contact -// @Produce json -// @Param clubID path string true "Club ID" -// @Param pocID path string true "Point of Contact ID" -// @Success 204 {object} nil -// @Failure 400 {object} error -// @Failure 404 {object} error -// @Failure 500 {object} error -// @Router /clubs/{clubID}/poc/{pocID} [delete] -func (cpoc *ClubPointOfContactController) DeleteClubPointOfContact(c *fiber.Ctx) error { - err := cpoc.clubPointOfContactService.DeleteClubPointOfContact(c.Params("clubID"), c.Params("pocID")) - if err != nil { - return err - } - - return c.SendStatus(http.StatusNoContent) -} diff --git a/backend/entities/clubs/pocs/routes.go b/backend/entities/clubs/pocs/routes.go deleted file mode 100644 index bd57f188c..000000000 --- a/backend/entities/clubs/pocs/routes.go +++ /dev/null @@ -1,36 +0,0 @@ -package pocs - -import ( - authMiddleware "github.com/GenerateNU/sac/backend/middleware/auth" - "github.com/GenerateNU/sac/backend/types" -) - -func ClubPointOfContact(clubParams types.RouteParams) { - clubPointOfContactController := NewClubPointOfContactController(NewClubPointOfContactService(clubParams.ServiceParams)) - - clubPointOfContacts := clubParams.Router.Group("/pocs") - - // api/v1/clubs/:clubID/pocs/* - clubPointOfContacts.Get("/", clubPointOfContactController.GetClubPointOfContacts) - clubPointOfContacts.Get("/:pocID", clubPointOfContactController.GetClubPointOfContact) - clubPointOfContacts.Post( - "/", - authMiddleware.AttachExtractor(clubParams.AuthMiddleware.ClubAuthorizeById, authMiddleware.ExtractFromParams("clubID")), - clubPointOfContactController.CreateClubPointOfContact, - ) - clubPointOfContacts.Patch( - "/:pocID", - authMiddleware.AttachExtractor(clubParams.AuthMiddleware.ClubAuthorizeById, authMiddleware.ExtractFromParams("clubID")), - clubPointOfContactController.UpdateClubPointOfContact, - ) - clubPointOfContacts.Patch( - "/:pocID/photo", - authMiddleware.AttachExtractor(clubParams.AuthMiddleware.ClubAuthorizeById, authMiddleware.ExtractFromParams("clubID")), - clubPointOfContactController.UpdateClubPointOfContactPhoto, - ) - clubPointOfContacts.Delete( - "/:pocID", - authMiddleware.AttachExtractor(clubParams.AuthMiddleware.ClubAuthorizeById, authMiddleware.ExtractFromParams("clubID")), - clubPointOfContactController.DeleteClubPointOfContact, - ) -} diff --git a/backend/entities/clubs/pocs/service.go b/backend/entities/clubs/pocs/service.go deleted file mode 100644 index 82472507e..000000000 --- a/backend/entities/clubs/pocs/service.go +++ /dev/null @@ -1,206 +0,0 @@ -package pocs - -import ( - "mime/multipart" - - "github.com/GenerateNU/sac/backend/entities/files/base" - "github.com/GenerateNU/sac/backend/entities/models" - - "github.com/GenerateNU/sac/backend/integrations/file" - "github.com/GenerateNU/sac/backend/types" - "github.com/GenerateNU/sac/backend/utilities" -) - -type ClubPointOfContactServiceInterface interface { - GetClubPointOfContacts(clubID string) ([]models.PointOfContact, error) - GetClubPointOfContact(clubID, pocID string) (*models.PointOfContact, error) - CreateClubPointOfContact(clubID string, pointOfContactBody CreatePointOfContactBody, fileHeader *multipart.FileHeader) (*models.PointOfContact, error) - UpdateClubPointOfContactPhoto(clubID, pocID string, fileHeader *multipart.FileHeader) (*models.PointOfContact, error) - UpdateClubPointOfContact(clubID, pocID string, pointOfContactBody UpdatePointOfContactBody) (*models.PointOfContact, error) - DeleteClubPointOfContact(clubID, pocID string) error -} - -type ClubPointOfContactService struct { - types.ServiceParams -} - -func NewClubPointOfContactService(serviceParams types.ServiceParams) ClubPointOfContactServiceInterface { - return &ClubPointOfContactService{serviceParams} -} - -func (cpoc *ClubPointOfContactService) GetClubPointOfContacts(clubID string) ([]models.PointOfContact, error) { - clubIdAsUUID, err := utilities.ValidateID(clubID) - if err != nil { - return nil, err - } - - return GetClubPointOfContacts(cpoc.DB, *clubIdAsUUID) -} - -func (cpoc *ClubPointOfContactService) GetClubPointOfContact(clubID, pocID string) (*models.PointOfContact, error) { - clubIdAsUUID, err := utilities.ValidateID(clubID) - if err != nil { - return nil, err - } - - pocIdAsUUID, err := utilities.ValidateID(pocID) - if err != nil { - return nil, err - } - - return GetClubPointOfContact(cpoc.DB, *clubIdAsUUID, *pocIdAsUUID) -} - -func (cpoc *ClubPointOfContactService) CreateClubPointOfContact(clubID string, pointOfContactBody CreatePointOfContactBody, fileHeader *multipart.FileHeader) (*models.PointOfContact, error) { - if err := utilities.Validate(cpoc.Validate, pointOfContactBody); err != nil { - return nil, err - } - - clubIdAsUUID, err := utilities.ValidateID(clubID) - if err != nil { - return nil, err - } - - _, err = GetClubPointOfContactByClubIDAndEmail(cpoc.DB, *clubIdAsUUID, pointOfContactBody.Email) - if err == nil { - return nil, err - } - - fileInfo, err := cpoc.Integrations.File.UploadFile("point_of_contacts", fileHeader, []file.FileType{file.IMAGE}) - if err != nil { - return nil, err - } - - tx := cpoc.DB.Begin() - defer func() { - if r := recover(); r != nil { - tx.Rollback() - } - }() - - poc, err := CreateClubPointOfContact(tx, *clubIdAsUUID, pointOfContactBody) - if err != nil { - tx.Rollback() - return nil, err - } - - file, err := base.CreateFile(tx, poc.ID, "point_of_contacts", *fileInfo) - if err != nil { - tx.Rollback() - return nil, err - } - - if err := tx.Commit().Error; err != nil { - if err := cpoc.Integrations.File.DeleteFile(fileInfo.FileURL); err != nil { - return nil, err - } - - return nil, err - } - - poc.PhotoFile = *file - - return poc, nil -} - -func (cpoc *ClubPointOfContactService) UpdateClubPointOfContactPhoto(clubID, pocID string, fileHeader *multipart.FileHeader) (*models.PointOfContact, error) { - clubIdAsUUID, err := utilities.ValidateID(clubID) - if err != nil { - return nil, err - } - - pocIdAsUUID, err := utilities.ValidateID(pocID) - if err != nil { - return nil, err - } - - pointOfContact, err := GetClubPointOfContact(cpoc.DB, *clubIdAsUUID, *pocIdAsUUID) - if err != nil { - return nil, err - } - - if err := cpoc.Integrations.File.DeleteFile(pointOfContact.PhotoFile.FileURL); err != nil { - return nil, err - } - - fileInfo, err := cpoc.Integrations.File.UploadFile("point_of_contacts", fileHeader, []file.FileType{file.IMAGE}) - if err != nil { - return nil, err - } - - file, err := base.UpdateFile(cpoc.DB, pointOfContact.PhotoFile.ID, *fileInfo) - if err != nil { - return nil, err - } - - pointOfContact.PhotoFile = *file - - return pointOfContact, nil -} - -func (cpoc *ClubPointOfContactService) UpdateClubPointOfContact(clubID, pocID string, pointOfContactBody UpdatePointOfContactBody) (*models.PointOfContact, error) { - if err := utilities.Validate(cpoc.Validate, pointOfContactBody); err != nil { - return nil, err - } - - clubIdAsUUID, err := utilities.ValidateID(clubID) - if err != nil { - return nil, err - } - - pocIdAsUUID, err := utilities.ValidateID(pocID) - if err != nil { - return nil, err - } - - return UpdateClubPointOfContact(cpoc.DB, *clubIdAsUUID, *pocIdAsUUID, pointOfContactBody) -} - -func (cpoc *ClubPointOfContactService) DeleteClubPointOfContact(clubID, pocID string) error { - clubIdAsUUID, err := utilities.ValidateID(clubID) - if err != nil { - return err - } - - pocIdAsUUID, err := utilities.ValidateID(pocID) - if err != nil { - return err - } - - pointOfContact, err := GetClubPointOfContact(cpoc.DB, *clubIdAsUUID, *pocIdAsUUID) - if err != nil { - return err - } - - if err := cpoc.Integrations.File.DeleteFile(pointOfContact.PhotoFile.FileURL); err != nil { - return err - } - - tx := cpoc.DB.Begin() - defer func() { - if r := recover(); r != nil { - tx.Rollback() - } - }() - if err := tx.Error; err != nil { - return err - } - - err = base.DeleteFile(tx, pointOfContact.PhotoFile.ID) - if err != nil { - tx.Rollback() - return err - } - - err = DeleteClubPointOfContact(tx, *clubIdAsUUID, *pocIdAsUUID) - if err != nil { - tx.Rollback() - return err - } - - if err := tx.Commit().Error; err != nil { - return err - } - - return nil -} diff --git a/backend/entities/clubs/pocs/transactions.go b/backend/entities/clubs/pocs/transactions.go deleted file mode 100644 index 5233fdfaa..000000000 --- a/backend/entities/clubs/pocs/transactions.go +++ /dev/null @@ -1,92 +0,0 @@ -package pocs - -import ( - "errors" - - "github.com/GenerateNU/sac/backend/entities/models" - - "github.com/GenerateNU/sac/backend/utilities" - "github.com/google/uuid" - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -func GetClubPointOfContacts(db *gorm.DB, clubID uuid.UUID) ([]models.PointOfContact, error) { - var pointOfContacts []models.PointOfContact - - result := db.Preload("PhotoFile").Where("club_id = ?", clubID).Find(&pointOfContacts) - if result.Error != nil { - if errors.Is(result.Error, gorm.ErrRecordNotFound) { - return nil, utilities.ErrNotFound - } - return nil, result.Error - } - - return pointOfContacts, nil -} - -func GetClubPointOfContact(db *gorm.DB, clubID uuid.UUID, pocID uuid.UUID) (*models.PointOfContact, error) { - var pointOfContact models.PointOfContact - - if err := db.Preload("PhotoFile").First(&pointOfContact, "id = ? AND club_id = ?", pocID, clubID).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, utilities.ErrNotFound - } - return nil, err - } - - return &pointOfContact, nil -} - -func GetClubPointOfContactByClubIDAndEmail(db *gorm.DB, clubID uuid.UUID, email string) (*models.PointOfContact, error) { - var pointOfContact models.PointOfContact - - if err := db.First(&pointOfContact, "email = ? AND club_id = ?", email, clubID).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, utilities.ErrNotFound - } - return nil, err - } - - return &pointOfContact, nil -} - -func CreateClubPointOfContact(db *gorm.DB, clubID uuid.UUID, pointOfContactBody CreatePointOfContactBody) (*models.PointOfContact, error) { - pointOfContact := models.PointOfContact{ - Name: pointOfContactBody.Name, - Email: pointOfContactBody.Email, - Position: pointOfContactBody.Position, - ClubID: clubID, - } - - if err := db.Create(&pointOfContact).Error; err != nil { - return nil, err - } - return &pointOfContact, nil -} - -func UpdateClubPointOfContact(db *gorm.DB, clubID uuid.UUID, pocID uuid.UUID, pointOfContactBody UpdatePointOfContactBody) (*models.PointOfContact, error) { - pointOfContact, err := GetClubPointOfContact(db, clubID, pocID) - if err != nil { - return nil, err - } - if err := db.Model(&pointOfContact).Clauses(clause.Returning{}).Updates(models.PointOfContact{ - Name: pointOfContactBody.Name, - Email: pointOfContactBody.Email, - Position: pointOfContactBody.Position, - }).Error; err != nil { - return nil, err - } - - return pointOfContact, nil -} - -func DeleteClubPointOfContact(db *gorm.DB, clubID uuid.UUID, pocID uuid.UUID) error { - if result := db.Delete(&models.PointOfContact{}, "id = ? AND club_id = ?", pocID, clubID); result.RowsAffected == 0 { - if result.Error == nil { - return utilities.ErrNotFound - } - return result.Error - } - return nil -} diff --git a/backend/entities/clubs/recruitment/controller.go b/backend/entities/clubs/recruitment/controller.go new file mode 100644 index 000000000..cf321d1f2 --- /dev/null +++ b/backend/entities/clubs/recruitment/controller.go @@ -0,0 +1,121 @@ +package recruitment + +import ( + "net/http" + + "github.com/GenerateNU/sac/backend/utilities" + "github.com/garrettladley/fiberpaginate" + "github.com/gofiber/fiber/v2" +) + +type ClubRecruitmentController struct { + clubRecruitmentService ClubRecruitmentServicer +} + +func NewClubRecruitmentController(clubRecruitmentService ClubRecruitmentServicer) *ClubRecruitmentController { + return &ClubRecruitmentController{clubRecruitmentService: clubRecruitmentService} +} + +func (cr *ClubRecruitmentController) CreateClubRecruitment(c *fiber.Ctx) error { + var body CreateClubRecruitmentRequestBody + if err := c.BodyParser(&body); err != nil { + return utilities.InvalidJSON() + } + + recruitment, err := cr.clubRecruitmentService.CreateClubRecruitment(c.UserContext(), c.Params("clubID"), body) + if err != nil { + return err + } + + return c.Status(http.StatusCreated).JSON(recruitment) +} + +func (cr *ClubRecruitmentController) GetClubRecruitment(c *fiber.Ctx) error { + recruitment, err := cr.clubRecruitmentService.GetClubRecruitment(c.UserContext(), c.Params("clubID")) + if err != nil { + return err + } + + return c.Status(http.StatusOK).JSON(recruitment) +} + +func (cr *ClubRecruitmentController) UpdateClubRecruitment(c *fiber.Ctx) error { + var body UpdateClubRecruitmentRequestBody + if err := c.BodyParser(&body); err != nil { + return utilities.InvalidJSON() + } + + recruitment, err := cr.clubRecruitmentService.UpdateClubRecruitment(c.UserContext(), c.Params("clubID"), body) + if err != nil { + return err + } + + return c.Status(http.StatusOK).JSON(recruitment) +} + +func (cr *ClubRecruitmentController) DeleteClubRecruitment(c *fiber.Ctx) error { + if err := cr.clubRecruitmentService.DeleteClubRecruitment(c.UserContext(), c.Params("clubID")); err != nil { + return err + } + + return c.SendStatus(http.StatusNoContent) +} + +func (cr *ClubRecruitmentController) CreateClubRecruitmentApplication(c *fiber.Ctx) error { + var body CreateApplicationRequestBody + if err := c.BodyParser(&body); err != nil { + return utilities.InvalidJSON() + } + + application, err := cr.clubRecruitmentService.CreateClubRecruitmentApplication(c.UserContext(), c.Params("clubID"), body) + if err != nil { + return err + } + + return c.Status(http.StatusCreated).JSON(application) +} + +func (cr *ClubRecruitmentController) GetClubRecruitmentApplications(c *fiber.Ctx) error { + pageInfo, ok := fiberpaginate.FromContext(c) + if !ok { + return utilities.ErrExpectedPageInfo + } + + applications, err := cr.clubRecruitmentService.GetClubRecruitmentApplications(c.UserContext(), c.Params("clubID"), *pageInfo) + if err != nil { + return err + } + + return c.Status(http.StatusOK).JSON(applications) +} + +func (cr *ClubRecruitmentController) GetClubRecruitmentApplication(c *fiber.Ctx) error { + application, err := cr.clubRecruitmentService.GetClubRecruitmentApplication(c.UserContext(), c.Params("clubID"), c.Params("applicationID")) + if err != nil { + return err + } + + return c.Status(http.StatusOK).JSON(application) +} + +func (cr *ClubRecruitmentController) UpdateClubRecruitmentApplication(c *fiber.Ctx) error { + var body UpdateApplicationRequestBody + if err := c.BodyParser(&body); err != nil { + return utilities.InvalidJSON() + } + + application, err := cr.clubRecruitmentService.UpdateClubRecruitmentApplication(c.UserContext(), c.Params("clubID"), c.Params("applicationID"), body) + if err != nil { + return err + } + + return c.Status(http.StatusOK).JSON(application) +} + +func (cr *ClubRecruitmentController) DeleteClubRecruitmentApplication(c *fiber.Ctx) error { + if err := cr.clubRecruitmentService.DeleteClubRecruitmentApplication(c.UserContext(), c.Params("clubID"), c.Params("applicationID")); err != nil { + return err + } + + return c.SendStatus(http.StatusNoContent) +} diff --git a/backend/entities/clubs/recruitment/models.go b/backend/entities/clubs/recruitment/models.go new file mode 100644 index 000000000..11c6e95e4 --- /dev/null +++ b/backend/entities/clubs/recruitment/models.go @@ -0,0 +1,24 @@ +package recruitment + +import "github.com/GenerateNU/sac/backend/entities/models" + +type CreateClubRecruitmentRequestBody struct { + Cycle models.RecruitmentCycle `json:"cycle" validate:"required,oneof=fall spring fallSpring always"` + Type models.RecruitmentType `json:"type" validate:"required,oneof=unrestricted tryout application"` +} + +type CreateApplicationRequestBody struct { + Title string `json:"title" validate:"required,max=255"` + Link string `json:"link" validate:"required,http_url,max=255"` +} + +type UpdateClubRecruitmentRequestBody struct { + Cycle models.RecruitmentCycle `json:"cycle" validate:"omitempty,oneof=fall spring fallSpring always"` + Type models.RecruitmentType `json:"type" validate:"omitempty,oneof=unrestricted tryout application"` + IsRecruiting *bool `json:"is_recruiting" validate:"omitempty"` +} + +type UpdateApplicationRequestBody struct { + Title string `json:"title" validate:"omitempty,max=255"` + Link string `json:"link" validate:"omitempty,http_url,max=255"` +} diff --git a/backend/entities/clubs/recruitment/routes.go b/backend/entities/clubs/recruitment/routes.go new file mode 100644 index 000000000..47ca3db20 --- /dev/null +++ b/backend/entities/clubs/recruitment/routes.go @@ -0,0 +1,69 @@ +package recruitment + +import ( + authMiddleware "github.com/GenerateNU/sac/backend/middleware/auth" + "github.com/GenerateNU/sac/backend/types" +) + +func ClubRecruitment(clubParams types.RouteParams) { + clubRecruitmentController := NewClubRecruitmentController(NewClubRecruitmentService(clubParams.ServiceParams)) + + // api/v1/clubs/:clubID/recruitment/* + clubRecruitment := clubParams.Router.Group("/recruitment") + + clubRecruitment.Get( + "/", + authMiddleware.AttachExtractor(clubParams.AuthMiddleware.ClubAuthorizeById, authMiddleware.ExtractFromParams("clubID")), + clubRecruitmentController.GetClubRecruitment, + ) + clubRecruitment.Post( + "/", + authMiddleware.AttachExtractor(clubParams.AuthMiddleware.ClubAuthorizeById, authMiddleware.ExtractFromParams("clubID")), + clubRecruitmentController.CreateClubRecruitment, + ) + clubRecruitment.Patch( + "/", + authMiddleware.AttachExtractor(clubParams.AuthMiddleware.ClubAuthorizeById, authMiddleware.ExtractFromParams("clubID")), + clubRecruitmentController.UpdateClubRecruitment, + ) + + clubRecruitment.Delete( + "/", + authMiddleware.AttachExtractor(clubParams.AuthMiddleware.ClubAuthorizeById, authMiddleware.ExtractFromParams("clubID")), + clubRecruitmentController.DeleteClubRecruitment, + ) + + // api/v1/clubs/:clubID/recruitment/applications/* + clubRecruitmentApplications := clubRecruitment.Group("/applications") + + clubRecruitmentApplications.Get( + "/", + clubParams.UtilityMiddleware.Paginator, + authMiddleware.AttachExtractor(clubParams.AuthMiddleware.ClubAuthorizeById, authMiddleware.ExtractFromParams("clubID")), + clubRecruitmentController.GetClubRecruitmentApplications, + ) + clubRecruitmentApplications.Post( + "/", + authMiddleware.AttachExtractor(clubParams.AuthMiddleware.ClubAuthorizeById, authMiddleware.ExtractFromParams("clubID")), + clubRecruitmentController.CreateClubRecruitmentApplication, + ) + + // api/v1/clubs/:clubID/recruitment/applications/:applicationID/* + clubRecruitmentApplicationsID := clubRecruitmentApplications.Group("/:applicationID") + + clubRecruitmentApplicationsID.Get( + "/", + authMiddleware.AttachExtractor(clubParams.AuthMiddleware.ClubAuthorizeById, authMiddleware.ExtractFromParams("clubID")), + clubRecruitmentController.GetClubRecruitmentApplication, + ) + clubRecruitmentApplicationsID.Patch( + "/", + authMiddleware.AttachExtractor(clubParams.AuthMiddleware.ClubAuthorizeById, authMiddleware.ExtractFromParams("clubID")), + clubRecruitmentController.UpdateClubRecruitmentApplication, + ) + clubRecruitmentApplicationsID.Delete( + "/", + authMiddleware.AttachExtractor(clubParams.AuthMiddleware.ClubAuthorizeById, authMiddleware.ExtractFromParams("clubID")), + clubRecruitmentController.DeleteClubRecruitmentApplication, + ) +} diff --git a/backend/entities/clubs/recruitment/service.go b/backend/entities/clubs/recruitment/service.go new file mode 100644 index 000000000..a2696bb09 --- /dev/null +++ b/backend/entities/clubs/recruitment/service.go @@ -0,0 +1,201 @@ +package recruitment + +import ( + "context" + "time" + + "github.com/GenerateNU/sac/backend/constants" + "github.com/GenerateNU/sac/backend/entities/models" + "github.com/GenerateNU/sac/backend/errs" + "github.com/GenerateNU/sac/backend/types" + "github.com/GenerateNU/sac/backend/utilities" + "github.com/garrettladley/fiberpaginate" +) + +type ClubRecruitmentServicer interface { + CreateClubRecruitment(ctx context.Context, clubID string, body CreateClubRecruitmentRequestBody) (*models.Recruitment, error) + GetClubRecruitment(ctx context.Context, clubID string) (*models.Recruitment, error) + UpdateClubRecruitment(ctx context.Context, clubID string, body UpdateClubRecruitmentRequestBody) (*models.Recruitment, error) + DeleteClubRecruitment(ctx context.Context, clubID string) error + + CreateClubRecruitmentApplication(ctx context.Context, clubID string, body CreateApplicationRequestBody) (*models.Application, error) + GetClubRecruitmentApplications(ctx context.Context, clubID string, pageInfo fiberpaginate.PageInfo) ([]models.Application, error) + + GetClubRecruitmentApplication(ctx context.Context, clubID string, applicationID string) (*models.Application, error) + UpdateClubRecruitmentApplication(ctx context.Context, clubID string, applicationID string, body UpdateApplicationRequestBody) (*models.Application, error) + DeleteClubRecruitmentApplication(ctx context.Context, clubID string, applicationID string) error +} + +type ClubRecruitmentService struct { + types.ServiceParams +} + +func NewClubRecruitmentService(params types.ServiceParams) ClubRecruitmentServicer { + return &ClubRecruitmentService{params} +} + +func (s *ClubRecruitmentService) CreateClubRecruitment(ctx context.Context, clubID string, body CreateClubRecruitmentRequestBody) (*models.Recruitment, error) { + idAsUUID, err := utilities.ValidateID(clubID) + if err != nil { + return nil, err + } + + if err := utilities.Validate(s.Validate, body); err != nil { + return nil, err + } + + recruitment, err := utilities.MapJsonTags(body, &models.Recruitment{}) + if err != nil { + return nil, err + } + + createClubRecruitmentCtx, createClubRecruitmentCancel := context.WithTimeoutCause(ctx, constants.DB_TIMEOUT+1*time.Second, errs.ErrDatabaseTimeout) + defer createClubRecruitmentCancel() + return CreateClubRecruitment(createClubRecruitmentCtx, s.DB, idAsUUID, recruitment) +} + +func (s *ClubRecruitmentService) GetClubRecruitment(ctx context.Context, clubID string) (*models.Recruitment, error) { + idAsUUID, err := utilities.ValidateID(clubID) + if err != nil { + return nil, err + } + + getClubRecruitmentCtx, getClubRecruitmentCancel := context.WithTimeoutCause(ctx, constants.DB_TIMEOUT, errs.ErrDatabaseTimeout) + defer getClubRecruitmentCancel() + + return GetClubRecruitment(getClubRecruitmentCtx, s.DB, idAsUUID) +} + +func (s *ClubRecruitmentService) UpdateClubRecruitment(ctx context.Context, clubID string, body UpdateClubRecruitmentRequestBody) (*models.Recruitment, error) { + clubIDAsUUID, err := utilities.ValidateID(clubID) + if err != nil { + return nil, err + } + + if utilities.AtLeastOne(body, UpdateClubRecruitmentRequestBody{}) { + return nil, errs.ErrAtLeastOne + } + + if err := utilities.Validate(s.Validate, body); err != nil { + return nil, err + } + + recruitment, err := utilities.MapJsonTags(body, &models.Recruitment{}) + if err != nil { + return nil, err + } + + updateClubRecruitmentCtx, updateClubRecruitmentCancel := context.WithTimeoutCause(ctx, constants.DB_TIMEOUT, errs.ErrDatabaseTimeout) + defer updateClubRecruitmentCancel() + + return UpdateClubRecruitment(updateClubRecruitmentCtx, s.DB, clubIDAsUUID, recruitment) +} + +func (s *ClubRecruitmentService) DeleteClubRecruitment(ctx context.Context, clubID string) error { + idAsUUID, err := utilities.ValidateID(clubID) + if err != nil { + return err + } + + deleteClubRecruitmentCtx, deleteClubRecruitmentCancel := context.WithTimeoutCause(ctx, constants.DB_TIMEOUT, errs.ErrDatabaseTimeout) + defer deleteClubRecruitmentCancel() + + return DeleteClubRecruitment(deleteClubRecruitmentCtx, s.DB, idAsUUID) +} + +func (s *ClubRecruitmentService) CreateClubRecruitmentApplication(ctx context.Context, clubID string, body CreateApplicationRequestBody) (*models.Application, error) { + clubIDAsUUID, err := utilities.ValidateID(clubID) + if err != nil { + return nil, err + } + + if err := utilities.Validate(s.Validate, body); err != nil { + return nil, err + } + + application, err := utilities.MapJsonTags(body, &models.Application{}) + if err != nil { + return nil, err + } + + createApplicationCtx, createApplicationCancel := context.WithTimeoutCause(ctx, constants.DB_TIMEOUT, errs.ErrDatabaseTimeout) + defer createApplicationCancel() + + return CreateApplication(createApplicationCtx, s.DB, clubIDAsUUID, application) +} + +func (s *ClubRecruitmentService) GetClubRecruitmentApplications(ctx context.Context, clubID string, pageInfo fiberpaginate.PageInfo) ([]models.Application, error) { + clubIDAsUUID, err := utilities.ValidateID(clubID) + if err != nil { + return nil, err + } + + getClubRecruitmentApplicationsCtx, getClubRecruitmentApplicationsCancel := context.WithTimeoutCause(ctx, constants.DB_TIMEOUT, errs.ErrDatabaseTimeout) + defer getClubRecruitmentApplicationsCancel() + + return GetClubRecruitmentApplications(getClubRecruitmentApplicationsCtx, s.DB, clubIDAsUUID, pageInfo) +} + +func (s *ClubRecruitmentService) GetClubRecruitmentApplication(ctx context.Context, clubID string, applicationID string) (*models.Application, error) { + clubIDAsUUID, err := utilities.ValidateID(clubID) + if err != nil { + return nil, err + } + + applicationIDAsUUID, err := utilities.ValidateID(applicationID) + if err != nil { + return nil, err + } + + getClubRecruitmentApplicationCtx, getClubRecruitmentApplicationCancel := context.WithTimeoutCause(ctx, constants.DB_TIMEOUT, errs.ErrDatabaseTimeout) + defer getClubRecruitmentApplicationCancel() + + return GetClubRecruitmentApplication(getClubRecruitmentApplicationCtx, s.DB, clubIDAsUUID, applicationIDAsUUID) +} + +func (s *ClubRecruitmentService) UpdateClubRecruitmentApplication(ctx context.Context, clubID string, applicationID string, body UpdateApplicationRequestBody) (*models.Application, error) { + clubIDAsUUID, err := utilities.ValidateID(clubID) + if err != nil { + return nil, err + } + + applicationIDAsUUID, err := utilities.ValidateID(applicationID) + if err != nil { + return nil, err + } + + if utilities.AtLeastOne(body, UpdateApplicationRequestBody{}) { + return nil, errs.ErrAtLeastOne + } + + if err := utilities.Validate(s.Validate, body); err != nil { + return nil, err + } + + application, err := utilities.MapJsonTags(body, &models.Application{}) + if err != nil { + return nil, err + } + + updateClubRecruitmentApplicationCtx, updateClubRecruitmentApplicationCancel := context.WithTimeoutCause(ctx, constants.DB_TIMEOUT, errs.ErrDatabaseTimeout) + defer updateClubRecruitmentApplicationCancel() + + return UpdateClubRecruitmentApplication(updateClubRecruitmentApplicationCtx, s.DB, clubIDAsUUID, applicationIDAsUUID, application) +} + +func (s *ClubRecruitmentService) DeleteClubRecruitmentApplication(ctx context.Context, clubID string, applicationID string) error { + clubIDAsUUID, err := utilities.ValidateID(clubID) + if err != nil { + return err + } + + applicationIDAsUUID, err := utilities.ValidateID(applicationID) + if err != nil { + return err + } + + deleteClubRecruitmentApplicationCtx, deleteClubRecruitmentApplicationCancel := context.WithTimeoutCause(ctx, constants.DB_TIMEOUT, errs.ErrDatabaseTimeout) + defer deleteClubRecruitmentApplicationCancel() + + return DeleteClubRecruitmentApplication(deleteClubRecruitmentApplicationCtx, s.DB, clubIDAsUUID, applicationIDAsUUID) +} diff --git a/backend/entities/clubs/recruitment/transactions.go b/backend/entities/clubs/recruitment/transactions.go new file mode 100644 index 000000000..05a90a59e --- /dev/null +++ b/backend/entities/clubs/recruitment/transactions.go @@ -0,0 +1,144 @@ +package recruitment + +import ( + "context" + "errors" + + "github.com/GenerateNU/sac/backend/entities/clubs" + "github.com/GenerateNU/sac/backend/entities/models" + "github.com/GenerateNU/sac/backend/transactions" + "github.com/GenerateNU/sac/backend/utilities" + "github.com/garrettladley/fiberpaginate" + "github.com/google/uuid" + "gorm.io/gorm" +) + +func CreateClubRecruitment(ctx context.Context, db *gorm.DB, clubID *uuid.UUID, recruitment *models.Recruitment) (*models.Recruitment, error) { + _, err := clubs.GetClub(db, *clubID) + if err != nil { + return nil, err + } + + recruitment.ClubID = *clubID + + if err := db.WithContext(ctx).Save(recruitment).Error; err != nil { + return nil, err + } + + return recruitment, nil +} + +func GetClubRecruitment(ctx context.Context, db *gorm.DB, clubID *uuid.UUID) (*models.Recruitment, error) { + club, err := clubs.GetClub(db, *clubID) + if err != nil { + return nil, err + } + + var recruitment models.Recruitment + if err := db.WithContext(ctx).Model(&club).Association("Recruitment").Find(&recruitment); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, utilities.ErrNotFound + } + + return nil, err + } + + return &recruitment, nil +} + +func UpdateClubRecruitment(ctx context.Context, db *gorm.DB, clubID *uuid.UUID, recruitment *models.Recruitment) (*models.Recruitment, error) { + existingRecruitment, err := GetClubRecruitment(ctx, db, clubID) + if err != nil { + return nil, err + } + + if err := db.WithContext(ctx).Model(existingRecruitment).Updates(recruitment).Error; err != nil { + return nil, err + } + + return existingRecruitment, nil +} + +func DeleteClubRecruitment(ctx context.Context, db *gorm.DB, clubID *uuid.UUID) error { + club, err := clubs.GetClub(db, *clubID, transactions.PreloadRecruitment()) + if err != nil { + return err + } + + if err := db.WithContext(ctx).Model(&club).Association("Recruitment").Delete(); err != nil { + return err + } + + return nil +} + +func CreateApplication(ctx context.Context, db *gorm.DB, clubID *uuid.UUID, application *models.Application) (*models.Application, error) { + recruitment, err := GetClubRecruitment(ctx, db, clubID) + if err != nil { + return nil, err + } + + if err := db.WithContext(ctx).Model(&recruitment).Association("Application").Append(application); err != nil { + return nil, err + } + + return application, nil +} + +func GetClubRecruitmentApplications(ctx context.Context, db *gorm.DB, clubID *uuid.UUID, pageInfo fiberpaginate.PageInfo) ([]models.Application, error) { + recruitment, err := GetClubRecruitment(ctx, db, clubID) + if err != nil { + return nil, err + } + + var applications []models.Application + if err := db.WithContext(ctx).Scopes(utilities.IntoScope(pageInfo, db)).Model(&recruitment).Association("Application").Find(&applications); err != nil { + return nil, err + } + + return applications, nil +} + +func GetClubRecruitmentApplication(ctx context.Context, db *gorm.DB, clubID *uuid.UUID, applicationID *uuid.UUID) (*models.Application, error) { + recruitment, err := GetClubRecruitment(ctx, db, clubID) + if err != nil { + return nil, err + } + + var application models.Application + if err := db.WithContext(ctx).Model(&recruitment).Association("Application").Find(&application, applicationID); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, utilities.ErrNotFound + } + + return nil, err + } + + return &application, nil +} + +func UpdateClubRecruitmentApplication(ctx context.Context, db *gorm.DB, clubID *uuid.UUID, applicationID *uuid.UUID, application *models.Application) (*models.Application, error) { + existingApplication, err := GetClubRecruitmentApplication(ctx, db, clubID, applicationID) + if err != nil { + return nil, err + } + + if err := db.WithContext(ctx).Model(existingApplication).Updates(application).Error; err != nil { + return nil, err + } + + return existingApplication, nil +} + +func DeleteClubRecruitmentApplication(ctx context.Context, db *gorm.DB, clubID *uuid.UUID, applicationID *uuid.UUID) error { + application, err := GetClubRecruitmentApplication(ctx, db, clubID, applicationID) + if err != nil { + return err + } + + if err := db.WithContext(ctx).Delete(application).Error; err != nil { + return err + } + + return nil +} diff --git a/backend/entities/clubs/socials/controller.go b/backend/entities/clubs/socials/controller.go new file mode 100644 index 000000000..c573e5baf --- /dev/null +++ b/backend/entities/clubs/socials/controller.go @@ -0,0 +1,69 @@ +package socials + +import ( + "net/http" + + "github.com/GenerateNU/sac/backend/utilities" + "github.com/gofiber/fiber/v2" +) + +type ClubSocialController struct { + clubSocialService ClubSocialServiceInterface +} + +func NewClubSocialController(clubSocialService ClubSocialServiceInterface) *ClubSocialController { + return &ClubSocialController{clubSocialService: clubSocialService} +} + +// GetClubSocials godoc +// +// @Summary Retrieve all socials for a club +// @Description Retrieves all socials associated with a club +// @ID get-socials-by-club +// @Tags club-social +// @Produce json +// @Param clubID path string true "Club ID" +// @Success 200 {object} []models.Social +// @Failure 400 {object} error +// @Failure 404 {object} error +// @Failure 500 {object} error +// @Router /clubs/{clubID}/socials/ [get] +func (cc *ClubSocialController) GetClubSocials(c *fiber.Ctx) error { + socials, err := cc.clubSocialService.GetClubSocials(c.Params("clubID")) + if err != nil { + return err + } + + return c.Status(http.StatusOK).JSON(socials) +} + +// PutSocial godoc +// +// @Summary Creates a social +// @Description Creates a social +// @ID put-social +// @Tags club-social +// @Accept json +// @Produce json +// @Param clubID path string true "Club ID" +// @Param socialBody body PutSocialRequestBody true "Social Body" +// @Success 201 {object} models.Social +// @Failure 400 {object} error +// @Failure 401 {object} error +// @Failure 404 {object} error +// @Failure 500 {object} error +// @Router /clubs/{clubID}/socials/ [put] +func (cc *ClubSocialController) PutSocial(c *fiber.Ctx) error { + var socialBody PutSocialRequestBody + + if err := c.BodyParser(&socialBody); err != nil { + return utilities.InvalidJSON() + } + + social, err := cc.clubSocialService.PutClubSocial(c.Params("clubID"), socialBody) + if err != nil { + return err + } + + return c.Status(http.StatusOK).JSON(social) +} diff --git a/backend/entities/clubs/socials/models.go b/backend/entities/clubs/socials/models.go new file mode 100644 index 000000000..74f789c18 --- /dev/null +++ b/backend/entities/clubs/socials/models.go @@ -0,0 +1,8 @@ +package socials + +import "github.com/GenerateNU/sac/backend/entities/models" + +type PutSocialRequestBody struct { + Type models.SocialType `json:"type" validate:"required,max=255,oneof=facebook instagram x linkedin youtube github slack discord email customSite"` + Content string `json:"content" validate:"required,social_pointer,max=255"` +} diff --git a/backend/entities/clubs/socials/routes.go b/backend/entities/clubs/socials/routes.go new file mode 100644 index 000000000..91e4274d8 --- /dev/null +++ b/backend/entities/clubs/socials/routes.go @@ -0,0 +1,20 @@ +package socials + +import ( + authMiddleware "github.com/GenerateNU/sac/backend/middleware/auth" + "github.com/GenerateNU/sac/backend/types" +) + +func ClubSocial(clubParams types.RouteParams) { + clubSocialController := NewClubSocialController(NewClubSocialService(clubParams.ServiceParams)) + + clubSocials := clubParams.Router.Group("/socials") + + // api/v1/clubs/:clubID/socials/* + clubSocials.Get("/", clubSocialController.GetClubSocials) + clubSocials.Put( + "/", + authMiddleware.AttachExtractor(clubParams.AuthMiddleware.ClubAuthorizeById, authMiddleware.ExtractFromParams("clubID")), + clubSocialController.PutSocial, + ) +} diff --git a/backend/entities/clubs/socials/service.go b/backend/entities/clubs/socials/service.go new file mode 100644 index 000000000..4f5403563 --- /dev/null +++ b/backend/entities/clubs/socials/service.go @@ -0,0 +1,49 @@ +package socials + +import ( + "github.com/GenerateNU/sac/backend/entities/models" + "github.com/GenerateNU/sac/backend/types" + "github.com/GenerateNU/sac/backend/utilities" +) + +type ClubSocialServiceInterface interface { + GetClubSocials(clubID string) ([]models.Social, error) + PutClubSocial(clubID string, contactBody PutSocialRequestBody) (*models.Social, error) +} + +type ClubSocialService struct { + types.ServiceParams +} + +func NewClubSocialService(params types.ServiceParams) ClubSocialServiceInterface { + return &ClubSocialService{params} +} + +func (c *ClubSocialService) GetClubSocials(clubID string) ([]models.Social, error) { + idAsUUID, err := utilities.ValidateID(clubID) + if err != nil { + return nil, err + } + + return GetClubSocials(c.DB, *idAsUUID) +} + +func (c *ClubSocialService) PutClubSocial(clubID string, contactBody PutSocialRequestBody) (*models.Social, error) { + idAsUUID, err := utilities.ValidateID(clubID) + if err != nil { + return nil, err + } + + if err := utilities.Validate(c.Validate, contactBody); err != nil { + return nil, err + } + + contact, err := utilities.MapJsonTags(contactBody, &models.Social{}) + if err != nil { + return nil, err + } + + contact.ClubID = *idAsUUID + + return PutClubSocial(c.DB, *contact) +} diff --git a/backend/entities/clubs/contacts/transactions.go b/backend/entities/clubs/socials/transactions.go similarity index 60% rename from backend/entities/clubs/contacts/transactions.go rename to backend/entities/clubs/socials/transactions.go index 5c8ce2bd1..efd369e38 100644 --- a/backend/entities/clubs/contacts/transactions.go +++ b/backend/entities/clubs/socials/transactions.go @@ -1,8 +1,7 @@ -package contacts +package socials import ( "errors" - "fmt" "github.com/GenerateNU/sac/backend/entities/models" @@ -12,33 +11,29 @@ import ( "gorm.io/gorm/clause" ) -func PutClubContact(db *gorm.DB, contact models.Contact) (*models.Contact, error) { +func PutClubSocial(db *gorm.DB, social models.Social) (*models.Social, error) { err := db.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "club_id"}, {Name: "type"}}, DoUpdates: clause.AssignmentColumns([]string{"content"}), - }).Create(&contact).Error + }).Create(&social).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) || errors.Is(err, gorm.ErrForeignKeyViolated) { return nil, utilities.ErrNotFound } return nil, err } - return &contact, nil + return &social, nil } -func GetClubContacts(db *gorm.DB, clubID uuid.UUID) ([]models.Contact, error) { +func GetClubSocials(db *gorm.DB, clubID uuid.UUID) ([]models.Social, error) { var club models.Club - if err := db.Preload("Contact").First(&club, clubID).Error; err != nil { + if err := db.Preload("Social").First(&club, clubID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, utilities.ErrNotFound } return nil, err } - if club.Contact == nil { - return nil, fmt.Errorf("club with ID %s has no contacts", clubID) - } - - return club.Contact, nil + return club.Social, nil } diff --git a/backend/entities/clubs/transactions.go b/backend/entities/clubs/transactions.go index d501886d4..c00086607 100644 --- a/backend/entities/clubs/transactions.go +++ b/backend/entities/clubs/transactions.go @@ -36,7 +36,7 @@ func GetAdminIDs(db *gorm.DB, clubID uuid.UUID) ([]uuid.UUID, error) { return nil, err } - adminUUIDs := make([]uuid.UUID, 0) + adminUUIDs := make([]uuid.UUID, len(adminIDs)) for _, adminID := range adminIDs { adminUUIDs = append(adminUUIDs, adminID.ClubID) } diff --git a/backend/entities/contacts/base/controller.go b/backend/entities/contacts/base/controller.go deleted file mode 100644 index 46d8b3670..000000000 --- a/backend/entities/contacts/base/controller.go +++ /dev/null @@ -1,91 +0,0 @@ -package base - -import ( - "net/http" - - "github.com/GenerateNU/sac/backend/utilities" - "github.com/garrettladley/fiberpaginate" - "github.com/gofiber/fiber/v2" -) - -type ContactController struct { - contactService ContactServiceInterface -} - -func NewContactController(contactService ContactServiceInterface) *ContactController { - return &ContactController{contactService: contactService} -} - -// GetContact godoc -// -// @Summary Retrieves a contact -// @Description Retrieves a contact by id -// @ID get-contact -// @Tags contact -// @Accept json -// @Produce json -// @Param contactID path string true "Contact ID" -// @Success 201 {object} models.Contact -// @Failure 400 {string} error -// @Failure 404 {string} error -// @Failure 500 {string} error -// @Router /contacts/{contactID}/ [get] -func (co *ContactController) GetContact(c *fiber.Ctx) error { - contact, err := co.contactService.GetContact(c.Params("contactID")) - if err != nil { - return err - } - - return c.Status(http.StatusOK).JSON(contact) -} - -// GetContacts godoc -// -// @Summary Retrieve all contacts -// @Description Retrieves all contacts -// @ID get-contacts -// @Tags contact -// @Produce json -// @Param limit query int false "Limit" -// @Param page query int false "Page" -// @Success 200 {object} []models.Contact -// @Failure 400 {string} error -// @Failure 404 {string} error -// @Failure 500 {string} error -// @Router /contacts/ [get] -func (co *ContactController) GetContacts(c *fiber.Ctx) error { - pagination, ok := fiberpaginate.FromContext(c) - if !ok { - return utilities.ErrExpectedPagination - } - - contacts, err := co.contactService.GetContacts(*pagination) - if err != nil { - return err - } - - return c.Status(http.StatusOK).JSON(contacts) -} - -// DeleteContact godoc -// -// @Summary Deletes a contact -// @Description Deletes a contact -// @ID delete-contact -// @Tags contact -// @Accept json -// @Produce json -// @Param contactID path string true "Contact ID" -// @Success 201 {object} models.Contact -// @Failure 400 {string} error -// @Failure 404 {string} error -// @Failure 500 {string} error -// @Router /contacts/{contactID}/ [delete] -func (co *ContactController) DeleteContact(c *fiber.Ctx) error { - err := co.contactService.DeleteContact(c.Params("contactID")) - if err != nil { - return err - } - - return c.SendStatus(http.StatusNoContent) -} diff --git a/backend/entities/contacts/base/routes.go b/backend/entities/contacts/base/routes.go deleted file mode 100644 index 453705783..000000000 --- a/backend/entities/contacts/base/routes.go +++ /dev/null @@ -1,17 +0,0 @@ -package base - -import ( - "github.com/GenerateNU/sac/backend/auth" - "github.com/GenerateNU/sac/backend/types" -) - -func Contact(contactParams types.RouteParams) { - contactController := NewContactController(NewContactService(contactParams.ServiceParams)) - - // api/v1/contacts/* - contacts := contactParams.Router.Group("/contacts") - - contacts.Get("/", contactParams.UtilityMiddleware.Paginator, contactController.GetContacts) - contacts.Get("/:contactID", contactController.GetContact) - contacts.Delete("/:contactID", contactParams.AuthMiddleware.Authorize(auth.DeleteAll), contactController.DeleteContact) -} diff --git a/backend/entities/contacts/base/service.go b/backend/entities/contacts/base/service.go deleted file mode 100644 index 6b5967b2b..000000000 --- a/backend/entities/contacts/base/service.go +++ /dev/null @@ -1,44 +0,0 @@ -package base - -import ( - "github.com/GenerateNU/sac/backend/entities/models" - "github.com/GenerateNU/sac/backend/types" - "github.com/GenerateNU/sac/backend/utilities" - "github.com/garrettladley/fiberpaginate" -) - -type ContactServiceInterface interface { - GetContacts(pageInfo fiberpaginate.PageInfo) ([]models.Contact, error) - GetContact(contactID string) (*models.Contact, error) - DeleteContact(contactID string) error -} - -type ContactService struct { - types.ServiceParams -} - -func NewContactService(serviceParams types.ServiceParams) ContactServiceInterface { - return &ContactService{serviceParams} -} - -func (c *ContactService) GetContacts(pageInfo fiberpaginate.PageInfo) ([]models.Contact, error) { - return GetContacts(c.DB, pageInfo) -} - -func (c *ContactService) GetContact(contactID string) (*models.Contact, error) { - idAsUUID, err := utilities.ValidateID(contactID) - if err != nil { - return nil, err - } - - return GetContact(c.DB, *idAsUUID) -} - -func (c *ContactService) DeleteContact(contactID string) error { - idAsUUID, err := utilities.ValidateID(contactID) - if err != nil { - return err - } - - return DeleteContact(c.DB, *idAsUUID) -} diff --git a/backend/entities/contacts/base/transactions.go b/backend/entities/contacts/base/transactions.go deleted file mode 100644 index fa21fceab..000000000 --- a/backend/entities/contacts/base/transactions.go +++ /dev/null @@ -1,42 +0,0 @@ -package base - -import ( - "errors" - - "github.com/GenerateNU/sac/backend/entities/models" - "github.com/GenerateNU/sac/backend/utilities" - "github.com/garrettladley/fiberpaginate" - "github.com/google/uuid" - "gorm.io/gorm" -) - -func GetContacts(db *gorm.DB, pageInfo fiberpaginate.PageInfo) ([]models.Contact, error) { - var contacts []models.Contact - if err := db.Scopes(utilities.IntoScope(pageInfo, db)).Find(&contacts).Error; err != nil { - return nil, err - } - - return contacts, nil -} - -func GetContact(db *gorm.DB, id uuid.UUID) (*models.Contact, error) { - var contact models.Contact - if err := db.First(&contact, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, err - } - return nil, err - } - - return &contact, nil -} - -func DeleteContact(db *gorm.DB, id uuid.UUID) error { - if result := db.Delete(&models.Contact{}, id); result.RowsAffected == 0 { - if result.Error == nil { - return utilities.ErrNotFound - } - return result.Error - } - return nil -} diff --git a/backend/entities/events/base/controller.go b/backend/entities/events/base/controller.go index e53934759..f144be5b8 100644 --- a/backend/entities/events/base/controller.go +++ b/backend/entities/events/base/controller.go @@ -32,17 +32,32 @@ func NewEventController(eventService EventServiceInterface) *EventController { // @Failure 500 {object} error // @Router /events/ [get] func (e *EventController) GetAllEvents(c *fiber.Ctx) error { - pagination, ok := fiberpaginate.FromContext(c) + pageInfo, ok := fiberpaginate.FromContext(c) if !ok { - return utilities.ErrExpectedPagination + return utilities.ErrExpectedPageInfo } - events, err := e.eventService.GetEvents(*pagination) - if err != nil { - return err + usePagination := c.QueryBool("pagination", true) + if !usePagination { + pageInfo = nil } - return c.Status(http.StatusOK).JSON(events) + start := c.Query("start") + end := c.Query("end") + + if c.QueryBool("preview", false) { + events, err := e.eventService.GetEventsPreview(*pageInfo, start, end) + if err != nil { + return err + } + return c.Status(http.StatusOK).JSON(events) + } else { + events, err := e.eventService.GetEvents(*pageInfo, start, end) + if err != nil { + return err + } + return c.Status(http.StatusOK).JSON(events) + } } // GetEvent godoc diff --git a/backend/entities/events/base/models.go b/backend/entities/events/base/models.go new file mode 100644 index 000000000..ad8f9a120 --- /dev/null +++ b/backend/entities/events/base/models.go @@ -0,0 +1,21 @@ +package base + +import ( + "time" + + "github.com/GenerateNU/sac/backend/entities/models" + "github.com/google/uuid" +) + +type EventPreview struct { + ID uuid.UUID `json:"id"` + + Title string `json:"title"` + + EventType models.EventType `json:"event_type"` + Location string `json:"location"` + Link string `json:"link"` + + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` +} diff --git a/backend/entities/events/base/service.go b/backend/entities/events/base/service.go index 0f7c9fa1e..9f971867e 100644 --- a/backend/entities/events/base/service.go +++ b/backend/entities/events/base/service.go @@ -1,6 +1,8 @@ package base import ( + "errors" + "github.com/GenerateNU/sac/backend/entities/events" "github.com/GenerateNU/sac/backend/entities/models" @@ -11,7 +13,8 @@ import ( type EventServiceInterface interface { // getters - GetEvents(pageInfo fiberpaginate.PageInfo) ([]models.Event, error) + GetEvents(pageInfo fiberpaginate.PageInfo, start string, end string) ([]models.Event, error) + GetEventsPreview(pageInfo fiberpaginate.PageInfo, start string, end string) ([]EventPreview, error) GetEvent(eventID string) (*models.Event, error) // event cud CreateEvent(body events.CreateEventRequestBody) (*models.Event, error) @@ -27,8 +30,48 @@ func NewEventService(serviceParams types.ServiceParams) EventServiceInterface { return &EventService{serviceParams} } -func (e *EventService) GetEvents(pageInfo fiberpaginate.PageInfo) ([]models.Event, error) { - return GetEvents(e.DB, pageInfo) +func (e *EventService) GetEvents(pageInfo fiberpaginate.PageInfo, start string, end string) ([]models.Event, error) { + if start != "" && end != "" { + return GetEvents(e.DB, pageInfo) + } + + startTime, err := utilities.ParseTime(start, utilities.YYYY_dash_MM_dash_DD) + if err != nil { + return nil, utilities.BadRequest(err) + } + + endTime, err := utilities.ParseTime(end, utilities.YYYY_dash_MM_dash_DD) + if err != nil { + return nil, utilities.BadRequest(err) + } + + if startTime.After(endTime) { + return nil, utilities.BadRequest(errors.New("start time must be before end time")) + } + + return GetEventsByTime(e.DB, pageInfo, startTime, endTime) +} + +func (e *EventService) GetEventsPreview(pageInfo fiberpaginate.PageInfo, start string, end string) ([]EventPreview, error) { + if start != "" && end != "" { + return GetEventsPreview(e.DB, pageInfo) + } + + startTime, err := utilities.ParseTime(start, utilities.YYYY_dash_MM_dash_DD) + if err != nil { + return nil, utilities.BadRequest(err) + } + + endTime, err := utilities.ParseTime(end, utilities.YYYY_dash_MM_dash_DD) + if err != nil { + return nil, utilities.BadRequest(err) + } + + if startTime.After(endTime) { + return nil, utilities.BadRequest(errors.New("start time must be before end time")) + } + + return GetEventsPreviewByTime(e.DB, pageInfo, startTime, endTime) } func (e *EventService) GetEvent(eventID string) (*models.Event, error) { diff --git a/backend/entities/events/base/transactions.go b/backend/entities/events/base/transactions.go index 10b47a15e..961bc069a 100644 --- a/backend/entities/events/base/transactions.go +++ b/backend/entities/events/base/transactions.go @@ -3,6 +3,7 @@ package base import ( "errors" "log/slog" + "time" "github.com/GenerateNU/sac/backend/constants" "github.com/GenerateNU/sac/backend/entities/events" @@ -18,12 +19,19 @@ import ( ) func GetEvents(db *gorm.DB, pageInfo fiberpaginate.PageInfo) ([]models.Event, error) { - var events []models.Event - if err := db.Scopes(utilities.IntoScope(pageInfo, db)).Find(&events).Error; err != nil { - return nil, err - } + return getEvents[models.Event](db, &pageInfo, nil, nil) +} - return events, nil +func GetEventsPreview(db *gorm.DB, pageInfo fiberpaginate.PageInfo) ([]EventPreview, error) { + return getEvents[EventPreview](db, &pageInfo, nil, nil) +} + +func GetEventsByTime(db *gorm.DB, pageInfo fiberpaginate.PageInfo, startTime time.Time, endTime time.Time) ([]models.Event, error) { + return getEvents[models.Event](db, &pageInfo, &startTime, &endTime) +} + +func GetEventsPreviewByTime(db *gorm.DB, pageInfo fiberpaginate.PageInfo, startTime time.Time, endTime time.Time) ([]EventPreview, error) { + return getEvents[EventPreview](db, &pageInfo, &startTime, &endTime) } func CreateEvent(db *gorm.DB, event models.Event) (*models.Event, error) { @@ -101,3 +109,22 @@ func DeleteEvent(db *gorm.DB, id uuid.UUID) error { return nil } + +func getEvents[T any](db *gorm.DB, pageInfo *fiberpaginate.PageInfo, startTime *time.Time, endTime *time.Time) ([]T, error) { + var events []T + query := db.Model(&events) + + if startTime != nil && endTime != nil { + query = query.Where("start_time >= ? AND end_time <= ?", *startTime, *endTime) + } + + if pageInfo != nil { + query = query.Scopes(utilities.IntoScope(*pageInfo, db)) + } + + if err := query.Find(&events).Error; err != nil { + return nil, err + } + + return events, nil +} diff --git a/backend/entities/files/base/controller.go b/backend/entities/files/base/controller.go index de0efaed3..48c8a58b2 100644 --- a/backend/entities/files/base/controller.go +++ b/backend/entities/files/base/controller.go @@ -32,12 +32,12 @@ func NewFileController(fileService FileServiceInterface) *FileController { // @Failure 500 {object} error // @Router /files/ [get] func (f *FileController) GetFiles(c *fiber.Ctx) error { - pagination, ok := fiberpaginate.FromContext(c) + pageInfo, ok := fiberpaginate.FromContext(c) if !ok { - return utilities.ErrExpectedPagination + return utilities.ErrExpectedPageInfo } - files, err := f.fileService.GetFiles(*pagination) + files, err := f.fileService.GetFiles(*pageInfo) if err != nil { return err } diff --git a/backend/entities/leadership/base/controller.go b/backend/entities/leadership/base/controller.go new file mode 100644 index 000000000..ee8efc6b7 --- /dev/null +++ b/backend/entities/leadership/base/controller.go @@ -0,0 +1,67 @@ +package base + +import ( + "net/http" + + "github.com/GenerateNU/sac/backend/utilities" + "github.com/garrettladley/fiberpaginate" + "github.com/gofiber/fiber/v2" +) + +type LeaderController struct { + leaderService LeaderServiceInterface +} + +func NewLeaderController(leaderService LeaderServiceInterface) *LeaderController { + return &LeaderController{leaderService: leaderService} +} + +// GetLeaders godoc +// +// @Summary Retrieve all point of contacts +// @Description Retrieves all point of contacts +// @ID get-point-of-contacts +// @Tags point of contact +// @Produce json +// @Param limit query int false "Limit" +// @Param page query int false "Page" +// @Success 200 {object} []models.Leader +// @Failure 400 {string} error +// @Failure 404 {string} error +// @Failure 500 {string} error +// @Router /leader/ [get] +func (l *LeaderController) GetLeadership(c *fiber.Ctx) error { + pageInfo, ok := fiberpaginate.FromContext(c) + if !ok { + return utilities.ErrExpectedPageInfo + } + + Leaders, err := l.leaderService.GetLeaders(*pageInfo) + if err != nil { + return err + } + + return c.Status(http.StatusOK).JSON(Leaders) +} + +// GetLeader godoc +// +// @Summary Retrieves a point of contact +// @Description Retrieves a point of contact by id +// @ID get-point-of-contact +// @Tags point of contact +// @Produce json +// @Param leaderID path string true "Point of Contact ID" +// @Success 200 {object} models.Leader +// @Failure 400 {string} error +// @Failure 404 {string} error +// @Failure 500 {string} error +// @Router /leader/{leaderID}/ [get] +func (l *LeaderController) GetLeader(c *fiber.Ctx) error { + Leader, err := l.leaderService.GetLeader(c.Params("leaderID")) + if err != nil { + return err + } + + return c.Status(http.StatusOK).JSON(Leader) +} diff --git a/backend/entities/leadership/base/routes.go b/backend/entities/leadership/base/routes.go new file mode 100644 index 000000000..265322da3 --- /dev/null +++ b/backend/entities/leadership/base/routes.go @@ -0,0 +1,16 @@ +package base + +import ( + "github.com/GenerateNU/sac/backend/types" +) + +func Leader(leaderParams types.RouteParams) { + LeaderController := NewLeaderController(NewLeaderService(leaderParams.ServiceParams)) + + // api/v1/leader/* + Leader := leaderParams.Router.Group("/leadership") + + Leader.Get("/", leaderParams.UtilityMiddleware.Paginator, LeaderController.GetLeadership) + Leader.Get("/:leaderID", LeaderController.GetLeader) + // Leader.Get("/:leaderID/file", LeaderController.GetPointOfContacFileInfo)) +} diff --git a/backend/entities/leadership/base/service.go b/backend/entities/leadership/base/service.go new file mode 100644 index 000000000..5a9ac7092 --- /dev/null +++ b/backend/entities/leadership/base/service.go @@ -0,0 +1,34 @@ +package base + +import ( + "github.com/GenerateNU/sac/backend/entities/models" + "github.com/GenerateNU/sac/backend/types" + "github.com/GenerateNU/sac/backend/utilities" + "github.com/garrettladley/fiberpaginate" +) + +type LeaderServiceInterface interface { + GetLeaders(pageInfo fiberpaginate.PageInfo) ([]models.Leader, error) + GetLeader(leaderID string) (*models.Leader, error) +} + +type LeaderService struct { + types.ServiceParams +} + +func NewLeaderService(serviceParams types.ServiceParams) LeaderServiceInterface { + return &LeaderService{serviceParams} +} + +func (ls *LeaderService) GetLeaders(pageInfo fiberpaginate.PageInfo) ([]models.Leader, error) { + return GetLeadership(ls.DB, pageInfo) +} + +func (ls *LeaderService) GetLeader(leaderID string) (*models.Leader, error) { + idAsUUID, err := utilities.ValidateID(leaderID) + if err != nil { + return nil, err + } + + return GetLeader(ls.DB, *idAsUUID) +} diff --git a/backend/entities/pocs/base/transactions.go b/backend/entities/leadership/base/transactions.go similarity index 55% rename from backend/entities/pocs/base/transactions.go rename to backend/entities/leadership/base/transactions.go index a31d5c0c9..86d1ce431 100644 --- a/backend/entities/pocs/base/transactions.go +++ b/backend/entities/leadership/base/transactions.go @@ -10,9 +10,9 @@ import ( "gorm.io/gorm" ) -func GetPointOfContacts(db *gorm.DB, pageInfo fiberpaginate.PageInfo) ([]models.PointOfContact, error) { - var pointOfContacts []models.PointOfContact - result := db.Preload("PhotoFile").Scopes(utilities.IntoScope(pageInfo, db)).Find(&pointOfContacts) +func GetLeadership(db *gorm.DB, pageInfo fiberpaginate.PageInfo) ([]models.Leader, error) { + var leadership []models.Leader + result := db.Preload("PhotoFile").Scopes(utilities.IntoScope(pageInfo, db)).Find(&leadership) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, utilities.ErrNotFound @@ -20,18 +20,18 @@ func GetPointOfContacts(db *gorm.DB, pageInfo fiberpaginate.PageInfo) ([]models. return nil, result.Error } - return pointOfContacts, nil + return leadership, nil } -func GetPointOfContact(db *gorm.DB, id uuid.UUID) (*models.PointOfContact, error) { - var pointOfContact models.PointOfContact +func GetLeader(db *gorm.DB, id uuid.UUID) (*models.Leader, error) { + var leader models.Leader - if err := db.Preload("PhotoFile").First(&pointOfContact, id).Error; err != nil { + if err := db.Preload("PhotoFile").First(&leader, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, utilities.ErrNotFound } return nil, err } - return &pointOfContact, nil + return &leader, nil } diff --git a/backend/entities/models/application.go b/backend/entities/models/application.go new file mode 100644 index 000000000..0cf6bff91 --- /dev/null +++ b/backend/entities/models/application.go @@ -0,0 +1,12 @@ +package models + +import "github.com/google/uuid" + +type Application struct { + Model + + Title string `json:"title"` + Link string `json:"link"` + + RecruitmentID *uuid.UUID `json:"-"` +} diff --git a/backend/entities/models/club.go b/backend/entities/models/club.go index 3eb8445cb..5aa2015c4 100644 --- a/backend/entities/models/club.go +++ b/backend/entities/models/club.go @@ -10,31 +10,30 @@ type Club struct { SoftDeletedAt gorm.DeletedAt `json:"-"` - Name string `json:"name"` - Preview string `json:"preview"` - Description string `json:"description"` - NumMembers int `json:"num_members"` - IsRecruiting bool `json:"is_recruiting"` - RecruitmentCycle RecruitmentCycle `json:"recruitment_cycle"` - RecruitmentType RecruitmentType `json:"recruitment_type"` - WeeklyTimeCommitment int `json:"weekly_time_commitment"` - OneWordToDescribeUs string `json:"one_word_to_describe_us"` - ApplicationLink string `json:"application_link"` - Logo string `json:"logo"` - - Parent *uuid.UUID `gorm:"foreignKey:Parent;" json:"-"` - Tag []Tag `gorm:"many2many:club_tags;" json:"-"` - Member []User `gorm:"many2many:user_club_members;" json:"-"` - Follower []User `gorm:"many2many:user_club_followers;" json:"-"` - IntendedApplicant []User `gorm:"many2many:user_club_intended_applicants;" json:"-"` - Comment []Comment `json:"-"` - PointOfContact []PointOfContact `json:"-"` - Contact []Contact `json:"-"` - - // Event - HostEvent []Event `gorm:"foreignKey:Host;" json:"-"` - Event []Event `gorm:"many2many:club_events;" json:"-"` - Notifcation []Notification `gorm:"polymorphic:Reference;" json:"-"` + Name string `json:"name"` + Preview string `json:"preview"` + Description string `json:"description"` + NumMembers int `json:"num_members"` + + WeeklyTimeCommitment int `json:"weekly_time_commitment"` + OneWordToDescribeUs string `json:"one_word_to_describe_us"` + + Recruitment Recruitment `json:"-"` + + Logo string `json:"logo"` + + Parent *uuid.UUID `gorm:"foreignKey:Parent;" json:"-"` + Tag []Tag `gorm:"many2many:club_tags;" json:"-"` + Member []User `gorm:"many2many:user_club_members;" json:"-"` + Follower []User `gorm:"many2many:user_club_followers;" json:"-"` + IntendedApplicant []User `gorm:"many2many:user_club_intended_applicants;" json:"-"` + Comment []Comment `json:"-"` + Leadership []Leader `json:"-"` + Social []Social `json:"-"` + + HostEvent []Event `gorm:"foreignKey:Host;" json:"-"` + Event []Event `gorm:"many2many:club_events;" json:"-"` + Notification []Notification `gorm:"polymorphic:Reference;" json:"-"` } type ClubSearchDocument struct { @@ -59,3 +58,7 @@ func (c *Club) ToSearchDocument() interface{} { Tags: tagIds, } } + +func (c Club) Preload(db *gorm.DB) *gorm.DB { + return db.Preload("Tag") +} diff --git a/backend/entities/models/contact.go b/backend/entities/models/contact.go deleted file mode 100644 index 82b3de1d3..000000000 --- a/backend/entities/models/contact.go +++ /dev/null @@ -1,50 +0,0 @@ -package models - -import "github.com/google/uuid" - -type ContactType string - -const ( - Facebook ContactType = "facebook" - Instagram ContactType = "instagram" - X ContactType = "x" - LinkedIn ContactType = "linkedin" - YouTube ContactType = "youtube" - GitHub ContactType = "github" - Slack ContactType = "slack" - Discord ContactType = "discord" - Email ContactType = "email" - CustomSite ContactType = "customSite" -) - -func GetContentPrefix(contactType ContactType) string { - switch contactType { - case Facebook: - return "https://facebook.com/" - case Instagram: - return "https://instagram.com/" - case X: - return "https://x.com/" - case LinkedIn: - return "https://linkedin.com/" - case YouTube: - return "https://youtube.com/" - case GitHub: - return "https://github.com/" - case Slack: - return "https://join.slack.com/" - case Discord: - return "https://discord.gg/" - default: - return "" - } -} - -type Contact struct { - Model - - Type ContactType `json:"type"` - Content string `json:"content"` - - ClubID uuid.UUID `json:"-"` -} diff --git a/backend/entities/models/event.go b/backend/entities/models/event.go index 3856203fc..edf5395a6 100644 --- a/backend/entities/models/event.go +++ b/backend/entities/models/event.go @@ -4,6 +4,7 @@ import ( "time" "github.com/google/uuid" + "gorm.io/gorm" ) type EventType string @@ -82,3 +83,7 @@ func (c *Event) ToSearchDocument() interface{} { Clubs: clubIds, } } + +func (c Event) Preload(db *gorm.DB) *gorm.DB { + return db.Preload("Tag").Preload("Club") +} diff --git a/backend/entities/models/poc.go b/backend/entities/models/leader.go similarity index 75% rename from backend/entities/models/poc.go rename to backend/entities/models/leader.go index 9a94b45dd..17508c99d 100644 --- a/backend/entities/models/poc.go +++ b/backend/entities/models/leader.go @@ -4,7 +4,7 @@ import ( "github.com/google/uuid" ) -type PointOfContact struct { +type Leader struct { Model Name string `json:"name"` @@ -15,3 +15,7 @@ type PointOfContact struct { PhotoFile File `gorm:"polymorphic:Owner;" json:"photo_file"` } + +func (l Leader) TableName() string { + return "leadership" +} diff --git a/backend/entities/models/recruitment.go b/backend/entities/models/recruitment.go new file mode 100644 index 000000000..841fc7c36 --- /dev/null +++ b/backend/entities/models/recruitment.go @@ -0,0 +1,18 @@ +package models + +import "github.com/google/uuid" + +type Recruitment struct { + Model + + Cycle RecruitmentCycle `json:"cycle"` + Type RecruitmentType `gorm:"column:_type" json:"type"` + + ClubID uuid.UUID `json:"-"` + + Application []Application `json:"-"` +} + +func (r Recruitment) TableName() string { + return "recruitment" +} diff --git a/backend/entities/models/recruitment_type.go b/backend/entities/models/recruitment_type.go index 6ebcc6542..f4b6097e0 100644 --- a/backend/entities/models/recruitment_type.go +++ b/backend/entities/models/recruitment_type.go @@ -3,7 +3,7 @@ package models type RecruitmentType string const ( - Unrestricted RecruitmentType = "unrestricted" - Tryout RecruitmentType = "tryout" - Application RecruitmentType = "application" + RecruitmentTypeUnrestricted RecruitmentType = "unrestricted" + RecruitmentTypeTryout RecruitmentType = "tryout" + RecruitmentTypeApplication RecruitmentType = "application" ) diff --git a/backend/entities/models/social.go b/backend/entities/models/social.go new file mode 100644 index 000000000..0e140a0b5 --- /dev/null +++ b/backend/entities/models/social.go @@ -0,0 +1,50 @@ +package models + +import "github.com/google/uuid" + +type SocialType string + +const ( + Facebook SocialType = "facebook" + Instagram SocialType = "instagram" + X SocialType = "x" + LinkedIn SocialType = "linkedin" + YouTube SocialType = "youtube" + GitHub SocialType = "github" + Slack SocialType = "slack" + Discord SocialType = "discord" + Email SocialType = "email" + CustomSite SocialType = "customSite" +) + +func GetSocialPrefix(contactType SocialType) string { + switch contactType { + case Facebook: + return "https://facebook.com/" + case Instagram: + return "https://instagram.com/" + case X: + return "https://x.com/" + case LinkedIn: + return "https://linkedin.com/" + case YouTube: + return "https://youtube.com/" + case GitHub: + return "https://github.com/" + case Slack: + return "https://join.slack.com/" + case Discord: + return "https://discord.gg/" + default: + return "" + } +} + +type Social struct { + Model + + Type SocialType `json:"type"` + Content string `json:"content"` + + ClubID uuid.UUID `json:"-"` +} diff --git a/backend/entities/oauth/base/controller.go b/backend/entities/oauth/base/controller.go index 2259f2f48..4c0343e41 100644 --- a/backend/entities/oauth/base/controller.go +++ b/backend/entities/oauth/base/controller.go @@ -26,7 +26,7 @@ func (oc *OAuthController) Authorize(c *fiber.Ctx) error { } // Extract the user making the call: - userID, err := locals.UserID(c) + userID, err := locals.UserIDFrom(c) if err != nil { return err } @@ -54,7 +54,7 @@ func (oc *OAuthController) Token(c *fiber.Ctx) error { } // Extract the user making the call: - userID, err := locals.UserID(c) + userID, err := locals.UserIDFrom(c) if err != nil { return err } @@ -76,7 +76,7 @@ func (oc *OAuthController) Revoke(c *fiber.Ctx) error { } // Extract the user making the call: - userID, err := locals.UserID(c) + userID, err := locals.UserIDFrom(c) if err != nil { return err } diff --git a/backend/entities/pocs/base/controller.go b/backend/entities/pocs/base/controller.go deleted file mode 100644 index e0d610a34..000000000 --- a/backend/entities/pocs/base/controller.go +++ /dev/null @@ -1,67 +0,0 @@ -package base - -import ( - "net/http" - - "github.com/GenerateNU/sac/backend/utilities" - "github.com/garrettladley/fiberpaginate" - "github.com/gofiber/fiber/v2" -) - -type PointOfContactController struct { - pointOfContactService PointOfContactServiceInterface -} - -func NewPointOfContactController(pointOfContactService PointOfContactServiceInterface) *PointOfContactController { - return &PointOfContactController{pointOfContactService: pointOfContactService} -} - -// GetPointOfContacts godoc -// -// @Summary Retrieve all point of contacts -// @Description Retrieves all point of contacts -// @ID get-point-of-contacts -// @Tags point of contact -// @Produce json -// @Param limit query int false "Limit" -// @Param page query int false "Page" -// @Success 200 {object} []models.PointOfContact -// @Failure 400 {string} error -// @Failure 404 {string} error -// @Failure 500 {string} error -// @Router /pocs/ [get] -func (poc *PointOfContactController) GetPointOfContacts(c *fiber.Ctx) error { - pagination, ok := fiberpaginate.FromContext(c) - if !ok { - return utilities.ErrExpectedPagination - } - - pointOfContacts, err := poc.pointOfContactService.GetPointOfContacts(*pagination) - if err != nil { - return err - } - - return c.Status(http.StatusOK).JSON(pointOfContacts) -} - -// GetPointOfContact godoc -// -// @Summary Retrieves a point of contact -// @Description Retrieves a point of contact by id -// @ID get-point-of-contact -// @Tags point of contact -// @Produce json -// @Param pocID path string true "Point of Contact ID" -// @Success 200 {object} models.PointOfContact -// @Failure 400 {string} error -// @Failure 404 {string} error -// @Failure 500 {string} error -// @Router /pocs/{pocID}/ [get] -func (poc *PointOfContactController) GetPointOfContact(c *fiber.Ctx) error { - pointOfContact, err := poc.pointOfContactService.GetPointOfContact(c.Params("pocID")) - if err != nil { - return err - } - - return c.Status(http.StatusOK).JSON(pointOfContact) -} diff --git a/backend/entities/pocs/base/routes.go b/backend/entities/pocs/base/routes.go deleted file mode 100644 index 907459002..000000000 --- a/backend/entities/pocs/base/routes.go +++ /dev/null @@ -1,16 +0,0 @@ -package base - -import ( - "github.com/GenerateNU/sac/backend/types" -) - -func PointOfContact(pointOfContactParams types.RouteParams) { - pointOfContactController := NewPointOfContactController(NewPointOfContactService(pointOfContactParams.ServiceParams)) - - // api/v1/pocs/* - pointofContact := pointOfContactParams.Router.Group("/pocs") - - pointofContact.Get("/", pointOfContactParams.UtilityMiddleware.Paginator, pointOfContactController.GetPointOfContacts) - pointofContact.Get("/:pocID", pointOfContactController.GetPointOfContact) - // pointOfContact.Get("/:pocID/file", pointOfContactController.GetPointOfContacFileInfo)) -} diff --git a/backend/entities/pocs/base/service.go b/backend/entities/pocs/base/service.go deleted file mode 100644 index 3c2b0efc9..000000000 --- a/backend/entities/pocs/base/service.go +++ /dev/null @@ -1,34 +0,0 @@ -package base - -import ( - "github.com/GenerateNU/sac/backend/entities/models" - "github.com/GenerateNU/sac/backend/types" - "github.com/GenerateNU/sac/backend/utilities" - "github.com/garrettladley/fiberpaginate" -) - -type PointOfContactServiceInterface interface { - GetPointOfContacts(pageInfo fiberpaginate.PageInfo) ([]models.PointOfContact, error) - GetPointOfContact(pocID string) (*models.PointOfContact, error) -} - -type PointOfContactService struct { - types.ServiceParams -} - -func NewPointOfContactService(serviceParams types.ServiceParams) PointOfContactServiceInterface { - return &PointOfContactService{serviceParams} -} - -func (poc *PointOfContactService) GetPointOfContacts(pageInfo fiberpaginate.PageInfo) ([]models.PointOfContact, error) { - return GetPointOfContacts(poc.DB, pageInfo) -} - -func (poc *PointOfContactService) GetPointOfContact(pocID string) (*models.PointOfContact, error) { - idAsUUID, err := utilities.ValidateID(pocID) - if err != nil { - return nil, err - } - - return GetPointOfContact(poc.DB, *idAsUUID) -} diff --git a/backend/entities/socials/base/controller.go b/backend/entities/socials/base/controller.go new file mode 100644 index 000000000..7fa004dc2 --- /dev/null +++ b/backend/entities/socials/base/controller.go @@ -0,0 +1,91 @@ +package base + +import ( + "net/http" + + "github.com/GenerateNU/sac/backend/utilities" + "github.com/garrettladley/fiberpaginate" + "github.com/gofiber/fiber/v2" +) + +type SocialController struct { + socialService SocialServiceInterface +} + +func NewSocialController(socialService SocialServiceInterface) *SocialController { + return &SocialController{socialService: socialService} +} + +// GetSocial godoc +// +// @Summary Retrieves a social +// @Description Retrieves a social by id +// @ID get-social +// @Tags social +// @Accept json +// @Produce json +// @Param socialID path string true "Social ID" +// @Success 201 {object} models.Social +// @Failure 400 {string} error +// @Failure 404 {string} error +// @Failure 500 {string} error +// @Router /socials/{socialID}/ [get] +func (co *SocialController) GetSocial(c *fiber.Ctx) error { + social, err := co.socialService.GetSocial(c.Params("socialID")) + if err != nil { + return err + } + + return c.Status(http.StatusOK).JSON(social) +} + +// GetSocials godoc +// +// @Summary Retrieve all socials +// @Description Retrieves all socials +// @ID get-socials +// @Tags social +// @Produce json +// @Param limit query int false "Limit" +// @Param page query int false "Page" +// @Success 200 {object} []models.Social +// @Failure 400 {string} error +// @Failure 404 {string} error +// @Failure 500 {string} error +// @Router /socials/ [get] +func (co *SocialController) GetSocials(c *fiber.Ctx) error { + pageInfo, ok := fiberpaginate.FromContext(c) + if !ok { + return utilities.ErrExpectedPageInfo + } + + socials, err := co.socialService.GetSocials(*pageInfo) + if err != nil { + return err + } + + return c.Status(http.StatusOK).JSON(socials) +} + +// DeleteSocial godoc +// +// @Summary Deletes a social +// @Description Deletes a social +// @ID delete-social +// @Tags social +// @Accept json +// @Produce json +// @Param socialID path string true "Social ID" +// @Success 201 {object} models.Social +// @Failure 400 {string} error +// @Failure 404 {string} error +// @Failure 500 {string} error +// @Router /socials/{socialID}/ [delete] +func (co *SocialController) DeleteSocial(c *fiber.Ctx) error { + err := co.socialService.DeleteSocial(c.Params("socialID")) + if err != nil { + return err + } + + return c.SendStatus(http.StatusNoContent) +} diff --git a/backend/entities/socials/base/routes.go b/backend/entities/socials/base/routes.go new file mode 100644 index 000000000..2b0187240 --- /dev/null +++ b/backend/entities/socials/base/routes.go @@ -0,0 +1,17 @@ +package base + +import ( + "github.com/GenerateNU/sac/backend/auth" + "github.com/GenerateNU/sac/backend/types" +) + +func Social(socialParams types.RouteParams) { + socialController := NewSocialController(NewSocialService(socialParams.ServiceParams)) + + // api/v1/socials/* + socials := socialParams.Router.Group("/socials") + + socials.Get("/", socialParams.UtilityMiddleware.Paginator, socialController.GetSocials) + socials.Get("/:socialID", socialController.GetSocial) + socials.Delete("/:socialID", socialParams.AuthMiddleware.Authorize(auth.DeleteAll), socialController.DeleteSocial) +} diff --git a/backend/entities/socials/base/service.go b/backend/entities/socials/base/service.go new file mode 100644 index 000000000..e7f7924f1 --- /dev/null +++ b/backend/entities/socials/base/service.go @@ -0,0 +1,44 @@ +package base + +import ( + "github.com/GenerateNU/sac/backend/entities/models" + "github.com/GenerateNU/sac/backend/types" + "github.com/GenerateNU/sac/backend/utilities" + "github.com/garrettladley/fiberpaginate" +) + +type SocialServiceInterface interface { + GetSocials(pageInfo fiberpaginate.PageInfo) ([]models.Social, error) + GetSocial(socialID string) (*models.Social, error) + DeleteSocial(socialID string) error +} + +type SocialService struct { + types.ServiceParams +} + +func NewSocialService(serviceParams types.ServiceParams) SocialServiceInterface { + return &SocialService{serviceParams} +} + +func (c *SocialService) GetSocials(pageInfo fiberpaginate.PageInfo) ([]models.Social, error) { + return GetSocials(c.DB, pageInfo) +} + +func (c *SocialService) GetSocial(socialID string) (*models.Social, error) { + idAsUUID, err := utilities.ValidateID(socialID) + if err != nil { + return nil, err + } + + return GetSocial(c.DB, *idAsUUID) +} + +func (c *SocialService) DeleteSocial(socialID string) error { + idAsUUID, err := utilities.ValidateID(socialID) + if err != nil { + return err + } + + return DeleteSocial(c.DB, *idAsUUID) +} diff --git a/backend/entities/socials/base/transactions.go b/backend/entities/socials/base/transactions.go new file mode 100644 index 000000000..f60927713 --- /dev/null +++ b/backend/entities/socials/base/transactions.go @@ -0,0 +1,42 @@ +package base + +import ( + "errors" + + "github.com/GenerateNU/sac/backend/entities/models" + "github.com/GenerateNU/sac/backend/utilities" + "github.com/garrettladley/fiberpaginate" + "github.com/google/uuid" + "gorm.io/gorm" +) + +func GetSocials(db *gorm.DB, pageInfo fiberpaginate.PageInfo) ([]models.Social, error) { + var socials []models.Social + if err := db.Scopes(utilities.IntoScope(pageInfo, db)).Find(&socials).Error; err != nil { + return nil, err + } + + return socials, nil +} + +func GetSocial(db *gorm.DB, id uuid.UUID) (*models.Social, error) { + var social models.Social + if err := db.First(&social, id).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + return nil, err + } + + return &social, nil +} + +func DeleteSocial(db *gorm.DB, id uuid.UUID) error { + if result := db.Delete(&models.Social{}, id); result.RowsAffected == 0 { + if result.Error == nil { + return utilities.ErrNotFound + } + return result.Error + } + return nil +} diff --git a/backend/entities/tags/base/service.go b/backend/entities/tags/base/service.go index 6c11a40d6..f236f50da 100644 --- a/backend/entities/tags/base/service.go +++ b/backend/entities/tags/base/service.go @@ -1,10 +1,9 @@ package base import ( - "errors" - "github.com/GenerateNU/sac/backend/entities/models" "github.com/GenerateNU/sac/backend/entities/tags" + "github.com/GenerateNU/sac/backend/errs" "github.com/GenerateNU/sac/backend/types" "github.com/GenerateNU/sac/backend/utilities" @@ -59,7 +58,7 @@ func (t *TagService) UpdateTag(tagID string, tagBody UpdateTagRequestBody) (*mod } if utilities.AtLeastOne(tagBody, UpdateTagRequestBody{}) { - return nil, errors.New("at least one field must be present") + return nil, errs.ErrAtLeastOne } if err := utilities.Validate(t.Validate, tagBody); err != nil { diff --git a/backend/entities/users/base/controller.go b/backend/entities/users/base/controller.go index 91c596acc..202387350 100644 --- a/backend/entities/users/base/controller.go +++ b/backend/entities/users/base/controller.go @@ -34,12 +34,12 @@ func NewUserController(userService UserServiceInterface) *UserController { // @Failure 500 {object} error // @Router /users/ [get] func (u *UserController) GetUsers(c *fiber.Ctx) error { - pagination, ok := fiberpaginate.FromContext(c) + pageInfo, ok := fiberpaginate.FromContext(c) if !ok { - return utilities.ErrExpectedPagination + return utilities.ErrExpectedPageInfo } - users, err := u.userService.GetUsers(*pagination) + users, err := u.userService.GetUsers(*pageInfo) if err != nil { return err } @@ -63,7 +63,7 @@ func (u *UserController) GetUsers(c *fiber.Ctx) error { // @Failure 500 {object} error // @Router /auth/me [get] func (u *UserController) GetMe(c *fiber.Ctx) error { - userID, err := locals.UserID(c) + userID, err := locals.UserIDFrom(c) if err != nil { return err } diff --git a/backend/entities/users/base/service.go b/backend/entities/users/base/service.go index 15dfa8bd5..272fe3612 100644 --- a/backend/entities/users/base/service.go +++ b/backend/entities/users/base/service.go @@ -1,11 +1,10 @@ package base import ( - "errors" - "github.com/GenerateNU/sac/backend/auth" authEntities "github.com/GenerateNU/sac/backend/entities/auth" "github.com/GenerateNU/sac/backend/entities/models" + "github.com/GenerateNU/sac/backend/errs" "github.com/garrettladley/fiberpaginate" "github.com/google/uuid" @@ -55,7 +54,7 @@ func (u *UserService) UpdateUser(id string, userBody UpdateUserRequestBody) (*mo } if utilities.AtLeastOne(userBody, UpdateUserRequestBody{}) { - return nil, errors.New("no fields to update") + return nil, errs.ErrAtLeastOne } if err := utilities.Validate(u.Validate, userBody); err != nil { diff --git a/backend/entities/users/followers/controller.go b/backend/entities/users/followers/controller.go index b41741937..7c6e50704 100644 --- a/backend/entities/users/followers/controller.go +++ b/backend/entities/users/followers/controller.go @@ -3,7 +3,6 @@ package followers import ( "net/http" - "github.com/GenerateNU/sac/backend/utilities" "github.com/gofiber/fiber/v2" ) @@ -15,70 +14,11 @@ func NewUserFollowerController(userFollowerService UserFollowerServiceInterface) return &UserFollowerController{userFollowerService: userFollowerService} } -// CreateFollowing godoc -// -// @Summary Follow a club -// @Description Follow a club -// @ID create-following -// @Tags user-follower -// @Accept json -// @Produce json -// @Param userID path string true "User ID" -// @Param clubID path string true "Club ID" -// @Success 201 {object} utilities.SuccessResponse -// @Failure 401 {object} error -// @Failure 404 {object} error -// @Failure 500 {object} error -// @Router /users/{userID}/follower/{clubID}/ [post] -func (uf *UserFollowerController) CreateFollowing(c *fiber.Ctx) error { - err := uf.userFollowerService.CreateFollowing(c.Params("userID"), c.Params("clubID")) +func (ufc *UserFollowerController) GetFollowing(c *fiber.Ctx) error { + followers, err := ufc.userFollowerService.GetFollowing(c.Params("userID")) if err != nil { return err } - return utilities.FiberMessage(c, http.StatusCreated, "Successfully followed club") -} -// DeleteFollowing godoc -// -// @Summary Unfollow a club -// @Description Unfollow a club -// @ID delete-following -// @Tags user-follower -// @Accept json -// @Produce json -// @Param userID path string true "User ID" -// @Param clubID path string true "Club ID" -// @Success 204 {object} utilities.SuccessResponse -// @Failure 401 {object} error -// @Failure 404 {object} error -// @Failure 500 {object} error -// @Router /users/{userID}/follower/{clubID}/ [delete] -func (uf *UserFollowerController) DeleteFollowing(c *fiber.Ctx) error { - err := uf.userFollowerService.DeleteFollowing(c.Params("userID"), c.Params("clubID")) - if err != nil { - return err - } - return c.SendStatus(http.StatusNoContent) -} - -// GetAllFollowing godoc -// -// @Summary Retrieve all clubs a user is following -// @Description Retrieves all clubs a user is following -// @ID get-following -// @Tags user-follower -// @Produce json -// @Param userID path string true "User ID" -// @Success 200 {object} []models.Club -// @Failure 400 {object} error -// @Failure 401 {object} error -// @Failure 404 {object} error -// @Failure 500 {object} error -// @Router /users/{userID}/follower/ [get] -func (uf *UserFollowerController) GetFollowing(c *fiber.Ctx) error { - clubs, err := uf.userFollowerService.GetFollowing(c.Params("userID")) - if err != nil { - return err - } - return c.Status(http.StatusOK).JSON(clubs) + return c.Status(http.StatusOK).JSON(followers) } diff --git a/backend/entities/users/followers/routes.go b/backend/entities/users/followers/routes.go index 9928f0932..2112ee2fc 100644 --- a/backend/entities/users/followers/routes.go +++ b/backend/entities/users/followers/routes.go @@ -11,6 +11,4 @@ func UserFollower(userParams types.RouteParams) { userFollower := userParams.Router.Group("/follower") userFollower.Get("/", userFollowerController.GetFollowing) - userFollower.Post("/:clubID", userParams.AuthMiddleware.UserAuthorizeById, userFollowerController.CreateFollowing) - userFollower.Delete("/:clubID", userParams.AuthMiddleware.UserAuthorizeById, userFollowerController.DeleteFollowing) } diff --git a/backend/entities/users/followers/service.go b/backend/entities/users/followers/service.go index 2d2af7519..4e0da6ffc 100644 --- a/backend/entities/users/followers/service.go +++ b/backend/entities/users/followers/service.go @@ -7,9 +7,7 @@ import ( ) type UserFollowerServiceInterface interface { - CreateFollowing(userId string, clubId string) error - DeleteFollowing(userId string, clubId string) error - GetFollowing(userId string) ([]models.Club, error) + GetFollowing(userID string) ([]models.Club, error) } type UserFollowerService struct { @@ -20,35 +18,11 @@ func NewUserFollowerService(serviceParams types.ServiceParams) UserFollowerServi return &UserFollowerService{serviceParams} } -func (u *UserFollowerService) CreateFollowing(userId string, clubId string) error { - userIdAsUUID, err := utilities.ValidateID(userId) - if err != nil { - return err - } - clubIdAsUUID, err := utilities.ValidateID(clubId) - if err != nil { - return err - } - return CreateFollowing(u.DB, *userIdAsUUID, *clubIdAsUUID) -} - -func (u *UserFollowerService) DeleteFollowing(userId string, clubId string) error { - userIdAsUUID, err := utilities.ValidateID(userId) - if err != nil { - return err - } - clubIdAsUUID, err := utilities.ValidateID(clubId) - if err != nil { - return err - } - return DeleteFollowing(u.DB, *userIdAsUUID, *clubIdAsUUID) -} - -func (u *UserFollowerService) GetFollowing(userId string) ([]models.Club, error) { - userIdAsUUID, err := utilities.ValidateID(userId) +func (u *UserFollowerService) GetFollowing(userID string) ([]models.Club, error) { + userIDAsUUID, err := utilities.ValidateID(userID) if err != nil { return nil, err } - return GetClubFollowing(u.DB, *userIdAsUUID) + return GetClubFollowing(u.DB, *userIDAsUUID) } diff --git a/backend/entities/users/followers/transactions.go b/backend/entities/users/followers/transactions.go index 169f2e9d7..fa4ef7aa8 100644 --- a/backend/entities/users/followers/transactions.go +++ b/backend/entities/users/followers/transactions.go @@ -1,7 +1,6 @@ package followers import ( - "github.com/GenerateNU/sac/backend/entities/clubs" "github.com/GenerateNU/sac/backend/entities/models" "github.com/GenerateNU/sac/backend/entities/users" @@ -9,42 +8,6 @@ import ( "gorm.io/gorm" ) -func CreateFollowing(db *gorm.DB, userID uuid.UUID, clubID uuid.UUID) error { - user, err := users.GetUser(db, userID) - if err != nil { - return err - } - - club, err := clubs.GetClub(db, clubID) - if err != nil { - return err - } - - if err := db.Model(&user).Association("Follower").Append(club); err != nil { - return err - } - - return nil -} - -func DeleteFollowing(db *gorm.DB, userID uuid.UUID, clubID uuid.UUID) error { - user, err := users.GetUser(db, userID) - if err != nil { - return err - } - - club, err := clubs.GetClub(db, clubID) - if err != nil { - return err - } - - if err := db.Model(&user).Association("Follower").Delete(club); err != nil { - return err - } - - return nil -} - func GetClubFollowing(db *gorm.DB, userID uuid.UUID) ([]models.Club, error) { var clubs []models.Club diff --git a/backend/entities/users/members/controller.go b/backend/entities/users/members/controller.go index 5fb0ae78c..89fc60dbe 100644 --- a/backend/entities/users/members/controller.go +++ b/backend/entities/users/members/controller.go @@ -14,20 +14,6 @@ func NewUserMemberController(clubMemberService UserMemberServiceInterface) *User return &UserMemberController{clubMemberService: clubMemberService} } -// GetMembership godoc -// -// @Summary Retrieve all clubs a user is a member of -// @Description Retrieves all clubs a user is a member of -// @ID get-membership -// @Tags user-member -// @Produce json -// @Param userID path string true "User ID" -// @Success 200 {object} []models.Club -// @Failure 400 {object} error -// @Failure 401 {object} error -// @Failure 404 {object} error -// @Failure 500 {object} error -// @Router /users/{userID}/member/ [get] func (um *UserMemberController) GetMembership(c *fiber.Ctx) error { followers, err := um.clubMemberService.GetMembership(c.Params("userID")) if err != nil { diff --git a/backend/errs/db.go b/backend/errs/db.go new file mode 100644 index 000000000..8bfe733ec --- /dev/null +++ b/backend/errs/db.go @@ -0,0 +1,5 @@ +package errs + +import "errors" + +var ErrDatabaseTimeout = errors.New("database timeout") diff --git a/backend/errs/validate.go b/backend/errs/validate.go new file mode 100644 index 000000000..661a1f1dc --- /dev/null +++ b/backend/errs/validate.go @@ -0,0 +1,5 @@ +package errs + +import "errors" + +var ErrAtLeastOne = errors.New("at least one field must be provided") diff --git a/backend/locals/claims.go b/backend/locals/claims.go index 67155ef56..86f55273f 100644 --- a/backend/locals/claims.go +++ b/backend/locals/claims.go @@ -8,7 +8,7 @@ import ( "github.com/gofiber/fiber/v2" ) -func CustomClaims(c *fiber.Ctx) (*auth.CustomClaims, error) { +func CustomClaimsFrom(c *fiber.Ctx) (*auth.CustomClaims, error) { rawClaims := c.Locals(claimsKey) if rawClaims == nil { return nil, utilities.Forbidden() diff --git a/backend/locals/user_id.go b/backend/locals/user_id.go index f2962438b..8500a77be 100644 --- a/backend/locals/user_id.go +++ b/backend/locals/user_id.go @@ -8,7 +8,7 @@ import ( "github.com/google/uuid" ) -func UserID(c *fiber.Ctx) (*uuid.UUID, error) { +func UserIDFrom(c *fiber.Ctx) (*uuid.UUID, error) { userID := c.Locals(userIDKey) if userID == nil { return nil, utilities.Forbidden() diff --git a/backend/main.go b/backend/main.go index 8adbe970e..b8b8f2cbe 100644 --- a/backend/main.go +++ b/backend/main.go @@ -35,8 +35,7 @@ func main() { constants.SEARCH_URI = config.Search.URI - err = checkServerRunning(config.Application.Host, config.Application.Port) - if err == nil { + if checkServerRunning(config.Application.Host, config.Application.Port) == nil { utilities.Exit("A server is already running on %s:%d.\n", config.Application.Host, config.Application.Port) } diff --git a/backend/middleware/auth/auth.go b/backend/middleware/auth/auth.go index 99be76f45..df01b1e1c 100644 --- a/backend/middleware/auth/auth.go +++ b/backend/middleware/auth/auth.go @@ -21,9 +21,8 @@ import ( ) func (m *AuthMiddlewareService) IsSuper(c *fiber.Ctx) bool { - claims, err := locals.CustomClaims(c) + claims, err := locals.CustomClaimsFrom(c) if err != nil { - _ = err return false } if claims == nil { @@ -99,7 +98,7 @@ func (m *AuthMiddlewareService) Authorize(requiredPermissions ...auth.Permission return utilities.Unauthorized() } - claims, err := locals.CustomClaims(c) + claims, err := locals.CustomClaimsFrom(c) if err != nil { return err } diff --git a/backend/middleware/auth/club.go b/backend/middleware/auth/club.go index 8531c78a6..56ae9473d 100644 --- a/backend/middleware/auth/club.go +++ b/backend/middleware/auth/club.go @@ -25,7 +25,7 @@ func (m *AuthMiddlewareService) ClubAuthorizeById(c *fiber.Ctx, extractor Extrac return err } - userID, err := locals.UserID(c) + userID, err := locals.UserIDFrom(c) if err != nil { return err } diff --git a/backend/middleware/auth/event.go b/backend/middleware/auth/event.go index 5d0c13c39..c0fc03e9f 100644 --- a/backend/middleware/auth/event.go +++ b/backend/middleware/auth/event.go @@ -25,7 +25,7 @@ func (m *AuthMiddlewareService) EventAuthorizeById(c *fiber.Ctx, extractor Extra return err } - userID, err := locals.UserID(c) + userID, err := locals.UserIDFrom(c) if err != nil { return err } diff --git a/backend/middleware/auth/user.go b/backend/middleware/auth/user.go index 647570643..9c06cc2d8 100644 --- a/backend/middleware/auth/user.go +++ b/backend/middleware/auth/user.go @@ -21,7 +21,7 @@ func (m *AuthMiddlewareService) UserAuthorizeById(c *fiber.Ctx) error { return err } - userID, err := locals.UserID(c) + userID, err := locals.UserIDFrom(c) if err != nil { return err } diff --git a/backend/migrations/000001_init.down.sql b/backend/migrations/000001_init.down.sql index cf79cfbe6..7b7223876 100644 --- a/backend/migrations/000001_init.down.sql +++ b/backend/migrations/000001_init.down.sql @@ -1,14 +1,131 @@ BEGIN; -DROP TABLE IF EXISTS clubs CASCADE; - -DROP TABLE IF EXISTS events CASCADE; - -DROP TABLE IF EXISTS users CASCADE; - -DROP TABLE IF EXISTS categories CASCADE; - -DROP TABLE IF EXISTS tags CASCADE; +ALTER TABLE + "users" +ALTER COLUMN + id DROP DEFAULT; + +ALTER TABLE + recruitment +ALTER COLUMN + id DROP DEFAULT; + +ALTER TABLE + applications +ALTER COLUMN + id DROP DEFAULT; + +ALTER TABLE + clubs +ALTER COLUMN + id DROP DEFAULT; + +ALTER TABLE + series +ALTER COLUMN + id DROP DEFAULT; + +ALTER TABLE + EVENTS +ALTER COLUMN + id DROP DEFAULT; + +ALTER TABLE + categories +ALTER COLUMN + id DROP DEFAULT; + +ALTER TABLE + tags +ALTER COLUMN + id DROP DEFAULT; + +ALTER TABLE + club_events +ALTER COLUMN + club_id DROP DEFAULT, +ALTER COLUMN + event_id DROP DEFAULT; + +ALTER TABLE + club_tags +ALTER COLUMN + tag_id DROP DEFAULT, +ALTER COLUMN + club_id DROP DEFAULT; + +ALTER TABLE + contacts +ALTER COLUMN + id DROP DEFAULT; + +ALTER TABLE + event_tags +ALTER COLUMN + tag_id DROP DEFAULT, +ALTER COLUMN + event_id DROP DEFAULT; + +ALTER TABLE + files +ALTER COLUMN + id DROP DEFAULT; + +-- ALTER TABLE notifications ALTER COLUMN id DROP DEFAULT; -- Uncomment if notifications table is created +ALTER TABLE + leadership +ALTER COLUMN + id DROP DEFAULT; + +ALTER TABLE + user_club_followers +ALTER COLUMN + user_id DROP DEFAULT, +ALTER COLUMN + club_id DROP DEFAULT; + +ALTER TABLE + user_club_intended_applicants +ALTER COLUMN + user_id DROP DEFAULT, +ALTER COLUMN + club_id DROP DEFAULT; + +ALTER TABLE + user_club_members +ALTER COLUMN + user_id DROP DEFAULT; + +ALTER TABLE + user_event_rsvps +ALTER COLUMN + user_id DROP DEFAULT, +ALTER COLUMN + event_id DROP DEFAULT; + +ALTER TABLE + user_event_waitlists +ALTER COLUMN + user_id DROP DEFAULT, +ALTER COLUMN + event_id DROP DEFAULT; + +ALTER TABLE + user_tags +ALTER COLUMN + user_id DROP DEFAULT, +ALTER COLUMN + tag_id DROP DEFAULT; + +ALTER TABLE + verifications +ALTER COLUMN + user_id DROP DEFAULT; + +ALTER TABLE + user_oauth_tokens +ALTER COLUMN + user_id DROP DEFAULT; DROP TABLE IF EXISTS club_events CASCADE; @@ -16,15 +133,12 @@ DROP TABLE IF EXISTS club_tags CASCADE; DROP TABLE IF EXISTS contacts CASCADE; -DROP TABLE IF EXISTS series CASCADE; - DROP TABLE IF EXISTS event_tags CASCADE; DROP TABLE IF EXISTS files CASCADE; -DROP TABLE IF EXISTS notifications CASCADE; - -DROP TABLE IF EXISTS point_of_contacts CASCADE; +-- DROP TABLE IF EXISTS notifications CASCADE; -- Uncomment if notifications table is created +DROP TABLE IF EXISTS leadership CASCADE; DROP TABLE IF EXISTS user_club_followers CASCADE; @@ -40,10 +154,32 @@ DROP TABLE IF EXISTS user_tags CASCADE; DROP TABLE IF EXISTS verifications CASCADE; -DROP TYPE IF EXISTS OAuthResourceType CASCADE; - DROP TABLE IF EXISTS user_oauth_tokens CASCADE; +DROP TABLE IF EXISTS EVENTS CASCADE; + +DROP TABLE IF EXISTS series CASCADE; + +DROP TABLE IF EXISTS categories CASCADE; + +DROP TABLE IF EXISTS tags CASCADE; + +DROP TABLE IF EXISTS clubs CASCADE; + +DROP TABLE IF EXISTS applications CASCADE; + +DROP TABLE IF EXISTS recruitment CASCADE; + +DROP TABLE IF EXISTS "users" CASCADE; + +DROP TYPE IF EXISTS recruitment_cycle CASCADE; + +DROP TYPE IF EXISTS recruitment_type CASCADE; + +DROP TYPE IF EXISTS event_type CASCADE; + +DROP TYPE IF EXISTS OAuthResourceType CASCADE; + DROP EXTENSION IF EXISTS "uuid-ossp"; -COMMIT; +COMMIT; \ No newline at end of file diff --git a/backend/migrations/000001_init.up.sql b/backend/migrations/000001_init.up.sql index 8163d0fc5..f62171440 100644 --- a/backend/migrations/000001_init.up.sql +++ b/backend/migrations/000001_init.up.sql @@ -2,11 +2,11 @@ BEGIN; CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -CREATE TABLE IF NOT EXISTS users( +CREATE TABLE IF NOT EXISTS "users"( id uuid NOT NULL DEFAULT uuid_generate_v4(), - created_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - role varchar(255) NOT NULL DEFAULT 'student'::character varying, + created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + role varchar(255) NOT NULL DEFAULT 'student' :: character varying, first_name varchar(255) NOT NULL, last_name varchar(255) NOT NULL, email varchar(255) NOT NULL, @@ -21,82 +21,113 @@ CREATE TABLE IF NOT EXISTS users( PRIMARY KEY(id) ); -CREATE UNIQUE INDEX IF NOT EXISTS uni_users_email ON users USING btree ("email"); +CREATE UNIQUE INDEX IF NOT EXISTS uni_users_email ON "users" USING btree ("email"); CREATE TABLE IF NOT EXISTS clubs( id uuid NOT NULL DEFAULT uuid_generate_v4(), - created_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - soft_deleted_at timestamp with time zone, + created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + soft_deleted_at timestamp WITH time zone, name varchar(255) NOT NULL, preview varchar(255) NOT NULL, description text NOT NULL, num_members bigint NOT NULL, - is_recruiting boolean NOT NULL DEFAULT false, - recruitment_cycle varchar(255) NOT NULL DEFAULT 'always'::character varying, - recruitment_type varchar(255) NOT NULL DEFAULT 'unrestricted'::character varying, weekly_time_commitment bigint, - one_word_to_describe_us varchar(255) DEFAULT NULL::character varying, - application_link varchar(255) DEFAULT NULL::character varying, - logo varchar(255) DEFAULT NULL::character varying, - parent text, + one_word_to_describe_us varchar(255) DEFAULT NULL :: character varying, + recruitment_id uuid, + logo varchar(255) DEFAULT NULL :: character varying, + parent uuid, PRIMARY KEY(id) ); CREATE UNIQUE INDEX IF NOT EXISTS uni_clubs_name ON clubs USING btree ("name"); + CREATE INDEX IF NOT EXISTS idx_clubs_num_members ON clubs USING btree ("num_members"); + CREATE INDEX IF NOT EXISTS idx_clubs_one_word_to_describe_us ON clubs USING btree ("one_word_to_describe_us"); +CREATE TYPE recruitment_cycle AS ENUM ('fall', 'spring', 'fallSpring', 'always'); + +CREATE TYPE recruitment_type AS ENUM ('unrestricted', 'tryout', 'application'); + +CREATE TABLE IF NOT EXISTS recruitment ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + cycle recruitment_cycle, + _type recruitment_type, + club_id uuid NOT NULL, + PRIMARY KEY(id), + CONSTRAINT fk_clubs_recruitment FOREIGN KEY(club_id) REFERENCES clubs(id) ON UPDATE CASCADE ON DELETE CASCADE +); + +ALTER TABLE + clubs +ADD + CONSTRAINT fk_clubs_recruitment FOREIGN KEY(recruitment_id) REFERENCES recruitment(id) ON UPDATE CASCADE ON DELETE CASCADE; + +CREATE TABLE IF NOT EXISTS applications ( + id uuid NOT NULL DEFAULT uuid_generate_v4(), + created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + recruitment_id uuid NOT NULL, + title varchar(255) NOT NULL, + link varchar(255) NOT NULL, + PRIMARY KEY(id), + CONSTRAINT fk_recruitment_application FOREIGN KEY(recruitment_id) REFERENCES recruitment(id) ON UPDATE CASCADE ON DELETE CASCADE +); + CREATE TABLE IF NOT EXISTS series( id uuid NOT NULL DEFAULT uuid_generate_v4(), - created_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY(id) ); -CREATE TABLE IF NOT EXISTS events( +CREATE TYPE event_type AS ENUM ('hybrid', 'in_person', 'virtual'); + +CREATE TABLE IF NOT EXISTS "events"( id uuid NOT NULL DEFAULT uuid_generate_v4(), - created_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, name varchar(255) NOT NULL, preview varchar(255) NOT NULL, description text NOT NULL, - event_type varchar(255) NOT NULL, + event_type event_type NOT NULL, location varchar(255), link varchar(255), is_public boolean NOT NULL, is_draft boolean NOT NULL, is_archived boolean NOT NULL, - start_time timestamp with time zone NOT NULL, - end_time timestamp with time zone NOT NULL, + start_time timestamp WITH time zone NOT NULL, + end_time timestamp WITH time zone NOT NULL, host uuid NOT NULL, series_id uuid, PRIMARY KEY(id), - CONSTRAINT fk_clubs_host_event FOREIGN key(host) REFERENCES clubs(id) ON UPDATE CASCADE ON DELETE CASCADE, - CONSTRAINT fk_series_event FOREIGN key(series_id) REFERENCES series(id) ON UPDATE CASCADE ON DELETE CASCADE + CONSTRAINT fk_clubs_host_event FOREIGN KEY(host) REFERENCES clubs(id) ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT fk_series_event FOREIGN KEY(series_id) REFERENCES series(id) ON UPDATE CASCADE ON DELETE CASCADE ); -CREATE INDEX IF NOT EXISTS idx_events_series_id ON events USING btree ("series_id"); +CREATE INDEX IF NOT EXISTS idx_events_series_id ON "events" USING btree ("series_id"); CREATE TABLE IF NOT EXISTS categories( id uuid NOT NULL DEFAULT uuid_generate_v4(), - created_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, name varchar(255) NOT NULL, PRIMARY KEY(id) ); CREATE UNIQUE INDEX IF NOT EXISTS uni_categories_name ON categories USING btree ("name"); - CREATE TABLE IF NOT EXISTS tags( id uuid NOT NULL DEFAULT uuid_generate_v4(), - created_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, name varchar(255) NOT NULL, category_id uuid NOT NULL, PRIMARY KEY(id), - CONSTRAINT fk_categories_tag FOREIGN key(category_id) REFERENCES categories(id) ON UPDATE CASCADE ON DELETE CASCADE + CONSTRAINT fk_categories_tag FOREIGN KEY(category_id) REFERENCES categories(id) ON UPDATE CASCADE ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_tags_category_id ON tags USING btree ("category_id"); @@ -104,44 +135,44 @@ CREATE INDEX IF NOT EXISTS idx_tags_category_id ON tags USING btree ("category_i CREATE TABLE IF NOT EXISTS club_events( club_id uuid NOT NULL DEFAULT uuid_generate_v4(), event_id uuid NOT NULL DEFAULT uuid_generate_v4(), - PRIMARY KEY(club_id,event_id), - CONSTRAINT fk_club_events_event FOREIGN key(event_id) REFERENCES events(id) ON UPDATE CASCADE ON DELETE CASCADE, - CONSTRAINT fk_club_events_club FOREIGN key(club_id) REFERENCES clubs(id) ON UPDATE CASCADE ON DELETE CASCADE + PRIMARY KEY(club_id, event_id), + CONSTRAINT fk_club_events_event FOREIGN KEY(event_id) REFERENCES "events"(id) ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT fk_club_events_club FOREIGN KEY(club_id) REFERENCES clubs(id) ON UPDATE CASCADE ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS club_tags( tag_id uuid NOT NULL DEFAULT uuid_generate_v4(), club_id uuid NOT NULL DEFAULT uuid_generate_v4(), - PRIMARY KEY(tag_id,club_id), - CONSTRAINT fk_club_tags_tag FOREIGN key(tag_id) REFERENCES tags(id) ON UPDATE CASCADE ON DELETE CASCADE, - CONSTRAINT fk_club_tags_club FOREIGN key(club_id) REFERENCES clubs(id ) ON UPDATE CASCADE ON DELETE CASCADE + PRIMARY KEY(tag_id, club_id), + CONSTRAINT fk_club_tags_tag FOREIGN KEY(tag_id) REFERENCES tags(id) ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT fk_club_tags_club FOREIGN KEY(club_id) REFERENCES clubs(id) ON UPDATE CASCADE ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS contacts( id uuid NOT NULL DEFAULT uuid_generate_v4(), - created_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, "type" varchar(255) NOT NULL, content varchar(255) NOT NULL, club_id uuid NOT NULL, PRIMARY KEY(id), - CONSTRAINT fk_clubs_contact FOREIGN key(club_id) REFERENCES clubs(id) ON UPDATE CASCADE ON DELETE CASCADE + CONSTRAINT fk_clubs_contact FOREIGN KEY(club_id) REFERENCES clubs(id) ON UPDATE CASCADE ON DELETE CASCADE ); -CREATE UNIQUE INDEX IF NOT EXISTS idx_contact_type ON contacts USING btree ("type","club_id"); +CREATE UNIQUE INDEX IF NOT EXISTS idx_contact_type ON contacts USING btree ("type", "club_id"); CREATE TABLE IF NOT EXISTS event_tags( tag_id uuid NOT NULL DEFAULT uuid_generate_v4(), event_id uuid NOT NULL DEFAULT uuid_generate_v4(), - PRIMARY KEY(tag_id,event_id), - CONSTRAINT fk_event_tags_tag FOREIGN key(tag_id) REFERENCES tags(id) ON UPDATE CASCADE ON DELETE CASCADE, - CONSTRAINT fk_event_tags_event FOREIGN key(event_id) REFERENCES events(id) ON UPDATE CASCADE ON DELETE CASCADE + PRIMARY KEY(tag_id, event_id), + CONSTRAINT fk_event_tags_tag FOREIGN KEY(tag_id) REFERENCES tags(id) ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT fk_event_tags_event FOREIGN KEY(event_id) REFERENCES "events"(id) ON UPDATE CASCADE ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS files( id uuid NOT NULL DEFAULT uuid_generate_v4(), - created_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, owner_id uuid NOT NULL, owner_type varchar(255) NOT NULL, file_name varchar(255) NOT NULL, @@ -151,7 +182,9 @@ CREATE TABLE IF NOT EXISTS files( object_key varchar(255) NOT NULL, PRIMARY KEY(id) ); + CREATE INDEX IF NOT EXISTS idx_files_owner_type ON files USING btree ("owner_type"); + CREATE INDEX IF NOT EXISTS idx_files_owner_id ON files USING btree ("owner_id"); -- CREATE TABLE IF NOT EXISTS notifications( @@ -167,81 +200,81 @@ CREATE INDEX IF NOT EXISTS idx_files_owner_id ON files USING btree ("owner_id"); -- reference_type varchar(255) NOT NULL, -- PRIMARY KEY(id) -- ); - -CREATE TABLE IF NOT EXISTS point_of_contacts( +CREATE TABLE IF NOT EXISTS leadership( id uuid NOT NULL DEFAULT uuid_generate_v4(), - created_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp WITH time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, name varchar(255) NOT NULL, email varchar(255) NOT NULL, position varchar(255) NOT NULL, club_id uuid NOT NULL, PRIMARY KEY(id), - CONSTRAINT fk_clubs_point_of_contact FOREIGN key(club_id) REFERENCES clubs(id) ON UPDATE CASCADE ON DELETE CASCADE + CONSTRAINT fk_clubs_point_of_contact FOREIGN KEY(club_id) REFERENCES clubs(id) ON UPDATE CASCADE ON DELETE CASCADE ); -CREATE UNIQUE INDEX IF NOT EXISTS compositeindex ON point_of_contacts USING btree ("email","club_id"); -CREATE INDEX IF NOT EXISTS idx_point_of_contacts_club_id ON point_of_contacts USING btree ("club_id"); -CREATE INDEX IF NOT EXISTS idx_point_of_contacts_email ON point_of_contacts USING btree ("email"); +CREATE UNIQUE INDEX IF NOT EXISTS compositeindex ON leadership USING btree ("email", "club_id"); + +CREATE INDEX IF NOT EXISTS idx_leadership_club_id ON leadership USING btree ("club_id"); +CREATE INDEX IF NOT EXISTS idx_leadership_email ON leadership USING btree ("email"); CREATE TABLE IF NOT EXISTS user_club_followers( user_id uuid NOT NULL DEFAULT uuid_generate_v4(), club_id uuid NOT NULL DEFAULT uuid_generate_v4(), - PRIMARY KEY(user_id,club_id), - CONSTRAINT fk_user_club_followers_user FOREIGN key(user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE, - CONSTRAINT fk_user_club_followers_club FOREIGN key(club_id) REFERENCES clubs(id) ON UPDATE CASCADE ON DELETE CASCADE + PRIMARY KEY(user_id, club_id), + CONSTRAINT fk_user_club_followers_user FOREIGN KEY(user_id) REFERENCES "users"(id) ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT fk_user_club_followers_club FOREIGN KEY(club_id) REFERENCES clubs(id) ON UPDATE CASCADE ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS user_club_intended_applicants( user_id uuid NOT NULL DEFAULT uuid_generate_v4(), club_id uuid NOT NULL DEFAULT uuid_generate_v4(), - PRIMARY KEY(user_id,club_id), - CONSTRAINT fk_user_club_intended_applicants_user FOREIGN key(user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE, - CONSTRAINT fk_user_club_intended_applicants_club FOREIGN key(club_id) REFERENCES clubs(id) ON UPDATE CASCADE ON DELETE CASCADE + PRIMARY KEY(user_id, club_id), + CONSTRAINT fk_user_club_intended_applicants_user FOREIGN KEY(user_id) REFERENCES "users"(id) ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT fk_user_club_intended_applicants_club FOREIGN KEY(club_id) REFERENCES clubs(id) ON UPDATE CASCADE ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS user_club_members( user_id uuid NOT NULL, club_id uuid NOT NULL, - membership_type varchar(255) NOT NULL DEFAULT 'member'::character varying, - PRIMARY KEY(user_id,club_id), - CONSTRAINT fk_user_club_members_user FOREIGN key(user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE, - CONSTRAINT fk_user_club_members_club FOREIGN key(club_id) REFERENCES clubs(id) ON UPDATE CASCADE ON DELETE CASCADE + membership_type varchar(255) NOT NULL DEFAULT 'member' :: character varying, + PRIMARY KEY(user_id, club_id), + CONSTRAINT fk_user_club_members_user FOREIGN KEY(user_id) REFERENCES "users"(id) ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT fk_user_club_members_club FOREIGN KEY(club_id) REFERENCES clubs(id) ON UPDATE CASCADE ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS user_event_rsvps( user_id uuid NOT NULL DEFAULT uuid_generate_v4(), event_id uuid NOT NULL DEFAULT uuid_generate_v4(), - PRIMARY KEY(user_id,event_id), - CONSTRAINT fk_user_event_rsvps_user FOREIGN key(user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE, - CONSTRAINT fk_user_event_rsvps_event FOREIGN key(event_id) REFERENCES events(id) ON UPDATE CASCADE ON DELETE CASCADE + PRIMARY KEY(user_id, event_id), + CONSTRAINT fk_user_event_rsvps_user FOREIGN KEY(user_id) REFERENCES "users"(id) ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT fk_user_event_rsvps_event FOREIGN KEY(event_id) REFERENCES "events"(id) ON UPDATE CASCADE ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS user_event_waitlists( user_id uuid NOT NULL DEFAULT uuid_generate_v4(), event_id uuid NOT NULL DEFAULT uuid_generate_v4(), - PRIMARY KEY(user_id,event_id), - CONSTRAINT fk_user_event_waitlists_user FOREIGN key(user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE, - CONSTRAINT fk_user_event_waitlists_event FOREIGN key(event_id) REFERENCES events(id) ON UPDATE CASCADE ON DELETE CASCADE + PRIMARY KEY(user_id, event_id), + CONSTRAINT fk_user_event_waitlists_user FOREIGN KEY(user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT fk_user_event_waitlists_event FOREIGN KEY(event_id) REFERENCES "events"(id) ON UPDATE CASCADE ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS user_tags( user_id uuid NOT NULL DEFAULT uuid_generate_v4(), tag_id uuid NOT NULL DEFAULT uuid_generate_v4(), - PRIMARY KEY(user_id,tag_id), - CONSTRAINT fk_user_tags_user FOREIGN key(user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE, - CONSTRAINT fk_user_tags_tag FOREIGN key(tag_id) REFERENCES tags(id) ON UPDATE CASCADE ON DELETE CASCADE + PRIMARY KEY(user_id, tag_id), + CONSTRAINT fk_user_tags_user FOREIGN KEY(user_id) REFERENCES "users"(id) ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT fk_user_tags_tag FOREIGN KEY(tag_id) REFERENCES tags(id) ON UPDATE CASCADE ON DELETE CASCADE ); -CREATE UNIQUE INDEX IF NOT EXISTS uni_users_email ON users USING btree ("email"); +CREATE UNIQUE INDEX IF NOT EXISTS uni_users_email ON "users" USING btree ("email"); CREATE TABLE IF NOT EXISTS verifications( user_id uuid NOT NULL, token varchar(255), - expires_at timestamp with time zone NOT NULL, + expires_at timestamp WITH time zone NOT NULL, "type" varchar(255) NOT NULL, - PRIMARY KEY(user_id,expires_at) + PRIMARY KEY(user_id, expires_at) ); CREATE UNIQUE INDEX IF NOT EXISTS uni_verifications_token ON verifications USING btree ("token"); @@ -254,8 +287,8 @@ CREATE TABLE IF NOT EXISTS user_oauth_tokens( access_token varchar, csrf_token varchar(255), resource_type OAuthResourceType, - expires_at timestamp with time zone NOT NULL, - PRIMARY KEY(user_id,resource_type) + expires_at timestamp WITH time zone NOT NULL, + PRIMARY KEY(user_id, resource_type) ); -COMMIT; +COMMIT; \ No newline at end of file diff --git a/backend/redis_entrypoint.sh b/backend/redis_entrypoint.sh new file mode 100644 index 000000000..ffa4913bf --- /dev/null +++ b/backend/redis_entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# set up redis configuration directory +mkdir -p /usr/local/etc/redis + +# dynamically generate redis configuration and ACL files here, using environment variables +echo "aclfile /usr/local/etc/redis/custom_aclfile.acl" > /usr/local/etc/redis/redis.conf + +# generate ACL file using environment variables +if [ -n ${REDIS_USERNAME} ] && [ -n ${REDIS_PASSWORD} ]; then + echo "user ${REDIS_USERNAME} on allkeys allchannels allcommands >${REDIS_PASSWORD} " > /usr/local/etc/redis/custom_aclfile.acl +fi + +# disable default user +if [ $(echo ${REDIS_DISABLE_DEFAULT_USER}) == "true" ]; then + echo "user default off nopass nocommands" >> /usr/local/etc/redis/custom_aclfile.acl +fi + +# call the original docker entrypoint script with redis-server and the path to the custom redis configuration +exec docker-entrypoint.sh redis-server /usr/local/etc/redis/redis.conf \ No newline at end of file diff --git a/backend/search/base/controller.go b/backend/search/base/controller.go index 4f7e4b8d7..bd8a6b474 100644 --- a/backend/search/base/controller.go +++ b/backend/search/base/controller.go @@ -20,12 +20,12 @@ func NewSearchController(searchService SearchServiceInterface) *SearchController // // @Summary Searches through clubs // @Description Searches through clubs -// @ID search-club +// @ID search-clubs // @Tags search // @Accept json // @Produce json -// @Param searchQuery query models.SearchQueryParams true "Search Body" -// @Success 200 {object} []models.ClubSearchResult +// @Param searchQuery query search_types.ClubSearchRequest true "Search Body" +// @Success 200 {object} []search_types.SearchResult[models.Club] // @Failure 404 {object} error // @Failure 500 {object} error // @Router /search/clubs [get] @@ -48,12 +48,12 @@ func (s *SearchController) SearchClubs(c *fiber.Ctx) error { // // @Summary Searches through events // @Description Searches through events -// @ID search-club +// @ID search-event // @Tags search // @Accept json // @Produce json -// @Param searchQuery query models.SearchQueryParams true "Search Body" -// @Success 200 {object} []models.EventsSearchResult +// @Param searchQuery query search_types.EventSearchRequest true "Search Body" +// @Success 200 {object} []search_types.SearchResult[models.Event] // @Failure 404 {object} error // @Failure 500 {object} error // @Router /search/events [get] diff --git a/backend/search/base/transactions.go b/backend/search/base/transactions.go index 9ea68146a..8ddfdce38 100644 --- a/backend/search/base/transactions.go +++ b/backend/search/base/transactions.go @@ -66,7 +66,11 @@ func Search[T types.Searchable](db *gorm.DB, query types.SearchRequest) (*types. func Upsert[T types.Searchable](db *gorm.DB, index string, uuid string, model types.ToSearchDocument) error { var elem T - if err := db.Model(&model).Preload("Tag").Where("Clubs").Where("id = ?", uuid).Find(&elem).Error; err != nil { + query := db.Model(&elem) + + query = elem.Preload(query) + + if err := query.Where("id = ?", uuid).Find(&elem).Error; err != nil { return err } diff --git a/backend/search/types/utilities.go b/backend/search/types/utilities.go index 4edfee64f..364bf464d 100644 --- a/backend/search/types/utilities.go +++ b/backend/search/types/utilities.go @@ -1,6 +1,9 @@ package types -import "github.com/GenerateNU/sac/backend/entities/models" +import ( + "github.com/GenerateNU/sac/backend/entities/models" + "gorm.io/gorm" +) type Json map[string]interface{} @@ -21,4 +24,5 @@ type BulkRequestCreate struct { type Searchable interface { models.Club | models.Event + Preload(db *gorm.DB) *gorm.DB } diff --git a/backend/server/server.go b/backend/server/server.go index 55e272e95..bf466e1f1 100644 --- a/backend/server/server.go +++ b/backend/server/server.go @@ -13,11 +13,11 @@ import ( auth "github.com/GenerateNU/sac/backend/entities/auth/base" categories "github.com/GenerateNU/sac/backend/entities/categories/base" clubs "github.com/GenerateNU/sac/backend/entities/clubs/base" - contacts "github.com/GenerateNU/sac/backend/entities/contacts/base" events "github.com/GenerateNU/sac/backend/entities/events/base" files "github.com/GenerateNU/sac/backend/entities/files/base" + leader "github.com/GenerateNU/sac/backend/entities/leadership/base" oauth "github.com/GenerateNU/sac/backend/entities/oauth/base" - pocs "github.com/GenerateNU/sac/backend/entities/pocs/base" + socials "github.com/GenerateNU/sac/backend/entities/socials/base" tags "github.com/GenerateNU/sac/backend/entities/tags/base" users "github.com/GenerateNU/sac/backend/entities/users/base" "github.com/GenerateNU/sac/backend/integrations" @@ -86,8 +86,8 @@ func allRoutes(app *fiber.App, routeParams types.RouteParams) { auth.Auth(routeParams) users.UserRoutes(routeParams) clubs.ClubRoutes(routeParams) - contacts.Contact(routeParams) - pocs.PointOfContact(routeParams) + socials.Social(routeParams) + leader.Leader(routeParams) tags.Tag(routeParams) categories.CategoryRoutes(routeParams) events.EventRoutes(routeParams) diff --git a/backend/transactions/preloaders.go b/backend/transactions/preloaders.go index b4c873c73..7f06a182d 100644 --- a/backend/transactions/preloaders.go +++ b/backend/transactions/preloaders.go @@ -27,3 +27,9 @@ func PreloadEvent() OptionalQuery { return db.Preload("Event") } } + +func PreloadRecruitment() OptionalQuery { + return func(db *gorm.DB) *gorm.DB { + return db.Preload("Recruitment") + } +} diff --git a/backend/utilities/api_error.go b/backend/utilities/api_error.go index f4ecc154e..7d6e297e6 100644 --- a/backend/utilities/api_error.go +++ b/backend/utilities/api_error.go @@ -10,9 +10,9 @@ import ( ) var ( - ErrNotFound = errors.New("not found") - ErrDuplicate = errors.New("duplicate") - ErrExpectedPagination = errors.New("expected pagination. make sure to use the fiberpaginate middleware") + ErrNotFound = errors.New("not found") + ErrDuplicate = errors.New("duplicate") + ErrExpectedPageInfo = errors.New("expected page info. make sure to use the fiberpaginate middleware") ) func IsNotFound(err error) bool { @@ -23,8 +23,8 @@ func IsDuplicate(err error) bool { return errors.Is(err, ErrDuplicate) } -func IsExpectedPagination(err error) bool { - return errors.Is(err, ErrExpectedPagination) +func IsExpectedPageInfo(err error) bool { + return errors.Is(err, ErrExpectedPageInfo) } type APIError struct { @@ -86,7 +86,7 @@ func ErrorHandler(c *fiber.Ctx, err error) error { apiErr = NewAPIError(http.StatusNotFound, err) case ErrDuplicate: apiErr = NewAPIError(http.StatusConflict, err) - case ErrExpectedPagination: + case ErrExpectedPageInfo: apiErr = NewAPIError(http.StatusInternalServerError, err) default: if castedErr, ok := err.(APIError); ok { diff --git a/backend/utilities/ctx.go b/backend/utilities/ctx.go new file mode 100644 index 000000000..005f5ef9e --- /dev/null +++ b/backend/utilities/ctx.go @@ -0,0 +1,44 @@ +package utilities + +import ( + "context" +) + +func ExecuteWithTimeout(ctx context.Context, fn func() error) error { + errChan := make(chan error) + + go func() { + err := fn() + errChan <- err + }() + + select { + case <-ctx.Done(): + return ctx.Err() + case err := <-errChan: + return err + } +} + +func ExecuteWithTimeoutResult[Result any](ctx context.Context, fn func() (*Result, error)) (*Result, error) { + resultChan := make(chan *Result) + errChan := make(chan error) + + go func() { + result, err := fn() + if err != nil { + errChan <- err + } else { + resultChan <- result + } + }() + + select { + case <-ctx.Done(): + return nil, ctx.Err() + case err := <-errChan: + return nil, err + case result := <-resultChan: + return result, nil + } +} diff --git a/backend/utilities/time.go b/backend/utilities/time.go new file mode 100644 index 000000000..5676509cc --- /dev/null +++ b/backend/utilities/time.go @@ -0,0 +1,13 @@ +package utilities + +import "time" + +type TimeFormat string + +const ( + YYYY_dash_MM_dash_DD TimeFormat = "2006-01-02" +) + +func ParseTime(s string, format TimeFormat) (time.Time, error) { + return time.Parse(string(format), s) +} diff --git a/backend/utilities/validator.go b/backend/utilities/validator.go index 22edbf8a9..a8d1ac444 100644 --- a/backend/utilities/validator.go +++ b/backend/utilities/validator.go @@ -24,8 +24,8 @@ func RegisterCustomValidators() (*validator.Validate, error) { return nil, err } - if err := validate.RegisterValidation("contact_pointer", func(fl validator.FieldLevel) bool { - return validateContactPointer(validate, fl) + if err := validate.RegisterValidation("social_pointer", func(fl validator.FieldLevel) bool { + return validateSocialPointer(validate, fl) }); err != nil { return nil, err } @@ -58,16 +58,16 @@ func validateS3URL(fl validator.FieldLevel) bool { return strings.HasPrefix(fl.Field().String(), "https://s3.amazonaws.com/") } -func validateContactPointer(validate *validator.Validate, fl validator.FieldLevel) bool { - contact, ok := fl.Parent().Interface().(struct { - Type models.ContactType +func validateSocialPointer(validate *validator.Validate, fl validator.FieldLevel) bool { + social, ok := fl.Parent().Interface().(struct { + Type models.SocialType Content string }) if !ok { return false } - validationRules := map[models.ContactType]string{ + validationRules := map[models.SocialType]string{ models.Facebook: "http_url", models.Instagram: "http_url", models.X: "http_url", @@ -80,12 +80,12 @@ func validateContactPointer(validate *validator.Validate, fl validator.FieldLeve models.CustomSite: "http_url", } - rule, ok := validationRules[contact.Type] + rule, ok := validationRules[social.Type] if !ok { return false // invalid contact type } - return validate.Var(contact.Content, rule) == nil && strings.HasPrefix(contact.Content, models.GetContentPrefix(contact.Type)) + return validate.Var(social.Content, rule) == nil && strings.HasPrefix(social.Content, models.GetSocialPrefix(social.Type)) } func validateNotEqualIfNotEmpty(fl validator.FieldLevel) bool { diff --git a/cli/go.sum b/cli/go.sum index 3803ea3ae..b1cae943d 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -1,38 +1,92 @@ github.com/GenerateNU/sac/backend v0.0.0-20240427140745-74eb4ec0f597 h1:bhattf8C4aGIdJ+0L168MztGMKcznF0ZiEDhvLlHMEU= +github.com/GenerateNU/sac/backend v0.0.0-20240427140745-74eb4ec0f597/go.mod h1:VEuNTR6WQ0OQ5fjMjoRcIymCMjRLdRUM611roK9yQYQ= 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/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +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= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/garrettladley/mattress v0.4.0 h1:ZB3iqyc5q6bqIryNfsh2FMcbMdnV1XEryvqivouceQE= +github.com/garrettladley/mattress v0.4.0/go.mod h1:OWKIRc9wC3gtD3Ng/nUuNEiR1TJvRYLmn/KZYw9nl5Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= +github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= +golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cli/helpers/config.go b/cli/helpers/config.go index bdcb76d36..89b68f754 100644 --- a/cli/helpers/config.go +++ b/cli/helpers/config.go @@ -15,7 +15,7 @@ var ( WEB_DIR = filepath.Join(FRONTEND_DIR, "/web") BACKEND_DIR = filepath.Join(ROOT_DIR, "/backend") CLI_DIR = filepath.Join(ROOT_DIR, "/cli") - CONFIG = filepath.Join(ROOT_DIR, "/config") + CONFIG_FILE = filepath.Join(ROOT_DIR, "/config/.env.template") MIGRATIONS = filepath.Join(BACKEND_DIR, "/migrations") MOCK_FILE = filepath.Join(BACKEND_DIR, "/mock/data.sql") ) diff --git a/cli/helpers/database.go b/cli/helpers/database.go index d6be0e299..ad2951c00 100644 --- a/cli/helpers/database.go +++ b/cli/helpers/database.go @@ -22,7 +22,7 @@ func InitDB() error { } func DownDB() error { - config, err := config.GetConfiguration(CONFIG) + config, err := config.GetConfiguration(CONFIG_FILE) if err != nil { return err } @@ -33,8 +33,7 @@ func DownDB() error { cmd.Dir = ROOT_DIR cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - err = cmd.Run() - if err != nil { + if cmd.Run() != nil { return fmt.Errorf("error running down migrations: %w", err) } @@ -42,7 +41,7 @@ func DownDB() error { } func CleanTestDBs() error { - config, err := config.GetConfiguration(CONFIG) + config, err := config.GetConfiguration(CONFIG_FILE) if err != nil { return err } @@ -102,7 +101,7 @@ func CleanTestDBs() error { } func InsertDB() error { - config, err := config.GetConfiguration(CONFIG) + config, err := config.GetConfiguration(CONFIG_FILE) if err != nil { return err } diff --git a/config/.env.template b/config/.env.template index 7cf187614..864f6a331 100644 --- a/config/.env.template +++ b/config/.env.template @@ -10,7 +10,7 @@ SAC_DB_NAME="sac" SAC_DB_REQUIRE_SSL="false" SAC_REDIS_ACTIVE_TOKENS_USERNAME="redis_active_tokens" -SAC_REDIS_ACTIVE_TOKENS_PASSWORD="redis_active_token!#1" +SAC_REDIS_ACTIVE_TOKENS_PASSWORD="redis_active_tokens!#1" SAC_REDIS_ACTIVE_TOKENS_HOST="127.0.0.1" SAC_REDIS_ACTIVE_TOKENS_PORT="6379" SAC_REDIS_ACTIVE_TOKENS_DB="0" @@ -27,7 +27,6 @@ SAC_REDIS_LIMITER_HOST="127.0.0.1" SAC_REDIS_LIMITER_PORT="6381" SAC_REDIS_LIMITER_DB="0" - SAC_AWS_BUCKET_NAME="SAC_AWS_BUCKET_NAME" SAC_AWS_ID="SAC_AWS_ID" SAC_AWS_SECRET="SAC_AWS_SECRET" @@ -54,6 +53,6 @@ SAC_GOOGLE_OAUTH_REDIRECT_URI="http://127.0.0.1:3000" SAC_OUTLOOK_OAUTH_CLIENT_ID=test SAC_OUTLOOK_OAUTH_CLIENT_SECRET=test -SAC_OUTLOOK_OAUTH_REDIRECT_URI="http://127.0.0.1:3000 +SAC_OUTLOOK_OAUTH_REDIRECT_URI="http://127.0.0.1:3000" -SAC_SEARCH_URI=""http://127.0.0.1:9200" \ No newline at end of file +SAC_SEARCH_URI="http://127.0.0.1:9200" \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index bc9ff62c3..000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,43 +0,0 @@ -services: - opensearch-node1: - image: opensearchproject/opensearch:latest - container_name: opensearch-node1 - environment: - - cluster.name=opensearch-cluster - - node.name=opensearch-node1 - - discovery.type=single-node - - bootstrap.memory_lock=true # along with the memlock settings below, disables swapping - - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" # minimum and maximum Java heap size, recommend setting both to 50% of system RAM - - DISABLE_SECURITY_PLUGIN=true # - ulimits: - memlock: - soft: -1 - hard: -1 - nofile: - soft: 65536 # maximum number of open files for the OpenSearch user, set to at least 65536 on modern systems - hard: 65536 - volumes: - - opensearch-data1:/usr/share/opensearch/data - ports: - - 9200:9200 - - 9600:9600 # required for Performance Analyzer - networks: - - opensearch-net - opensearch-dashboards: - image: opensearchproject/opensearch-dashboards:latest - container_name: opensearch-dashboards - ports: - - 5601:5601 - expose: - - "5601" - environment: - OPENSEARCH_HOSTS: '["http://opensearch-node1:9200"]' - DISABLE_SECURITY_DASHBOARDS_PLUGIN: true - networks: - - opensearch-net - -volumes: - opensearch-data1: - -networks: - opensearch-net: \ No newline at end of file diff --git a/frontend/mobile/app.json b/frontend/mobile/app.json index eb1f6e1da..39f875296 100644 --- a/frontend/mobile/app.json +++ b/frontend/mobile/app.json @@ -18,7 +18,8 @@ "usesNonExemptEncryption": false }, "supportsTablet": true, - "bundleIdentifier": "com.generatesac.studentactivitycalendar" + "bundleIdentifier": "com.generatesac.studentactivitycalendar", + "backgroundColor": "#ffffff" }, "android": { "adaptiveIcon": { diff --git a/frontend/mobile/app/(app)/(tabs)/_layout.tsx b/frontend/mobile/app/(app)/(tabs)/_layout.tsx index b0447b614..feb2cfbcf 100644 --- a/frontend/mobile/app/(app)/(tabs)/_layout.tsx +++ b/frontend/mobile/app/(app)/(tabs)/_layout.tsx @@ -6,10 +6,10 @@ import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; import { faCalendarDays } from '@fortawesome/free-solid-svg-icons/faCalendarDays'; import { faHouse } from '@fortawesome/free-solid-svg-icons/faHouse'; import { faUser } from '@fortawesome/free-solid-svg-icons/faUser'; -import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; import { Text } from '@/app/(design-system)'; -import { Box, Colors } from '@/app/(design-system)'; +import { Box } from '@/app/(design-system)'; +import { Icon } from '@/app/(design-system)/components/Icon/Icon'; interface TabBarLabelProps { focused: boolean; @@ -29,11 +29,7 @@ interface TabBarIconProps { const TabBarIcon: React.FC = ({ focused, icon }) => ( - + ); @@ -41,13 +37,16 @@ const Layout = () => { return ( { return ( - - Calendar - + + + ); }; diff --git a/frontend/mobile/app/(app)/(tabs)/profile.tsx b/frontend/mobile/app/(app)/(tabs)/profile.tsx index 7d6ca1183..041086c06 100644 --- a/frontend/mobile/app/(app)/(tabs)/profile.tsx +++ b/frontend/mobile/app/(app)/(tabs)/profile.tsx @@ -1,10 +1,14 @@ import { StyleSheet, Text, View } from 'react-native'; +import { GlobalLayout } from '@/app/(design-system)/components/GlobalLayout/GlobalLayout'; + const ProfilePage = () => { return ( - - Profile - + + + Profile + + ); }; diff --git a/frontend/mobile/app/(design-system)/components/Button/Button.tsx b/frontend/mobile/app/(design-system)/components/Button/Button.tsx index 08db025e9..b331b734c 100644 --- a/frontend/mobile/app/(design-system)/components/Button/Button.tsx +++ b/frontend/mobile/app/(design-system)/components/Button/Button.tsx @@ -20,6 +20,7 @@ interface BaseButtonProps { disabled?: boolean; children?: React.ReactNode; color?: SACColors; + outline?: boolean; } interface StandardButtonProps { @@ -49,15 +50,16 @@ const StandardButton: React.FC< disabled = false, color = defaultColor, size = 'full', + outline = false, ...rest }) => { + const localStyles = computeButtonStyles({ disabled, color, size, outline }); + return ( {children} @@ -77,18 +79,18 @@ const IconButton: React.FC< iconPosition = 'left', justify = 'center', size = 'full', + outline = false, ...rest }) => { const buttonIcon = ; + const localStyles = computeButtonStyles({ disabled, color, size, outline }); return ( {iconPosition === 'left' && buttonIcon} @@ -108,34 +110,56 @@ export const Button: React.FC = (props) => { return null; }; +const computeButtonStyles = (props: Partial) => { + let buttonStyles = { + ...styles.base, + ...buttonSizeStyles[props.size ?? 'full'], + backgroundColor: props.disabled ? 'gray' : props.color + }; + if (props.outline) { + buttonStyles = { ...buttonStyles, ...styles.buttonOutline }; + } + if (props.variant === 'iconButton') { + buttonStyles = { ...buttonStyles, ...styles.iconButton }; + } + + return buttonStyles; +}; + const styles = createStyles({ - standardButton: { + base: { alignItems: 'center', justifyContent: 'center', paddingHorizontal: 'l', paddingVertical: 'm', - borderRadius: 'base' + borderRadius: 'md' }, iconButton: { - alignItems: 'center', - justifyContent: 'center', - paddingHorizontal: 'l', - paddingVertical: 'm', - borderRadius: 'base', flexDirection: 'row', gap: 's' + }, + buttonOutline: { + borderColor: 'darkGray', + borderWidth: 0.3 } }); const buttonSizeStyles = createStyles({ - small: { + xs: { + paddingHorizontal: 's', + paddingVertical: 'xxs' + }, + sm: { minWidth: 80, paddingVertical: 'xs', paddingHorizontal: 'm' }, - medium: { + md: { minWidth: 115 }, + lg: { + minWidth: 150 + }, full: { minWidth: '100%' } diff --git a/frontend/mobile/app/(design-system)/components/Calendar/Agenda/AgendaDateSection.tsx b/frontend/mobile/app/(design-system)/components/Calendar/Agenda/AgendaDateSection.tsx new file mode 100644 index 000000000..29edae969 --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/Calendar/Agenda/AgendaDateSection.tsx @@ -0,0 +1,53 @@ +import { forwardRef } from 'react'; + +import { formatTime } from '@/utils/time'; + +import { Box } from '../../Box/Box'; +import { Text } from '../../Text/Text'; +import { EventSection } from '../mockData'; +import AgendaTimeSection from './AgendaTimeSection'; + +type AgendaSectionProps = { + section: EventSection; +}; + +function formatSectionHeader(date: string) { + let addedZero = false; + const { date: dayOfMonth, dayOfWeek } = formatTime( + new Date(date + 'T00:00:00') + ); + if (dayOfMonth.length < 2) { + addedZero = true; + } + + return `${dayOfWeek} ${addedZero ? '0' : ''}${dayOfMonth}`; +} + +export const AgendaDateSection = forwardRef(function AgendaDateSection( + { section }: AgendaSectionProps, + ref +) { + const keys = Object.keys(section.data).sort(); + + return ( + + + {formatSectionHeader(section.date)} + + {keys.map((key, index) => { + return ( + + ); + })} + {keys.length === 0 && ( + + No Events Today + + )} + + ); +}); diff --git a/frontend/mobile/app/(design-system)/components/Calendar/Agenda/AgendaList.tsx b/frontend/mobile/app/(design-system)/components/Calendar/Agenda/AgendaList.tsx new file mode 100644 index 000000000..b3b35305a --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/Calendar/Agenda/AgendaList.tsx @@ -0,0 +1,103 @@ +import { useCallback, useContext, useEffect, useRef, useState } from 'react'; +import { ListRenderItem, ViewToken } from 'react-native'; +import { CalendarContext } from 'react-native-calendars-sac'; +import { UpdateSources } from 'react-native-calendars-sac/src/expandableCalendar/commons'; +import { FlatList } from 'react-native-gesture-handler'; + +import { Box } from '../../Box/Box'; +import { EventCardCalendarSkeleton } from '../../EventCard/Skeletons/EventCardCalendarSkeleton'; +import { EventSection } from '../mockData'; +import { AgendaDateSection } from './AgendaDateSection'; + +// Add back my old code: +export type AgendaListProps = { + sections: EventSection[]; + fetchData: () => void; + isLoading: boolean; + autoScroll: boolean; + setAutoScroll: React.Dispatch>; +}; + +export default function AgendaList({ + sections, + fetchData, + isLoading, + autoScroll, + setAutoScroll +}: AgendaListProps) { + const { date, setDate } = useContext(CalendarContext); + const eventList = useRef | null>(null); + const topSection = useRef(null); + const [scrolling, setScrolling] = useState(false); + + const scrollToDate = useCallback( + (dateSections: EventSection[]) => { + const index = dateSections.findIndex( + (section) => section.date === date + ); + if (index !== -1) { + setScrolling(true); + eventList.current?.scrollToIndex({ + index, + animated: true, + viewOffset: 1, + viewPosition: 0 + }); + setTimeout(() => { + setScrolling(false); + setAutoScroll(false); + }, 500); + topSection.current = dateSections[index].date; + } + }, + [date, setAutoScroll] + ); + + const onViewableItemsChanged = ({ + viewableItems + }: { + viewableItems: ViewToken[]; + }) => { + const topItem = viewableItems?.[0]?.item.date; + if (topItem && topItem !== date && !scrolling) { + topSection.current = topItem; + setDate?.(viewableItems[0].item.date, UpdateSources.LIST_DRAG); + } + }; + + useEffect(() => { + if (topSection.current === date || !autoScroll) return; + scrollToDate(sections); + }, [date, autoScroll, scrollToDate, sections]); + + const renderItem: ListRenderItem = ({ item, index }) => { + return ; + }; + + const loadingComponent = () => { + if (isLoading) { + return ( + + + + + + ); + } else { + return null; + } + }; + + return ( + + ); +} diff --git a/frontend/mobile/app/(design-system)/components/Calendar/Agenda/AgendaTimeSection.tsx b/frontend/mobile/app/(design-system)/components/Calendar/Agenda/AgendaTimeSection.tsx new file mode 100644 index 000000000..45910f7d8 --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/Calendar/Agenda/AgendaTimeSection.tsx @@ -0,0 +1,96 @@ +import { Box } from '../../Box/Box'; +import { Button } from '../../Button/Button'; +import { EventCard } from '../../EventCard/EventCard'; +import { Text } from '../../Text/Text'; +import { EventSectionData } from '../mockData'; + +export type AgendaTimeSectionProps = { + time: number; + data: EventSectionData; +}; + +function convertNumToTime(num: number) { + let time = num; + let meridiem = num === 23 ? 'AM' : 'PM'; + let minutes = num % 100; + + if (num !== 0 && (num < 1000 || num > 2300)) { + throw new Error('Invalid time'); + } + + time = time / 100; + + if (num < 12) { + time += 12; + meridiem = 'AM'; + } + if (num > 12) { + time -= 12; + } + + return { + hour: Math.floor(time), + minute: minutes === 0 ? undefined : minutes.toString(), + meridiem + }; +} + +function TimeHeader({ time }: { time: number }) { + const { hour, minute, meridiem } = convertNumToTime(time); + return ( + + + {hour} + {minute ? `:${minute}` : ''} {meridiem} + + + + ); +} + +export default function AgendaTimeSection({ + time, + data +}: AgendaTimeSectionProps) { + return ( + + + + + + {data.events.map((event, index) => { + return ( + + + + ); + })} + {data.overflow && ( + + )} + + + ); +} diff --git a/frontend/mobile/app/(design-system)/components/Calendar/Calendar.tsx b/frontend/mobile/app/(design-system)/components/Calendar/Calendar.tsx new file mode 100644 index 000000000..758f68f5f --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/Calendar/Calendar.tsx @@ -0,0 +1,106 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { + CalendarProvider, + ExpandableCalendar +} from 'react-native-calendars-sac'; +import { Theme } from 'react-native-calendars-sac/src/types'; + +import { getMarkedDates } from '@/utils/mock'; + +import { createStyles } from '../../theme'; +import { Box } from '../Box/Box'; +import { GlobalLayout } from '../GlobalLayout/GlobalLayout'; +import AgendaList from './Agenda/AgendaList'; +import { EventSection, fetchEvents } from './mockData'; + +const FETCH_RANGE = 1000 * 60 * 60 * 24 * 13; +const NEXT_DAY = 1000 * 60 * 60 * 24; + +const calendarTheme: Theme = { + selectedDayBackgroundColor: 'black', + todayTextColor: 'black', + arrowColor: 'black', + textMonthFontWeight: 'bold' +}; + +export default function Calendar() { + const [isLoading, setIsLoading] = useState(true); + const [events, setEvents] = useState([]); + const [autoScroll, setAutoScroll] = useState(false); + const [minDate, _] = useState(new Date().getTime()); + const [maxDate, setMaxDate] = useState(new Date().getTime()); + + const fetchData = useCallback( + async (direction = 'up') => { + setIsLoading(true); + setTimeout(() => { + const time = + direction === 'up' ? minDate - FETCH_RANGE : maxDate; + fetchEvents(time, FETCH_RANGE).then((data) => { + setEvents((oldData) => [...oldData, ...data]); + setIsLoading(false); + setMaxDate((oldDate) => oldDate + FETCH_RANGE + NEXT_DAY); + }); + }, 1000); + }, + [minDate, maxDate] + ); + + const fetchNextPage = async () => { + if (isLoading) return; + fetchData('down'); + }; + + useEffect(() => { + fetchData('down'); + }, [fetchData]); + + return ( + + + + setAutoScroll(true)} + closeOnDayPress={false} + disableWeekScroll + /> + + + + + + + ); +} + +const styles = createStyles({ + expandableCalendarContainer: { + backgroundColor: 'white', + borderBottomStartRadius: 'lg', + borderBottomEndRadius: 'lg', + shadowColor: 'black', + shadowOffset: { width: 0, height: 5 }, + shadowOpacity: 0.2 + }, + expandableCalendarSubContainer: { + borderBottomStartRadius: 'lg', + borderBottomEndRadius: 'lg', + backgroundColor: 'white', + overflow: 'hidden' + } +}); diff --git a/frontend/mobile/app/(design-system)/components/Calendar/mockData.ts b/frontend/mobile/app/(design-system)/components/Calendar/mockData.ts new file mode 100644 index 000000000..5352ac8db --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/Calendar/mockData.ts @@ -0,0 +1,342 @@ +/** + * TODO: + * - Change the background color the app to white + */ +import { Tag } from '@generatesac/lib'; + +const tags: Tag[] = [ + { + id: '1', + created_at: new Date(), + updated_at: new Date(), + name: 'Software', + category_id: '1' + }, + { + id: '2', + created_at: new Date(), + updated_at: new Date(), + name: 'Free Food', + category_id: '2' + }, + { + id: '3', + created_at: new Date(), + updated_at: new Date(), + name: 'Panel Discussion', + category_id: '3' + }, + { + id: '4', + created_at: new Date(), + updated_at: new Date(), + name: 'Seminar', + category_id: '4' + }, + { + id: '5', + created_at: new Date(), + updated_at: new Date(), + name: 'Hackathon', + category_id: '5' + } +]; + +export type EventSection = { + date: string; + data: EventSectionTimes; +}; + +export type EventSectionTimes = { + [key: string]: EventSectionData; +}; + +export type EventSectionData = { + overflow: boolean; + events: EventPreview[]; +}; + +export type EventPreview = { + title: string; + startTime: string; + endTime: string; + host: string; + tags: Tag[]; +}; + +async function generateEvents(startTime: number, dateSpan: number) { + let time = startTime; + const increment = 1000 * 60 * 60 * 24; + const events: EventSection[] = []; + + while (time <= startTime + dateSpan) { + const date = new Date(time).toISOString().split('T')[0]; + events.push({ + date, + data: { + 2000: { + overflow: true, + events: [ + { + title: 'Gym Workout', + startTime: date, + endTime: new Date( + new Date(date).getTime() + 1000 * 60 * 60 + ).toISOString(), + host: 'Generate', + tags: tags + } + ] + }, + 2015: { + overflow: false, + events: [ + { + title: 'Gym Workout', + startTime: date, + endTime: new Date( + new Date(date).getTime() + 1000 * 60 * 60 + ).toISOString(), + host: 'Generate', + tags: tags + } + ] + } + } + }); + time += increment; + } + + return events; +} + +// function generateEvents(endTime: number) { +// let startTime = endTime; +// const events: EventSection[] = []; +// const randomNum = Math.floor(Math.random() * 10) + 1 + +// for (let i = 0; i < randomNum; i++) { +// startTime += 1000 * 60 * 60 * 24; +// const dateStart = new Date(startTime); + +// events.push({ +// date: dateStart.toISOString().split('T')[0], +// data: { +// 2000: [{ +// title: 'Gym Workout', +// startTime: dateStart.toISOString(), +// endTime: dateStart.toISOString(), +// host: 'Generate', +// tags: tags, +// }] +// } +// }) +// } + +// return { events, startTime }; +// } + +export async function fetchEvents(startTime: number, dateSpan: number) { + const events = await generateEvents(startTime, dateSpan); + return events; +} + +export const mockEvents: EventSection[] = [ + { + date: '2024-06-03', + data: {} + }, + { + date: '2024-06-05', + data: { + 2000: { + overflow: true, + events: [ + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + }, + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + }, + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + } + ] + }, + 2015: { + overflow: true, + events: [ + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + }, + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + } + ] + }, + 2100: { + overflow: true, + events: [ + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + } + ] + } + } + }, + { + date: '2024-06-07', + data: { + 1900: { + overflow: true, + events: [ + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + }, + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + }, + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + } + ] + }, + 1800: { + overflow: false, + events: [ + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + }, + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + } + ] + }, + 2100: { + overflow: false, + events: [ + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + }, + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + } + ] + } + } + }, + { + date: '2024-06-08', + data: { + 2000: { + overflow: true, + events: [ + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + }, + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + } + ] + }, + 2015: { + overflow: true, + events: [ + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + }, + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + } + ] + }, + 2100: { + overflow: true, + events: [ + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + }, + { + title: 'Gym Workout', + startTime: '12am', + endTime: '1am', + host: 'Generate', + tags: tags + } + ] + } + } + } +]; diff --git a/frontend/mobile/app/(design-system)/components/ClubIcon/ClubIcon.tsx b/frontend/mobile/app/(design-system)/components/ClubIcon/ClubIcon.tsx new file mode 100644 index 000000000..bc9082a1e --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/ClubIcon/ClubIcon.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +import { Avatar } from '@rneui/themed'; + +interface ClubIconProps { + imageUrl: string; +} + +export const ClubIcon: React.FC = ({ imageUrl }) => { + return ( + + ); +}; diff --git a/frontend/mobile/app/(design-system)/components/ClubRecruitment/RecruitmentItem/ClubRecruitmentItem.tsx b/frontend/mobile/app/(design-system)/components/ClubRecruitment/RecruitmentItem/ClubRecruitmentItem.tsx index 6553c3348..a7eba1029 100644 --- a/frontend/mobile/app/(design-system)/components/ClubRecruitment/RecruitmentItem/ClubRecruitmentItem.tsx +++ b/frontend/mobile/app/(design-system)/components/ClubRecruitment/RecruitmentItem/ClubRecruitmentItem.tsx @@ -21,7 +21,7 @@ export const RecruitmentItem = ({ return ( - + {firstLetterUppercase(title)} @@ -37,7 +37,8 @@ const styles = createStyles({ recruitmentItem: { alignItems: 'center', borderWidth: 1, - borderRadius: 'base', + borderRadius: 'sm', + borderColor: 'gray', width: '33%' }, recruitmentItemContent: { diff --git a/frontend/mobile/app/(design-system)/components/EventCard/EventCard.tsx b/frontend/mobile/app/(design-system)/components/EventCard/EventCard.tsx index e5538921f..7c831c1fb 100644 --- a/frontend/mobile/app/(design-system)/components/EventCard/EventCard.tsx +++ b/frontend/mobile/app/(design-system)/components/EventCard/EventCard.tsx @@ -1,9 +1,9 @@ import { Tag } from '@generatesac/lib'; -import { EventCardBig } from './EventCardBig'; -import { EventCardCalendar } from './EventCardCalendar'; -import { EventCardClub } from './EventCardClub'; -import { EventCardSmall } from './EventCardSmall'; +import { EventCardBig } from './Variants/EventCardBig'; +import { EventCardCalendar } from './Variants/EventCardCalendar'; +import { EventCardClub } from './Variants/EventCardClub'; +import { EventCardSmall } from './Variants/EventCardSmall'; interface EventCardProps { event: string; diff --git a/frontend/mobile/app/(design-system)/components/EventCard/EventCardTags/EventCardTags.tsx b/frontend/mobile/app/(design-system)/components/EventCard/EventCardTags/EventCardTags.tsx new file mode 100644 index 000000000..0d00b50da --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/EventCard/EventCardTags/EventCardTags.tsx @@ -0,0 +1,41 @@ +import { Tag } from '@generatesac/lib'; + +import { Box, Tag as TagComponent, Text } from '@/app/(design-system)'; + +interface EventTagProps { + tags: Tag[]; +} + +export const EventTags = ({ tags }: EventTagProps) => { + const renderTags = () => { + let currentLength = 0; + return tags.filter((tag) => { + currentLength += tag.name.length; + return currentLength <= 25; + }); + }; + + const renderPlusTag = (remainingTagCount: number) => { + return ( + remainingTagCount > 0 && ( + + {`+${remainingTagCount}`} + + ) + ); + }; + + const renderedTags = renderTags(); + const remainingTagCount = tags.length - renderedTags.length; + + return ( + + {renderedTags.map((tag, index) => ( + + {tag.name} + + ))} + {renderPlusTag(remainingTagCount)} + + ); +}; diff --git a/frontend/mobile/app/(design-system)/components/EventCard/Skeletons/EventCardCalendarSkeleton.tsx b/frontend/mobile/app/(design-system)/components/EventCard/Skeletons/EventCardCalendarSkeleton.tsx new file mode 100644 index 000000000..00bf16635 --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/EventCard/Skeletons/EventCardCalendarSkeleton.tsx @@ -0,0 +1,102 @@ +import React from 'react'; + +import { Skeleton } from '@rneui/base'; + +import { Box, createStyles } from '@/app/(design-system)'; + +export const EventCardCalendarSkeleton = () => { + return ( + + + + + + + + + + + + + + + + + ); +}; + +const styles = createStyles({ + calendarImage: { + flex: 1, + borderRadius: 'sm' + }, + isHappeningContainer: { + zIndex: 1, + position: 'absolute', + marginRight: 'xl', + top: '-15%', + right: '-5%', + flexDirection: 'row', + justifyContent: 'flex-end' + }, + isHappeningContent: { + paddingHorizontal: 's', + paddingVertical: 'xxs', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + borderRadius: 'sm', + backgroundColor: 'darkRed', + shadowColor: 'black', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.3, + shadowRadius: 2 + }, + cardContainer: { + borderRadius: 'md', + shadowColor: 'black', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.1, + shadowRadius: 2, + backgroundColor: 'white' + }, + cardSubContainer: { + width: '100%', + borderRadius: 'md', + padding: 'm', + backgroundColor: 'white', + flexDirection: 'row', + justifyContent: 'space-between', + gap: 's' + }, + cardContentContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 'xxs', + flexWrap: 'wrap' + } +}); diff --git a/frontend/mobile/app/(design-system)/components/EventCard/Variants/EventCardBig.tsx b/frontend/mobile/app/(design-system)/components/EventCard/Variants/EventCardBig.tsx new file mode 100644 index 000000000..e8bfac5d7 --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/EventCard/Variants/EventCardBig.tsx @@ -0,0 +1,67 @@ +// EventCardBig.tsx +import React from 'react'; +import { TouchableOpacity } from 'react-native-gesture-handler'; + +import { Image } from '@rneui/base'; + +import { Box, Text } from '@/app/(design-system)'; +import { calculateDuration, createOptions, eventTime } from '@/utils/time'; + +interface EventCardBigProps { + event: string; + club: string; + startTime: Date; + endTime: Date; + image: string; +} + +export const EventCardBig: React.FC = ({ + event, + club, + startTime, + endTime, + image +}) => { + return ( + + + + + + {event} + + {calculateDuration(startTime, endTime)} + + {club} + + + {eventTime( + startTime, + endTime, + createOptions('dayOfWeek', 'monthAndDate') + )} + + + {eventTime( + startTime, + endTime, + createOptions('start') + )} + + + + + + ); +}; diff --git a/frontend/mobile/app/(design-system)/components/EventCard/Variants/EventCardCalendar.tsx b/frontend/mobile/app/(design-system)/components/EventCard/Variants/EventCardCalendar.tsx new file mode 100644 index 000000000..c68925459 --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/EventCard/Variants/EventCardCalendar.tsx @@ -0,0 +1,136 @@ +import React from 'react'; +import { TouchableOpacity } from 'react-native'; + +import { Tag } from '@generatesac/lib'; +import { Avatar, Image } from '@rneui/base'; + +import { Box, Text, createStyles } from '@/app/(design-system)'; +import { createOptions, eventTime, happeningNow } from '@/utils/time'; + +import { EventTags } from '../EventCardTags/EventCardTags'; +import { sharedStyles } from '../shared/styles'; + +interface EventCardCalendarProps { + event: string; + club: string; + startTime: Date; + endTime: Date; + image: string; + logo: string; + tags?: Tag[]; +} + +export const EventCardCalendar: React.FC = ({ + event, + club, + startTime, + endTime, + image, + logo, + tags +}) => { + const isHappening = happeningNow(startTime, endTime); + + return ( + + + {isHappening && ( + + + + NOW + + + + )} + + + + + {event} + + + {`${eventTime( + startTime, + endTime, + createOptions('start', 'end') + )} •`} + + + + + {club} + + + + {tags && } + + + + + + ); +}; + +const styles = createStyles({ + calendarImage: { + flex: 1, + borderRadius: 'sm' + }, + isHappeningContainer: { + zIndex: 1, + position: 'absolute', + marginRight: 'xl', + top: '-15%', + right: '-5%', + flexDirection: 'row', + justifyContent: 'flex-end' + }, + isHappeningContent: { + paddingHorizontal: 's', + paddingVertical: 'xxs', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + borderRadius: 'sm', + backgroundColor: 'darkRed', + shadowColor: 'black', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.3, + shadowRadius: 2 + }, + cardContainer: { + borderRadius: 'md', + shadowColor: 'black', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.2, + shadowRadius: 8, + backgroundColor: 'white' + }, + cardSubContainer: { + width: '100%', + borderRadius: 'md', + padding: 'm', + backgroundColor: 'white', + flexDirection: 'row', + justifyContent: 'space-between', + gap: 's' + }, + cardContentContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 'xxs', + flexWrap: 'wrap' + } +}); diff --git a/frontend/mobile/app/(design-system)/components/EventCard/Variants/EventCardClub.tsx b/frontend/mobile/app/(design-system)/components/EventCard/Variants/EventCardClub.tsx new file mode 100644 index 000000000..15bee8d91 --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/EventCard/Variants/EventCardClub.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { TouchableOpacity } from 'react-native'; + +import { Tag } from '@generatesac/lib'; +import { Avatar, Image } from '@rneui/base'; + +import { Box, Text, createStyles } from '@/app/(design-system)'; +import { createOptions, eventTime } from '@/utils/time'; + +import { EventTags } from '../EventCardTags/EventCardTags'; +import { sharedStyles } from '../shared/styles'; + +interface EventCardClubProps { + event: string; + club: string; + startTime: Date; + endTime: Date; + image: string; + logo: string; + tags?: Tag[]; +} + +export const EventCardClub: React.FC = ({ + event, + club, + startTime, + endTime, + image, + logo, + tags +}) => { + return ( + + + + + + + + {eventTime( + startTime, + endTime, + createOptions( + 'monthAndDate', + 'year', + 'start', + 'end' + ) + )} + + {event} + + + Hosted by {club} + + + + {tags && } + + + + + + ); +}; + +const styles = createStyles({ + cardContainer: { + shadowColor: 'black', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.1, + shadowRadius: 2, + backgroundColor: 'white', + borderRadius: 'md' + }, + cardSubContainer: { + width: '100%', + borderRadius: 'md', + padding: 'm', + backgroundColor: 'white', + flexDirection: 'row', + justifyContent: 'space-between', + gap: 's' + } +}); diff --git a/frontend/mobile/app/(design-system)/components/EventCard/Variants/EventCardSmall.tsx b/frontend/mobile/app/(design-system)/components/EventCard/Variants/EventCardSmall.tsx new file mode 100644 index 000000000..3b0eabc27 --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/EventCard/Variants/EventCardSmall.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { Dimensions } from 'react-native'; +import { TouchableOpacity } from 'react-native-gesture-handler'; + +import { Image } from '@rneui/base'; + +import { Box, Text } from '@/app/(design-system)'; +import { createOptions, eventTime } from '@/utils/time'; + +interface EventCardSmallProps { + event: string; + club: string; + startTime: Date; + endTime: Date; + image: string; +} + +export const EventCardSmall: React.FC = ({ + event, + club, + startTime, + endTime, + image +}) => { + const screenWidth = Dimensions.get('window').width; + const boxWidth = screenWidth * 0.4; + + return ( + + + + + {event} + + {eventTime( + startTime, + endTime, + createOptions('dayOfWeek', 'monthAndDate') + )} + + + {club} + + + + + ); +}; diff --git a/frontend/mobile/app/(design-system)/components/EventCard/shared/styles.ts b/frontend/mobile/app/(design-system)/components/EventCard/shared/styles.ts new file mode 100644 index 000000000..4d61ece09 --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/EventCard/shared/styles.ts @@ -0,0 +1,9 @@ +import { createStyles } from '@/app/(design-system)/theme'; + +export const sharedStyles = createStyles({ + clubInfoContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 'xxs' + } +}); diff --git a/frontend/mobile/app/(design-system)/components/GlobalLayout/GlobalLayout.tsx b/frontend/mobile/app/(design-system)/components/GlobalLayout/GlobalLayout.tsx new file mode 100644 index 000000000..62af6fe11 --- /dev/null +++ b/frontend/mobile/app/(design-system)/components/GlobalLayout/GlobalLayout.tsx @@ -0,0 +1,17 @@ +import { View } from 'react-native'; + +import { createBox } from '@shopify/restyle'; + +type GlobalLayoutProps = { + children: React.ReactNode; +}; + +const ViewBase = createBox(View); + +export const GlobalLayout = ({ children }: GlobalLayoutProps) => { + return ( + + {children} + + ); +}; diff --git a/frontend/mobile/app/(design-system)/components/Icon/Icon.tsx b/frontend/mobile/app/(design-system)/components/Icon/Icon.tsx index 49adb8d43..05dcff140 100644 --- a/frontend/mobile/app/(design-system)/components/Icon/Icon.tsx +++ b/frontend/mobile/app/(design-system)/components/Icon/Icon.tsx @@ -13,7 +13,7 @@ type IconProps = { export const Icon: React.FC = ({ icon, - size = 'medium', + size = 'md', color = defaultColor }) => { return ( @@ -26,12 +26,18 @@ export const Icon: React.FC = ({ }; const styles = createStyles({ - small: { + xs: { + fontSize: 12 + }, + sm: { fontSize: 16 }, - medium: { + md: { fontSize: 24 }, + lg: { + fontSize: 32 + }, full: { fontSize: 24 } diff --git a/frontend/mobile/app/(design-system)/components/Tag/Tag.tsx b/frontend/mobile/app/(design-system)/components/Tag/Tag.tsx index b80f784f1..1d5a0da89 100644 --- a/frontend/mobile/app/(design-system)/components/Tag/Tag.tsx +++ b/frontend/mobile/app/(design-system)/components/Tag/Tag.tsx @@ -10,32 +10,33 @@ type TagProps = { onPress?: () => void; borderColor?: SACColors; color?: SACColors; - state?: 'selected' | 'unselected' | 'remove'; + variant?: 'selected' | 'unselected' | 'remove' | 'eventCard'; }; export const Tag: React.FC = ({ children, color = defaultColor, - state = 'selected', borderColor, + variant = 'selected', onPress }) => { - if (state === 'selected' || state === 'unselected') { + if (variant === 'selected' || variant === 'unselected') { return ( ); } - if (state === 'remove') { + if (variant === 'remove') { return ( ); } + if (variant === 'eventCard') { + return ( + + ); + } return null; }; diff --git a/frontend/mobile/app/(design-system)/components/Text/TextVariants.ts b/frontend/mobile/app/(design-system)/components/Text/TextVariants.ts index 9432a8550..fc26f6c0d 100644 --- a/frontend/mobile/app/(design-system)/components/Text/TextVariants.ts +++ b/frontend/mobile/app/(design-system)/components/Text/TextVariants.ts @@ -16,7 +16,8 @@ const Texts = { fontFamily: 'DMSans-Medium', fontSize: 20, fontStyle: 'normal', - fontWeight: '500' + fontWeight: '500', + lineHeight: 'normal' }, 'body-1': { fontFamily: 'DMSans-Regular', diff --git a/frontend/mobile/app/(design-system)/shared/border.ts b/frontend/mobile/app/(design-system)/shared/border.ts index 01caed8f4..801c9b078 100644 --- a/frontend/mobile/app/(design-system)/shared/border.ts +++ b/frontend/mobile/app/(design-system)/shared/border.ts @@ -1,6 +1,9 @@ -export const Border = { - small: 4, - base: 8, - medium: 12, +import { ComponentSizes } from './types'; + +export const Border: Record = { + xs: 4, + sm: 8, + md: 12, + lg: 24, full: 999 }; diff --git a/frontend/mobile/app/(design-system)/shared/colors.ts b/frontend/mobile/app/(design-system)/shared/colors.ts index cd8254f0d..11d0f321f 100644 --- a/frontend/mobile/app/(design-system)/shared/colors.ts +++ b/frontend/mobile/app/(design-system)/shared/colors.ts @@ -12,52 +12,14 @@ export const Colors = { white: '#FFFFFF', black: '#000000', gray: '#C3C9D0', - darkGray: '#7A7A7A' + darkGray: '#7A7A7A', + transparent: '' }; export type SACColors = keyof typeof Colors; export const defaultColor: keyof typeof Colors = 'black'; -export const BackgroundColorVariants = { - darkBlue: { - backgroundColor: 'darkBlue' - }, - darkRed: { - backgroundColor: 'darkRed' - }, - green: { - backgroundColor: 'green' - }, - blue: { - backgroundColor: 'blue' - }, - aqua: { - backgroundColor: 'aqua' - }, - purple: { - backgroundColor: 'purple' - }, - red: { - backgroundColor: 'red' - }, - orange: { - backgroundColor: 'orange' - }, - yellow: { - backgroundColor: 'yellow' - }, - white: { - backgroundColor: 'white' - }, - black: { - backgroundColor: 'black' - }, - disabled: { - backgroundColor: 'disabled' - } -} as const; - export const textColorVariants = { darkBlue: 'white', darkRed: 'white', @@ -70,5 +32,7 @@ export const textColorVariants = { yellow: 'white', white: 'black', black: 'white', - gray: 'white' + gray: 'white', + darkGray: 'white', + transparent: 'black' } as const; diff --git a/frontend/mobile/app/(design-system)/shared/types.ts b/frontend/mobile/app/(design-system)/shared/types.ts index f665b58f1..316aef091 100644 --- a/frontend/mobile/app/(design-system)/shared/types.ts +++ b/frontend/mobile/app/(design-system)/shared/types.ts @@ -1,3 +1,3 @@ -type ComponentSizes = 'small' | 'medium' | 'full'; +type ComponentSizes = 'xs' | 'sm' | 'md' | 'lg' | 'full'; export { ComponentSizes }; diff --git a/frontend/mobile/package.json b/frontend/mobile/package.json index 484ac07ca..401c8f678 100644 --- a/frontend/mobile/package.json +++ b/frontend/mobile/package.json @@ -32,6 +32,7 @@ "@rneui/base": "^4.0.0-rc.8", "@rneui/themed": "^4.0.0-rc.8", "@shopify/restyle": "^2.4.4", + "@stream-io/flat-list-mvcp": "^0.10.3", "@svgr/core": "^8.1.0", "expo": "~51.0.2", "expo-dev-client": "~4.0.14", @@ -52,6 +53,8 @@ "react-native-confirmation-code-field": "^7.4.0", "react-native-element-dropdown": "^2.12.0", "react-native-geocoding": "^0.5.0", + "react-native-bidirectional-infinite-scroll": "^0.3.3", + "react-native-calendars-sac": "^1.22.25", "react-native-gesture-handler": "^2.16.2", "react-native-maps": "^1.15.6", "react-native-open-maps": "^0.4.3", @@ -60,6 +63,7 @@ "react-native-screens": "3.31.1", "react-native-svg": "^15.3.0", "react-native-svg-transformer": "^1.4.0", + "react-native-vector-icons": "^10.1.0", "react-native-web": "~0.19.10", "react-redux": "^9.1.2", "zod": "^3.23.8" diff --git a/frontend/mobile/utils/mock.ts b/frontend/mobile/utils/mock.ts new file mode 100644 index 000000000..508535f75 --- /dev/null +++ b/frontend/mobile/utils/mock.ts @@ -0,0 +1,361 @@ +export type MarkedDates = { + [key: string]: { marked?: boolean }; +}; + +export type EventInformation = { + hour: string; + duration: string; + title: string; + location: string; +}; + +export const dates = [ + '2024-04-01', + '2024-04-02', + '2024-04-03', + '2024-04-04', + '2024-04-05', + '2024-04-06', + '2024-04-07', + '2024-04-08', + '2024-04-09', + '2024-04-10', + '2024-04-11', + '2024-04-12', + '2024-04-13', + '2024-04-14', + '2024-04-15', + '2024-04-16', + '2024-04-17', + '2024-04-18', + '2024-04-19', + '2024-04-20', + '2024-04-21', + '2024-04-22', + '2024-04-23', + '2024-04-24', + '2024-04-25', + '2024-04-26', + '2024-04-27', + '2024-04-28', + '2024-04-29', + '2024-04-30' +]; + +export const agendaItems = [ + { + title: dates[0], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Club GM', + location: 'West Village G 102' + } + ] + }, + { + title: dates[1], + data: [ + { + hour: '4pm', + duration: '1h', + title: 'Intramural game', + location: 'Marino Center' + }, + { + hour: '5pm', + duration: '1h', + title: 'Generate Team Meeting', + location: 'Sherman Center' + }, + { + hour: '5pm', + duration: '1h', + title: 'Other commitment', + location: 'Online' + } + ] + }, + { + title: dates[2], + data: [ + { + hour: '1pm', + duration: '1h', + title: 'Dance Crew Rehearsal', + location: 'Curry Ballroom' + }, + { + hour: '2pm', + duration: '1h', + title: 'Intramural game', + location: 'Carter Field 1' + }, + { + hour: '3pm', + duration: '1h', + title: 'Club GM', + location: 'West Village G 102' + } + ] + }, + { + title: dates[3], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Ashtanga Yoga', + location: 'Marino Center' + } + ] + }, + { + title: dates[5], + data: [ + { + hour: '11pm', + duration: '1h', + title: 'Generate Team Meeting', + location: 'Big Sherm' + }, + { + hour: '12pm', + duration: '1h', + title: 'Running Group', + location: 'Marino Center' + } + ] + }, + { + title: dates[6], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Intramural game', + location: 'Carter Field 1' + } + ] + }, + { + title: dates[7], + data: [] + }, + { + title: dates[8], + data: [ + { + hour: '9pm', + duration: '1h', + title: 'Cheese Club GM', + location: 'Richards 325' + }, + { + hour: '10pm', + duration: '1h', + title: 'Club GM', + location: 'Ryder Hall 289' + }, + { + hour: '11pm', + duration: '1h', + title: 'Intramural game', + location: 'Carter Field 1' + }, + { + hour: '12pm', + duration: '1h', + title: 'Running Group', + location: 'Marino Center' + } + ] + }, + { + title: dates[9], + data: [ + { + hour: '1pm', + duration: '1h', + title: 'Intramural game', + location: 'Carter Field 1' + }, + { + hour: '2pm', + duration: '1h', + title: 'Club GM', + location: 'West Village G 102' + }, + { + hour: '3pm', + duration: '1h', + title: 'Marino Yoga', + location: 'Marino Center' + } + ] + }, + { + title: dates[10], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Gym Workout', + location: 'Marino Center' + } + ] + }, + { + title: dates[11], + data: [ + { + hour: '1pm', + duration: '1h', + title: 'Intramural game', + location: 'Carter Field 1' + }, + { + hour: '2pm', + duration: '1h', + title: 'Club GM', + location: 'Ryder Hall 289' + }, + { + hour: '3pm', + duration: '1h', + title: 'Marino Yoga', + location: 'Marino Center' + } + ] + }, + { + title: dates[12], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Gym Workout', + location: 'Marino Center' + } + ] + }, + { + title: dates[15], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Gym Workout', + location: 'Marino Center' + } + ] + }, + { + title: dates[16], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Gym Workout', + location: 'Marino Center' + } + ] + }, + { + title: dates[18], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Gym Workout', + location: 'Marino Center' + }, + { + hour: '1pm', + duration: '1h', + title: 'Intramural game', + location: 'Carter Field 1' + }, + { + hour: '2pm', + duration: '1h', + title: 'Club GM', + location: 'West Village G 102' + }, + { + hour: '3pm', + duration: '1h', + title: 'Marino Yoga', + location: 'Marino Center' + } + ] + }, + { + title: dates[19], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Gym Workout', + location: 'Marino Center' + } + ] + }, + { + title: dates[20], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Gym Workout', + location: 'Marino Center' + } + ] + }, + { + title: dates[22], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Gym Workout', + location: 'Marino Center' + } + ] + }, + { + title: dates[24], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Gym Workout', + location: 'Marino Center' + } + ] + }, + { + title: dates[25], + data: [ + { + hour: '12am', + duration: '1h', + title: 'Gym Workout', + location: 'Marino Center' + } + ] + } +]; + +export function getMarkedDates() { + const marked: MarkedDates = {}; + + agendaItems.forEach((item) => { + // NOTE: only mark dates with data + if (item.data.length) { + marked[item.title] = { marked: true }; + } + }); + return marked; +} diff --git a/frontend/mobile/utils/time.ts b/frontend/mobile/utils/time.ts index b0be27842..2732aba50 100644 --- a/frontend/mobile/utils/time.ts +++ b/frontend/mobile/utils/time.ts @@ -1,4 +1,5 @@ -import { FormattedTime } from '../types/time'; +import { FormattedTime } from '@/types/time'; + import { Options } from './timeOptions'; // calculate duration of event diff --git a/mock_data/requirements.txt b/mock_data/requirements.txt index 7d93bcee2..ae2abc396 100644 --- a/mock_data/requirements.txt +++ b/mock_data/requirements.txt @@ -1,4 +1,4 @@ openai==1.24.0 progress==1.6 -Requests==2.31.0 -psycopg2==2.9.9 \ No newline at end of file +Requests==2.32.0 +psycopg2==2.9.9