diff --git a/.gitignore b/.gitignore index 68da839..2e564db 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .idea/ *.iml +node_modules + # VSCODE .vscode/ diff --git a/backend/app/database/migrations/00000004_team_members.up.sql b/backend/app/database/migrations/00000004_team_members.up.sql index 30b2431..5429dbd 100644 --- a/backend/app/database/migrations/00000004_team_members.up.sql +++ b/backend/app/database/migrations/00000004_team_members.up.sql @@ -1,9 +1,10 @@ CREATE TABLE IF NOT EXISTS team_members ( - id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, - team_id UUID references teams(id) ON DELETE CASCADE, - user_id UUID references users(id) ON DELETE CASCADE, - role TEXT NOT NULL, - created_on TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE('utc')) + team_id UUID NOT NULL references teams(id) ON DELETE CASCADE, + user_id UUID NOT NULL references users(id) ON DELETE CASCADE, + team_role TEXT NOT NULL, + created_on TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE('utc')), + PRIMARY KEY (team_id, user_id) ); -ALTER TABLE teams DROP COLUMN IF EXISTS owner_user_id; \ No newline at end of file +ALTER TABLE teams DROP COLUMN IF EXISTS owner_user_id; + diff --git a/backend/app/database/structs.go b/backend/app/database/structs.go new file mode 100644 index 0000000..adc142b --- /dev/null +++ b/backend/app/database/structs.go @@ -0,0 +1,139 @@ +package database + +import ( + "github.com/jackc/pgx/v5/pgtype" +) + +type DBTeam struct { + Id pgtype.UUID `db:"id"` + EventId pgtype.UUID `db:"event_id"` + Name string `db:"name"` + Visibility string `db:"visibility"` + Timezone string `db:"timezone"` + Technologies string `db:"technologies"` + Availability string `db:"availability"` + Description string `db:"description"` + CreatedOn pgtype.Timestamptz `db:"created_on"` + InviteCode string `db:"invite_code"` +} + +type CreateTeamMember struct { + UserId pgtype.UUID `db:"user_id"` + TeamId pgtype.UUID `db:"team_id"` + TeamRole string `db:"team_role"` +} + +// has all the user info & role to pass to be read client-side +type DBTeamMemberInfo struct { + DBUser // embed the DBUser fields into the struct + TeamRole string `db:"team_role"` +} + +type DBTeamMember struct { + TeamId pgtype.UUID `db:"team_id"` + UserId pgtype.UUID `db:"user_id"` + TeamRole string `db:"team_role"` + CreatedOn pgtype.Timestamp `db:"user_created_on"` +} + +type DBUserTeams struct { + DBTeam + DisplayName string `db:"display_name"` + TeamRole string `db:"team_role"` + AvatarId string `db:"avatar_id"` +} + +type DBTeamAndTeamMember struct { + Id pgtype.UUID `db:"id"` + EventId pgtype.UUID `db:"event_id"` + Name string `db:"name"` + Visibility string `db:"visibility"` + Timezone string `db:"timezone"` + Technologies string `db:"technologies"` + Availability string `db:"availability"` + Description string `db:"description"` + CreatedOn pgtype.Timestamptz `db:"team_created_on"` + InviteCode string `db:"invite_code"` + MembershipCreatedOn pgtype.Timestamptz `db:"membership_created_on"` + TeamId pgtype.UUID `db:"team_id"` + UserId pgtype.UUID `db:"user_id"` + TeamRole string `db:"team_role"` + DisplayName string `db:"display_name"` + AvatarId string `db:"avatar_id"` + ServiceUserId string `db:"service_user_id"` +} + +type UITeam struct { + Id pgtype.UUID `db:"id"` + EventId pgtype.UUID `db:"event_id"` + Name string `db:"name"` + Visibility string `db:"visibility"` + Timezone string `db:"timezone"` + Technologies string `db:"technologies"` + Availability string `db:"availability"` + Description string `db:"description"` + CreatedOn pgtype.Timestamptz `db:"team_created_on"` + InviteCode string `db:"invite_code"` +} + +type UITeamMember struct { + TeamId pgtype.UUID `db:"team_id"` + UserId pgtype.UUID `db:"user_id"` + MembershipCreatedOn pgtype.Timestamptz `db:"membership_created_on"` + TeamRole string `db:"team_role"` +} + +type TeamMember struct { + UITeamMember + DisplayName string `db:"display_name"` + AvatarId string `db:"avatar_id"` + ServiceUserId string `db:"service_user_id"` +} + +type TeamAndMembers struct { + UITeam + TeamMembers []TeamMember +} + +type UserTeamMember struct { + UserId pgtype.UUID `json:"UserId"` + DisplayName string `json:"DisplayName"` + TeamRole string `json:"TeamRole"` + AvatarId string `json:"AvatarId"` + ServiceUserId string `json:"ServiceUserId"` + UserCreatedOn pgtype.Timestamptz `json:"UserCreatedOn"` +} + +type UserTeam struct { + Id pgtype.UUID `json:"Id"` + EventId pgtype.UUID `json:"EventId"` + Name string `json:"Name"` + Description string `json:"Description"` + Visibility string `json:"Visibility"` + Technologies string `json:"Technologies"` + Availability string `json:"Availability"` + TeamCreatedOn pgtype.Timestamptz `json:"TeamCreatedOn"` + InviteCode string `json:"InviteCode"` + TeamMembers []UserTeamMember `json:"TeamMembers"` +} + +type UserTeamAndMembers struct { + Id pgtype.UUID `db:"id"` + EventId pgtype.UUID `db:"event_id"` + Name string `db:"name"` + Visibility string `db:"visibility"` + Timezone string `db:"timezone"` + Technologies string `db:"technologies"` + Availability string `db:"availability"` + Description string `db:"description"` + TeamCreatedOn pgtype.Timestamptz `db:"team_created_on"` + InviteCode string `db:"invite_code"` + TeamId pgtype.UUID `db:"team_id"` + UserId pgtype.UUID `db:"user_id"` + TeamRole string `db:"team_role"` + MembershipCreatedOn pgtype.Timestamptz `db:"membership_created_on"` + DisplayName string `db:"display_name"` + AvatarId string `db:"avatar_id"` + ServiceUserId string `db:"service_user_id"` + CurrentUserRole string `db:"current_user_role"` +} diff --git a/backend/app/database/teams.go b/backend/app/database/teams.go index 022db63..c42a806 100644 --- a/backend/app/database/teams.go +++ b/backend/app/database/teams.go @@ -5,46 +5,6 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -type DBTeam struct { - Id pgtype.UUID `db:"id"` - EventId pgtype.UUID `db:"event_id"` - Name string `db:"name"` - Visibility string `db:"visibility"` - Timezone string `db:"timezone"` - Technologies string `db:"technologies"` - Availability string `db:"availability"` - Description string `db:"description"` - CreatedOn pgtype.Timestamp `db:"created_on" json:"createdOn-hidden"` - InviteCode string `db:"invite_code"` -} - -type CreateTeamMember struct { - UserId pgtype.UUID `db:"user_id"` - TeamId pgtype.UUID `db:"team_id"` - TeamRole string `db:"team_role"` -} - -// has all the user info & role to pass to be read client-side -type DBTeamMemberInfo struct { - DBUser // embed the DBUser fields into the struct - TeamRole string `db:"team_role"` -} - -// For team_member table. -type DBTeamMember struct { - Id pgtype.UUID `db:"id"` - TeamId pgtype.UUID `db:"team_id"` - UserId pgtype.UUID `db:"user_id"` - TeamRole string `db:"team_role"` - CreatedOn pgtype.Timestamp `db:"created_on" json:"createdOn-hidden"` -} - -type DBUserTeams struct { - DBTeam - DisplayName string `db:"display_name"` - TeamRole string `db:"team_role"` -} - func CreateTeam(team DBTeam) (pgtype.UUID, error) { team, err := GetRow[DBTeam]( `INSERT INTO teams @@ -60,7 +20,7 @@ func CreateTeam(team DBTeam) (pgtype.UUID, error) { return team.Id, err } -// stepp 5: used to construct the GetTeamResponse struct +// stepp 5: used to construct the GetTeamResponse struct in server/teams.go func GetTeam(teamId pgtype.UUID) (DBTeam, error) { team, err := GetRow[DBTeam]( `SELECT @@ -79,6 +39,7 @@ func GetTeam(teamId pgtype.UUID) (DBTeam, error) { teamId) // `SELECT * FROM teams WHERE id = $1`, // teamId) + if err != nil { logger.Error("===DB/GetTeam error: ", err) return DBTeam{}, err @@ -87,6 +48,7 @@ func GetTeam(teamId pgtype.UUID) (DBTeam, error) { } func GetTeamByInvite(inviteCode string) (DBTeam, error) { + // "/invite/:invitecode" team, err := GetRow[DBTeam]( `SELECT teams.id, @@ -109,58 +71,239 @@ func GetTeamByInvite(inviteCode string) (DBTeam, error) { return team, nil } -func GetTeams() ([]DBTeam, error) { - result, err := GetRows[DBTeam](`SELECT * FROM teams`) - return result, err +func MapToTeamAndMember(data []DBTeamAndTeamMember) []TeamAndMembers { + // instantiates array to store output, mapped by team id (uuid) for key + teamMap := make(map[pgtype.UUID]*TeamAndMembers) + for _, item := range data { + // Check if team already exists in the map. This map loopkup returns: + // 1) value associated with the key if it exsits + // 2) boolean indicating whether key was found in the map + team, ok := teamMap[item.TeamId] + if !ok { + // Create a new team + team = &TeamAndMembers{ + UITeam: UITeam{ + Id: item.Id, + EventId: item.EventId, + Name: item.Name, + Visibility: item.Visibility, + Timezone: item.Timezone, + Technologies: item.Technologies, + Availability: item.Availability, + Description: item.Description, + CreatedOn: item.CreatedOn, + InviteCode: item.InviteCode, + }, + TeamMembers: []TeamMember{}, + } + teamMap[item.TeamId] = team + } + // Add team member to TeamMembers slice + member := TeamMember{ + UITeamMember: UITeamMember{ + TeamId: item.TeamId, + UserId: item.UserId, + MembershipCreatedOn: item.MembershipCreatedOn, + TeamRole: item.TeamRole, + }, + DisplayName: item.DisplayName, + AvatarId: item.AvatarId, + ServiceUserId: item.ServiceUserId, + } + team.TeamMembers = append(team.TeamMembers, member) + } + // Convert map back to slice + var result []TeamAndMembers + for _, team := range teamMap { + result = append(result, *team) + } + fmt.Println(result) + return result } -func GetUserTeams(userId pgtype.UUID) ([]DBUserTeams, error) { - result, err := GetRows[DBUserTeams]( - `SELECT +func GetTeams() (*[]TeamAndMembers, error) { + teamAndMember, err := GetRows[DBTeamAndTeamMember]( // returns { team 1: { userA: {display_name: "momo"}}, team 1...} + `SELECT + t.id, + t.event_id, + t.name, + t.visibility, + t.timezone, + t.technologies, + t.availability, + t.description, + t.created_on AS team_created_on, + t.invite_code, u.display_name, - t.*, + u.avatar_id, + u.service_user_id, + tm.team_id, + tm.user_id, + tm.created_on AS membership_created_on, tm.team_role - FROM users u - INNER JOIN team_members tm ON u.id = tm.user_id - INNER JOIN teams t ON tm.team_id = t.id - WHERE u.id = $1`, - userId) + FROM teams t + INNER JOIN team_members tm ON (tm.team_id = t.id) + INNER JOIN users u ON (u.id = tm.user_id) + ORDER BY t.id + `, + ) if err != nil { - fmt.Println("that didn't work: database.GetUserTeams") return nil, err } + for _, t := range teamAndMember { + fmt.Printf("%v\n", t) + } + UITeamAndMember := MapToTeamAndMember(teamAndMember) - return result, err // Try look at the table + return &UITeamAndMember, err +} + +func MapToUserTeamAndMember(userTeams *[]UserTeamAndMembers) []UserTeam { + // initialize a json dictionary + teamMap := make(map[pgtype.UUID]*UserTeam) + + // loop through all the rows from the query. + // UserTeams aka []UserTeam is a single row from query. + for _, ut := range *userTeams { + // If team doesn't exist in map, create it + if _, exists := teamMap[ut.Id]; !exists { + teamMap[ut.Id] = &UserTeam{ + Id: ut.Id, + EventId: ut.EventId, + Name: ut.Name, + Description: ut.Description, + Visibility: ut.Visibility, + Technologies: ut.Technologies, + Availability: ut.Availability, + TeamCreatedOn: ut.TeamCreatedOn, + InviteCode: ut.InviteCode, + TeamMembers: []UserTeamMember{}, + } + } + + // create a member + TeamMember := UserTeamMember{ + UserId: ut.UserId, + DisplayName: ut.DisplayName, + TeamRole: ut.TeamRole, + AvatarId: ut.AvatarId, + ServiceUserId: ut.ServiceUserId, + UserCreatedOn: ut.MembershipCreatedOn, + } + + // add member to teamMap dictionary + teamMap[ut.Id].TeamMembers = append(teamMap[ut.Id].TeamMembers, TeamMember) + } + + // Convert map to slice + teams := make([]UserTeam, 0, len(teamMap)) + for _, team := range teamMap { + teams = append(teams, *team) + } + return teams +} + +func GetUserTeams(userId pgtype.UUID) (*[]UserTeam, error) { + result, err := GetRows[UserTeamAndMembers]( + `SELECT + t.id, + t.event_id, + t.name, + t.visibility, + t.timezone, + t.technologies, + t.availability, + t.description, + t.created_on AS team_created_on, + t.invite_code, + tm.team_id, + tm.user_id, + tm.team_role, + tm.created_on AS membership_created_on, + tmu.display_name, + tmu.avatar_id, + tmu.service_user_id, + mtm.team_role AS current_user_role + FROM team_members mtm + INNER JOIN teams t ON t.id = mtm.team_id + INNER JOIN team_members tm ON t.id = tm.team_id + INNER JOIN users tmu ON tmu.id = tm.user_id + WHERE mtm.user_id = $1 + ORDER BY team_created_on`, + userId) + if err != nil { + logger.Error("failed to retrieve team info for user: %v", userId, err) + } + + UIUserTeamAndMember := MapToUserTeamAndMember(&result) + return &UIUserTeamAndMember, err } func UpdateTeam(team DBTeam) (DBTeam, error) { - event, err := GetRow[DBTeam]( + updatedTeam, err := GetRow[DBTeam]( `UPDATE teams SET name=$2, visibility=$3, timezone=$4, technologies=$5, availability=$6, - description=$7, + description=$7 WHERE id=$1 RETURNING *`, team.Id, team.Name, team.Visibility, team.Timezone, team.Technologies, team.Availability, team.Description) - return event, err + if err != nil { + return DBTeam{}, fmt.Errorf("failed to update team with ID %v: %w", team.Id, err) + } + return updatedTeam, err +} + +type DBTeamInviteCode struct { + InviteCode string `db:"invite_code"` +} + +func GetTeamInviteCode(teamId pgtype.UUID) (inviteCode DBTeamInviteCode, err error) { + fmt.Println(teamId) + teamInviteCode, err := GetRow[DBTeamInviteCode]( + `SELECT teams.invite_code + FROM teams + WHERE teams.id = $1`, teamId) + if err != nil { + logger.Error("failed to retrieve invite code for teamId %v: %w", teamId, err) + } + return teamInviteCode, err } // fields: userid, teamid, role // called at server/teams.go createTeam & when someone clicks "join team" // DONT MESS WITH BELOW. IT WORKS. func AddTeamMember(userId pgtype.UUID, teamUUID pgtype.UUID, role string) (userID pgtype.UUID, err error) { - fmt.Println("=== line 100 userId", userId) + // userId prints something like: {[22 162 173 240 222 76 79 42 174 62 196 207 243 22 25 78] true} + teamMember, err := GetRow[CreateTeamMember]( `INSERT INTO team_members (user_id, team_id, team_role) VALUES ($1, $2, $3) RETURNING user_id, team_id, team_role`, userId, teamUUID, role) + if err != nil { + fmt.Println(err) + return userId, err + } return teamMember.UserId, err } +func RemoveTeamMember(teamId pgtype.UUID, userId pgtype.UUID) (DBTeamMember, error) { + deletedMember, err := GetRow[DBTeamMember](` + DELETE FROM team_members + WHERE team_id = $1 AND user_id = $2 + RETURNING team_id, user_id, team_role, created_on AS user_created_on`, + teamId, userId) + if err != nil { + logger.Error("Failed to remove team member: %v", err) + return DBTeamMember{}, err + } + return deletedMember, nil +} + func GetMembersByTeamId(teamId pgtype.UUID) (*[]DBTeamMemberInfo, error) { // In Go, you never return slice-data. // Having * in sig means I'm returning the slice-header, which means I need & in my return diff --git a/backend/app/database/users.go b/backend/app/database/users.go index a991264..e2a2101 100644 --- a/backend/app/database/users.go +++ b/backend/app/database/users.go @@ -1,18 +1,19 @@ package database import ( + "fmt" "github.com/emicklei/pgtalk/convert" "github.com/jackc/pgx/v5/pgtype" ) type DBUser struct { - Id pgtype.UUID `db:"id" ` + Id pgtype.UUID `db:"id"` ServiceName string `db:"service_name"` ServiceUserId string `db:"service_user_id"` ServiceUserName string `db:"service_user_name"` Role string `db:"role"` DisplayName string `db:"display_name"` - AvatarUrl *string `db:"avatar_url"` + AvatarId *string `db:"avatar_id"` AccountStatus string `db:"account_status"` LockDisplayName bool `db:"lock_display_name"` CreatedOn pgtype.Timestamp `db:"created_on" json:"-"` @@ -23,16 +24,17 @@ const ( Admin = "ADMIN" ) -func CreateUser(serviceName string, serviceUserId string, serviceDisplayName string, avatarUrl string) DBUser { +func CreateUser(serviceName string, serviceUserId string, serviceDisplayName string, avatarId string) DBUser { user, err := GetRow[DBUser]( - `INSERT INTO users (service_name, service_user_id, service_user_name, display_name, avatar_url) - VALUES ($1, $2, $3, $3, $4) - ON CONFLICT (service_name, service_user_id) - DO UPDATE - SET service_user_name = $3, avatar_url = $4 - RETURNING *`, - serviceName, serviceUserId, serviceDisplayName, avatarUrl) + `INSERT INTO users (service_name, service_user_id, service_user_name, display_name, avatar_id) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (service_name, service_user_id) + DO UPDATE + SET service_user_name = $3, avatar_id = $5 + RETURNING service_name, service_user_id, display_name, avatar_id`, + serviceName, serviceUserId, serviceDisplayName, avatarId) if err != nil { + fmt.Println(serviceName, serviceUserId, serviceDisplayName, avatarId) logger.Error("error getting user: %v", err) } return user @@ -40,10 +42,9 @@ func CreateUser(serviceName string, serviceUserId string, serviceDisplayName str func GetUser(userId pgtype.UUID) (DBUser, error) { user, err := GetRow[DBUser]( - `SELECT - * - FROM users - WHERE id = $1`, + `SELECT * + FROM users + WHERE id = $1`, userId) if err != nil { logger.Error("error getting user: id: %s, error: %v", convert.UUIDToString(userId), err) @@ -54,9 +55,9 @@ func GetUser(userId pgtype.UUID) (DBUser, error) { func UpdateUser(user DBUser) (DBUser, error) { user, err := GetRow[DBUser]( `UPDATE users - SET display_name = $2 - WHERE id = $1 - RETURNING *`, + SET display_name = $2 + WHERE id = $1 + RETURNING *`, user.Id, user.DisplayName) if err != nil { logger.Error("error updating user: %v", err) diff --git a/backend/app/integrations/generic.go b/backend/app/integrations/generic.go index 88936d3..a651a15 100644 --- a/backend/app/integrations/generic.go +++ b/backend/app/integrations/generic.go @@ -14,7 +14,7 @@ type IntegrationUser struct { IntegrationName string UserId string ServiceUserName string - AvatarUrl string + AvatarId string } func getGitHubUser(accessToken string) *IntegrationUser { @@ -31,11 +31,14 @@ func getGitHubUser(accessToken string) *IntegrationUser { func getDiscordUser(accessToken string) *IntegrationUser { user := discord.GetUser(accessToken) + avatar, ok := user["avatar"].(string) + if !ok { + avatar = "" // Or set a default avatar URL if preferred + } if user == nil { logger.Error("User not found for token: %s", accessToken) return nil } else { - var avatar = "" if user["avatar"] != nil { avatar = user["avatar"].(string) } @@ -43,7 +46,7 @@ func getDiscordUser(accessToken string) *IntegrationUser { IntegrationName: "discord", UserId: user["id"].(string), ServiceUserName: user["global_name"].(string), - AvatarUrl: avatar, + AvatarId: avatar, } } } @@ -57,5 +60,4 @@ func GetUser(integrationName string, accessToken string) *IntegrationUser { default: return nil } - } diff --git a/backend/app/main.go b/backend/app/main.go index fbc6eaa..5dc2db1 100644 --- a/backend/app/main.go +++ b/backend/app/main.go @@ -1,14 +1,20 @@ package main import ( + "codejam.io/config" "codejam.io/database" + //"codejam.io/logging" + "flag" + "codejam.io/server" "github.com/gin-gonic/gin" ) func main() { + debugArg := flag.Bool("debug", false, "Enable debug mode.") + flag.Parse() // TODO setup logger // Disables the debug logging... Comment this out to enable debug logging for GIN @@ -30,6 +36,7 @@ func main() { server := server.Server{ Config: *config, + Debug: *debugArg, } server.StartServer() } diff --git a/backend/app/server/oauth.go b/backend/app/server/oauth.go index 1cb6359..00e85f2 100644 --- a/backend/app/server/oauth.go +++ b/backend/app/server/oauth.go @@ -1,6 +1,11 @@ package server import ( + "fmt" + "net/http" + "os" + "strings" + "codejam.io/database" "codejam.io/integrations" "github.com/emicklei/pgtalk/convert" @@ -8,13 +13,20 @@ import ( "github.com/gin-gonic/gin" "golang.org/x/oauth2" githubOAuth "golang.org/x/oauth2/github" - "net/http" - "os" - "strings" ) +type StateData struct { + Token string + Redirect string +} + // SetupOAuth initializes the OAuth provider specified in the application config. func (server *Server) SetupOAuth() { + if server.Debug { + logger.Warn("Debug mode is set. No OAuth Providers are set!") + return + } + var endpoint oauth2.Endpoint switch strings.ToLower(server.Config.OAuth.Provider) { @@ -40,13 +52,92 @@ func (server *Server) SetupOAuth() { } func (server *Server) GetOAuthRedirect(ctx *gin.Context) { - url := server.OAuth.AuthCodeURL(ctx.Request.Header.Get("Referer")) + session := sessions.Default(ctx) + token, err := GenerateToken(16) + + if err != nil { + ctx.String(500, "Internal Server Error") + } + + redirect := ctx.Query("redirect") + if redirect == "" { + redirect = "/" + } + + if !strings.HasPrefix(redirect, "/") { + redirect = "/" + } else if strings.HasPrefix(redirect, "/oauth") { + redirect = "/" + } else { + redirect = fmt.Sprintf("/#%s", redirect) + } + + state := StateData{Token: token, Redirect: redirect} + session.Set("state", state) + err = session.Save() + + if err != nil { + logger.Error("Error saving session: %v", err) + } + + if server.Debug { + ctx.Redirect(http.StatusFound, "/oauth/debug-login") + return + } + + url := server.OAuth.AuthCodeURL(token) ctx.Redirect(http.StatusFound, url) } +func (server *Server) GetDebugSession(ctx *gin.Context) { + redir := ctx.Query("state") + + dbUser := database.CreateUser("discord", "0", "DebugCow", "") + session := sessions.Default(ctx) + session.Set("userId", convert.UUIDToString(dbUser.Id)) + session.Set("displayName", dbUser.DisplayName) + + err := session.Save() + + if err != nil { + logger.Error("Error saving debug session: %v", err) + } + + ctx.Redirect(http.StatusFound, redir) +} + func (server *Server) GetOAuthCallback(ctx *gin.Context) { authCode := ctx.Query("code") - redir := ctx.Query("state") + stateCode := ctx.Query("state") + fmt.Println(stateCode) + + if len(stateCode) == 0 { + ctx.String(400, "Bad Request: Missing State Value.") + return + } + + session := sessions.Default(ctx) + stateI := session.Get("state") + + session.Clear() + session.Save() + + var stateData *StateData + // give the stateI a defined type/struct of stateData instead of interface{}/any: + stateData, ok := stateI.(*StateData) + if !ok { + ctx.String(400, "Bad Request: Invalid State Data.") + return + } + + if stateData.Token != stateCode { + logger.Error("Invalid state token provided.") + ctx.String(400, "Bad Request: Invalid State Code Provided.") + return + } + + redir := stateData.Redirect + token, err := server.OAuth.Exchange(oauth2.NoContext, authCode) if err != nil { // todo - can any of these be handled? @@ -57,8 +148,7 @@ func (server *Server) GetOAuthCallback(ctx *gin.Context) { integrationName := strings.ToLower(server.Config.OAuth.Provider) providerUser := integrations.GetUser(integrationName, token.AccessToken) if providerUser != nil { - dbUser := database.CreateUser(integrationName, providerUser.UserId, providerUser.ServiceUserName, providerUser.AvatarUrl) - session := sessions.Default(ctx) + dbUser := database.CreateUser(integrationName, providerUser.UserId, providerUser.ServiceUserName, providerUser.AvatarId) session.Set("userId", convert.UUIDToString(dbUser.Id)) session.Set("displayName", dbUser.DisplayName) err = session.Save() @@ -80,5 +170,6 @@ func (server *Server) SetupOAuthRoutes() { { group.GET("/redirect", server.GetOAuthRedirect) group.GET("/callback", server.GetOAuthCallback) + group.GET("/debug-login", server.GetDebugSession) } } diff --git a/backend/app/server/server.go b/backend/app/server/server.go index 177fbe6..1536db1 100644 --- a/backend/app/server/server.go +++ b/backend/app/server/server.go @@ -3,6 +3,7 @@ package server import ( "codejam.io/config" "codejam.io/logging" + "encoding/gob" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/redis" "github.com/gin-gonic/gin" @@ -18,6 +19,7 @@ type Server struct { Config config.Config OAuth *oauth2.Config Gin *gin.Engine + Debug bool } func (server *Server) SetupSessionStore() { @@ -46,6 +48,8 @@ func (server *Server) SetupSessionStore() { func (server *Server) StartServer() { logger.Info("Starting server...") + gob.Register(&StateData{}) + server.Gin = gin.Default() server.SetupSessionStore() diff --git a/backend/app/server/teams.go b/backend/app/server/teams.go index 0f030d7..df44c66 100644 --- a/backend/app/server/teams.go +++ b/backend/app/server/teams.go @@ -30,9 +30,23 @@ type CreateTeamRequest struct { } type GetTeamResponse struct { - Team *database.DBTeam - Event *database.DBEvent - Members *[]database.DBTeamMemberInfo // array(slice) of a struct + Team *database.DBTeam + Event *database.DBEvent + TeamMembers *[]database.DBTeamMemberInfo // array(slice) of a struct +} + +type InvitePayload struct { + TeamId string `json:"teamId"` + InviteCode string `json:"inviteCode"` +} + +type JoinPayload struct { + TeamId string `json:"teamId"` +} + +type MemberPayload struct { + TeamId string `json:"teamId"` + MemberId string `json:"memberId"` } func MD5HashCode(teamName string) (string, error) { @@ -58,26 +72,28 @@ func (server *Server) signupsAllowed(eventId string) bool { } } +// for path teams/browse func (server *Server) GetAllTeams(ctx *gin.Context) { + // what if there's no session => no user id teams, err := database.GetTeams() if err == nil { ctx.JSON(http.StatusOK, teams) } else { + fmt.Println("ERROR: ", err) ctx.Status(http.StatusInternalServerError) + return } } func (server *Server) GetUserTeams(ctx *gin.Context) { session := sessions.Default(ctx) userId := session.Get("userId") - strUserId := userId.(string) if userId == nil { ctx.Status(http.StatusNotFound) return } - // var teamResponse GetTeamResponse - // var teamsResponse []GetTeamResponse + strUserId := userId.(string) teams, err := database.GetUserTeams(convert.StringToUUID(strUserId)) if err != nil { @@ -85,62 +101,62 @@ func (server *Server) GetUserTeams(ctx *gin.Context) { return } - //1. join databse to return members ctx.JSON(http.StatusOK, teams) - - // add all the GetTeamResponse to []GetTeamResponse - // loop through teams, get team id - // assign each team to teamResponse type.. - } -// stepp 4: GET team info // purpose is to construct the DBTeamMemberInfo -func (server *Server) GetTeamInfo(ctx *gin.Context) { - id := convert.StringToUUID(ctx.Param("id")) +func (server *Server) GetTeamInfo(id pgtype.UUID) (*GetTeamResponse, error) { var teamResponse GetTeamResponse var team database.DBTeam var event database.DBEvent - var members *[]database.DBTeamMemberInfo //user info based on teamId - + var teamMembers *[]database.DBTeamMemberInfo //user info based on teamId + // fmt.Println("========id: ", id) prints: {[204 69 126 62 33 10 77 93 131 216 8 153 66 109 252 147] true} team, err := database.GetTeam(id) if err != nil { logger.Error("failed to get team: %v", err) - ctx.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get team: %v", err)}) - return + return nil, err } event, err = database.GetEvent(team.EventId) if err != nil { logger.Error("failed to get event: %v", err) - ctx.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get event: %v", err)}) - return + return nil, err } - members, err = database.GetMembersByTeamId(team.Id) + teamMembers, err = database.GetMembersByTeamId(team.Id) if err != nil { logger.Error("failed to get event: %v", err) - ctx.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get members: %v", err)}) - return + return nil, err } // attach all 3 structures to GetTeamResponse --> nested structs turn into nested JSON (with ctx.JSON) teamResponse.Team = &team teamResponse.Event = &event - teamResponse.Members = members + teamResponse.TeamMembers = teamMembers + + return &teamResponse, nil +} + +func (server *Server) sendTeamInfo(ctx *gin.Context) { + id := convert.StringToUUID(ctx.Param("id")) + + teamResponse, err := server.GetTeamInfo(id) + if err != nil { + ctx.Status(http.StatusBadRequest) + } + fmt.Printf("%+v\n", teamResponse.TeamMembers) ctx.JSON(http.StatusOK, teamResponse) } func (server *Server) GetTeamInfoByInviteCode(ctx *gin.Context) { inviteCode := ctx.Param("invitecode") - fmt.Println("\n===server getteam by invite code: ", inviteCode) var teamResponse GetTeamResponse var team database.DBTeam var event database.DBEvent - var members *[]database.DBTeamMemberInfo //user info based on teamId + var teamMembers *[]database.DBTeamMemberInfo //user info based on teamId team, err := database.GetTeamByInvite(inviteCode) if err != nil { @@ -158,7 +174,7 @@ func (server *Server) GetTeamInfoByInviteCode(ctx *gin.Context) { return } - members, err = database.GetMembersByTeamId(team.Id) + teamMembers, err = database.GetMembersByTeamId(team.Id) if err != nil { logger.Error("failed to get event: %v", err) ctx.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get members: %v", err)}) @@ -168,7 +184,7 @@ func (server *Server) GetTeamInfoByInviteCode(ctx *gin.Context) { // attach all 3 structures to GetTeamResponse --> nested structs turn into nested JSON (with ctx.JSON) teamResponse.Team = &team teamResponse.Event = &event - teamResponse.Members = members + teamResponse.TeamMembers = teamMembers fmt.Println(teamResponse) ctx.JSON(http.StatusOK, teamResponse) @@ -187,10 +203,7 @@ func (server *Server) CreateTeam(ctx *gin.Context) { var team database.DBTeam var teamReq CreateTeamRequest - // var tempMember CreateTeamMember - // shouldbindJSON binds the POST-req-JSON-info to the provided structure in () - // err should be (ctx feature) err := ctx.ShouldBindJSON(&teamReq) if err != nil { logger.Error("CreateTeam Request ShouldBindJSON error: %v", err) @@ -221,6 +234,7 @@ func (server *Server) CreateTeam(ctx *gin.Context) { team.InviteCode = md5code fmt.Printf("%+v", team) + // INSERTS TEAM into DB // PART 1/2 DONE teamUUID, err := database.CreateTeam(team) @@ -229,13 +243,13 @@ func (server *Server) CreateTeam(ctx *gin.Context) { ctx.Status(http.StatusBadRequest) return } + fmt.Println(convert.StringToUUID(strUserId), teamUUID) // PART 2/2 DONE // construct TeamMember _, err = database.AddTeamMember(convert.StringToUUID(strUserId), teamUUID, "owner") if err == nil { - fmt.Println("Successfully added team member") ctx.JSON(http.StatusCreated, map[string]pgtype.UUID{ "id": teamUUID, }) @@ -250,36 +264,146 @@ func (server *Server) CreateTeam(ctx *gin.Context) { func (server *Server) UpdateTeam(ctx *gin.Context) { session := sessions.Default(ctx) userId := session.Get("userId") + if userId != nil { var team database.DBTeam - err := ctx.ShouldBindJSON(&team) + err := ctx.ShouldBindJSON(&team) // "message incoming data to this struct" + if err != nil { logger.Error("UpdateEvent Request ShouldBindJSON error: %v", err) ctx.Status(http.StatusBadRequest) return } - team, err = database.UpdateTeam(team) + + updatedTeam, err := database.UpdateTeam(team) if err != nil { - logger.Error("Error calling database.UpdateEvent: %v", err) + logger.Error("Error calling database.UpdateTeam: %v", err) ctx.Status(http.StatusInternalServerError) } else { - ctx.JSON(http.StatusOK, team) + ctx.JSON(http.StatusOK, updatedTeam) } } else { ctx.Status(http.StatusUnauthorized) } } +func (server *Server) RemoveTeamMember(ctx *gin.Context) { + session := sessions.Default(ctx) + userId := session.Get("userId") + if userId == nil { + ctx.Status(http.StatusUnauthorized) + return + } + + var MemberPayload MemberPayload + + if err := ctx.ShouldBindJSON(&MemberPayload); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + uuidTeamId := convert.StringToUUID(MemberPayload.TeamId) + uuidMemberId := convert.StringToUUID(MemberPayload.MemberId) + + removedMember, err := database.RemoveTeamMember(uuidTeamId, uuidMemberId) + if err != nil { + fmt.Println("Error removing team member: ", err) + } + + strTeamId := convert.UUIDToString(removedMember.TeamId) + + ctx.JSON(http.StatusOK, strTeamId) +} + +func (server *Server) MemberJoin(ctx *gin.Context) { + session := sessions.Default(ctx) + userId := session.Get("userId") + + if userId == nil { + ctx.Status(http.StatusUnauthorized) + return + } + + strUserId := userId.(string) + uuidUserId := convert.StringToUUID(strUserId) + + var teamId JoinPayload + + if err := ctx.ShouldBindJSON(&teamId); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // teamId prints: {cc457e3e-210a-4d5d-83d8-0899426dfc93} + uuidTeamId := convert.StringToUUID(teamId.TeamId) + teamInfo, err := server.GetTeamInfo(uuidTeamId) + + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // verifies team is public + var isPublic string = teamInfo.Team.Visibility + if isPublic == "private" { + ctx.Status(http.StatusForbidden) + return + } + _, err = database.AddTeamMember(uuidUserId, uuidTeamId, "member") + if err != nil { + ctx.JSON(http.StatusConflict, err) + return + } + strTeamId := convert.UUIDToString(teamInfo.Team.Id) + ctx.JSON(http.StatusOK, strTeamId) +} + +func (server *Server) MemberInvite(ctx *gin.Context) { + session := sessions.Default(ctx) + userId := session.Get("userId") + + var payload InvitePayload + // bind the message context to the structure, and do an error check + if err := ctx.ShouldBindJSON(&payload); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + inviteCode := payload.InviteCode + teamId := payload.TeamId + uuidTeamId := convert.StringToUUID(teamId) + strUserId := userId.(string) + uuidUserId := convert.StringToUUID(strUserId) + + // I want to check if the inviteCode matches the teams invite code + dbInviteCode, err := database.GetTeamInviteCode(uuidTeamId) + if err != nil { + ctx.JSON(http.StatusBadRequest, inviteCode) + } + fmt.Println("server dbInviteCode: ", dbInviteCode) + if inviteCode != dbInviteCode.InviteCode { + fmt.Println("invalid request") + } else { + _, err = database.AddTeamMember(uuidUserId, uuidTeamId, "member") + if err != nil { + ctx.JSON(http.StatusConflict, err) + return + } + } +} + func (server *Server) SetupTeamRoutes() { group := server.Gin.Group("/team") { group.POST("/", server.CreateTeam) - group.GET("/", server.GetAllTeams) - group.GET("/:id", server.GetTeamInfo) + group.POST("/join", server.MemberJoin) + group.POST("/:invitecode", server.MemberInvite) + + group.GET("/:id", server.sendTeamInfo) group.GET("/invite/:invitecode", server.GetTeamInfoByInviteCode) - // group.PUT("/:id", server.UpdateTeam) - // Step 3: Post Team Data API - } + group.PUT("/edit/:teamid", server.UpdateTeam) // for admin to remove people + group.DELETE("/:teamid/member/:memberid", server.RemoveTeamMember) + } server.Gin.GET("/teams", server.GetUserTeams) // I think this works rofl + server.Gin.GET("/teams/browse", server.GetAllTeams) } diff --git a/backend/app/server/utils.go b/backend/app/server/utils.go new file mode 100644 index 0000000..147ed60 --- /dev/null +++ b/backend/app/server/utils.go @@ -0,0 +1,17 @@ +package server + +import ( + "crypto/rand" + "encoding/base64" +) + +func GenerateToken(length uint8) (string, error) { + token := make([]byte, length) + + _, err := rand.Read(token) + if err != nil { + return "", err + } + + return base64.URLEncoding.EncodeToString(token), nil +} diff --git a/html/src/App.svelte b/html/src/App.svelte index 308f017..88cef65 100644 --- a/html/src/App.svelte +++ b/html/src/App.svelte @@ -1,12 +1,10 @@ - - -
+
\ No newline at end of file diff --git a/html/src/lib/components/CreateTeamForm.svelte b/html/src/lib/components/CreateTeamForm.svelte index c1ab41e..e5d4e8a 100644 --- a/html/src/lib/components/CreateTeamForm.svelte +++ b/html/src/lib/components/CreateTeamForm.svelte @@ -1,22 +1,19 @@ @@ -95,7 +94,7 @@ function saveForm() {
Public Team - (If you want your team to be searchable) + (If you want your team to be searchable.)
Private Team @@ -117,7 +116,6 @@ function saveForm() { - + {#if loading} +

Loading...

+ {:else if error} +

{error}

{:else} -
- Must be logged in to join a team. -
- - - - {/if} - +

Join {teamData?.Name}

+ {#if $loggedInStore} +
+ {#if !isUserInTeam(teamMembers)} +
Hi {$userStore?.DisplayName},
+

Click below to join {teamMembers[0]?.DisplayName}'s team:

+ + {:else if isUserInTeam(teamMembers)} +

Hi {$userStore?.DisplayName}, Looks like you're already in this team! Go here to view it.

+ + {/if} +
+ {:else} +
+

Must be logged in to join a team.

+ +
+ {/if} + {/if} diff --git a/html/src/lib/pages/MyTeam.svelte b/html/src/lib/pages/MyTeam.svelte index 8fed596..0ac30ba 100644 --- a/html/src/lib/pages/MyTeam.svelte +++ b/html/src/lib/pages/MyTeam.svelte @@ -1,47 +1,93 @@ + - - Home - Team {teamData?.Name} + + Home + Team {teamData?.Name} @@ -50,20 +96,30 @@ {:else if error}
{error}
{:else if teamData !== null} -
{teamMembers[0]?.DisplayName}, your team has been successfully created!
+ {#if teamCreated} +
+ {teamMembers[0]?.DisplayName}, your team has been successfully created! +
+ {/if}

{teamEvent?.Title}

Team {teamData.Name}

- (edit) + {#if $userStore?.DisplayName == getTeamOwner(teamMembers)} + + (edit) + {/if}
+ + Owner: {getTeamOwner(teamMembers)} + Team Members: -
    - {#each teamMembers as member} + {#each teamMembers as member} +
    • {member.DisplayName}
    • - {/each} -
    +
+ {/each}
Visibility: {teamData.Visibility} @@ -77,7 +133,20 @@ Description: {teamData.Description} -

Invite Link: here

+ + Invite Link: + +
+ + +
{/if}
diff --git a/html/src/lib/pages/ProfilePage.svelte b/html/src/lib/pages/ProfilePage.svelte index 67f714d..02a66f9 100644 --- a/html/src/lib/pages/ProfilePage.svelte +++ b/html/src/lib/pages/ProfilePage.svelte @@ -1,25 +1,22 @@ - @@ -78,4 +74,4 @@ - \ No newline at end of file +
diff --git a/html/src/lib/pages/TeamEdit.svelte b/html/src/lib/pages/TeamEdit.svelte new file mode 100644 index 0000000..c8f9e93 --- /dev/null +++ b/html/src/lib/pages/TeamEdit.svelte @@ -0,0 +1,292 @@ + + + + + Home + Team Options + Edit Team + + +
+ {#if loading} +
Loading...
+ {:else if error} +
{error}
+ {:else if formData !== null} +
+
+ + + +
+ Public Team + (If you want your team to be searchable.) +
+
+ Private Team + (Your team will be invite only) +
+ + + + + + + + + + + + +
+

Team Members

+ + + Avatar + Username + Role + + Delete + + + + {#each teamTeamMembers as Member} + + + + + {Member.DisplayName} + {Member.TeamRole} + + {#if Member.TeamRole !== 'owner'} + + {/if} + + + {/each} + +
+ {:else} + + {/if} +
+
diff --git a/html/src/lib/pages/TeamsBrowse.svelte b/html/src/lib/pages/TeamsBrowse.svelte index dd87054..85d7030 100644 --- a/html/src/lib/pages/TeamsBrowse.svelte +++ b/html/src/lib/pages/TeamsBrowse.svelte @@ -1,32 +1,192 @@ - - - - -

Current Teams

-
-
- - -

Team 1

-

Join us, we're the best!

-

- - -
-
- -
- \ No newline at end of file + export const params: Record = {}; + + let teamData: CodeJamTeam | null = null; + let loading: boolean = true; + let error: string | null = null; + let allTeams: CodeJamTeam[] = []; + let publicTeams: CodeJamTeam[] = []; + let clickOutsideModal = false; + let avatarUrls: Record = {}; + $: currUserId = $userStore?.Id; + console.log('currUserId (teamsbrowse.svelte):', currUserId); + + interface ErrorResponse { + Severity: string; + Detail?: string; + Code: string; + Message: string; + Hint?: string; + Position?: number; + InternalPosition?: number; + InternalQuery?: string; + Where?: string; + SchemaName?: string; + } + + async function loadData() { + try { + const response = await getTeams(); + allTeams = await response.json(); // Array of teams... + if (allTeams == null) { + allTeams = []; + } + console.log('allTeams: ', allTeams); + } catch (err) { + error = `Failed to load team data: ${err}`; + } finally { + loading = false; + } + } + + async function getAvatarUrl(member: TeamMember): Promise { + let ext = member.AvatarId.startsWith('a_') ? '.gif' : '.png'; + return `https://cdn.discordapp.com/avatars/${member.ServiceUserId}/${member.AvatarId}${ext}`; + } + + async function loadAvatarUrls() { + let members: TeamMember[] = []; + + for (let team of allTeams) { + members.push(...team.TeamMembers); + } + + const promises = members.map(async (member) => { + const url = await getAvatarUrl(member); + avatarUrls[member.UserId] = url; + }); + + await Promise.all(promises); + } + + // to tell if join button works or not: + function isUserInTeam(teamMembers: TeamMember[]): boolean { + for (let teamMember of teamMembers) { + if (teamMember.UserId == currUserId) { + return true; + } + } + return false; + } + + // to display the owner the team + function getTeamOwner(teamMembers: TeamMember[]): string { + let owner = teamMembers.find((member) => member.TeamRole === 'owner'); + if (owner) { + return owner.DisplayName; + } else { + return 'No owner found.'; + } + } + + onMount(() => { + loadData(); + }); + + $: allTeams, loadAvatarUrls(); + $: publicTeams = allTeams.filter((t) => t.Visibility == 'public'); + + function isValidTeamId(resTeamId: string | ErrorResponse): resTeamId is string { + // Check if resTeamId is an object with a 'Severity' property indicating an error + if ( + typeof resTeamId === 'object' && + 'Severity' in resTeamId && + resTeamId.Severity === 'ERROR' + ) { + return false; // Not a valid Team ID + } + return true; // Valid Team ID + } + + + + +

Browse All Teams

+ + {#if loading} +
Loading...
+ {:else if error} +
{error}
+ {:else} + {#each publicTeams as Team} + +
+

Team {Team.Name}

+
+ + Owner: {getTeamOwner(Team.TeamMembers)} + + + Members: +
+ {#each Team.TeamMembers as Member} + + {/each} +
+
+ + + Visibility: {Team.Visibility} + + + Technologies: {Team.Technologies} + + + Availability: {Team.Availability} + + + Description: {Team.Description} + + + + {#if !$loggedInStore} + + +

+ Login with Discord to join a team! +

+
+ {:else if $loggedInStore} + {#if !isUserInTeam(Team.TeamMembers)} + + {:else if isUserInTeam(Team.TeamMembers)} + + {/if} + {/if} +
+ {:else} +
+ There's no existing teams. Be the first to create one! +
+ {/each} + {/if} +
+
diff --git a/html/src/lib/pages/UserTeams.svelte b/html/src/lib/pages/UserTeams.svelte index f7db226..567bdf3 100644 --- a/html/src/lib/pages/UserTeams.svelte +++ b/html/src/lib/pages/UserTeams.svelte @@ -1,112 +1,142 @@ - - + Home My Teams -

Your Teams

- +

Teams You Own

+ {#if loading}
Loading...
{:else if error}
{error}
- - {:else if userTeams.length == 0} -
- Looks like you don't have any teams! Go to browse teams to join one! -
- {:else if userTeams == null} -
- Error, please contact admin. -
- {:else} - hi - - {userTeams} - {#each userTeams as userTeam} - -
-

Team {userTeam.Name}

-
- - Visibility: {userTeam.Visibility} - - - Technologies: {userTeam.Technologies} - - - Availability: {userTeam.Availability} - - - Description: {userTeam.Description} - - - -
- - - {/each} + {:else if userTeams === null} +
You currently don't have any teams!
+ {:else if userTeams.length === 0} +
+ Looks like you don't have any teams. Go to browse teams to + join one or create your own team! +
+ {:else} + {#each userTeams as userTeam} + + {#if getTeamOwner(userTeam.TeamMembers) == $userStore?.DisplayName} +
+

{userTeam.Name}

+ Edit your team +
+ {:else} +
+

Team {userTeam.Name}

+
+ {/if} + + Owner: {getTeamOwner(userTeam.TeamMembers)} + + + Members: +
+ {#each userTeam.TeamMembers as Member} + + {/each} +
+
- + + Visibility: {userTeam.Visibility} + + + Technologies: {userTeam.Technologies} + + + Availability: {userTeam.Availability} + + + Description: {userTeam.Description} + +
+ {/each} {/if}
diff --git a/html/src/lib/pages/admin/EventList.svelte b/html/src/lib/pages/admin/EventList.svelte index 2c573e8..7b8a540 100644 --- a/html/src/lib/pages/admin/EventList.svelte +++ b/html/src/lib/pages/admin/EventList.svelte @@ -11,7 +11,7 @@ import {eventStatusStore} from "../../stores/stores"; let events : Array = new Array(); -let statuses = null +let statuses: any = null // Implicit any type? function editEvent(eventId: string) { window.location.href = '/#/admin/event/' + eventId; @@ -25,7 +25,7 @@ function getEventStatus(statusId: number): string { if (event !== null) { return event.Title; } - return ''; + return ''; // Should this be unindented? (In the outter scope) - Mysty {See line 20 : string} } } @@ -50,7 +50,7 @@ onMount(() => { -

My Events

+

Events

diff --git a/html/src/lib/services/services.ts b/html/src/lib/services/services.ts index 94fcd6e..2ac331a 100644 --- a/html/src/lib/services/services.ts +++ b/html/src/lib/services/services.ts @@ -6,7 +6,7 @@ import { } from "../stores/stores"; import CodeJamEvent from "../models/event"; import CodeJamTeam from "../models/team"; -import type {ActiveUser, User} from "../models/user"; +import type { ActiveUser, User } from "../models/user"; // This shouldn't ever need to be set since dev and prod environments will just use relative endpoints export let baseApiUrl: string = ""; @@ -32,12 +32,12 @@ export async function getUser() { .then((response) => { if (response.status === 401) { userStore.set(null); - activeUserStore.set({user: null, loggedIn: false}); + activeUserStore.set({ user: null, loggedIn: false }); } else { response.json() .then((data) => { userStore.set(data); - activeUserStore.set({user: data as User, loggedIn: true}) + activeUserStore.set({ user: data as User, loggedIn: true }) }) .catch((err) => { console.error("error deserializing user", err); @@ -65,7 +65,7 @@ export async function logout() { return fetch(baseApiUrl + "/user/logout") .then(() => { userStore.set(null); - activeUserStore.set({user: null, loggedIn: false}); + activeUserStore.set({ user: null, loggedIn: false }); }) .catch((err) => { console.error("Logout error", err); @@ -127,7 +127,36 @@ export async function postTeam(team: CodeJamTeam) { }); } -export async function getUserTeams(){ +export async function putTeam(team: CodeJamTeam) { + console.log("baseApiUrl:", baseApiUrl); + console.log("team.Id before URL construction:", team.Id); + + if (team.Id == undefined) { + console.log("team.Id is undefined:", team.Id); + } else { + console.log("team.Id is:", team.Id); + } + let teamid: string = team.Id + // Construct the URL using template literals and encodeURIComponent for safety + const url = `${baseApiUrl}/team/edit/${teamid}`; + console.log("Constructed PUT request URL:", url); + + const response = await fetch(url, { + method: "PUT", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(team) + }); + return response +} + +export async function getTeams() { + return await fetch(baseApiUrl + "/teams/browse") +} + + +export async function getUserTeams() { return fetch(baseApiUrl + "/teams"); } @@ -141,21 +170,65 @@ export async function getTeamByInvite(inviteCode: string) { } -// any one who has the link joinTeam connects to can join the team. +// any one who has a link joinTeam() works on, can join the team. // make sure invite_code matches -export async function joinTeam(team: CodeJamTeam, userId: string, invite_code: string) { + +export async function joinTeam(teamId: string, inviteCode: string) { // making a post to team_members - return await fetch(baseApiUrl + "/team/" + invite_code, + return await fetch(baseApiUrl + "/team/" + inviteCode, { method: "POST", - body: JSON.stringify({ team, userId, invite_code }) + body: JSON.stringify({ teamId, inviteCode }) } ) } + +// write a joinPublicTeam function that accepts team Id. +// server: check if team is public. +export async function joinPublicTeam(teamId: string) { + const response = await fetch(baseApiUrl + "/team/join", + { + method: "POST", + body: JSON.stringify({ teamId }) + } + ) + return response.json() +} + +export async function removeMemberFromTeam(teamId: string, memberId: string) { + // DELETE request to remove a member from a specific team + + const response = await fetch(baseApiUrl + `/team/${teamId}/member/${memberId}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ teamId, memberId }) + }); + + // Check if response status is OK (successful deletion) + if (response.ok) { + // Log the full response for debugging purposes + console.log("Full response:", response); + + // Check if the response has JSON content + const contentType = response.headers.get("content-type"); + if (contentType && contentType.includes("application/json")) { + const jsonResponse = await response.json(); + console.log("JSON Response:", jsonResponse); // Log the JSON response + return jsonResponse; + } + + console.log("Response has no JSON content, returning empty object."); + return {}; // Return an empty object if no JSON response is provided + } else { + throw new Error(`Failed to delete member: ${response.status} ${response.statusText}`); + } +} + // Always call at startup to get the initial states async function initialLoad() { await Promise.all([getUser(), getActiveEvent(), getEventStatuses()]); } - initialLoad(); \ No newline at end of file diff --git a/html/src/lib/stores/stores.ts b/html/src/lib/stores/stores.ts index 19fe274..e13b65c 100644 --- a/html/src/lib/stores/stores.ts +++ b/html/src/lib/stores/stores.ts @@ -8,11 +8,13 @@ export const activeContent = writable(''); /** * Deprecated - use activeUserStore.user instead */ + export const userStore = writable(null); /** * Deprecated - use activeUserStore.loggedIn instead */ + export const loggedInStore = derived(userStore, (userData) => userData != null); /** diff --git a/html/src/routes.ts b/html/src/routes.ts index d4681c8..5090528 100644 --- a/html/src/routes.ts +++ b/html/src/routes.ts @@ -9,20 +9,19 @@ import Invite from "./lib/pages/Invite.svelte"; import UserTeams from "./lib/pages/UserTeams.svelte"; import ProfilePage from "./lib/pages/ProfilePage.svelte"; import UserList from "./lib/pages/admin/UserList.svelte"; - +import TeamEdit from "./lib/pages/TeamEdit.svelte"; export default { '/': HomePage, '/home': HomePage, - '/team': TeamOptions, - '/team/:id': MyTeam, // link to one of your teams (sharable) We get an id here in this route... + '/team/:id': MyTeam, // link to one of your teams (sharable) We get an id here in this route... '/team/invite/:invitecode': Invite, // sharable '/teams': UserTeams, // displays all the user's teams (private) '/teams/browse': TeamsBrowse, '/teams/create': TeamsCreate, '/profile': ProfilePage, - + '/team/edit/:id': TeamEdit, '/admin/events': EventList, '/admin/event/:id': EventEdit, '/admin/users': UserList, diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a4d9579 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,250 @@ +{ + "name": "Codejam2024", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "svelte-french-toast": "^1.2.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "peer": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "peer": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "peer": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "peer": true + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/code-red": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", + "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", + "peer": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "@types/estree": "^1.0.1", + "acorn": "^8.10.0", + "estree-walker": "^3.0.3", + "periscopic": "^3.1.0" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "peer": true, + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "peer": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/is-reference": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", + "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", + "peer": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "peer": true + }, + "node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "peer": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "peer": true + }, + "node_modules/periscopic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", + "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", + "peer": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^3.0.0", + "is-reference": "^3.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svelte": { + "version": "4.2.19", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz", + "integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==", + "peer": true, + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@jridgewell/sourcemap-codec": "^1.4.15", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/estree": "^1.0.1", + "acorn": "^8.9.0", + "aria-query": "^5.3.0", + "axobject-query": "^4.0.0", + "code-red": "^1.0.3", + "css-tree": "^2.3.1", + "estree-walker": "^3.0.3", + "is-reference": "^3.0.1", + "locate-character": "^3.0.0", + "magic-string": "^0.30.4", + "periscopic": "^3.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/svelte-french-toast": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/svelte-french-toast/-/svelte-french-toast-1.2.0.tgz", + "integrity": "sha512-5PW+6RFX3xQPbR44CngYAP1Sd9oCq9P2FOox4FZffzJuZI2mHOB7q5gJBVnOiLF5y3moVGZ7u2bYt7+yPAgcEQ==", + "dependencies": { + "svelte-writable-derived": "^3.1.0" + }, + "peerDependencies": { + "svelte": "^3.57.0 || ^4.0.0" + } + }, + "node_modules/svelte-writable-derived": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/svelte-writable-derived/-/svelte-writable-derived-3.1.1.tgz", + "integrity": "sha512-w4LR6/bYZEuCs7SGr+M54oipk/UQKtiMadyOhW0PTwAtJ/Ai12QS77sLngEcfBx2q4H8ZBQucc9ktSA5sUGZWw==", + "funding": { + "url": "https://ko-fi.com/pixievoltno1" + }, + "peerDependencies": { + "svelte": "^3.2.1 || ^4.0.0-next.1 || ^5.0.0-next.94" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6b62c9f --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "svelte-french-toast": "^1.2.0" + } +}