diff options
| author | Nidboj132 <lol2s@vp.plm> | 2023-09-05 18:23:11 +0200 |
|---|---|---|
| committer | Nidboj132 <lol2s@vp.plm> | 2023-09-05 18:23:11 +0200 |
| commit | 3869cb67351ccf3bc45b076f31afdc7133292c39 (patch) | |
| tree | dc03341e147dde0964bf6be84b14e13424c647b7 /backend | |
| parent | added graph and fixed some css (diff) | |
| parent | fix: create map summary, why the fuck does this have to be a pointer integer?? (diff) | |
| download | lphub-3869cb67351ccf3bc45b076f31afdc7133292c39.tar.gz lphub-3869cb67351ccf3bc45b076f31afdc7133292c39.tar.bz2 lphub-3869cb67351ccf3bc45b076f31afdc7133292c39.zip | |
Merge branch 'main' of https://github.com/pektezol/LeastPortalsHub
Former-commit-id: 221385f463b7f5b0fc43a093b2c7c46e68d46d68
Diffstat (limited to 'backend')
| -rw-r--r-- | backend/api/auth.go (renamed from backend/middleware/auth.go) | 17 | ||||
| -rw-r--r-- | backend/api/routes.go | 41 | ||||
| -rw-r--r-- | backend/controllers/userController.go | 286 | ||||
| -rw-r--r-- | backend/database/history.sql | 522 | ||||
| -rw-r--r-- | backend/database/init.sql | 27 | ||||
| -rw-r--r-- | backend/database/maps.sql | 4 | ||||
| -rw-r--r-- | backend/handlers/home.go (renamed from backend/controllers/homeController.go) | 169 | ||||
| -rw-r--r-- | backend/handlers/login.go (renamed from backend/controllers/loginController.go) | 34 | ||||
| -rw-r--r-- | backend/handlers/logs.go | 189 | ||||
| -rw-r--r-- | backend/handlers/map.go (renamed from backend/controllers/mapController.go) | 131 | ||||
| -rw-r--r-- | backend/handlers/mod.go (renamed from backend/controllers/modController.go) | 113 | ||||
| -rw-r--r-- | backend/handlers/record.go (renamed from backend/controllers/recordController.go) | 37 | ||||
| -rw-r--r-- | backend/handlers/user.go | 719 | ||||
| -rw-r--r-- | backend/models/models.go | 63 | ||||
| -rw-r--r-- | backend/models/requests.go | 39 | ||||
| -rw-r--r-- | backend/models/responses.go | 64 | ||||
| -rw-r--r-- | backend/routes/routes.go | 41 |
17 files changed, 1576 insertions, 920 deletions
diff --git a/backend/middleware/auth.go b/backend/api/auth.go index 0744b3d..91ef80c 100644 --- a/backend/middleware/auth.go +++ b/backend/api/auth.go | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | package middleware | 1 | package api |
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "fmt" | 4 | "fmt" |
| @@ -16,7 +16,7 @@ func CheckAuth(c *gin.Context) { | |||
| 16 | // Validate token | 16 | // Validate token |
| 17 | token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { | 17 | token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { |
| 18 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { | 18 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { |
| 19 | return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) | 19 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) |
| 20 | } | 20 | } |
| 21 | return []byte(os.Getenv("SECRET_KEY")), nil | 21 | return []byte(os.Getenv("SECRET_KEY")), nil |
| 22 | }) | 22 | }) |
| @@ -44,14 +44,19 @@ func CheckAuth(c *gin.Context) { | |||
| 44 | return | 44 | return |
| 45 | } | 45 | } |
| 46 | // Get user titles from DB | 46 | // Get user titles from DB |
| 47 | user.Titles = []string{} | 47 | var moderator bool |
| 48 | rows, _ := database.DB.Query(`SELECT t.title_name FROM titles t WHERE t.user_id = $1`, user.SteamID) | 48 | user.Titles = []models.Title{} |
| 49 | rows, _ := database.DB.Query(`SELECT t.title_name, t.title_color FROM titles t INNER JOIN user_titles ut ON t.id=ut.title_id WHERE ut.user_id = $1`, user.SteamID) | ||
| 49 | for rows.Next() { | 50 | for rows.Next() { |
| 50 | var title string | 51 | var title models.Title |
| 51 | rows.Scan(&title) | 52 | rows.Scan(&title.Name, &title.Color) |
| 53 | if title.Name == "Moderator" { | ||
| 54 | moderator = true | ||
| 55 | } | ||
| 52 | user.Titles = append(user.Titles, title) | 56 | user.Titles = append(user.Titles, title) |
| 53 | } | 57 | } |
| 54 | c.Set("user", user) | 58 | c.Set("user", user) |
| 59 | c.Set("mod", moderator) | ||
| 55 | c.Next() | 60 | c.Next() |
| 56 | } else { | 61 | } else { |
| 57 | c.Next() | 62 | c.Next() |
diff --git a/backend/api/routes.go b/backend/api/routes.go new file mode 100644 index 0000000..fd3b8cc --- /dev/null +++ b/backend/api/routes.go | |||
| @@ -0,0 +1,41 @@ | |||
| 1 | package api | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "github.com/gin-gonic/gin" | ||
| 5 | "github.com/pektezol/leastportalshub/backend/handlers" | ||
| 6 | swaggerfiles "github.com/swaggo/files" | ||
| 7 | ginSwagger "github.com/swaggo/gin-swagger" | ||
| 8 | ) | ||
| 9 | |||
| 10 | func InitRoutes(router *gin.Engine) { | ||
| 11 | api := router.Group("/api") | ||
| 12 | { | ||
| 13 | v1 := api.Group("/v1") | ||
| 14 | v1.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) | ||
| 15 | v1.GET("/", func(c *gin.Context) { | ||
| 16 | c.File("docs/index.html") | ||
| 17 | }) | ||
| 18 | v1.GET("/token", handlers.GetCookie) | ||
| 19 | v1.DELETE("/token", handlers.DeleteCookie) | ||
| 20 | v1.GET("/login", handlers.Login) | ||
| 21 | v1.GET("/profile", CheckAuth, handlers.Profile) | ||
| 22 | v1.PUT("/profile", CheckAuth, handlers.UpdateCountryCode) | ||
| 23 | v1.POST("/profile", CheckAuth, handlers.UpdateUser) | ||
| 24 | v1.GET("/users/:id", CheckAuth, handlers.FetchUser) | ||
| 25 | v1.GET("/demos", handlers.DownloadDemoWithID) | ||
| 26 | v1.GET("/maps/:id/summary", handlers.FetchMapSummary) | ||
| 27 | v1.POST("/maps/:id/summary", CheckAuth, handlers.CreateMapSummary) | ||
| 28 | v1.PUT("/maps/:id/summary", CheckAuth, handlers.EditMapSummary) | ||
| 29 | v1.DELETE("/maps/:id/summary", CheckAuth, handlers.DeleteMapSummary) | ||
| 30 | v1.PUT("/maps/:id/image", CheckAuth, handlers.EditMapImage) | ||
| 31 | v1.GET("/maps/:id/leaderboards", handlers.FetchMapLeaderboards) | ||
| 32 | v1.POST("/maps/:id/record", CheckAuth, handlers.CreateRecordWithDemo) | ||
| 33 | v1.GET("/rankings", handlers.Rankings) | ||
| 34 | v1.GET("/search", handlers.SearchWithQuery) | ||
| 35 | v1.GET("/games", handlers.FetchGames) | ||
| 36 | v1.GET("/games/:id", handlers.FetchChapters) | ||
| 37 | v1.GET("/chapters/:id", handlers.FetchChapterMaps) | ||
| 38 | v1.GET("/logs/score", handlers.ScoreLogs) | ||
| 39 | v1.GET("/logs/mod", CheckAuth, handlers.ModLogs) | ||
| 40 | } | ||
| 41 | } | ||
diff --git a/backend/controllers/userController.go b/backend/controllers/userController.go deleted file mode 100644 index 6aa77fc..0000000 --- a/backend/controllers/userController.go +++ /dev/null | |||
| @@ -1,286 +0,0 @@ | |||
| 1 | package controllers | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "net/http" | ||
| 5 | "os" | ||
| 6 | "regexp" | ||
| 7 | "time" | ||
| 8 | |||
| 9 | "github.com/gin-gonic/gin" | ||
| 10 | "github.com/pektezol/leastportalshub/backend/database" | ||
| 11 | "github.com/pektezol/leastportalshub/backend/models" | ||
| 12 | ) | ||
| 13 | |||
| 14 | // GET Profile | ||
| 15 | // | ||
| 16 | // @Description Get profile page of session user. | ||
| 17 | // @Tags users | ||
| 18 | // @Accept json | ||
| 19 | // @Produce json | ||
| 20 | // @Param Authorization header string true "JWT Token" | ||
| 21 | // @Success 200 {object} models.Response{data=models.ProfileResponse} | ||
| 22 | // @Failure 400 {object} models.Response | ||
| 23 | // @Failure 401 {object} models.Response | ||
| 24 | // @Router /profile [get] | ||
| 25 | func Profile(c *gin.Context) { | ||
| 26 | // Check if user exists | ||
| 27 | user, exists := c.Get("user") | ||
| 28 | if !exists { | ||
| 29 | c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) | ||
| 30 | return | ||
| 31 | } | ||
| 32 | // Retrieve singleplayer records | ||
| 33 | var scoresSP []models.ScoreResponse | ||
| 34 | sql := `SELECT id, map_id, score_count, score_time, demo_id, record_date FROM records_sp WHERE user_id = $1 ORDER BY map_id` | ||
| 35 | rows, err := database.DB.Query(sql, user.(models.User).SteamID) | ||
| 36 | if err != nil { | ||
| 37 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 38 | return | ||
| 39 | } | ||
| 40 | var recordsSP []models.RecordSP | ||
| 41 | for rows.Next() { | ||
| 42 | var mapID int | ||
| 43 | var record models.RecordSP | ||
| 44 | rows.Scan(&record.RecordID, &mapID, &record.ScoreCount, &record.ScoreTime, &record.DemoID, &record.RecordDate) | ||
| 45 | // More than one record in one map | ||
| 46 | if len(scoresSP) != 0 && mapID == scoresSP[len(scoresSP)-1].MapID { | ||
| 47 | scoresSP[len(scoresSP)-1].Records = append(scoresSP[len(scoresSP)-1].Records.([]models.RecordSP), record) | ||
| 48 | continue | ||
| 49 | } | ||
| 50 | // New map | ||
| 51 | recordsSP = []models.RecordSP{} | ||
| 52 | recordsSP = append(recordsSP, record) | ||
| 53 | scoresSP = append(scoresSP, models.ScoreResponse{ | ||
| 54 | MapID: mapID, | ||
| 55 | Records: recordsSP, | ||
| 56 | }) | ||
| 57 | } | ||
| 58 | // Retrieve multiplayer records | ||
| 59 | var scoresMP []models.ScoreResponse | ||
| 60 | sql = `SELECT id, map_id, host_id, partner_id, score_count, score_time, host_demo_id, partner_demo_id, record_date FROM records_mp | ||
| 61 | WHERE host_id = $1 OR partner_id = $2 ORDER BY map_id` | ||
| 62 | rows, err = database.DB.Query(sql, user.(models.User).SteamID, user.(models.User).SteamID) | ||
| 63 | if err != nil { | ||
| 64 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 65 | return | ||
| 66 | } | ||
| 67 | var recordsMP []models.RecordMP | ||
| 68 | for rows.Next() { | ||
| 69 | var mapID int | ||
| 70 | var record models.RecordMP | ||
| 71 | rows.Scan(&record.RecordID, &mapID, &record.HostID, &record.PartnerID, &record.ScoreCount, &record.ScoreTime, &record.HostDemoID, &record.PartnerDemoID, &record.RecordDate) | ||
| 72 | // More than one record in one map | ||
| 73 | if len(scoresMP) != 0 && mapID == scoresMP[len(scoresMP)-1].MapID { | ||
| 74 | scoresMP[len(scoresMP)-1].Records = append(scoresMP[len(scoresMP)-1].Records.([]models.RecordMP), record) | ||
| 75 | continue | ||
| 76 | } | ||
| 77 | // New map | ||
| 78 | recordsMP = []models.RecordMP{} | ||
| 79 | recordsMP = append(recordsMP, record) | ||
| 80 | scoresMP = append(scoresMP, models.ScoreResponse{ | ||
| 81 | MapID: mapID, | ||
| 82 | Records: recordsMP, | ||
| 83 | }) | ||
| 84 | } | ||
| 85 | c.JSON(http.StatusOK, models.Response{ | ||
| 86 | Success: true, | ||
| 87 | Message: "Successfully retrieved user scores.", | ||
| 88 | Data: models.ProfileResponse{ | ||
| 89 | Profile: true, | ||
| 90 | SteamID: user.(models.User).SteamID, | ||
| 91 | UserName: user.(models.User).UserName, | ||
| 92 | AvatarLink: user.(models.User).AvatarLink, | ||
| 93 | CountryCode: user.(models.User).CountryCode, | ||
| 94 | ScoresSP: scoresSP, | ||
| 95 | ScoresMP: scoresMP, | ||
| 96 | }, | ||
| 97 | }) | ||
| 98 | return | ||
| 99 | } | ||
| 100 | |||
| 101 | // GET User | ||
| 102 | // | ||
| 103 | // @Description Get profile page of another user. | ||
| 104 | // @Tags users | ||
| 105 | // @Accept json | ||
| 106 | // @Produce json | ||
| 107 | // @Param id path int true "User ID" | ||
| 108 | // @Success 200 {object} models.Response{data=models.ProfileResponse} | ||
| 109 | // @Failure 400 {object} models.Response | ||
| 110 | // @Failure 404 {object} models.Response | ||
| 111 | // @Router /users/{id} [get] | ||
| 112 | func FetchUser(c *gin.Context) { | ||
| 113 | id := c.Param("id") | ||
| 114 | // Check if id is all numbers and 17 length | ||
| 115 | match, _ := regexp.MatchString("^[0-9]{17}$", id) | ||
| 116 | if !match { | ||
| 117 | c.JSON(http.StatusNotFound, models.ErrorResponse("User not found.")) | ||
| 118 | return | ||
| 119 | } | ||
| 120 | // Check if user exists | ||
| 121 | var user models.User | ||
| 122 | err := database.DB.QueryRow(`SELECT * FROM users WHERE steam_id = $1`, id).Scan( | ||
| 123 | &user.SteamID, &user.UserName, &user.AvatarLink, &user.CountryCode, | ||
| 124 | &user.CreatedAt, &user.UpdatedAt) | ||
| 125 | if user.SteamID == "" { | ||
| 126 | // User does not exist | ||
| 127 | c.JSON(http.StatusNotFound, models.ErrorResponse("User not found.")) | ||
| 128 | return | ||
| 129 | } | ||
| 130 | if err != nil { | ||
| 131 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 132 | return | ||
| 133 | } | ||
| 134 | // Retrieve singleplayer records | ||
| 135 | var scoresSP []models.ScoreResponse | ||
| 136 | sql := `SELECT id, map_id, score_count, score_time, demo_id, record_date FROM records_sp WHERE user_id = $1 ORDER BY map_id` | ||
| 137 | rows, err := database.DB.Query(sql, user.SteamID) | ||
| 138 | if err != nil { | ||
| 139 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 140 | return | ||
| 141 | } | ||
| 142 | var recordsSP []models.RecordSP | ||
| 143 | for rows.Next() { | ||
| 144 | var mapID int | ||
| 145 | var record models.RecordSP | ||
| 146 | rows.Scan(&record.RecordID, &mapID, &record.ScoreCount, &record.ScoreTime, &record.DemoID, &record.RecordDate) | ||
| 147 | // More than one record in one map | ||
| 148 | if len(scoresSP) != 0 && mapID == scoresSP[len(scoresSP)-1].MapID { | ||
| 149 | scoresSP[len(scoresSP)-1].Records = append(scoresSP[len(scoresSP)-1].Records.([]models.RecordSP), record) | ||
| 150 | continue | ||
| 151 | } | ||
| 152 | // New map | ||
| 153 | recordsSP = []models.RecordSP{} | ||
| 154 | recordsSP = append(recordsSP, record) | ||
| 155 | scoresSP = append(scoresSP, models.ScoreResponse{ | ||
| 156 | MapID: mapID, | ||
| 157 | Records: recordsSP, | ||
| 158 | }) | ||
| 159 | } | ||
| 160 | // Retrieve multiplayer records | ||
| 161 | var scoresMP []models.ScoreResponse | ||
| 162 | sql = `SELECT id, map_id, host_id, partner_id, score_count, score_time, host_demo_id, partner_demo_id, record_date FROM records_mp | ||
| 163 | WHERE host_id = $1 OR partner_id = $2 ORDER BY map_id` | ||
| 164 | rows, err = database.DB.Query(sql, user.SteamID, user.SteamID) | ||
| 165 | if err != nil { | ||
| 166 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 167 | return | ||
| 168 | } | ||
| 169 | var recordsMP []models.RecordMP | ||
| 170 | for rows.Next() { | ||
| 171 | var mapID int | ||
| 172 | var record models.RecordMP | ||
| 173 | rows.Scan(&record.RecordID, &mapID, &record.HostID, &record.PartnerID, &record.ScoreCount, &record.ScoreTime, &record.HostDemoID, &record.PartnerDemoID, &record.RecordDate) | ||
| 174 | // More than one record in one map | ||
| 175 | if len(scoresMP) != 0 && mapID == scoresMP[len(scoresMP)-1].MapID { | ||
| 176 | scoresMP[len(scoresMP)-1].Records = append(scoresMP[len(scoresMP)-1].Records.([]models.RecordMP), record) | ||
| 177 | continue | ||
| 178 | } | ||
| 179 | // New map | ||
| 180 | recordsMP = []models.RecordMP{} | ||
| 181 | recordsMP = append(recordsMP, record) | ||
| 182 | scoresMP = append(scoresMP, models.ScoreResponse{ | ||
| 183 | MapID: mapID, | ||
| 184 | Records: recordsMP, | ||
| 185 | }) | ||
| 186 | } | ||
| 187 | c.JSON(http.StatusOK, models.Response{ | ||
| 188 | Success: true, | ||
| 189 | Message: "Successfully retrieved user scores.", | ||
| 190 | Data: models.ProfileResponse{ | ||
| 191 | Profile: true, | ||
| 192 | SteamID: user.SteamID, | ||
| 193 | UserName: user.UserName, | ||
| 194 | AvatarLink: user.AvatarLink, | ||
| 195 | CountryCode: user.CountryCode, | ||
| 196 | ScoresSP: scoresSP, | ||
| 197 | ScoresMP: scoresMP, | ||
| 198 | }, | ||
| 199 | }) | ||
| 200 | return | ||
| 201 | } | ||
| 202 | |||
| 203 | // PUT Profile | ||
| 204 | // | ||
| 205 | // @Description Update profile page of session user. | ||
| 206 | // @Tags users | ||
| 207 | // @Accept json | ||
| 208 | // @Produce json | ||
| 209 | // @Param Authorization header string true "JWT Token" | ||
| 210 | // @Success 200 {object} models.Response{data=models.ProfileResponse} | ||
| 211 | // @Failure 400 {object} models.Response | ||
| 212 | // @Failure 401 {object} models.Response | ||
| 213 | // @Router /profile [post] | ||
| 214 | func UpdateUser(c *gin.Context) { | ||
| 215 | // Check if user exists | ||
| 216 | user, exists := c.Get("user") | ||
| 217 | if !exists { | ||
| 218 | c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) | ||
| 219 | return | ||
| 220 | } | ||
| 221 | profile, err := GetPlayerSummaries(user.(models.User).SteamID, os.Getenv("API_KEY")) | ||
| 222 | if err != nil { | ||
| 223 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 224 | return | ||
| 225 | } | ||
| 226 | // Update profile | ||
| 227 | _, err = database.DB.Exec(`UPDATE users SET username = $1, avatar_link = $2, country_code = $3, updated_at = $4 | ||
| 228 | WHERE steam_id = $5`, profile.PersonaName, profile.AvatarFull, profile.LocCountryCode, time.Now().UTC(), user.(models.User).SteamID) | ||
| 229 | if err != nil { | ||
| 230 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 231 | return | ||
| 232 | } | ||
| 233 | c.JSON(http.StatusOK, models.Response{ | ||
| 234 | Success: true, | ||
| 235 | Message: "Successfully updated user.", | ||
| 236 | Data: models.ProfileResponse{ | ||
| 237 | Profile: true, | ||
| 238 | SteamID: user.(models.User).SteamID, | ||
| 239 | UserName: profile.PersonaName, | ||
| 240 | AvatarLink: profile.AvatarFull, | ||
| 241 | CountryCode: profile.LocCountryCode, | ||
| 242 | }, | ||
| 243 | }) | ||
| 244 | } | ||
| 245 | |||
| 246 | // PUT Profile/CountryCode | ||
| 247 | // | ||
| 248 | // @Description Update country code of session user. | ||
| 249 | // @Tags users | ||
| 250 | // @Accept json | ||
| 251 | // @Produce json | ||
| 252 | // @Param Authorization header string true "JWT Token" | ||
| 253 | // @Param country_code query string true "Country Code [XX]" | ||
| 254 | // @Success 200 {object} models.Response | ||
| 255 | // @Failure 400 {object} models.Response | ||
| 256 | // @Failure 401 {object} models.Response | ||
| 257 | // @Router /profile [put] | ||
| 258 | func UpdateCountryCode(c *gin.Context) { | ||
| 259 | // Check if user exists | ||
| 260 | user, exists := c.Get("user") | ||
| 261 | if !exists { | ||
| 262 | c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) | ||
| 263 | return | ||
| 264 | } | ||
| 265 | code := c.Query("country_code") | ||
| 266 | if code == "" { | ||
| 267 | c.JSON(http.StatusNotFound, models.ErrorResponse("Enter a valid country code.")) | ||
| 268 | return | ||
| 269 | } | ||
| 270 | var validCode string | ||
| 271 | err := database.DB.QueryRow(`SELECT country_code FROM countries WHERE country_code = $1`, code).Scan(&validCode) | ||
| 272 | if err != nil { | ||
| 273 | c.JSON(http.StatusNotFound, models.ErrorResponse(err.Error())) | ||
| 274 | return | ||
| 275 | } | ||
| 276 | // Valid code, update profile | ||
| 277 | _, err = database.DB.Exec(`UPDATE users SET country_code = $1 WHERE steam_id = $2`, validCode, user.(models.User).SteamID) | ||
| 278 | if err != nil { | ||
| 279 | c.JSON(http.StatusNotFound, models.ErrorResponse(err.Error())) | ||
| 280 | return | ||
| 281 | } | ||
| 282 | c.JSON(http.StatusOK, models.Response{ | ||
| 283 | Success: true, | ||
| 284 | Message: "Successfully updated country code.", | ||
| 285 | }) | ||
| 286 | } | ||
diff --git a/backend/database/history.sql b/backend/database/history.sql index 320d72f..b30d10d 100644 --- a/backend/database/history.sql +++ b/backend/database/history.sql | |||
| @@ -1,279 +1,279 @@ | |||
| 1 | INSERT INTO map_history(map_id,category_id,user_name,score_count,record_date) VALUES | 1 | INSERT INTO map_history(map_id,category_id,user_name,score_count,record_date) VALUES |
| 2 | -- Portal 2 Singleplayer | 2 | -- Portal 2 Singleplayer |
| 3 | -- 1 | 3 | -- 1 |
| 4 | (3,1,'slmid1995',3,'2011-10-05 00:00:00'), | 4 | (3,1,'slmid1995',3,'2011-10-05'), |
| 5 | (3,1,'LookLikeAKango',1,'2011-10-06 00:00:00'), | 5 | (3,1,'LookLikeAKango',1,'2011-10-06'), |
| 6 | (3,1,'Bananasaurus Rex',0,'2011-10-24 00:00:00'), | 6 | (3,1,'Bananasaurus Rex',0,'2011-10-24'), |
| 7 | (4,1,'Tyronis',1,'2011-10-05 00:00:00'), | 7 | (4,1,'Tyronis',1,'2011-10-05'), |
| 8 | (4,1,'Krzyhau',0,'2019-05-10 00:00:00'), | 8 | (4,1,'Krzyhau',0,'2019-05-10'), |
| 9 | (5,1,'LookLikeAKango',2,'2011-10-05 00:00:00'), | 9 | (5,1,'LookLikeAKango',2,'2011-10-05'), |
| 10 | (5,1,'Jetwash',1,'2013-12-03 00:00:00'), | 10 | (5,1,'Jetwash',1,'2013-12-03'), |
| 11 | (6,1,'Stimich',4,'2011-10-08 00:00:00'), | 11 | (6,1,'Stimich',4,'2011-10-08'), |
| 12 | (6,1,'aepaePolakrn',3,'2011-10-19 00:00:00'), | 12 | (6,1,'aepaePolakrn',3,'2011-10-19'), |
| 13 | (6,1,'Krzyhau',2,'2020-10-10 00:00:00'), | 13 | (6,1,'Krzyhau',2,'2020-10-10'), |
| 14 | (9,1,'slmid1995',4,'2011-10-05 00:00:00'), | 14 | (9,1,'slmid1995',4,'2011-10-05'), |
| 15 | (9,1,'Jokie',3,'2011-10-05 00:00:00'), | 15 | (9,1,'Jokie',3,'2011-10-05'), |
| 16 | (9,1,'Tyronis',2,'2011-10-05 00:00:00'), | 16 | (9,1,'Tyronis',2,'2011-10-05'), |
| 17 | (9,1,'sicklebrick',0,'2013-03-13 00:00:00'), | 17 | (9,1,'sicklebrick',0,'2013-03-13'), |
| 18 | -- 2 | 18 | -- 2 |
| 19 | (10,1,'Paraxade0',2,'2011-04-21 00:00:00'), | 19 | (10,1,'Paraxade0',2,'2011-04-21'), |
| 20 | (10,1,'PerOculos',0,'2011-04-21 00:00:00'), | 20 | (10,1,'PerOculos',0,'2011-04-21'), |
| 21 | (11,1,'Tyronis',2,'2011-10-05 00:00:00'), | 21 | (11,1,'Tyronis',2,'2011-10-05'), |
| 22 | (11,1,'Krzyhau',0,'2018-06-09 00:00:00'), | 22 | (11,1,'Krzyhau',0,'2018-06-09'), |
| 23 | (12,1,'slmid1995',2,'2011-10-04 00:00:00'), | 23 | (12,1,'slmid1995',2,'2011-10-04'), |
| 24 | (13,1,'LookLikeAKango',3,'2011-10-05 00:00:00'), | 24 | (13,1,'LookLikeAKango',3,'2011-10-05'), |
| 25 | (13,1,'Imanex',2,'2011-12-08 00:00:00'), | 25 | (13,1,'Imanex',2,'2011-12-08'), |
| 26 | (13,1,'jyjey',0,'2012-08-22 00:00:00'), | 26 | (13,1,'jyjey',0,'2012-08-22'), |
| 27 | (15,1,'Tyronis',2,'2011-10-05 00:00:00'), | 27 | (15,1,'Tyronis',2,'2011-10-05'), |
| 28 | (16,1,'LookLikeAKango',2,'2011-10-05 00:00:00'), | 28 | (16,1,'LookLikeAKango',2,'2011-10-05'), |
| 29 | (16,1,'jyjey',0,'2012-08-25 00:00:00'), | 29 | (16,1,'jyjey',0,'2012-08-25'), |
| 30 | (17,1,'rocoty',2,'2011-10-05 00:00:00'), | 30 | (17,1,'rocoty',2,'2011-10-05'), |
| 31 | (17,1,'Nidboj132',0,'2023-02-05 00:00:00'), | 31 | (17,1,'Nidboj132',0,'2023-02-05'), |
| 32 | -- 3 | 32 | -- 3 |
| 33 | (18,1,'The Last Tofus',5,'2011-05-08 00:00:00'), | 33 | (18,1,'The Last Tofus',5,'2011-05-08'), |
| 34 | (18,1,'Schlepian',4,'2011-10-08 00:00:00'), | 34 | (18,1,'Schlepian',4,'2011-10-08'), |
| 35 | (18,1,'szeimartin',3,'2013-10-08 00:00:00'), | 35 | (18,1,'szeimartin',3,'2013-10-08'), |
| 36 | (18,1,'Krzyhau',2,'2020-05-15 00:00:00'), | 36 | (18,1,'Krzyhau',2,'2020-05-15'), |
| 37 | (18,1,'Krzyhau',0,'2022-07-02 00:00:00'), | 37 | (18,1,'Krzyhau',0,'2022-07-02'), |
| 38 | (19,1,'LookLikeAKango',2,'2011-10-06 00:00:00'), | 38 | (19,1,'LookLikeAKango',2,'2011-10-06'), |
| 39 | (20,1,'Djinndrache',5,'2011-10-20 00:00:00'), | 39 | (20,1,'Djinndrache',5,'2011-10-20'), |
| 40 | (20,1,'Schlepian',4,'2011-10-30 00:00:00'), | 40 | (20,1,'Schlepian',4,'2011-10-30'), |
| 41 | (20,1,'Jetwash',3,'2014-09-04 00:00:00'), | 41 | (20,1,'Jetwash',3,'2014-09-04'), |
| 42 | (20,1,'Krzyhau',2,'2022-04-24 00:00:00'), | 42 | (20,1,'Krzyhau',2,'2022-04-24'), |
| 43 | (21,1,'LookLikeAKango',4,'2011-10-06 00:00:00'), | 43 | (21,1,'LookLikeAKango',4,'2011-10-06'), |
| 44 | (21,1,'ncla',2,'2011-10-30 00:00:00'), | 44 | (21,1,'ncla',2,'2011-10-30'), |
| 45 | (21,1,'PerOculos',0,'2019-07-08 00:00:00'), | 45 | (21,1,'PerOculos',0,'2019-07-08'), |
| 46 | (22,1,'Tyronis',0,'2011-10-05 00:00:00'), | 46 | (22,1,'Tyronis',0,'2011-10-05'), |
| 47 | (23,1,'LookLikeAKango',2,'2011-10-06 00:00:00'), | 47 | (23,1,'LookLikeAKango',2,'2011-10-06'), |
| 48 | (23,1,'Krzyhau',0,'2018-08-01 00:00:00'), | 48 | (23,1,'Krzyhau',0,'2018-08-01'), |
| 49 | (24,1,'LeviHB',0,'2011-04-30 00:00:00'), | 49 | (24,1,'LeviHB',0,'2011-04-30'), |
| 50 | (25,1,'Tyronis',0,'2011-10-06 00:00:00'), | 50 | (25,1,'Tyronis',0,'2011-10-06'), |
| 51 | (26,1,'Schlepian',3,'2011-10-30 00:00:00'), | 51 | (26,1,'Schlepian',3,'2011-10-30'), |
| 52 | (26,1,'Tyronis',2,'2012-01-08 00:00:00'), | 52 | (26,1,'Tyronis',2,'2012-01-08'), |
| 53 | (26,1,'PerOculos',0,'2016-06-08 00:00:00'), | 53 | (26,1,'PerOculos',0,'2016-06-08'), |
| 54 | -- 4 | 54 | -- 4 |
| 55 | (27,1,'LeviHB',2,'2011-05-01 00:00:00'), | 55 | (27,1,'LeviHB',2,'2011-05-01'), |
| 56 | (27,1,'PerOculos',0,'2020-07-13 00:00:00'), | 56 | (27,1,'PerOculos',0,'2020-07-13'), |
| 57 | (28,1,'LeviHB',7,'2011-05-01 00:00:00'), | 57 | (28,1,'LeviHB',7,'2011-05-01'), |
| 58 | (28,1,'Andy M.J.',2,'2011-10-07 00:00:00'), | 58 | (28,1,'Andy M.J.',2,'2011-10-07'), |
| 59 | (28,1,'Krzyhau',0,'2018-05-19 00:00:00'), | 59 | (28,1,'Krzyhau',0,'2018-05-19'), |
| 60 | (29,1,'LeviHB',0,'2011-05-01 00:00:00'), | 60 | (29,1,'LeviHB',0,'2011-05-01'), |
| 61 | (30,1,'Schlepian',2,'2011-10-30 00:00:00'), | 61 | (30,1,'Schlepian',2,'2011-10-30'), |
| 62 | (31,1,'Tyronis',0,'2011-10-06 00:00:00'), | 62 | (31,1,'Tyronis',0,'2011-10-06'), |
| 63 | -- 5 | 63 | -- 5 |
| 64 | (32,1,'Tyronis',6,'2011-10-21 00:00:00'), | 64 | (32,1,'Tyronis',6,'2011-10-21'), |
| 65 | (32,1,'Nidboj132',5,'2022-04-24 00:00:00'), | 65 | (32,1,'Nidboj132',5,'2022-04-24'), |
| 66 | (33,1,'Tyronis',7,'2011-10-06 00:00:00'), | 66 | (33,1,'Tyronis',7,'2011-10-06'), |
| 67 | (33,1,'ISimmo',5,'2011-11-02 00:00:00'), | 67 | (33,1,'ISimmo',5,'2011-11-02'), |
| 68 | (33,1,'PerOculos',4,'2017-05-30 00:00:00'), | 68 | (33,1,'PerOculos',4,'2017-05-30'), |
| 69 | (34,1,'Schlepian',3,'2011-11-01 00:00:00'), | 69 | (34,1,'Schlepian',3,'2011-11-01'), |
| 70 | (34,1,'Krzyhau',2,'2020-10-14 00:00:00'), | 70 | (34,1,'Krzyhau',2,'2020-10-14'), |
| 71 | (34,1,'zach',0,'2022-11-02 00:00:00'), | 71 | (34,1,'zach',0,'2022-11-02'), |
| 72 | (35,1,'Krank',2,'2012-07-28 00:00:00'), | 72 | (35,1,'Krank',2,'2012-07-28'), |
| 73 | -- 6 | 73 | -- 6 |
| 74 | (36,1,'Tyronis',6,'2011-10-06 00:00:00'), | 74 | (36,1,'Tyronis',6,'2011-10-06'), |
| 75 | (36,1,'CalmlyFrenetic',5,'2011-10-09 00:00:00'), | 75 | (36,1,'CalmlyFrenetic',5,'2011-10-09'), |
| 76 | (36,1,'sicklebrick',4,'2012-09-13 00:00:00'), | 76 | (36,1,'sicklebrick',4,'2012-09-13'), |
| 77 | (36,1,'Nidboj132',2,'2023-03-04 00:00:00'), | 77 | (36,1,'Nidboj132',2,'2023-03-04'), |
| 78 | (37,1,'LookLikeAKango',7,'2011-10-06 00:00:00'), | 78 | (37,1,'LookLikeAKango',7,'2011-10-06'), |
| 79 | (37,1,'Schlepian',6,'2011-11-01 00:00:00'), | 79 | (37,1,'Schlepian',6,'2011-11-01'), |
| 80 | (37,1,'Tyronis',5,'2012-01-28 00:00:00'), | 80 | (37,1,'Tyronis',5,'2012-01-28'), |
| 81 | (37,1,'Nidboj132',4,'2021-08-22 00:00:00'), | 81 | (37,1,'Nidboj132',4,'2021-08-22'), |
| 82 | (38,1,'Andy M.J.',2,'2011-10-06 00:00:00'), | 82 | (38,1,'Andy M.J.',2,'2011-10-06'), |
| 83 | (38,1,'Sanguine Dagger',0,'2012-03-19 00:00:00'), | 83 | (38,1,'Sanguine Dagger',0,'2012-03-19'), |
| 84 | (39,1,'Lambda Core',6,'2011-05-13 00:00:00'), | 84 | (39,1,'Lambda Core',6,'2011-05-13'), |
| 85 | (39,1,'The Last Tofus',5,'2011-05-13 00:00:00'), | 85 | (39,1,'The Last Tofus',5,'2011-05-13'), |
| 86 | (39,1,'LookLikeAKango',4,'2011-10-16 00:00:00'), | 86 | (39,1,'LookLikeAKango',4,'2011-10-16'), |
| 87 | (39,1,'Kittaye',3,'2013-03-25 00:00:00'), | 87 | (39,1,'Kittaye',3,'2013-03-25'), |
| 88 | (40,1,'LookLikeAKango',7,'2011-10-07 00:00:00'), | 88 | (40,1,'LookLikeAKango',7,'2011-10-07'), |
| 89 | (40,1,'Schlepian',6,'2011-11-05 00:00:00'), | 89 | (40,1,'Schlepian',6,'2011-11-05'), |
| 90 | (40,1,'Kittaye',4,'2013-04-01 00:00:00'), | 90 | (40,1,'Kittaye',4,'2013-04-01'), |
| 91 | (40,1,'Kittaye',3,'2014-09-13 00:00:00'), | 91 | (40,1,'Kittaye',3,'2014-09-13'), |
| 92 | (40,1,'szeimartin',2,'2014-09-13 00:00:00'), | 92 | (40,1,'szeimartin',2,'2014-09-13'), |
| 93 | (40,1,'Kittaye',0,'2014-09-15 00:00:00'), | 93 | (40,1,'Kittaye',0,'2014-09-15'), |
| 94 | (41,1,'CalmlyFrenetic',7,'2011-10-09 00:00:00'), | 94 | (41,1,'CalmlyFrenetic',7,'2011-10-09'), |
| 95 | (41,1,'Jaso',6,'2011-10-11 00:00:00'), | 95 | (41,1,'Jaso',6,'2011-10-11'), |
| 96 | (41,1,'Krank',5,'2012-07-17 00:00:00'), | 96 | (41,1,'Krank',5,'2012-07-17'), |
| 97 | -- 7 | 97 | -- 7 |
| 98 | (42,1,'LookLikeAKango',4,'2011-05-17 00:00:00'), | 98 | (42,1,'LookLikeAKango',4,'2011-05-17'), |
| 99 | (42,1,'ISimmo',2,'2011-11-07 00:00:00'), | 99 | (42,1,'ISimmo',2,'2011-11-07'), |
| 100 | (43,1,'lmao4ever',5,'2011-10-30 00:00:00'), | 100 | (43,1,'lmao4ever',5,'2011-10-30'), |
| 101 | (43,1,'Jaso',2,'2011-11-09 00:00:00'), | 101 | (43,1,'Jaso',2,'2011-11-09'), |
| 102 | (43,1,'feliser',0,'2022-06-26 00:00:00'), | 102 | (43,1,'feliser',0,'2022-06-26'), |
| 103 | (44,1,'LookLikeAKango',18,'2011-10-07 00:00:00'), | 103 | (44,1,'LookLikeAKango',18,'2011-10-07'), |
| 104 | (44,1,'Tyronis',13,'2011-10-30 00:00:00'), | 104 | (44,1,'Tyronis',13,'2011-10-30'), |
| 105 | (44,1,'Tyronis',12,'2011-11-10 00:00:00'), | 105 | (44,1,'Tyronis',12,'2011-11-10'), |
| 106 | (44,1,'Jetwash',11,'2017-06-12 00:00:00'), | 106 | (44,1,'Jetwash',11,'2017-06-12'), |
| 107 | (44,1,'Krzyhau',9,'2022-01-02 00:00:00'), | 107 | (44,1,'Krzyhau',9,'2022-01-02'), |
| 108 | (45,1,'LookLikeAKango',23,'2011-10-08 00:00:00'), | 108 | (45,1,'LookLikeAKango',23,'2011-10-08'), |
| 109 | (45,1,'CalmlyFrenetic',22,'2011-10-09 00:00:00'), | 109 | (45,1,'CalmlyFrenetic',22,'2011-10-09'), |
| 110 | (45,1,'cgreactor',17,'2011-10-09 00:00:00'), | 110 | (45,1,'cgreactor',17,'2011-10-09'), |
| 111 | (45,1,'CalmlyFrenetic',16,'2011-10-10 00:00:00'), | 111 | (45,1,'CalmlyFrenetic',16,'2011-10-10'), |
| 112 | (45,1,'LookLikeAKango',15,'2011-10-19 00:00:00'), | 112 | (45,1,'LookLikeAKango',15,'2011-10-19'), |
| 113 | (45,1,'Jaso',12,'2012-07-19 00:00:00'), | 113 | (45,1,'Jaso',12,'2012-07-19'), |
| 114 | (45,1,'Krank',10,'2013-01-31 00:00:00'), | 114 | (45,1,'Krank',10,'2013-01-31'), |
| 115 | (45,1,'Kittaye',7,'2013-04-04 00:00:00'), | 115 | (45,1,'Kittaye',7,'2013-04-04'), |
| 116 | (45,1,'PerOculos',4,'2014-09-13 00:00:00'), | 116 | (45,1,'PerOculos',4,'2014-09-13'), |
| 117 | -- 8 | 117 | -- 8 |
| 118 | (46,1,'sparkle1princess',6,'2012-03-24 00:00:00'), | 118 | (46,1,'sparkle1princess',6,'2012-03-24'), |
| 119 | (46,1,'Krzyhau',2,'2019-11-21 00:00:00'), | 119 | (46,1,'Krzyhau',2,'2019-11-21'), |
| 120 | (47,1,'holydevel',2,'2011-10-06 00:00:00'), | 120 | (47,1,'holydevel',2,'2011-10-06'), |
| 121 | (47,1,'JesusCatFace',0,'2015-01-16 00:00:00'), | 121 | (47,1,'JesusCatFace',0,'2015-01-16'), |
| 122 | (48,1,'LookLikeAKango',5,'2011-10-08 00:00:00'), | 122 | (48,1,'LookLikeAKango',5,'2011-10-08'), |
| 123 | (48,1,'Tyronis',2,'2011-10-08 00:00:00'), | 123 | (48,1,'Tyronis',2,'2011-10-08'), |
| 124 | (48,1,'adzicents',0,'2011-10-09 00:00:00'), | 124 | (48,1,'adzicents',0,'2011-10-09'), |
| 125 | (49,1,'adzicents',4,'2011-10-07 00:00:00'), | 125 | (49,1,'adzicents',4,'2011-10-07'), |
| 126 | (49,1,'Schlepian',2,'2011-10-08 00:00:00'), | 126 | (49,1,'Schlepian',2,'2011-10-08'), |
| 127 | (49,1,'Nidboj132',0,'2022-09-26 00:00:00'), | 127 | (49,1,'Nidboj132',0,'2022-09-26'), |
| 128 | (50,1,'LookLikeAKango',4,'2011-10-08 00:00:00'), | 128 | (50,1,'LookLikeAKango',4,'2011-10-08'), |
| 129 | (50,1,'Tyronis',2,'2011-10-11 00:00:00'), | 129 | (50,1,'Tyronis',2,'2011-10-11'), |
| 130 | (50,1,'sicklebrick',0,'2013-03-20 00:00:00'), | 130 | (50,1,'sicklebrick',0,'2013-03-20'), |
| 131 | (51,1,'Andy M.J.',3,'2011-10-08 00:00:00'), | 131 | (51,1,'Andy M.J.',3,'2011-10-08'), |
| 132 | (51,1,'LookLikeAKango',2,'2011-10-20 00:00:00'), | 132 | (51,1,'LookLikeAKango',2,'2011-10-20'), |
| 133 | (52,1,'Jaso',0,'2011-10-10 00:00:00'), | 133 | (52,1,'Jaso',0,'2011-10-10'), |
| 134 | (53,1,'LookLikeAKango',9,'2011-10-08 00:00:00'), | 134 | (53,1,'LookLikeAKango',9,'2011-10-08'), |
| 135 | (53,1,'LookLikeAKango',2,'2011-10-20 00:00:00'), | 135 | (53,1,'LookLikeAKango',2,'2011-10-20'), |
| 136 | (53,1,'Schlepian',0,'2011-11-06 00:00:00'), | 136 | (53,1,'Schlepian',0,'2011-11-06'), |
| 137 | (54,1,'LookLikeAKango',7,'2011-06-01 00:00:00'), | 137 | (54,1,'LookLikeAKango',7,'2011-06-01'), |
| 138 | (54,1,'Jaso',6,'2011-10-09 00:00:00'), | 138 | (54,1,'Jaso',6,'2011-10-09'), |
| 139 | (54,1,'Schlepian',5,'2011-11-06 00:00:00'), | 139 | (54,1,'Schlepian',5,'2011-11-06'), |
| 140 | (54,1,'Spyrunite',4,'2012-08-30 00:00:00'), | 140 | (54,1,'Spyrunite',4,'2012-08-30'), |
| 141 | (54,1,'Krzyhau',3,'2019-04-22 00:00:00'), | 141 | (54,1,'Krzyhau',3,'2019-04-22'), |
| 142 | (55,1,'LookLikeAKango',7,'2011-10-08 00:00:00'), | 142 | (55,1,'LookLikeAKango',7,'2011-10-08'), |
| 143 | (55,1,'CalmlyFrenetic',3,'2011-10-09 00:00:00'), | 143 | (55,1,'CalmlyFrenetic',3,'2011-10-09'), |
| 144 | (55,1,'Jaso',2,'2011-11-26 00:00:00'), | 144 | (55,1,'Jaso',2,'2011-11-26'), |
| 145 | (55,1,'PerOculos',0,'2021-02-06 00:00:00'), | 145 | (55,1,'PerOculos',0,'2021-02-06'), |
| 146 | (56,1,'CalmlyFrenetic',9,'2011-10-08 00:00:00'), | 146 | (56,1,'CalmlyFrenetic',9,'2011-10-08'), |
| 147 | (56,1,'LookLikeAKango',5,'2011-10-09 00:00:00'), | 147 | (56,1,'LookLikeAKango',5,'2011-10-09'), |
| 148 | (56,1,'CalmlyFrenetic',4,'2011-10-09 00:00:00'), | 148 | (56,1,'CalmlyFrenetic',4,'2011-10-09'), |
| 149 | (56,1,'Jetwash',2,'2014-09-05 00:00:00'), | 149 | (56,1,'Jetwash',2,'2014-09-05'), |
| 150 | -- 9 | 150 | -- 9 |
| 151 | (57,1,'JNS',7,'2011-07-21 00:00:00'), | 151 | (57,1,'JNS',7,'2011-07-21'), |
| 152 | (57,1,'Krank',5,'2012-07-29 00:00:00'), | 152 | (57,1,'Krank',5,'2012-07-29'), |
| 153 | (57,1,'Krzyhau',0,'2017-10-29 00:00:00'), | 153 | (57,1,'Krzyhau',0,'2017-10-29'), |
| 154 | (58,1,'Stimich',2,'2011-10-11 00:00:00'), | 154 | (58,1,'Stimich',2,'2011-10-11'), |
| 155 | (59,1,'Isimmo',7,'2011-11-04 00:00:00'), | 155 | (59,1,'Isimmo',7,'2011-11-04'), |
| 156 | (59,1,'sicklebrick',6,'2013-03-20 00:00:00'), | 156 | (59,1,'sicklebrick',6,'2013-03-20'), |
| 157 | (60,1,'CalmlyFrenetic',7,'2011-10-19 00:00:00'), | 157 | (60,1,'CalmlyFrenetic',7,'2011-10-19'), |
| 158 | (60,1,'Tyronis',6,'2011-11-01 00:00:00'), | 158 | (60,1,'Tyronis',6,'2011-11-01'), |
| 159 | -- Portal 2 Cooperative | 159 | -- Portal 2 Cooperative |
| 160 | -- 1 | 160 | -- 1 |
| 161 | (63,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01 00:00:00'), | 161 | (63,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01'), |
| 162 | (64,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), | 162 | (64,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01'), |
| 163 | (64,1,'Chubfish & Exhale',2,'2011-11-01 00:00:00'), | 163 | (64,1,'Chubfish & Exhale',2,'2011-11-01'), |
| 164 | (65,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), | 164 | (65,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01'), |
| 165 | (65,1,'Nidboj132 & Oryn',3,'2022-02-03 00:00:00'), | 165 | (65,1,'Nidboj132 & Oryn',3,'2022-02-03'), |
| 166 | (66,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), | 166 | (66,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01'), |
| 167 | (66,1,'Schlepian & Chubfish',2,'2011-10-01 00:00:00'), | 167 | (66,1,'Schlepian & Chubfish',2,'2011-10-01'), |
| 168 | (67,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01 00:00:00'), | 168 | (67,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01'), |
| 169 | (68,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01 00:00:00'), | 169 | (68,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01'), |
| 170 | -- 2 | 170 | -- 2 |
| 171 | (69,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), | 171 | (69,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01'), |
| 172 | (70,1,'Mathias123961 & Sir Spawn Alot',6,'2011-08-01 00:00:00'), | 172 | (70,1,'Mathias123961 & Sir Spawn Alot',6,'2011-08-01'), |
| 173 | (70,1,'Schlepian & Chubfish',4,'2011-10-01 00:00:00'), | 173 | (70,1,'Schlepian & Chubfish',4,'2011-10-01'), |
| 174 | (70,1,'Gocnak & z1mb0bw4y',2,'2012-08-03 00:00:00'), | 174 | (70,1,'Gocnak & z1mb0bw4y',2,'2012-08-03'), |
| 175 | (70,1,'DM_ & VEGA',0,'2017-10-01 00:00:00'), | 175 | (70,1,'DM_ & VEGA',0,'2017-10-01'), |
| 176 | (71,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), | 176 | (71,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01'), |
| 177 | (71,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01 00:00:00'), | 177 | (71,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01'), |
| 178 | (72,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), | 178 | (72,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01'), |
| 179 | (72,1,'Schlepian & LongJohnDickWeed',2,'2011-10-01 00:00:00'), | 179 | (72,1,'Schlepian & LongJohnDickWeed',2,'2011-10-01'), |
| 180 | (73,1,'Stimich & HiTMaRkS',9,'2011-05-09 00:00:00'), | 180 | (73,1,'Stimich & HiTMaRkS',9,'2011-05-09'), |
| 181 | (73,1,'Mathias123961 & Sir Spawn Alot',8,'2011-08-01 00:00:00'), | 181 | (73,1,'Mathias123961 & Sir Spawn Alot',8,'2011-08-01'), |
| 182 | (73,1,'Schlepian & Lemonsunshine',7,'2011-11-01 00:00:00'), | 182 | (73,1,'Schlepian & Lemonsunshine',7,'2011-11-01'), |
| 183 | (73,1,'DM_ & LsDK_',6,'2018-01-01 00:00:00'), | 183 | (73,1,'DM_ & LsDK_',6,'2018-01-01'), |
| 184 | (73,1,'Krzyhau & Klooger',4,'2018-11-01 00:00:00'), | 184 | (73,1,'Krzyhau & Klooger',4,'2018-11-01'), |
| 185 | (74,1,'Mathias123961 & Sir Spawn Alot',5,'2011-08-01 00:00:00'), | 185 | (74,1,'Mathias123961 & Sir Spawn Alot',5,'2011-08-01'), |
| 186 | (74,1,'Stimich & Pitkakorva',7,'2011-10-11 00:00:00'), | 186 | (74,1,'Stimich & Pitkakorva',7,'2011-10-11'), |
| 187 | (74,1,'Schlepian & Isimmo',3,'2011-10-28 00:00:00'), | 187 | (74,1,'Schlepian & Isimmo',3,'2011-10-28'), |
| 188 | (74,1,'Zypeh & szeimartin',2,'2013-11-01 00:00:00'), | 188 | (74,1,'Zypeh & szeimartin',2,'2013-11-01'), |
| 189 | (75,1,'Mathias123961 & Sir Spawn Alot',5,'2011-08-01 00:00:00'), | 189 | (75,1,'Mathias123961 & Sir Spawn Alot',5,'2011-08-01'), |
| 190 | (75,1,'Schlepian & Urination',4,'2011-10-01 00:00:00'), | 190 | (75,1,'Schlepian & Urination',4,'2011-10-01'), |
| 191 | (75,1,'Schlepian & Lemonsunshine',2,'2012-02-01 00:00:00'), | 191 | (75,1,'Schlepian & Lemonsunshine',2,'2012-02-01'), |
| 192 | (75,1,'DM_ & follon',0,'2015-04-01 00:00:00'), | 192 | (75,1,'DM_ & follon',0,'2015-04-01'), |
| 193 | (76,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), | 193 | (76,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01'), |
| 194 | (76,1,'Chubfish & Exhale',0,'2011-12-01 00:00:00'), | 194 | (76,1,'Chubfish & Exhale',0,'2011-12-01'), |
| 195 | -- 3 | 195 | -- 3 |
| 196 | (77,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), | 196 | (77,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01'), |
| 197 | (78,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), | 197 | (78,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01'), |
| 198 | (78,1,'DM_ & marK',3,'2016-11-01 00:00:00'), | 198 | (78,1,'DM_ & marK',3,'2016-11-01'), |
| 199 | (78,1,'Nidboj132 & Oryn',2,'2021-09-04 00:00:00'), | 199 | (78,1,'Nidboj132 & Oryn',2,'2021-09-04'), |
| 200 | (79,1,'ganonscrub & ?',5,'2011-04-01 00:00:00'), | 200 | (79,1,'ganonscrub & ?',5,'2011-04-01'), |
| 201 | (79,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), | 201 | (79,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01'), |
| 202 | (79,1,'Chubfish & Exhale',2,'2012-08-04 00:00:00'), | 202 | (79,1,'Chubfish & Exhale',2,'2012-08-04'), |
| 203 | (80,1,'Mathias123961 & Sir Spawn Alot',5,'2011-08-01 00:00:00'), | 203 | (80,1,'Mathias123961 & Sir Spawn Alot',5,'2011-08-01'), |
| 204 | (80,1,'Chubfish & Exhale',4,'2011-12-01 00:00:00'), | 204 | (80,1,'Chubfish & Exhale',4,'2011-12-01'), |
| 205 | (81,1,'Mathias123961 & Sir Spawn Alot',7,'2011-08-01 00:00:00'), | 205 | (81,1,'Mathias123961 & Sir Spawn Alot',7,'2011-08-01'), |
| 206 | (81,1,'Schlepian & Lemonsunshine',6,'2011-10-01 00:00:00'), | 206 | (81,1,'Schlepian & Lemonsunshine',6,'2011-10-01'), |
| 207 | (81,1,'takz & dawn',5,'2011-11-01 00:00:00'), | 207 | (81,1,'takz & dawn',5,'2011-11-01'), |
| 208 | (81,1,'Nidboj132 & Oryn',4,'2021-03-25 00:00:00'), | 208 | (81,1,'Nidboj132 & Oryn',4,'2021-03-25'), |
| 209 | (82,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), | 209 | (82,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01'), |
| 210 | (83,1,'Mathias123961 & Sir Spawn Alot',5,'2011-08-01 00:00:00'), | 210 | (83,1,'Mathias123961 & Sir Spawn Alot',5,'2011-08-01'), |
| 211 | (83,1,'Schlepian & Lemonsunshine',2,'2011-10-01 00:00:00'), | 211 | (83,1,'Schlepian & Lemonsunshine',2,'2011-10-01'), |
| 212 | (83,1,'Chubfish & Exhale',0,'2011-12-01 00:00:00'), | 212 | (83,1,'Chubfish & Exhale',0,'2011-12-01'), |
| 213 | (84,1,'Mathias123961 & Sir Spawn Alot',6,'2011-08-01 00:00:00'), | 213 | (84,1,'Mathias123961 & Sir Spawn Alot',6,'2011-08-01'), |
| 214 | (84,1,'Schlepian & Chubfish',4,'2011-10-01 00:00:00'), | 214 | (84,1,'Schlepian & Chubfish',4,'2011-10-01'), |
| 215 | (84,1,'Chubfish & Exhale',2,'2012-01-01 00:00:00'), | 215 | (84,1,'Chubfish & Exhale',2,'2012-01-01'), |
| 216 | (84,1,'DM_ & wS',0,'2015-05-01 00:00:00'), | 216 | (84,1,'DM_ & wS',0,'2015-05-01'), |
| 217 | -- 4 | 217 | -- 4 |
| 218 | (85,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), | 218 | (85,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01'), |
| 219 | (85,1,'Chubfish & Exhale',0,'2011-10-01 00:00:00'), | 219 | (85,1,'Chubfish & Exhale',0,'2011-10-01'), |
| 220 | (86,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), | 220 | (86,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01'), |
| 221 | (86,1,'Chubfish & Exhale',0,'2011-12-01 00:00:00'), | 221 | (86,1,'Chubfish & Exhale',0,'2011-12-01'), |
| 222 | (87,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), | 222 | (87,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01'), |
| 223 | (87,1,'Schlepian & Gopherdude',2,'2011-10-01 00:00:00'), | 223 | (87,1,'Schlepian & Gopherdude',2,'2011-10-01'), |
| 224 | (87,1,'DM_ & follon',0,'2015-04-01 00:00:00'), | 224 | (87,1,'DM_ & follon',0,'2015-04-01'), |
| 225 | (88,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), | 225 | (88,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01'), |
| 226 | (88,1,'Schlepian & Gopherdude',0,'2011-10-01 00:00:00'), | 226 | (88,1,'Schlepian & Gopherdude',0,'2011-10-01'), |
| 227 | (89,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01 00:00:00'), | 227 | (89,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01'), |
| 228 | (90,1,'Mathias123961 & Sir Spawn Alot',4,'2011-09-01 00:00:00'), | 228 | (90,1,'Mathias123961 & Sir Spawn Alot',4,'2011-09-01'), |
| 229 | (90,1,'Schlepian & Urination',2,'2011-10-01 00:00:00'), | 229 | (90,1,'Schlepian & Urination',2,'2011-10-01'), |
| 230 | (90,1,'Klooger & Jetwash',0,'2016-08-01 00:00:00'), | 230 | (90,1,'Klooger & Jetwash',0,'2016-08-01'), |
| 231 | (91,1,'Mathias123961 & Sir Spawn Alot',2,'2011-08-01 00:00:00'), | 231 | (91,1,'Mathias123961 & Sir Spawn Alot',2,'2011-08-01'), |
| 232 | (91,1,'Undead & Zypeh',0,'2013-05-19 00:00:00'), | 232 | (91,1,'Undead & Zypeh',0,'2013-05-19'), |
| 233 | (92,1,'txx478 & ?',5,'2011-05-01 00:00:00'), | 233 | (92,1,'txx478 & ?',5,'2011-05-01'), |
| 234 | (92,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), | 234 | (92,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01'), |
| 235 | (92,1,'Schlepian & Gopherdude',2,'2011-10-01 00:00:00'), | 235 | (92,1,'Schlepian & Gopherdude',2,'2011-10-01'), |
| 236 | (92,1,'ncla & takz',0,'2012-02-01 00:00:00'), | 236 | (92,1,'ncla & takz',0,'2012-02-01'), |
| 237 | (93,1,'Mathias123961 & Sir Spawn Alot',2,'2011-08-01 00:00:00'), | 237 | (93,1,'Mathias123961 & Sir Spawn Alot',2,'2011-08-01'), |
| 238 | (93,1,'Schlepian & Gopherdude',0,'2011-10-01 00:00:00'), | 238 | (93,1,'Schlepian & Gopherdude',0,'2011-10-01'), |
| 239 | -- 5 | 239 | -- 5 |
| 240 | (94,1,'Chubfish & Exhale',2,'2011-10-01 00:00:00'), | 240 | (94,1,'Chubfish & Exhale',2,'2011-10-01'), |
| 241 | (94,1,'Klooger & Imanex',0,'2013-08-01 00:00:00'), | 241 | (94,1,'Klooger & Imanex',0,'2013-08-01'), |
| 242 | (95,1,'Schlepian & Issimoi',2,'2011-10-01 00:00:00'), | 242 | (95,1,'Schlepian & Issimoi',2,'2011-10-01'), |
| 243 | (96,1,'ThePortalPatrol & ?',4,'2011-04-01 00:00:00'), | 243 | (96,1,'ThePortalPatrol & ?',4,'2011-04-01'), |
| 244 | (96,1,'sparkle1princess & Zypeh',2,'2014-01-01 00:00:00'), | 244 | (96,1,'sparkle1princess & Zypeh',2,'2014-01-01'), |
| 245 | (97,1,'Stimich & HiTMaRkS',7,'2011-05-13 00:00:00'), | 245 | (97,1,'Stimich & HiTMaRkS',7,'2011-05-13'), |
| 246 | (97,1,'Schlepian & Lemonsunshine',4,'2011-10-01 00:00:00'), | 246 | (97,1,'Schlepian & Lemonsunshine',4,'2011-10-01'), |
| 247 | (97,1,'DM_ & wS',2,'2014-05-01 00:00:00'), | 247 | (97,1,'DM_ & wS',2,'2014-05-01'), |
| 248 | (98,1,'Imanex & 00svo',0,'2011-11-01 00:00:00'), | 248 | (98,1,'Imanex & 00svo',0,'2011-11-01'), |
| 249 | (99,1,'Schlepian & Gopherdude',3,'2011-10-01 00:00:00'), | 249 | (99,1,'Schlepian & Gopherdude',3,'2011-10-01'), |
| 250 | (99,1,'Imanex & Klooger',2,'2013-08-01 00:00:00'), | 250 | (99,1,'Imanex & Klooger',2,'2013-08-01'), |
| 251 | (99,1,'DM_ & wS',0,'2015-05-01 00:00:00'), | 251 | (99,1,'DM_ & wS',0,'2015-05-01'), |
| 252 | (100,1,'Schlepian & Bananasaurus Rex',0,'2011-10-01 00:00:00'), | 252 | (100,1,'Schlepian & Bananasaurus Rex',0,'2011-10-01'), |
| 253 | (101,1,'Chubfish & Exhale',2,'2011-12-01 00:00:00'), | 253 | (101,1,'Chubfish & Exhale',2,'2011-12-01'), |
| 254 | (101,1,'DM_ & follon',0,'2015-04-01 00:00:00'), | 254 | (101,1,'DM_ & follon',0,'2015-04-01'), |
| 255 | -- 6 | 255 | -- 6 |
| 256 | (102,1,'dawn & takz',3,'2011-11-18 00:00:00'), | 256 | (102,1,'dawn & takz',3,'2011-11-18'), |
| 257 | (102,1,'Chubfish & Exhale',2,'2012-01-01 00:00:00'), | 257 | (102,1,'Chubfish & Exhale',2,'2012-01-01'), |
| 258 | (102,1,'Imanex & Klooger',0,'2013-08-01 00:00:00'), | 258 | (102,1,'Imanex & Klooger',0,'2013-08-01'), |
| 259 | (103,1,'Schlepian & Lemonsunshine',0,'2011-10-01 00:00:00'), | 259 | (103,1,'Schlepian & Lemonsunshine',0,'2011-10-01'), |
| 260 | (104,1,'Schlepian & Lemonsunshine',0,'2011-10-01 00:00:00'), | 260 | (104,1,'Schlepian & Lemonsunshine',0,'2011-10-01'), |
| 261 | (105,1,'Blaizerazer & ?',8,'2011-10-01 00:00:00'), | 261 | (105,1,'Blaizerazer & ?',8,'2011-10-01'), |
| 262 | (105,1,'Schlepian & Lemonsunshine',5,'2011-11-01 00:00:00'), | 262 | (105,1,'Schlepian & Lemonsunshine',5,'2011-11-01'), |
| 263 | (105,1,'Imanex & Klooger',4,'2013-08-01 00:00:00'), | 263 | (105,1,'Imanex & Klooger',4,'2013-08-01'), |
| 264 | (105,1,'DM_ & wS',3,'2014-05-01 00:00:00'), | 264 | (105,1,'DM_ & wS',3,'2014-05-01'), |
| 265 | (105,1,'DM_ & follon',2,'2015-04-01 00:00:00'), | 265 | (105,1,'DM_ & follon',2,'2015-04-01'), |
| 266 | (106,1,'Schlepian & Bananasaurus Rex',4,'2011-10-01 00:00:00'), | 266 | (106,1,'Schlepian & Bananasaurus Rex',4,'2011-10-01'), |
| 267 | (106,1,'Gig & takz',3,'2012-06-01 00:00:00'), | 267 | (106,1,'Gig & takz',3,'2012-06-01'), |
| 268 | (106,1,'Imanex & Klooger',0,'2013-06-01 00:00:00'), | 268 | (106,1,'Imanex & Klooger',0,'2013-06-01'), |
| 269 | (107,1,'Chubfish & Exhale',2,'2011-10-01 00:00:00'), | 269 | (107,1,'Chubfish & Exhale',2,'2011-10-01'), |
| 270 | (107,1,'DM_ & follon',0,'2015-04-01 00:00:00'), | 270 | (107,1,'DM_ & follon',0,'2015-04-01'), |
| 271 | (108,1,'DaFox & P',0,'2011-12-01 00:00:00'), | 271 | (108,1,'DaFox & P',0,'2011-12-01'), |
| 272 | (109,1,'Schlepian & Tyronis',5,'2011-10-01 00:00:00'), | 272 | (109,1,'Schlepian & Tyronis',5,'2011-10-01'), |
| 273 | (109,1,'Chubfish & Exhale',0,'2011-12-01 00:00:00'), | 273 | (109,1,'Chubfish & Exhale',0,'2011-12-01'), |
| 274 | (110,1,'Tyronis & mr.bob806',15,'2011-10-01 00:00:00'), | 274 | (110,1,'Tyronis & mr.bob806',15,'2011-10-01'), |
| 275 | (110,1,'Schlepian & Chubfish',6,'2011-11-01 00:00:00'), | 275 | (110,1,'Schlepian & Chubfish',6,'2011-11-01'), |
| 276 | (110,1,'00svo & z1mb0bw4y',5,'2012-08-08 00:00:00'), | 276 | (110,1,'00svo & z1mb0bw4y',5,'2012-08-08'), |
| 277 | (110,1,'00svo & z1mb0bw4y',4,'2012-08-10 00:00:00'), | 277 | (110,1,'00svo & z1mb0bw4y',4,'2012-08-10'), |
| 278 | (110,1,'Klooger & z1mb0bw4y',2,'2014-02-01 00:00:00'), | 278 | (110,1,'Klooger & z1mb0bw4y',2,'2014-02-01'), |
| 279 | (110,1,'DM_ & follon',0,'2015-04-01 00:00:00'); \ No newline at end of file | 279 | (110,1,'DM_ & follon',0,'2015-04-01'); \ No newline at end of file |
diff --git a/backend/database/init.sql b/backend/database/init.sql index 50e7c15..abace5c 100644 --- a/backend/database/init.sql +++ b/backend/database/init.sql | |||
| @@ -3,6 +3,10 @@ CREATE TABLE users ( | |||
| 3 | user_name TEXT NOT NULL, | 3 | user_name TEXT NOT NULL, |
| 4 | avatar_link TEXT NOT NULL, | 4 | avatar_link TEXT NOT NULL, |
| 5 | country_code CHAR(2) NOT NULL, | 5 | country_code CHAR(2) NOT NULL, |
| 6 | p2sr TEXT NOT NULL DEFAULT '-', | ||
| 7 | steam TEXT NOT NULL DEFAULT '-', | ||
| 8 | youtube TEXT NOT NULL DEFAULT '-', | ||
| 9 | twitch TEXT NOT NULL DEFAULT '-', | ||
| 6 | created_at TIMESTAMP NOT NULL DEFAULT now(), | 10 | created_at TIMESTAMP NOT NULL DEFAULT now(), |
| 7 | updated_at TIMESTAMP NOT NULL DEFAULT now(), | 11 | updated_at TIMESTAMP NOT NULL DEFAULT now(), |
| 8 | PRIMARY KEY (steam_id) | 12 | PRIMARY KEY (steam_id) |
| @@ -59,7 +63,7 @@ CREATE TABLE map_history ( | |||
| 59 | category_id SMALLINT NOT NULL, | 63 | category_id SMALLINT NOT NULL, |
| 60 | user_name TEXT NOT NULL, | 64 | user_name TEXT NOT NULL, |
| 61 | score_count SMALLINT NOT NULL, | 65 | score_count SMALLINT NOT NULL, |
| 62 | record_date TIMESTAMP NOT NULL, | 66 | record_date DATE NOT NULL, |
| 63 | PRIMARY KEY (id), | 67 | PRIMARY KEY (id), |
| 64 | FOREIGN KEY (category_id) REFERENCES categories(id), | 68 | FOREIGN KEY (category_id) REFERENCES categories(id), |
| 65 | FOREIGN KEY (map_id) REFERENCES maps(id), | 69 | FOREIGN KEY (map_id) REFERENCES maps(id), |
| @@ -117,9 +121,16 @@ CREATE TABLE records_mp ( | |||
| 117 | ); | 121 | ); |
| 118 | 122 | ||
| 119 | CREATE TABLE titles ( | 123 | CREATE TABLE titles ( |
| 120 | user_id TEXT, | 124 | id SERIAL, |
| 121 | title_name TEXT NOT NULL, | 125 | title_name TEXT NOT NULL, |
| 122 | PRIMARY KEY (user_id), | 126 | title_color CHAR(6) NOT NULL, |
| 127 | PRIMARY KEY (id) | ||
| 128 | ); | ||
| 129 | |||
| 130 | CREATE TABLE user_titles ( | ||
| 131 | title_id INT NOT NULL, | ||
| 132 | user_id TEXT NOT NULL, | ||
| 133 | FOREIGN KEY (title_id) REFERENCES titles(id), | ||
| 123 | FOREIGN KEY (user_id) REFERENCES users(steam_id) | 134 | FOREIGN KEY (user_id) REFERENCES users(steam_id) |
| 124 | ); | 135 | ); |
| 125 | 136 | ||
| @@ -128,3 +139,13 @@ CREATE TABLE countries ( | |||
| 128 | country_name TEXT NOT NULL, | 139 | country_name TEXT NOT NULL, |
| 129 | PRIMARY KEY (country_code) | 140 | PRIMARY KEY (country_code) |
| 130 | ); | 141 | ); |
| 142 | |||
| 143 | CREATE TABLE logs ( | ||
| 144 | id SERIAL, | ||
| 145 | user_id TEXT NOT NULL, | ||
| 146 | type TEXT NOT NULL, | ||
| 147 | description TEXT NOT NULL, | ||
| 148 | date TIMESTAMP NOT NULL DEFAULT now(), | ||
| 149 | PRIMARY KEY (id), | ||
| 150 | FOREIGN KEY (user_id) REFERENCES users(steam_id) | ||
| 151 | ); \ No newline at end of file | ||
diff --git a/backend/database/maps.sql b/backend/database/maps.sql index 50689e2..637d2c2 100644 --- a/backend/database/maps.sql +++ b/backend/database/maps.sql | |||
| @@ -15,7 +15,7 @@ INSERT INTO maps(game_id, chapter_id, name, description, showcase, is_disabled) | |||
| 15 | (1,2,'Laser Stairs','','',false), | 15 | (1,2,'Laser Stairs','','',false), |
| 16 | (1,2,'Dual Lasers','','',false), | 16 | (1,2,'Dual Lasers','','',false), |
| 17 | (1,2,'Laser Over Goo','','',false), | 17 | (1,2,'Laser Over Goo','','',false), |
| 18 | (1,2,'Catapult Intro','','',true), | 18 | (1,2,'Catapult Intro','','',false), |
| 19 | (1,2,'Trust Fling','','',false), | 19 | (1,2,'Trust Fling','','',false), |
| 20 | (1,2,'Pit Flings','','',false), | 20 | (1,2,'Pit Flings','','',false), |
| 21 | (1,2,'Fizzler Intro','','',false), | 21 | (1,2,'Fizzler Intro','','',false), |
| @@ -71,7 +71,7 @@ INSERT INTO maps(game_id, chapter_id, name, description, showcase, is_disabled) | |||
| 71 | (1,9,'Finale 4','','',false), | 71 | (1,9,'Finale 4','','',false), |
| 72 | -- Portal 2 Cooperative | 72 | -- Portal 2 Cooperative |
| 73 | -- 0 | 73 | -- 0 |
| 74 | (2,10,'Calibration','','',false), | 74 | (2,10,'Calibration','','',true), |
| 75 | (2,10,'Hub','','',true), | 75 | (2,10,'Hub','','',true), |
| 76 | -- 1 | 76 | -- 1 |
| 77 | (2,11,'Doors','','',false), | 77 | (2,11,'Doors','','',false), |
diff --git a/backend/controllers/homeController.go b/backend/handlers/home.go index c94590a..2095a74 100644 --- a/backend/controllers/homeController.go +++ b/backend/handlers/home.go | |||
| @@ -1,8 +1,9 @@ | |||
| 1 | package controllers | 1 | package handlers |
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "log" | 4 | "log" |
| 5 | "net/http" | 5 | "net/http" |
| 6 | "sort" | ||
| 6 | "strings" | 7 | "strings" |
| 7 | 8 | ||
| 8 | "github.com/gin-gonic/gin" | 9 | "github.com/gin-gonic/gin" |
| @@ -10,15 +11,15 @@ import ( | |||
| 10 | "github.com/pektezol/leastportalshub/backend/models" | 11 | "github.com/pektezol/leastportalshub/backend/models" |
| 11 | ) | 12 | ) |
| 12 | 13 | ||
| 13 | func Home(c *gin.Context) { | 14 | type SearchResponse struct { |
| 14 | user, exists := c.Get("user") | 15 | Players []models.UserShort `json:"players"` |
| 15 | if !exists { | 16 | Maps []models.MapShort `json:"maps"` |
| 16 | c.JSON(200, "no id, not auth") | 17 | } |
| 17 | } else { | 18 | |
| 18 | c.JSON(200, gin.H{ | 19 | type RankingsResponse struct { |
| 19 | "output": user, | 20 | Overall []models.UserRanking `json:"rankings_overall"` |
| 20 | }) | 21 | Singleplayer []models.UserRanking `json:"rankings_singleplayer"` |
| 21 | } | 22 | Multiplayer []models.UserRanking `json:"rankings_multiplayer"` |
| 22 | } | 23 | } |
| 23 | 24 | ||
| 24 | // GET Rankings | 25 | // GET Rankings |
| @@ -26,100 +27,104 @@ func Home(c *gin.Context) { | |||
| 26 | // @Description Get rankings of every player. | 27 | // @Description Get rankings of every player. |
| 27 | // @Tags rankings | 28 | // @Tags rankings |
| 28 | // @Produce json | 29 | // @Produce json |
| 29 | // @Success 200 {object} models.Response{data=models.RankingsResponse} | 30 | // @Success 200 {object} models.Response{data=RankingsResponse} |
| 30 | // @Failure 400 {object} models.Response | 31 | // @Failure 400 {object} models.Response |
| 31 | // @Router /rankings [get] | 32 | // @Router /rankings [get] |
| 32 | func Rankings(c *gin.Context) { | 33 | func Rankings(c *gin.Context) { |
| 33 | rows, err := database.DB.Query(`SELECT steam_id, user_name FROM users`) | 34 | response := RankingsResponse{ |
| 35 | Overall: []models.UserRanking{}, | ||
| 36 | Singleplayer: []models.UserRanking{}, | ||
| 37 | Multiplayer: []models.UserRanking{}, | ||
| 38 | } | ||
| 39 | // Singleplayer rankings | ||
| 40 | sql := `SELECT u.steam_id, u.user_name, COUNT(DISTINCT map_id), | ||
| 41 | (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false), | ||
| 42 | (SELECT SUM(min_score_count) AS total_min_score_count FROM ( | ||
| 43 | SELECT | ||
| 44 | user_id, | ||
| 45 | MIN(score_count) AS min_score_count | ||
| 46 | FROM records_sp | ||
| 47 | GROUP BY user_id, map_id | ||
| 48 | ) AS subquery | ||
| 49 | WHERE user_id = u.steam_id) | ||
| 50 | FROM records_sp sp JOIN users u ON u.steam_id = sp.user_id GROUP BY u.steam_id, u.user_name` | ||
| 51 | rows, err := database.DB.Query(sql) | ||
| 34 | if err != nil { | 52 | if err != nil { |
| 35 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 53 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 36 | return | 54 | return |
| 37 | } | 55 | } |
| 38 | var spRankings []models.UserRanking | ||
| 39 | var mpRankings []models.UserRanking | ||
| 40 | for rows.Next() { | 56 | for rows.Next() { |
| 41 | var userID, username string | 57 | ranking := models.UserRanking{} |
| 42 | err := rows.Scan(&userID, &username) | 58 | var currentCount int |
| 43 | if err != nil { | 59 | var totalCount int |
| 44 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 60 | err = rows.Scan(&ranking.User.SteamID, &ranking.User.UserName, ¤tCount, &totalCount, &ranking.TotalScore) |
| 45 | return | ||
| 46 | } | ||
| 47 | // Getting all sp records for each user | ||
| 48 | var uniqueSingleUserRecords, totalSingleMaps int | ||
| 49 | sql := `SELECT COUNT(DISTINCT map_id), (SELECT COUNT(map_name) FROM maps | ||
| 50 | WHERE is_coop = FALSE AND is_disabled = false) FROM records_sp WHERE user_id = $1` | ||
| 51 | err = database.DB.QueryRow(sql, userID).Scan(&uniqueSingleUserRecords, &totalSingleMaps) | ||
| 52 | if err != nil { | 61 | if err != nil { |
| 53 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 62 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 54 | return | 63 | return |
| 55 | } | 64 | } |
| 56 | // Has all singleplayer records | 65 | if currentCount != totalCount { |
| 57 | if uniqueSingleUserRecords == totalSingleMaps { | 66 | continue |
| 58 | var ranking models.UserRanking | ||
| 59 | ranking.UserID = userID | ||
| 60 | ranking.UserName = username | ||
| 61 | sql := `SELECT DISTINCT map_id, score_count FROM records_sp WHERE user_id = $1 ORDER BY map_id, score_count` | ||
| 62 | rows, err := database.DB.Query(sql, userID) | ||
| 63 | if err != nil { | ||
| 64 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 65 | return | ||
| 66 | } | ||
| 67 | totalScore := 0 | ||
| 68 | var maps []int | ||
| 69 | for rows.Next() { | ||
| 70 | var mapID, scoreCount int | ||
| 71 | rows.Scan(&mapID, &scoreCount) | ||
| 72 | if len(maps) != 0 && maps[len(maps)-1] == mapID { | ||
| 73 | continue | ||
| 74 | } | ||
| 75 | totalScore += scoreCount | ||
| 76 | maps = append(maps, mapID) | ||
| 77 | } | ||
| 78 | ranking.TotalScore = totalScore | ||
| 79 | spRankings = append(spRankings, ranking) | ||
| 80 | } | 67 | } |
| 81 | // Getting all mp records for each user | 68 | response.Singleplayer = append(response.Singleplayer, ranking) |
| 82 | var uniqueMultiUserRecords, totalMultiMaps int | 69 | } |
| 83 | sql = `SELECT COUNT(DISTINCT map_id), (SELECT COUNT(map_name) FROM maps | 70 | // Multiplayer rankings |
| 84 | WHERE is_coop = TRUE AND is_disabled = false) FROM records_mp WHERE host_id = $1 OR partner_id = $2` | 71 | sql = `SELECT u.steam_id, u.user_name, COUNT(DISTINCT map_id), |
| 85 | err = database.DB.QueryRow(sql, userID, userID).Scan(&uniqueMultiUserRecords, &totalMultiMaps) | 72 | (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false), |
| 73 | (SELECT SUM(min_score_count) AS total_min_score_count FROM ( | ||
| 74 | SELECT | ||
| 75 | host_id, | ||
| 76 | partner_id, | ||
| 77 | MIN(score_count) AS min_score_count | ||
| 78 | FROM records_mp | ||
| 79 | GROUP BY host_id, partner_id, map_id | ||
| 80 | ) AS subquery | ||
| 81 | WHERE host_id = u.steam_id OR partner_id = u.steam_id) | ||
| 82 | FROM records_mp mp JOIN users u ON u.steam_id = mp.host_id OR u.steam_id = mp.partner_id GROUP BY u.steam_id, u.user_name` | ||
| 83 | rows, err = database.DB.Query(sql) | ||
| 84 | if err != nil { | ||
| 85 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 86 | return | ||
| 87 | } | ||
| 88 | for rows.Next() { | ||
| 89 | ranking := models.UserRanking{} | ||
| 90 | var currentCount int | ||
| 91 | var totalCount int | ||
| 92 | err = rows.Scan(&ranking.User.SteamID, &ranking.User.UserName, ¤tCount, &totalCount, &ranking.TotalScore) | ||
| 86 | if err != nil { | 93 | if err != nil { |
| 87 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 94 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 88 | return | 95 | return |
| 89 | } | 96 | } |
| 90 | // Has all singleplayer records | 97 | if currentCount != totalCount { |
| 91 | if uniqueMultiUserRecords == totalMultiMaps { | 98 | continue |
| 92 | var ranking models.UserRanking | 99 | } |
| 93 | ranking.UserID = userID | 100 | response.Multiplayer = append(response.Multiplayer, ranking) |
| 94 | ranking.UserName = username | 101 | } |
| 95 | sql := `SELECT DISTINCT map_id, score_count FROM records_mp WHERE host_id = $1 OR partner_id = $2 ORDER BY map_id, score_count` | 102 | // Has both so they are qualified for overall ranking |
| 96 | rows, err := database.DB.Query(sql, userID, userID) | 103 | for _, spRanking := range response.Singleplayer { |
| 97 | if err != nil { | 104 | for _, mpRanking := range response.Multiplayer { |
| 98 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 105 | if spRanking.User.SteamID == mpRanking.User.SteamID { |
| 99 | return | 106 | totalScore := spRanking.TotalScore + mpRanking.TotalScore |
| 100 | } | 107 | overallRanking := models.UserRanking{ |
| 101 | totalScore := 0 | 108 | User: spRanking.User, |
| 102 | var maps []int | 109 | TotalScore: totalScore, |
| 103 | for rows.Next() { | ||
| 104 | var mapID, scoreCount int | ||
| 105 | rows.Scan(&mapID, &scoreCount) | ||
| 106 | if len(maps) != 0 && maps[len(maps)-1] == mapID { | ||
| 107 | continue | ||
| 108 | } | 110 | } |
| 109 | totalScore += scoreCount | 111 | response.Overall = append(response.Overall, overallRanking) |
| 110 | maps = append(maps, mapID) | ||
| 111 | } | 112 | } |
| 112 | ranking.TotalScore = totalScore | ||
| 113 | mpRankings = append(mpRankings, ranking) | ||
| 114 | } | 113 | } |
| 115 | } | 114 | } |
| 115 | sort.Slice(response.Singleplayer, func(i, j int) bool { | ||
| 116 | return response.Singleplayer[i].TotalScore < response.Singleplayer[j].TotalScore | ||
| 117 | }) | ||
| 118 | sort.Slice(response.Multiplayer, func(i, j int) bool { | ||
| 119 | return response.Multiplayer[i].TotalScore < response.Multiplayer[j].TotalScore | ||
| 120 | }) | ||
| 121 | sort.Slice(response.Overall, func(i, j int) bool { | ||
| 122 | return response.Overall[i].TotalScore < response.Overall[j].TotalScore | ||
| 123 | }) | ||
| 116 | c.JSON(http.StatusOK, models.Response{ | 124 | c.JSON(http.StatusOK, models.Response{ |
| 117 | Success: true, | 125 | Success: true, |
| 118 | Message: "Successfully retrieved rankings.", | 126 | Message: "Successfully retrieved rankings.", |
| 119 | Data: models.RankingsResponse{ | 127 | Data: response, |
| 120 | RankingsSP: spRankings, | ||
| 121 | RankingsMP: mpRankings, | ||
| 122 | }, | ||
| 123 | }) | 128 | }) |
| 124 | } | 129 | } |
| 125 | 130 | ||
| @@ -129,14 +134,14 @@ func Rankings(c *gin.Context) { | |||
| 129 | // @Tags search | 134 | // @Tags search |
| 130 | // @Produce json | 135 | // @Produce json |
| 131 | // @Param q query string false "Search user or map name." | 136 | // @Param q query string false "Search user or map name." |
| 132 | // @Success 200 {object} models.Response{data=models.SearchResponse} | 137 | // @Success 200 {object} models.Response{data=SearchResponse} |
| 133 | // @Failure 400 {object} models.Response | 138 | // @Failure 400 {object} models.Response |
| 134 | // @Router /search [get] | 139 | // @Router /search [get] |
| 135 | func SearchWithQuery(c *gin.Context) { | 140 | func SearchWithQuery(c *gin.Context) { |
| 136 | query := c.Query("q") | 141 | query := c.Query("q") |
| 137 | query = strings.ToLower(query) | 142 | query = strings.ToLower(query) |
| 138 | log.Println(query) | 143 | log.Println(query) |
| 139 | var response models.SearchResponse | 144 | var response SearchResponse |
| 140 | // Cache all maps for faster response | 145 | // Cache all maps for faster response |
| 141 | var maps = []models.MapShort{ | 146 | var maps = []models.MapShort{ |
| 142 | {ID: 1, Name: "Container Ride"}, | 147 | {ID: 1, Name: "Container Ride"}, |
diff --git a/backend/controllers/loginController.go b/backend/handlers/login.go index e907b22..85ffd63 100644 --- a/backend/controllers/loginController.go +++ b/backend/handlers/login.go | |||
| @@ -1,9 +1,9 @@ | |||
| 1 | package controllers | 1 | package handlers |
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "encoding/json" | 4 | "encoding/json" |
| 5 | "fmt" | 5 | "fmt" |
| 6 | "io/ioutil" | 6 | "io" |
| 7 | "net/http" | 7 | "net/http" |
| 8 | "os" | 8 | "os" |
| 9 | "time" | 9 | "time" |
| @@ -15,13 +15,17 @@ import ( | |||
| 15 | "github.com/solovev/steam_go" | 15 | "github.com/solovev/steam_go" |
| 16 | ) | 16 | ) |
| 17 | 17 | ||
| 18 | type LoginResponse struct { | ||
| 19 | Token string `json:"token"` | ||
| 20 | } | ||
| 21 | |||
| 18 | // Login | 22 | // Login |
| 19 | // | 23 | // |
| 20 | // @Description Get (redirect) login page for Steam auth. | 24 | // @Description Get (redirect) login page for Steam auth. |
| 21 | // @Tags login | 25 | // @Tags login |
| 22 | // @Accept json | 26 | // @Accept json |
| 23 | // @Produce json | 27 | // @Produce json |
| 24 | // @Success 200 {object} models.Response{data=models.LoginResponse} | 28 | // @Success 200 {object} models.Response{data=LoginResponse} |
| 25 | // @Failure 400 {object} models.Response | 29 | // @Failure 400 {object} models.Response |
| 26 | // @Router /login [get] | 30 | // @Router /login [get] |
| 27 | func Login(c *gin.Context) { | 31 | func Login(c *gin.Context) { |
| @@ -34,20 +38,18 @@ func Login(c *gin.Context) { | |||
| 34 | default: | 38 | default: |
| 35 | steamID, err := openID.ValidateAndGetId() | 39 | steamID, err := openID.ValidateAndGetId() |
| 36 | if err != nil { | 40 | if err != nil { |
| 41 | CreateLog(steamID, LogTypeUser, LogDescriptionUserLoginFailValidate) | ||
| 37 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 42 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 38 | return | 43 | return |
| 39 | } | 44 | } |
| 40 | // Create user if new | 45 | // Create user if new |
| 41 | var checkSteamID int64 | 46 | var checkSteamID int64 |
| 42 | err = database.DB.QueryRow("SELECT steam_id FROM users WHERE steam_id = $1", steamID).Scan(&checkSteamID) | 47 | database.DB.QueryRow("SELECT steam_id FROM users WHERE steam_id = $1", steamID).Scan(&checkSteamID) |
| 43 | // if err != nil { | ||
| 44 | // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 45 | // return | ||
| 46 | // } | ||
| 47 | // User does not exist | 48 | // User does not exist |
| 48 | if checkSteamID == 0 { | 49 | if checkSteamID == 0 { |
| 49 | user, err := GetPlayerSummaries(steamID, os.Getenv("API_KEY")) | 50 | user, err := GetPlayerSummaries(steamID, os.Getenv("API_KEY")) |
| 50 | if err != nil { | 51 | if err != nil { |
| 52 | CreateLog(steamID, LogTypeUser, LogDescriptionUserLoginFailSummary) | ||
| 51 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 53 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 52 | return | 54 | return |
| 53 | } | 55 | } |
| @@ -60,7 +62,7 @@ func Login(c *gin.Context) { | |||
| 60 | VALUES ($1, $2, $3, $4)`, steamID, user.PersonaName, user.AvatarFull, user.LocCountryCode) | 62 | VALUES ($1, $2, $3, $4)`, steamID, user.PersonaName, user.AvatarFull, user.LocCountryCode) |
| 61 | } | 63 | } |
| 62 | moderator := false | 64 | moderator := false |
| 63 | rows, _ := database.DB.Query("SELECT title_name FROM titles WHERE user_id = $1", steamID) | 65 | rows, _ := database.DB.Query("SELECT title_name FROM titles t INNER JOIN user_titles ut ON t.id=ut.title_id WHERE ut.user_id = $1", steamID) |
| 64 | for rows.Next() { | 66 | for rows.Next() { |
| 65 | var title string | 67 | var title string |
| 66 | rows.Scan(&title) | 68 | rows.Scan(&title) |
| @@ -77,15 +79,17 @@ func Login(c *gin.Context) { | |||
| 77 | // Sign and get the complete encoded token as a string using the secret | 79 | // Sign and get the complete encoded token as a string using the secret |
| 78 | tokenString, err := token.SignedString([]byte(os.Getenv("SECRET_KEY"))) | 80 | tokenString, err := token.SignedString([]byte(os.Getenv("SECRET_KEY"))) |
| 79 | if err != nil { | 81 | if err != nil { |
| 82 | CreateLog(steamID, LogTypeUser, LogDescriptionUserLoginFailToken) | ||
| 80 | c.JSON(http.StatusBadRequest, models.ErrorResponse("Failed to generate token.")) | 83 | c.JSON(http.StatusBadRequest, models.ErrorResponse("Failed to generate token.")) |
| 81 | return | 84 | return |
| 82 | } | 85 | } |
| 83 | c.SetCookie("token", tokenString, 3600*24*30, "/", "", true, true) | 86 | c.SetCookie("token", tokenString, 3600*24*30, "/", "", true, true) |
| 87 | CreateLog(steamID, LogTypeUser, LogDescriptionUserLoginSuccess) | ||
| 84 | c.Redirect(http.StatusTemporaryRedirect, "/") | 88 | c.Redirect(http.StatusTemporaryRedirect, "/") |
| 85 | // c.JSON(http.StatusOK, models.Response{ | 89 | // c.JSON(http.StatusOK, models.Response{ |
| 86 | // Success: true, | 90 | // Success: true, |
| 87 | // Message: "Successfully generated token.", | 91 | // Message: "Successfully generated token.", |
| 88 | // Data: models.LoginResponse{ | 92 | // Data: LoginResponse{ |
| 89 | // Token: tokenString, | 93 | // Token: tokenString, |
| 90 | // }, | 94 | // }, |
| 91 | // }) | 95 | // }) |
| @@ -99,7 +103,7 @@ func Login(c *gin.Context) { | |||
| 99 | // @Tags auth | 103 | // @Tags auth |
| 100 | // @Produce json | 104 | // @Produce json |
| 101 | // | 105 | // |
| 102 | // @Success 200 {object} models.Response{data=models.LoginResponse} | 106 | // @Success 200 {object} models.Response{data=LoginResponse} |
| 103 | // @Failure 404 {object} models.Response | 107 | // @Failure 404 {object} models.Response |
| 104 | // @Router /token [get] | 108 | // @Router /token [get] |
| 105 | func GetCookie(c *gin.Context) { | 109 | func GetCookie(c *gin.Context) { |
| @@ -111,7 +115,7 @@ func GetCookie(c *gin.Context) { | |||
| 111 | c.JSON(http.StatusOK, models.Response{ | 115 | c.JSON(http.StatusOK, models.Response{ |
| 112 | Success: true, | 116 | Success: true, |
| 113 | Message: "Token cookie successfully retrieved.", | 117 | Message: "Token cookie successfully retrieved.", |
| 114 | Data: models.LoginResponse{ | 118 | Data: LoginResponse{ |
| 115 | Token: cookie, | 119 | Token: cookie, |
| 116 | }, | 120 | }, |
| 117 | }) | 121 | }) |
| @@ -123,7 +127,7 @@ func GetCookie(c *gin.Context) { | |||
| 123 | // @Tags auth | 127 | // @Tags auth |
| 124 | // @Produce json | 128 | // @Produce json |
| 125 | // | 129 | // |
| 126 | // @Success 200 {object} models.Response{data=models.LoginResponse} | 130 | // @Success 200 {object} models.Response{data=LoginResponse} |
| 127 | // @Failure 404 {object} models.Response | 131 | // @Failure 404 {object} models.Response |
| 128 | // @Router /token [delete] | 132 | // @Router /token [delete] |
| 129 | func DeleteCookie(c *gin.Context) { | 133 | func DeleteCookie(c *gin.Context) { |
| @@ -136,7 +140,7 @@ func DeleteCookie(c *gin.Context) { | |||
| 136 | c.JSON(http.StatusOK, models.Response{ | 140 | c.JSON(http.StatusOK, models.Response{ |
| 137 | Success: true, | 141 | Success: true, |
| 138 | Message: "Token cookie successfully deleted.", | 142 | Message: "Token cookie successfully deleted.", |
| 139 | Data: models.LoginResponse{ | 143 | Data: LoginResponse{ |
| 140 | Token: cookie, | 144 | Token: cookie, |
| 141 | }, | 145 | }, |
| 142 | }) | 146 | }) |
| @@ -148,7 +152,7 @@ func GetPlayerSummaries(steamId, apiKey string) (*models.PlayerSummaries, error) | |||
| 148 | if err != nil { | 152 | if err != nil { |
| 149 | return nil, err | 153 | return nil, err |
| 150 | } | 154 | } |
| 151 | body, err := ioutil.ReadAll(resp.Body) | 155 | body, err := io.ReadAll(resp.Body) |
| 152 | if err != nil { | 156 | if err != nil { |
| 153 | return nil, err | 157 | return nil, err |
| 154 | } | 158 | } |
diff --git a/backend/handlers/logs.go b/backend/handlers/logs.go new file mode 100644 index 0000000..2b8223a --- /dev/null +++ b/backend/handlers/logs.go | |||
| @@ -0,0 +1,189 @@ | |||
| 1 | package handlers | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "fmt" | ||
| 5 | "net/http" | ||
| 6 | "time" | ||
| 7 | |||
| 8 | "github.com/gin-gonic/gin" | ||
| 9 | "github.com/pektezol/leastportalshub/backend/database" | ||
| 10 | "github.com/pektezol/leastportalshub/backend/models" | ||
| 11 | ) | ||
| 12 | |||
| 13 | const ( | ||
| 14 | LogTypeMod string = "Mod" | ||
| 15 | LogTypeUser string = "User" | ||
| 16 | LogTypeRecord string = "Record" | ||
| 17 | |||
| 18 | LogDescriptionUserLoginSuccess string = "LoginSuccess" | ||
| 19 | LogDescriptionUserLoginFailToken string = "LoginTokenFail" | ||
| 20 | LogDescriptionUserLoginFailValidate string = "LoginValidateFail" | ||
| 21 | LogDescriptionUserLoginFailSummary string = "LoginSummaryFail" | ||
| 22 | LogDescriptionUserUpdateSuccess string = "UpdateSuccess" | ||
| 23 | LogDescriptionUserUpdateFail string = "UpdateFail" | ||
| 24 | LogDescriptionUserUpdateSummaryFail string = "UpdateSummaryFail" | ||
| 25 | LogDescriptionUserUpdateCountrySuccess string = "UpdateCountrySuccess" | ||
| 26 | LogDescriptionUserUpdateCountryFail string = "UpdateCountryFail" | ||
| 27 | |||
| 28 | LogDescriptionMapSummaryCreate string = "MapSummaryCreate" | ||
| 29 | LogDescriptionMapSummaryEdit string = "MapSummaryEdit" | ||
| 30 | LogDescriptionMapSummaryEditImage string = "MapSummaryEditImage" | ||
| 31 | LogDescriptionMapSummaryDelete string = "MapSummaryDelete" | ||
| 32 | |||
| 33 | LogDescriptionRecordSuccess string = "Success" | ||
| 34 | LogDescriptionRecordFailInsertRecord string = "InsertRecordFail" | ||
| 35 | LogDescriptionRecordFailInsertDemo string = "InsertDemoFail" | ||
| 36 | LogDescriptionRecordFailProcessDemo string = "ProcessDemoFail" | ||
| 37 | LogDescriptionRecordFailCreateDemo string = "CreateDemoFail" | ||
| 38 | LogDescriptionRecordFailOpenDemo string = "OpenDemoFail" | ||
| 39 | LogDescriptionRecordFailSaveDemo string = "SaveDemoFail" | ||
| 40 | LogDescriptionRecordFailInvalidRequest string = "InvalidRequestFail" | ||
| 41 | ) | ||
| 42 | |||
| 43 | type Log struct { | ||
| 44 | User models.UserShort `json:"user"` | ||
| 45 | Type string `json:"type"` | ||
| 46 | Description string `json:"description"` | ||
| 47 | Date time.Time `json:"date"` | ||
| 48 | } | ||
| 49 | |||
| 50 | type LogsResponse struct { | ||
| 51 | Logs []LogsResponseDetails `json:"logs"` | ||
| 52 | } | ||
| 53 | |||
| 54 | type LogsResponseDetails struct { | ||
| 55 | User models.UserShort `json:"user"` | ||
| 56 | Log string `json:"detail"` | ||
| 57 | Date time.Time `json:"date"` | ||
| 58 | } | ||
| 59 | |||
| 60 | type ScoreLogsResponse struct { | ||
| 61 | Logs []ScoreLogsResponseDetails `json:"scores"` | ||
| 62 | } | ||
| 63 | |||
| 64 | type ScoreLogsResponseDetails struct { | ||
| 65 | Game models.Game `json:"game"` | ||
| 66 | User models.UserShort `json:"user"` | ||
| 67 | Map models.MapShort `json:"map"` | ||
| 68 | ScoreCount int `json:"score_count"` | ||
| 69 | ScoreTime int `json:"score_time"` | ||
| 70 | DemoID string `json:"demo_id"` | ||
| 71 | Date time.Time `json:"date"` | ||
| 72 | } | ||
| 73 | |||
| 74 | // GET Mod Logs | ||
| 75 | // | ||
| 76 | // @Description Get mod logs. | ||
| 77 | // @Tags logs | ||
| 78 | // @Produce json | ||
| 79 | // @Param Authorization header string true "JWT Token" | ||
| 80 | // @Success 200 {object} models.Response{data=LogsResponse} | ||
| 81 | // @Failure 400 {object} models.Response | ||
| 82 | // @Router /logs/mod [get] | ||
| 83 | func ModLogs(c *gin.Context) { | ||
| 84 | mod, exists := c.Get("mod") | ||
| 85 | if !exists || !mod.(bool) { | ||
| 86 | c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) | ||
| 87 | return | ||
| 88 | } | ||
| 89 | response := LogsResponse{Logs: []LogsResponseDetails{}} | ||
| 90 | sql := `SELECT u.user_name, l.user_id, l.type, l.description, l.date | ||
| 91 | FROM logs l INNER JOIN users u ON l.user_id = u.steam_id WHERE type != 'Score' | ||
| 92 | ORDER BY l.date DESC LIMIT 100;` | ||
| 93 | rows, err := database.DB.Query(sql) | ||
| 94 | if err != nil { | ||
| 95 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 96 | return | ||
| 97 | } | ||
| 98 | for rows.Next() { | ||
| 99 | log := Log{} | ||
| 100 | err = rows.Scan(&log.User.UserName, &log.User.SteamID, &log.Type, &log.Description, &log.Date) | ||
| 101 | if err != nil { | ||
| 102 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 103 | return | ||
| 104 | } | ||
| 105 | detail := fmt.Sprintf("%s.%s", log.Type, log.Description) | ||
| 106 | response.Logs = append(response.Logs, LogsResponseDetails{ | ||
| 107 | User: models.UserShort{ | ||
| 108 | SteamID: log.User.SteamID, | ||
| 109 | UserName: log.User.UserName, | ||
| 110 | }, | ||
| 111 | Log: detail, | ||
| 112 | Date: log.Date, | ||
| 113 | }) | ||
| 114 | } | ||
| 115 | c.JSON(http.StatusOK, models.Response{ | ||
| 116 | Success: true, | ||
| 117 | Message: "Successfully retrieved logs.", | ||
| 118 | Data: response, | ||
| 119 | }) | ||
| 120 | } | ||
| 121 | |||
| 122 | // GET Score Logs | ||
| 123 | // | ||
| 124 | // @Description Get score logs of every player. | ||
| 125 | // @Tags logs | ||
| 126 | // @Produce json | ||
| 127 | // @Success 200 {object} models.Response{data=ScoreLogsResponse} | ||
| 128 | // @Failure 400 {object} models.Response | ||
| 129 | // @Router /logs/score [get] | ||
| 130 | func ScoreLogs(c *gin.Context) { | ||
| 131 | response := ScoreLogsResponse{Logs: []ScoreLogsResponseDetails{}} | ||
| 132 | sql := `SELECT g.id, | ||
| 133 | g."name", | ||
| 134 | g.is_coop, | ||
| 135 | rs.map_id, | ||
| 136 | m.name AS map_name, | ||
| 137 | u.steam_id, | ||
| 138 | u.user_name, | ||
| 139 | rs.score_count, | ||
| 140 | rs.score_time, | ||
| 141 | rs.demo_id, | ||
| 142 | rs.record_date | ||
| 143 | FROM ( | ||
| 144 | SELECT id, map_id, user_id, score_count, score_time, demo_id, record_date | ||
| 145 | FROM records_sp | ||
| 146 | |||
| 147 | UNION ALL | ||
| 148 | |||
| 149 | SELECT id, map_id, host_id AS user_id, score_count, score_time, host_demo_id AS demo_id, record_date | ||
| 150 | FROM records_mp | ||
| 151 | |||
| 152 | UNION ALL | ||
| 153 | |||
| 154 | SELECT id, map_id, partner_id AS user_id, score_count, score_time, partner_demo_id AS demo_id, record_date | ||
| 155 | FROM records_mp | ||
| 156 | ) AS rs | ||
| 157 | JOIN users u ON rs.user_id = u.steam_id | ||
| 158 | JOIN maps m ON rs.map_id = m.id | ||
| 159 | JOIN games g ON m.game_id = g.id | ||
| 160 | ORDER BY rs.record_date DESC LIMIT 100;` | ||
| 161 | rows, err := database.DB.Query(sql) | ||
| 162 | if err != nil { | ||
| 163 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 164 | return | ||
| 165 | } | ||
| 166 | for rows.Next() { | ||
| 167 | score := ScoreLogsResponseDetails{} | ||
| 168 | err = rows.Scan(&score.Game.ID, &score.Game.Name, &score.Game.IsCoop, &score.Map.ID, &score.Map.Name, &score.User.SteamID, &score.User.UserName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) | ||
| 169 | if err != nil { | ||
| 170 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 171 | return | ||
| 172 | } | ||
| 173 | response.Logs = append(response.Logs, score) | ||
| 174 | } | ||
| 175 | c.JSON(http.StatusOK, models.Response{ | ||
| 176 | Success: true, | ||
| 177 | Message: "Successfully retrieved score logs.", | ||
| 178 | Data: response, | ||
| 179 | }) | ||
| 180 | } | ||
| 181 | |||
| 182 | func CreateLog(user_id string, log_type string, log_description string) (err error) { | ||
| 183 | sql := `INSERT INTO logs (user_id, "type", description) VALUES($1, $2, $3)` | ||
| 184 | _, err = database.DB.Exec(sql, user_id, log_type, log_description) | ||
| 185 | if err != nil { | ||
| 186 | return err | ||
| 187 | } | ||
| 188 | return nil | ||
| 189 | } | ||
diff --git a/backend/controllers/mapController.go b/backend/handlers/map.go index ebd65dd..1d9cee8 100644 --- a/backend/controllers/mapController.go +++ b/backend/handlers/map.go | |||
| @@ -1,26 +1,69 @@ | |||
| 1 | package controllers | 1 | package handlers |
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "net/http" | 4 | "net/http" |
| 5 | "strconv" | 5 | "strconv" |
| 6 | "time" | ||
| 6 | 7 | ||
| 7 | "github.com/gin-gonic/gin" | 8 | "github.com/gin-gonic/gin" |
| 8 | "github.com/pektezol/leastportalshub/backend/database" | 9 | "github.com/pektezol/leastportalshub/backend/database" |
| 9 | "github.com/pektezol/leastportalshub/backend/models" | 10 | "github.com/pektezol/leastportalshub/backend/models" |
| 10 | ) | 11 | ) |
| 11 | 12 | ||
| 13 | type MapSummaryResponse struct { | ||
| 14 | Map models.Map `json:"map"` | ||
| 15 | Summary models.MapSummary `json:"summary"` | ||
| 16 | } | ||
| 17 | |||
| 18 | type MapLeaderboardsResponse struct { | ||
| 19 | Map models.Map `json:"map"` | ||
| 20 | Records any `json:"records"` | ||
| 21 | } | ||
| 22 | |||
| 23 | type ChaptersResponse struct { | ||
| 24 | Game models.Game `json:"game"` | ||
| 25 | Chapters []models.Chapter `json:"chapters"` | ||
| 26 | } | ||
| 27 | |||
| 28 | type ChapterMapsResponse struct { | ||
| 29 | Chapter models.Chapter `json:"chapter"` | ||
| 30 | Maps []models.MapShort `json:"maps"` | ||
| 31 | } | ||
| 32 | |||
| 33 | type RecordSingleplayer struct { | ||
| 34 | Placement int `json:"placement"` | ||
| 35 | RecordID int `json:"record_id"` | ||
| 36 | ScoreCount int `json:"score_count"` | ||
| 37 | ScoreTime int `json:"score_time"` | ||
| 38 | User models.UserShortWithAvatar `json:"user"` | ||
| 39 | DemoID string `json:"demo_id"` | ||
| 40 | RecordDate time.Time `json:"record_date"` | ||
| 41 | } | ||
| 42 | |||
| 43 | type RecordMultiplayer struct { | ||
| 44 | Placement int `json:"placement"` | ||
| 45 | RecordID int `json:"record_id"` | ||
| 46 | ScoreCount int `json:"score_count"` | ||
| 47 | ScoreTime int `json:"score_time"` | ||
| 48 | Host models.UserShortWithAvatar `json:"host"` | ||
| 49 | Partner models.UserShortWithAvatar `json:"partner"` | ||
| 50 | HostDemoID string `json:"host_demo_id"` | ||
| 51 | PartnerDemoID string `json:"partner_demo_id"` | ||
| 52 | RecordDate time.Time `json:"record_date"` | ||
| 53 | } | ||
| 54 | |||
| 12 | // GET Map Summary | 55 | // GET Map Summary |
| 13 | // | 56 | // |
| 14 | // @Description Get map summary with specified id. | 57 | // @Description Get map summary with specified id. |
| 15 | // @Tags maps | 58 | // @Tags maps |
| 16 | // @Produce json | 59 | // @Produce json |
| 17 | // @Param id path int true "Map ID" | 60 | // @Param id path int true "Map ID" |
| 18 | // @Success 200 {object} models.Response{data=models.MapSummaryResponse} | 61 | // @Success 200 {object} models.Response{data=MapSummaryResponse} |
| 19 | // @Failure 400 {object} models.Response | 62 | // @Failure 400 {object} models.Response |
| 20 | // @Router /maps/{id}/summary [get] | 63 | // @Router /maps/{id}/summary [get] |
| 21 | func FetchMapSummary(c *gin.Context) { | 64 | func FetchMapSummary(c *gin.Context) { |
| 22 | id := c.Param("id") | 65 | id := c.Param("id") |
| 23 | response := models.MapSummaryResponse{Map: models.Map{}, Summary: models.MapSummary{Routes: []models.MapRoute{}}} | 66 | response := MapSummaryResponse{Map: models.Map{}, Summary: models.MapSummary{Routes: []models.MapRoute{}}} |
| 24 | intID, err := strconv.Atoi(id) | 67 | intID, err := strconv.Atoi(id) |
| 25 | if err != nil { | 68 | if err != nil { |
| 26 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 69 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| @@ -73,28 +116,29 @@ func FetchMapSummary(c *gin.Context) { | |||
| 73 | // @Tags maps | 116 | // @Tags maps |
| 74 | // @Produce json | 117 | // @Produce json |
| 75 | // @Param id path int true "Map ID" | 118 | // @Param id path int true "Map ID" |
| 76 | // @Success 200 {object} models.Response{data=models.Map{data=models.MapRecords}} | 119 | // @Success 200 {object} models.Response{data=MapLeaderboardsResponse} |
| 77 | // @Failure 400 {object} models.Response | 120 | // @Failure 400 {object} models.Response |
| 78 | // @Router /maps/{id}/leaderboards [get] | 121 | // @Router /maps/{id}/leaderboards [get] |
| 79 | func FetchMapLeaderboards(c *gin.Context) { | 122 | func FetchMapLeaderboards(c *gin.Context) { |
| 80 | // TODO: make new response type | 123 | // TODO: make new response type |
| 81 | id := c.Param("id") | 124 | id := c.Param("id") |
| 82 | // Get map data | 125 | // Get map data |
| 83 | var mapData models.Map | 126 | response := MapLeaderboardsResponse{Map: models.Map{}, Records: nil} |
| 84 | var mapRecordsData models.MapRecords | 127 | // var mapData models.Map |
| 128 | // var mapRecordsData models.MapRecords | ||
| 85 | var isDisabled bool | 129 | var isDisabled bool |
| 86 | intID, err := strconv.Atoi(id) | 130 | intID, err := strconv.Atoi(id) |
| 87 | if err != nil { | 131 | if err != nil { |
| 88 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 132 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 89 | return | 133 | return |
| 90 | } | 134 | } |
| 91 | mapData.ID = intID | 135 | response.Map.ID = intID |
| 92 | sql := `SELECT g.name, c.name, m.name, is_disabled, m.image | 136 | sql := `SELECT g.name, c.name, m.name, is_disabled, m.image, g.is_coop |
| 93 | FROM maps m | 137 | FROM maps m |
| 94 | INNER JOIN games g ON m.game_id = g.id | 138 | INNER JOIN games g ON m.game_id = g.id |
| 95 | INNER JOIN chapters c ON m.chapter_id = c.id | 139 | INNER JOIN chapters c ON m.chapter_id = c.id |
| 96 | WHERE m.id = $1` | 140 | WHERE m.id = $1` |
| 97 | err = database.DB.QueryRow(sql, id).Scan(&mapData.GameName, &mapData.ChapterName, &mapData.MapName, &isDisabled, &mapData.Image) | 141 | err = database.DB.QueryRow(sql, id).Scan(&response.Map.GameName, &response.Map.ChapterName, &response.Map.MapName, &isDisabled, &response.Map.Image, &response.Map.IsCoop) |
| 98 | if err != nil { | 142 | if err != nil { |
| 99 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 143 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 100 | return | 144 | return |
| @@ -104,17 +148,38 @@ func FetchMapLeaderboards(c *gin.Context) { | |||
| 104 | return | 148 | return |
| 105 | } | 149 | } |
| 106 | // TODO: avatar and names for host & partner | 150 | // TODO: avatar and names for host & partner |
| 107 | // Get records from the map | 151 | if response.Map.GameName == "Portal 2 - Cooperative" { |
| 108 | if mapData.GameName == "Portal 2 - Cooperative" { | 152 | records := []RecordMultiplayer{} |
| 109 | var records []models.RecordMP | 153 | sql = `SELECT |
| 110 | sql = `SELECT id, host_id, partner_id, score_count, score_time, host_demo_id, partner_demo_id, record_date | 154 | sub.id, |
| 111 | FROM ( | 155 | sub.host_id, |
| 112 | SELECT id, host_id, partner_id, score_count, score_time, host_demo_id, partner_demo_id, record_date, | 156 | host.user_name AS host_user_name, |
| 113 | ROW_NUMBER() OVER (PARTITION BY host_id, partner_id ORDER BY score_count, score_time) AS rn | 157 | host.avatar_link AS host_avatar_link, |
| 114 | FROM records_mp | 158 | sub.partner_id, |
| 115 | WHERE map_id = $1 | 159 | partner.user_name AS partner_user_name, |
| 116 | ) sub | 160 | partner.avatar_link AS partner_avatar_link, |
| 117 | WHERE rn = 1` | 161 | sub.score_count, |
| 162 | sub.score_time, | ||
| 163 | sub.host_demo_id, | ||
| 164 | sub.partner_demo_id, | ||
| 165 | sub.record_date | ||
| 166 | FROM ( | ||
| 167 | SELECT | ||
| 168 | id, | ||
| 169 | host_id, | ||
| 170 | partner_id, | ||
| 171 | score_count, | ||
| 172 | score_time, | ||
| 173 | host_demo_id, | ||
| 174 | partner_demo_id, | ||
| 175 | record_date, | ||
| 176 | ROW_NUMBER() OVER (PARTITION BY host_id, partner_id ORDER BY score_count, score_time) AS rn | ||
| 177 | FROM records_mp | ||
| 178 | WHERE map_id = $1 | ||
| 179 | ) sub | ||
| 180 | JOIN users AS host ON sub.host_id = host.steam_id | ||
| 181 | JOIN users AS partner ON sub.partner_id = partner.steam_id | ||
| 182 | WHERE sub.rn = 1;` | ||
| 118 | rows, err := database.DB.Query(sql, id) | 183 | rows, err := database.DB.Query(sql, id) |
| 119 | if err != nil { | 184 | if err != nil { |
| 120 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 185 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| @@ -123,8 +188,8 @@ func FetchMapLeaderboards(c *gin.Context) { | |||
| 123 | placement := 1 | 188 | placement := 1 |
| 124 | ties := 0 | 189 | ties := 0 |
| 125 | for rows.Next() { | 190 | for rows.Next() { |
| 126 | var record models.RecordMP | 191 | var record RecordMultiplayer |
| 127 | err := rows.Scan(&record.RecordID, &record.HostID, &record.PartnerID, &record.ScoreCount, &record.ScoreTime, &record.HostDemoID, &record.PartnerDemoID, &record.RecordDate) | 192 | err := rows.Scan(&record.RecordID, &record.Host.SteamID, &record.Host.UserName, &record.Host.AvatarLink, &record.Partner.SteamID, &record.Partner.UserName, &record.Partner.AvatarLink, &record.ScoreCount, &record.ScoreTime, &record.HostDemoID, &record.PartnerDemoID, &record.RecordDate) |
| 128 | if err != nil { | 193 | if err != nil { |
| 129 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 194 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 130 | return | 195 | return |
| @@ -138,9 +203,9 @@ func FetchMapLeaderboards(c *gin.Context) { | |||
| 138 | records = append(records, record) | 203 | records = append(records, record) |
| 139 | placement++ | 204 | placement++ |
| 140 | } | 205 | } |
| 141 | mapRecordsData.Records = records | 206 | response.Records = records |
| 142 | } else { | 207 | } else { |
| 143 | var records []models.RecordSP | 208 | records := []RecordSingleplayer{} |
| 144 | sql = `SELECT id, user_id, users.user_name, users.avatar_link, score_count, score_time, demo_id, record_date | 209 | sql = `SELECT id, user_id, users.user_name, users.avatar_link, score_count, score_time, demo_id, record_date |
| 145 | FROM ( | 210 | FROM ( |
| 146 | SELECT id, user_id, score_count, score_time, demo_id, record_date, | 211 | SELECT id, user_id, score_count, score_time, demo_id, record_date, |
| @@ -158,8 +223,8 @@ func FetchMapLeaderboards(c *gin.Context) { | |||
| 158 | placement := 1 | 223 | placement := 1 |
| 159 | ties := 0 | 224 | ties := 0 |
| 160 | for rows.Next() { | 225 | for rows.Next() { |
| 161 | var record models.RecordSP | 226 | var record RecordSingleplayer |
| 162 | err := rows.Scan(&record.RecordID, &record.UserID, &record.UserName, &record.UserAvatar, &record.ScoreCount, &record.ScoreTime, &record.DemoID, &record.RecordDate) | 227 | err := rows.Scan(&record.RecordID, &record.User.SteamID, &record.User.UserName, &record.User.AvatarLink, &record.ScoreCount, &record.ScoreTime, &record.DemoID, &record.RecordDate) |
| 163 | if err != nil { | 228 | if err != nil { |
| 164 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 229 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 165 | return | 230 | return |
| @@ -173,14 +238,12 @@ func FetchMapLeaderboards(c *gin.Context) { | |||
| 173 | records = append(records, record) | 238 | records = append(records, record) |
| 174 | placement++ | 239 | placement++ |
| 175 | } | 240 | } |
| 176 | mapRecordsData.Records = records | 241 | response.Records = records |
| 177 | } | 242 | } |
| 178 | // mapData.Data = mapRecordsData | ||
| 179 | // Return response | ||
| 180 | c.JSON(http.StatusOK, models.Response{ | 243 | c.JSON(http.StatusOK, models.Response{ |
| 181 | Success: true, | 244 | Success: true, |
| 182 | Message: "Successfully retrieved map leaderboards.", | 245 | Message: "Successfully retrieved map leaderboards.", |
| 183 | Data: mapData, | 246 | Data: response, |
| 184 | }) | 247 | }) |
| 185 | } | 248 | } |
| 186 | 249 | ||
| @@ -220,7 +283,7 @@ func FetchGames(c *gin.Context) { | |||
| 220 | // @Tags games & chapters | 283 | // @Tags games & chapters |
| 221 | // @Produce json | 284 | // @Produce json |
| 222 | // @Param id path int true "Game ID" | 285 | // @Param id path int true "Game ID" |
| 223 | // @Success 200 {object} models.Response{data=models.ChaptersResponse} | 286 | // @Success 200 {object} models.Response{data=ChaptersResponse} |
| 224 | // @Failure 400 {object} models.Response | 287 | // @Failure 400 {object} models.Response |
| 225 | // @Router /games/{id} [get] | 288 | // @Router /games/{id} [get] |
| 226 | func FetchChapters(c *gin.Context) { | 289 | func FetchChapters(c *gin.Context) { |
| @@ -230,7 +293,7 @@ func FetchChapters(c *gin.Context) { | |||
| 230 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 293 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 231 | return | 294 | return |
| 232 | } | 295 | } |
| 233 | var response models.ChaptersResponse | 296 | var response ChaptersResponse |
| 234 | rows, err := database.DB.Query(`SELECT c.id, c.name, g.name FROM chapters c INNER JOIN games g ON c.game_id = g.id WHERE game_id = $1`, gameID) | 297 | rows, err := database.DB.Query(`SELECT c.id, c.name, g.name FROM chapters c INNER JOIN games g ON c.game_id = g.id WHERE game_id = $1`, gameID) |
| 235 | if err != nil { | 298 | if err != nil { |
| 236 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 299 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| @@ -262,7 +325,7 @@ func FetchChapters(c *gin.Context) { | |||
| 262 | // @Tags games & chapters | 325 | // @Tags games & chapters |
| 263 | // @Produce json | 326 | // @Produce json |
| 264 | // @Param id path int true "Chapter ID" | 327 | // @Param id path int true "Chapter ID" |
| 265 | // @Success 200 {object} models.Response{data=models.ChapterMapsResponse} | 328 | // @Success 200 {object} models.Response{data=ChapterMapsResponse} |
| 266 | // @Failure 400 {object} models.Response | 329 | // @Failure 400 {object} models.Response |
| 267 | // @Router /chapters/{id} [get] | 330 | // @Router /chapters/{id} [get] |
| 268 | func FetchChapterMaps(c *gin.Context) { | 331 | func FetchChapterMaps(c *gin.Context) { |
| @@ -272,7 +335,7 @@ func FetchChapterMaps(c *gin.Context) { | |||
| 272 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 335 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 273 | return | 336 | return |
| 274 | } | 337 | } |
| 275 | var response models.ChapterMapsResponse | 338 | var response ChapterMapsResponse |
| 276 | rows, err := database.DB.Query(`SELECT m.id, m.name, c.name FROM maps m INNER JOIN chapters c ON m.chapter_id = c.id WHERE chapter_id = $1`, chapterID) | 339 | rows, err := database.DB.Query(`SELECT m.id, m.name, c.name FROM maps m INNER JOIN chapters c ON m.chapter_id = c.id WHERE chapter_id = $1`, chapterID) |
| 277 | if err != nil { | 340 | if err != nil { |
| 278 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 341 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
diff --git a/backend/controllers/modController.go b/backend/handlers/mod.go index e2add1f..9e93395 100644 --- a/backend/controllers/modController.go +++ b/backend/handlers/mod.go | |||
| @@ -1,23 +1,50 @@ | |||
| 1 | package controllers | 1 | package handlers |
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "net/http" | 4 | "net/http" |
| 5 | "strconv" | 5 | "strconv" |
| 6 | "time" | ||
| 6 | 7 | ||
| 7 | "github.com/gin-gonic/gin" | 8 | "github.com/gin-gonic/gin" |
| 8 | "github.com/pektezol/leastportalshub/backend/database" | 9 | "github.com/pektezol/leastportalshub/backend/database" |
| 9 | "github.com/pektezol/leastportalshub/backend/models" | 10 | "github.com/pektezol/leastportalshub/backend/models" |
| 10 | ) | 11 | ) |
| 11 | 12 | ||
| 13 | type CreateMapSummaryRequest struct { | ||
| 14 | CategoryID int `json:"category_id" binding:"required"` | ||
| 15 | Description string `json:"description" binding:"required"` | ||
| 16 | Showcase string `json:"showcase"` | ||
| 17 | UserName string `json:"user_name" binding:"required"` | ||
| 18 | ScoreCount *int `json:"score_count" binding:"required"` | ||
| 19 | RecordDate time.Time `json:"record_date" binding:"required"` | ||
| 20 | } | ||
| 21 | |||
| 22 | type EditMapSummaryRequest struct { | ||
| 23 | RouteID int `json:"route_id" binding:"required"` | ||
| 24 | Description string `json:"description" binding:"required"` | ||
| 25 | Showcase string `json:"showcase"` | ||
| 26 | UserName string `json:"user_name" binding:"required"` | ||
| 27 | ScoreCount int `json:"score_count" binding:"required"` | ||
| 28 | RecordDate time.Time `json:"record_date" binding:"required"` | ||
| 29 | } | ||
| 30 | |||
| 31 | type DeleteMapSummaryRequest struct { | ||
| 32 | RouteID int `json:"route_id" binding:"required"` | ||
| 33 | } | ||
| 34 | |||
| 35 | type EditMapImageRequest struct { | ||
| 36 | Image string `json:"image" binding:"required"` | ||
| 37 | } | ||
| 38 | |||
| 12 | // POST Map Summary | 39 | // POST Map Summary |
| 13 | // | 40 | // |
| 14 | // @Description Create map summary with specified map id. | 41 | // @Description Create map summary with specified map id. |
| 15 | // @Tags maps | 42 | // @Tags maps |
| 16 | // @Produce json | 43 | // @Produce json |
| 17 | // @Param Authorization header string true "JWT Token" | 44 | // @Param Authorization header string true "JWT Token" |
| 18 | // @Param id path int true "Map ID" | 45 | // @Param id path int true "Map ID" |
| 19 | // @Param request body models.CreateMapSummaryRequest true "Body" | 46 | // @Param request body CreateMapSummaryRequest true "Body" |
| 20 | // @Success 200 {object} models.Response{data=models.CreateMapSummaryRequest} | 47 | // @Success 200 {object} models.Response{data=CreateMapSummaryRequest} |
| 21 | // @Failure 400 {object} models.Response | 48 | // @Failure 400 {object} models.Response |
| 22 | // @Router /maps/{id}/summary [post] | 49 | // @Router /maps/{id}/summary [post] |
| 23 | func CreateMapSummary(c *gin.Context) { | 50 | func CreateMapSummary(c *gin.Context) { |
| @@ -27,13 +54,8 @@ func CreateMapSummary(c *gin.Context) { | |||
| 27 | c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) | 54 | c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) |
| 28 | return | 55 | return |
| 29 | } | 56 | } |
| 30 | var moderator bool | 57 | mod, exists := c.Get("mod") |
| 31 | for _, title := range user.(models.User).Titles { | 58 | if !exists || !mod.(bool) { |
| 32 | if title == "Moderator" { | ||
| 33 | moderator = true | ||
| 34 | } | ||
| 35 | } | ||
| 36 | if !moderator { | ||
| 37 | c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) | 59 | c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) |
| 38 | return | 60 | return |
| 39 | } | 61 | } |
| @@ -44,7 +66,7 @@ func CreateMapSummary(c *gin.Context) { | |||
| 44 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 66 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 45 | return | 67 | return |
| 46 | } | 68 | } |
| 47 | var request models.CreateMapSummaryRequest | 69 | var request CreateMapSummaryRequest |
| 48 | if err := c.BindJSON(&request); err != nil { | 70 | if err := c.BindJSON(&request); err != nil { |
| 49 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 71 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 50 | return | 72 | return |
| @@ -87,7 +109,7 @@ func CreateMapSummary(c *gin.Context) { | |||
| 87 | c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) | 109 | c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) |
| 88 | return | 110 | return |
| 89 | } | 111 | } |
| 90 | // Return response | 112 | CreateLog(user.(models.User).SteamID, LogTypeMod, LogDescriptionMapSummaryCreate) |
| 91 | c.JSON(http.StatusOK, models.Response{ | 113 | c.JSON(http.StatusOK, models.Response{ |
| 92 | Success: true, | 114 | Success: true, |
| 93 | Message: "Successfully created map summary.", | 115 | Message: "Successfully created map summary.", |
| @@ -100,10 +122,10 @@ func CreateMapSummary(c *gin.Context) { | |||
| 100 | // @Description Edit map summary with specified map id. | 122 | // @Description Edit map summary with specified map id. |
| 101 | // @Tags maps | 123 | // @Tags maps |
| 102 | // @Produce json | 124 | // @Produce json |
| 103 | // @Param Authorization header string true "JWT Token" | 125 | // @Param Authorization header string true "JWT Token" |
| 104 | // @Param id path int true "Map ID" | 126 | // @Param id path int true "Map ID" |
| 105 | // @Param request body models.EditMapSummaryRequest true "Body" | 127 | // @Param request body EditMapSummaryRequest true "Body" |
| 106 | // @Success 200 {object} models.Response{data=models.EditMapSummaryRequest} | 128 | // @Success 200 {object} models.Response{data=EditMapSummaryRequest} |
| 107 | // @Failure 400 {object} models.Response | 129 | // @Failure 400 {object} models.Response |
| 108 | // @Router /maps/{id}/summary [put] | 130 | // @Router /maps/{id}/summary [put] |
| 109 | func EditMapSummary(c *gin.Context) { | 131 | func EditMapSummary(c *gin.Context) { |
| @@ -113,13 +135,8 @@ func EditMapSummary(c *gin.Context) { | |||
| 113 | c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) | 135 | c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) |
| 114 | return | 136 | return |
| 115 | } | 137 | } |
| 116 | var moderator bool | 138 | mod, exists := c.Get("mod") |
| 117 | for _, title := range user.(models.User).Titles { | 139 | if !exists || !mod.(bool) { |
| 118 | if title == "Moderator" { | ||
| 119 | moderator = true | ||
| 120 | } | ||
| 121 | } | ||
| 122 | if !moderator { | ||
| 123 | c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) | 140 | c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) |
| 124 | return | 141 | return |
| 125 | } | 142 | } |
| @@ -130,7 +147,7 @@ func EditMapSummary(c *gin.Context) { | |||
| 130 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 147 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 131 | return | 148 | return |
| 132 | } | 149 | } |
| 133 | var request models.EditMapSummaryRequest | 150 | var request EditMapSummaryRequest |
| 134 | if err := c.BindJSON(&request); err != nil { | 151 | if err := c.BindJSON(&request); err != nil { |
| 135 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 152 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 136 | return | 153 | return |
| @@ -173,7 +190,7 @@ func EditMapSummary(c *gin.Context) { | |||
| 173 | c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) | 190 | c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) |
| 174 | return | 191 | return |
| 175 | } | 192 | } |
| 176 | // Return response | 193 | CreateLog(user.(models.User).SteamID, LogTypeMod, LogDescriptionMapSummaryEdit) |
| 177 | c.JSON(http.StatusOK, models.Response{ | 194 | c.JSON(http.StatusOK, models.Response{ |
| 178 | Success: true, | 195 | Success: true, |
| 179 | Message: "Successfully updated map summary.", | 196 | Message: "Successfully updated map summary.", |
| @@ -186,10 +203,10 @@ func EditMapSummary(c *gin.Context) { | |||
| 186 | // @Description Delete map summary with specified map id. | 203 | // @Description Delete map summary with specified map id. |
| 187 | // @Tags maps | 204 | // @Tags maps |
| 188 | // @Produce json | 205 | // @Produce json |
| 189 | // @Param Authorization header string true "JWT Token" | 206 | // @Param Authorization header string true "JWT Token" |
| 190 | // @Param id path int true "Map ID" | 207 | // @Param id path int true "Map ID" |
| 191 | // @Param request body models.DeleteMapSummaryRequest true "Body" | 208 | // @Param request body DeleteMapSummaryRequest true "Body" |
| 192 | // @Success 200 {object} models.Response{data=models.DeleteMapSummaryRequest} | 209 | // @Success 200 {object} models.Response{data=DeleteMapSummaryRequest} |
| 193 | // @Failure 400 {object} models.Response | 210 | // @Failure 400 {object} models.Response |
| 194 | // @Router /maps/{id}/summary [delete] | 211 | // @Router /maps/{id}/summary [delete] |
| 195 | func DeleteMapSummary(c *gin.Context) { | 212 | func DeleteMapSummary(c *gin.Context) { |
| @@ -199,13 +216,8 @@ func DeleteMapSummary(c *gin.Context) { | |||
| 199 | c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) | 216 | c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) |
| 200 | return | 217 | return |
| 201 | } | 218 | } |
| 202 | var moderator bool | 219 | mod, exists := c.Get("mod") |
| 203 | for _, title := range user.(models.User).Titles { | 220 | if !exists || !mod.(bool) { |
| 204 | if title == "Moderator" { | ||
| 205 | moderator = true | ||
| 206 | } | ||
| 207 | } | ||
| 208 | if !moderator { | ||
| 209 | c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) | 221 | c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) |
| 210 | return | 222 | return |
| 211 | } | 223 | } |
| @@ -216,7 +228,7 @@ func DeleteMapSummary(c *gin.Context) { | |||
| 216 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 228 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 217 | return | 229 | return |
| 218 | } | 230 | } |
| 219 | var request models.DeleteMapSummaryRequest | 231 | var request DeleteMapSummaryRequest |
| 220 | if err := c.BindJSON(&request); err != nil { | 232 | if err := c.BindJSON(&request); err != nil { |
| 221 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 233 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 222 | return | 234 | return |
| @@ -263,7 +275,7 @@ func DeleteMapSummary(c *gin.Context) { | |||
| 263 | c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) | 275 | c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) |
| 264 | return | 276 | return |
| 265 | } | 277 | } |
| 266 | // Return response | 278 | CreateLog(user.(models.User).SteamID, LogTypeMod, LogDescriptionMapSummaryDelete) |
| 267 | c.JSON(http.StatusOK, models.Response{ | 279 | c.JSON(http.StatusOK, models.Response{ |
| 268 | Success: true, | 280 | Success: true, |
| 269 | Message: "Successfully delete map summary.", | 281 | Message: "Successfully delete map summary.", |
| @@ -276,10 +288,10 @@ func DeleteMapSummary(c *gin.Context) { | |||
| 276 | // @Description Edit map image with specified map id. | 288 | // @Description Edit map image with specified map id. |
| 277 | // @Tags maps | 289 | // @Tags maps |
| 278 | // @Produce json | 290 | // @Produce json |
| 279 | // @Param Authorization header string true "JWT Token" | 291 | // @Param Authorization header string true "JWT Token" |
| 280 | // @Param id path int true "Map ID" | 292 | // @Param id path int true "Map ID" |
| 281 | // @Param request body models.EditMapImageRequest true "Body" | 293 | // @Param request body EditMapImageRequest true "Body" |
| 282 | // @Success 200 {object} models.Response{data=models.EditMapImageRequest} | 294 | // @Success 200 {object} models.Response{data=EditMapImageRequest} |
| 283 | // @Failure 400 {object} models.Response | 295 | // @Failure 400 {object} models.Response |
| 284 | // @Router /maps/{id}/image [put] | 296 | // @Router /maps/{id}/image [put] |
| 285 | func EditMapImage(c *gin.Context) { | 297 | func EditMapImage(c *gin.Context) { |
| @@ -289,13 +301,8 @@ func EditMapImage(c *gin.Context) { | |||
| 289 | c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) | 301 | c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) |
| 290 | return | 302 | return |
| 291 | } | 303 | } |
| 292 | var moderator bool | 304 | mod, exists := c.Get("mod") |
| 293 | for _, title := range user.(models.User).Titles { | 305 | if !exists || !mod.(bool) { |
| 294 | if title == "Moderator" { | ||
| 295 | moderator = true | ||
| 296 | } | ||
| 297 | } | ||
| 298 | if !moderator { | ||
| 299 | c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) | 306 | c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) |
| 300 | return | 307 | return |
| 301 | } | 308 | } |
| @@ -306,7 +313,7 @@ func EditMapImage(c *gin.Context) { | |||
| 306 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 313 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 307 | return | 314 | return |
| 308 | } | 315 | } |
| 309 | var request models.EditMapImageRequest | 316 | var request EditMapImageRequest |
| 310 | if err := c.BindJSON(&request); err != nil { | 317 | if err := c.BindJSON(&request); err != nil { |
| 311 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 318 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 312 | return | 319 | return |
| @@ -318,7 +325,7 @@ func EditMapImage(c *gin.Context) { | |||
| 318 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 325 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 319 | return | 326 | return |
| 320 | } | 327 | } |
| 321 | // Return response | 328 | CreateLog(user.(models.User).SteamID, LogTypeMod, LogDescriptionMapSummaryEditImage) |
| 322 | c.JSON(http.StatusOK, models.Response{ | 329 | c.JSON(http.StatusOK, models.Response{ |
| 323 | Success: true, | 330 | Success: true, |
| 324 | Message: "Successfully updated map image.", | 331 | Message: "Successfully updated map image.", |
diff --git a/backend/controllers/recordController.go b/backend/handlers/record.go index 951be41..3d29eb8 100644 --- a/backend/controllers/recordController.go +++ b/backend/handlers/record.go | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | package controllers | 1 | package handlers |
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "context" | 4 | "context" |
| @@ -19,6 +19,18 @@ import ( | |||
| 19 | "google.golang.org/api/drive/v3" | 19 | "google.golang.org/api/drive/v3" |
| 20 | ) | 20 | ) |
| 21 | 21 | ||
| 22 | type RecordRequest struct { | ||
| 23 | HostDemo *multipart.FileHeader `json:"host_demo" form:"host_demo" binding:"required" swaggerignore:"true"` | ||
| 24 | PartnerDemo *multipart.FileHeader `json:"partner_demo" form:"partner_demo" swaggerignore:"true"` | ||
| 25 | IsPartnerOrange bool `json:"is_partner_orange" form:"is_partner_orange"` | ||
| 26 | PartnerID string `json:"partner_id" form:"partner_id"` | ||
| 27 | } | ||
| 28 | |||
| 29 | type RecordResponse struct { | ||
| 30 | ScoreCount int `json:"score_count"` | ||
| 31 | ScoreTime int `json:"score_time"` | ||
| 32 | } | ||
| 33 | |||
| 22 | // POST Record | 34 | // POST Record |
| 23 | // | 35 | // |
| 24 | // @Description Post record with demo of a specific map. | 36 | // @Description Post record with demo of a specific map. |
| @@ -31,7 +43,7 @@ import ( | |||
| 31 | // @Param partner_demo formData file false "Partner Demo" | 43 | // @Param partner_demo formData file false "Partner Demo" |
| 32 | // @Param is_partner_orange formData boolean false "Is Partner Orange" | 44 | // @Param is_partner_orange formData boolean false "Is Partner Orange" |
| 33 | // @Param partner_id formData string false "Partner ID" | 45 | // @Param partner_id formData string false "Partner ID" |
| 34 | // @Success 200 {object} models.Response{data=models.RecordResponse} | 46 | // @Success 200 {object} models.Response{data=RecordResponse} |
| 35 | // @Failure 400 {object} models.Response | 47 | // @Failure 400 {object} models.Response |
| 36 | // @Failure 401 {object} models.Response | 48 | // @Failure 401 {object} models.Response |
| 37 | // @Router /maps/{id}/record [post] | 49 | // @Router /maps/{id}/record [post] |
| @@ -54,6 +66,7 @@ func CreateRecordWithDemo(c *gin.Context) { | |||
| 54 | return | 66 | return |
| 55 | } | 67 | } |
| 56 | if isDisabled { | 68 | if isDisabled { |
| 69 | CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInvalidRequest) | ||
| 57 | c.JSON(http.StatusBadRequest, models.ErrorResponse("Map is not available for competitive boards.")) | 70 | c.JSON(http.StatusBadRequest, models.ErrorResponse("Map is not available for competitive boards.")) |
| 58 | return | 71 | return |
| 59 | } | 72 | } |
| @@ -61,12 +74,14 @@ func CreateRecordWithDemo(c *gin.Context) { | |||
| 61 | isCoop = true | 74 | isCoop = true |
| 62 | } | 75 | } |
| 63 | // Get record request | 76 | // Get record request |
| 64 | var record models.RecordRequest | 77 | var record RecordRequest |
| 65 | if err := c.ShouldBind(&record); err != nil { | 78 | if err := c.ShouldBind(&record); err != nil { |
| 79 | CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInvalidRequest) | ||
| 66 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 80 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 67 | return | 81 | return |
| 68 | } | 82 | } |
| 69 | if isCoop && (record.PartnerDemo == nil || record.PartnerID == "") { | 83 | if isCoop && (record.PartnerDemo == nil || record.PartnerID == "") { |
| 84 | CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInvalidRequest) | ||
| 70 | c.JSON(http.StatusBadRequest, models.ErrorResponse("Invalid entry for coop record submission.")) | 85 | c.JSON(http.StatusBadRequest, models.ErrorResponse("Invalid entry for coop record submission.")) |
| 71 | return | 86 | return |
| 72 | } | 87 | } |
| @@ -96,23 +111,27 @@ func CreateRecordWithDemo(c *gin.Context) { | |||
| 96 | // Upload & insert into demos | 111 | // Upload & insert into demos |
| 97 | err = c.SaveUploadedFile(header, "backend/parser/"+uuid+".dem") | 112 | err = c.SaveUploadedFile(header, "backend/parser/"+uuid+".dem") |
| 98 | if err != nil { | 113 | if err != nil { |
| 114 | CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailSaveDemo) | ||
| 99 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 115 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 100 | return | 116 | return |
| 101 | } | 117 | } |
| 102 | defer os.Remove("backend/parser/" + uuid + ".dem") | 118 | defer os.Remove("backend/parser/" + uuid + ".dem") |
| 103 | f, err := os.Open("backend/parser/" + uuid + ".dem") | 119 | f, err := os.Open("backend/parser/" + uuid + ".dem") |
| 104 | if err != nil { | 120 | if err != nil { |
| 121 | CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailOpenDemo) | ||
| 105 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 122 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 106 | return | 123 | return |
| 107 | } | 124 | } |
| 108 | defer f.Close() | 125 | defer f.Close() |
| 109 | file, err := createFile(srv, uuid+".dem", "application/octet-stream", f, os.Getenv("GOOGLE_FOLDER_ID")) | 126 | file, err := createFile(srv, uuid+".dem", "application/octet-stream", f, os.Getenv("GOOGLE_FOLDER_ID")) |
| 110 | if err != nil { | 127 | if err != nil { |
| 128 | CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailCreateDemo) | ||
| 111 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 129 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 112 | return | 130 | return |
| 113 | } | 131 | } |
| 114 | hostDemoScoreCount, hostDemoScoreTime, err = parser.ProcessDemo("backend/parser/" + uuid + ".dem") | 132 | hostDemoScoreCount, hostDemoScoreTime, err = parser.ProcessDemo("backend/parser/" + uuid + ".dem") |
| 115 | if err != nil { | 133 | if err != nil { |
| 134 | CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailProcessDemo) | ||
| 116 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 135 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 117 | return | 136 | return |
| 118 | } | 137 | } |
| @@ -126,6 +145,7 @@ func CreateRecordWithDemo(c *gin.Context) { | |||
| 126 | _, err = tx.Exec(`INSERT INTO demos (id,location_id) VALUES ($1,$2)`, uuid, file.Id) | 145 | _, err = tx.Exec(`INSERT INTO demos (id,location_id) VALUES ($1,$2)`, uuid, file.Id) |
| 127 | if err != nil { | 146 | if err != nil { |
| 128 | deleteFile(srv, file.Id) | 147 | deleteFile(srv, file.Id) |
| 148 | CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInsertDemo) | ||
| 129 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 149 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 130 | return | 150 | return |
| 131 | } | 151 | } |
| @@ -147,6 +167,7 @@ func CreateRecordWithDemo(c *gin.Context) { | |||
| 147 | if err != nil { | 167 | if err != nil { |
| 148 | deleteFile(srv, hostDemoFileID) | 168 | deleteFile(srv, hostDemoFileID) |
| 149 | deleteFile(srv, partnerDemoFileID) | 169 | deleteFile(srv, partnerDemoFileID) |
| 170 | CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInsertRecord) | ||
| 150 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 171 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 151 | return | 172 | return |
| 152 | } | 173 | } |
| @@ -164,6 +185,7 @@ func CreateRecordWithDemo(c *gin.Context) { | |||
| 164 | _, err := tx.Exec(sql, mapId, hostDemoScoreCount, hostDemoScoreTime, user.(models.User).SteamID, hostDemoUUID) | 185 | _, err := tx.Exec(sql, mapId, hostDemoScoreCount, hostDemoScoreTime, user.(models.User).SteamID, hostDemoUUID) |
| 165 | if err != nil { | 186 | if err != nil { |
| 166 | deleteFile(srv, hostDemoFileID) | 187 | deleteFile(srv, hostDemoFileID) |
| 188 | CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInsertRecord) | ||
| 167 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 189 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 168 | return | 190 | return |
| 169 | } | 191 | } |
| @@ -180,10 +202,11 @@ func CreateRecordWithDemo(c *gin.Context) { | |||
| 180 | c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) | 202 | c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) |
| 181 | return | 203 | return |
| 182 | } | 204 | } |
| 205 | CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordSuccess) | ||
| 183 | c.JSON(http.StatusOK, models.Response{ | 206 | c.JSON(http.StatusOK, models.Response{ |
| 184 | Success: true, | 207 | Success: true, |
| 185 | Message: "Successfully created record.", | 208 | Message: "Successfully created record.", |
| 186 | Data: models.RecordResponse{ScoreCount: hostDemoScoreCount, ScoreTime: hostDemoScoreTime}, | 209 | Data: RecordResponse{ScoreCount: hostDemoScoreCount, ScoreTime: hostDemoScoreTime}, |
| 187 | }) | 210 | }) |
| 188 | } | 211 | } |
| 189 | 212 | ||
| @@ -216,6 +239,10 @@ func DownloadDemoWithID(c *gin.Context) { | |||
| 216 | url := "https://drive.google.com/uc?export=download&id=" + locationID | 239 | url := "https://drive.google.com/uc?export=download&id=" + locationID |
| 217 | fileName := uuid + ".dem" | 240 | fileName := uuid + ".dem" |
| 218 | output, err := os.Create(fileName) | 241 | output, err := os.Create(fileName) |
| 242 | if err != nil { | ||
| 243 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 244 | return | ||
| 245 | } | ||
| 219 | defer os.Remove(fileName) | 246 | defer os.Remove(fileName) |
| 220 | defer output.Close() | 247 | defer output.Close() |
| 221 | response, err := http.Get(url) | 248 | response, err := http.Get(url) |
| @@ -253,6 +280,7 @@ func serviceAccount() *http.Client { | |||
| 253 | return client | 280 | return client |
| 254 | } | 281 | } |
| 255 | 282 | ||
| 283 | // Create Gdrive file | ||
| 256 | func createFile(service *drive.Service, name string, mimeType string, content io.Reader, parentId string) (*drive.File, error) { | 284 | func createFile(service *drive.Service, name string, mimeType string, content io.Reader, parentId string) (*drive.File, error) { |
| 257 | f := &drive.File{ | 285 | f := &drive.File{ |
| 258 | MimeType: mimeType, | 286 | MimeType: mimeType, |
| @@ -269,6 +297,7 @@ func createFile(service *drive.Service, name string, mimeType string, content io | |||
| 269 | return file, nil | 297 | return file, nil |
| 270 | } | 298 | } |
| 271 | 299 | ||
| 300 | // Delete Gdrive file | ||
| 272 | func deleteFile(service *drive.Service, fileId string) { | 301 | func deleteFile(service *drive.Service, fileId string) { |
| 273 | service.Files.Delete(fileId) | 302 | service.Files.Delete(fileId) |
| 274 | } | 303 | } |
diff --git a/backend/handlers/user.go b/backend/handlers/user.go new file mode 100644 index 0000000..742a57c --- /dev/null +++ b/backend/handlers/user.go | |||
| @@ -0,0 +1,719 @@ | |||
| 1 | package handlers | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "net/http" | ||
| 5 | "os" | ||
| 6 | "regexp" | ||
| 7 | "time" | ||
| 8 | |||
| 9 | "github.com/gin-gonic/gin" | ||
| 10 | "github.com/pektezol/leastportalshub/backend/database" | ||
| 11 | "github.com/pektezol/leastportalshub/backend/models" | ||
| 12 | ) | ||
| 13 | |||
| 14 | type ProfileResponse struct { | ||
| 15 | Profile bool `json:"profile"` | ||
| 16 | SteamID string `json:"steam_id"` | ||
| 17 | UserName string `json:"user_name"` | ||
| 18 | AvatarLink string `json:"avatar_link"` | ||
| 19 | CountryCode string `json:"country_code"` | ||
| 20 | Titles []models.Title `json:"titles"` | ||
| 21 | Links models.Links `json:"links"` | ||
| 22 | Rankings ProfileRankings `json:"rankings"` | ||
| 23 | Records []ProfileRecords `json:"records"` | ||
| 24 | } | ||
| 25 | |||
| 26 | type ProfileRankings struct { | ||
| 27 | Overall ProfileRankingsDetails `json:"overall"` | ||
| 28 | Singleplayer ProfileRankingsDetails `json:"singleplayer"` | ||
| 29 | Cooperative ProfileRankingsDetails `json:"cooperative"` | ||
| 30 | } | ||
| 31 | |||
| 32 | type ProfileRankingsDetails struct { | ||
| 33 | Rank int `json:"rank"` | ||
| 34 | CompletionCount int `json:"completion_count"` | ||
| 35 | CompletionTotal int `json:"completion_total"` | ||
| 36 | } | ||
| 37 | type ProfileRecords struct { | ||
| 38 | GameID int `json:"game_id"` | ||
| 39 | CategoryID int `json:"category_id"` | ||
| 40 | MapID int `json:"map_id"` | ||
| 41 | MapName string `json:"map_name"` | ||
| 42 | MapWRCount int `json:"map_wr_count"` | ||
| 43 | Scores []ProfileScores `json:"scores"` | ||
| 44 | } | ||
| 45 | |||
| 46 | type ProfileScores struct { | ||
| 47 | DemoID string `json:"demo_id"` | ||
| 48 | ScoreCount int `json:"score_count"` | ||
| 49 | ScoreTime int `json:"score_time"` | ||
| 50 | Date time.Time `json:"date"` | ||
| 51 | } | ||
| 52 | |||
| 53 | type ScoreResponse struct { | ||
| 54 | MapID int `json:"map_id"` | ||
| 55 | Records any `json:"records"` | ||
| 56 | } | ||
| 57 | |||
| 58 | // GET Profile | ||
| 59 | // | ||
| 60 | // @Description Get profile page of session user. | ||
| 61 | // @Tags users | ||
| 62 | // @Accept json | ||
| 63 | // @Produce json | ||
| 64 | // @Param Authorization header string true "JWT Token" | ||
| 65 | // @Success 200 {object} models.Response{data=ProfileResponse} | ||
| 66 | // @Failure 400 {object} models.Response | ||
| 67 | // @Failure 401 {object} models.Response | ||
| 68 | // @Router /profile [get] | ||
| 69 | func Profile(c *gin.Context) { | ||
| 70 | // Check if user exists | ||
| 71 | user, exists := c.Get("user") | ||
| 72 | if !exists { | ||
| 73 | c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) | ||
| 74 | return | ||
| 75 | } | ||
| 76 | // Get user links | ||
| 77 | links := models.Links{} | ||
| 78 | sql := `SELECT u.p2sr, u.steam, u.youtube, u.twitch FROM users u WHERE u.steam_id = $1` | ||
| 79 | err := database.DB.QueryRow(sql, user.(models.User).SteamID).Scan(&links.P2SR, &links.Steam, &links.YouTube, &links.Twitch) | ||
| 80 | if err != nil { | ||
| 81 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 82 | return | ||
| 83 | } | ||
| 84 | // Get rankings (all maps done in one game) | ||
| 85 | rankings := ProfileRankings{ | ||
| 86 | Overall: ProfileRankingsDetails{}, | ||
| 87 | Singleplayer: ProfileRankingsDetails{}, | ||
| 88 | Cooperative: ProfileRankingsDetails{}, | ||
| 89 | } | ||
| 90 | // Get total map count | ||
| 91 | sql = `SELECT count(id), (SELECT count(id) FROM maps m WHERE m.game_id = 2 AND m.is_disabled = false) FROM maps m WHERE m.game_id = 1 AND m.is_disabled = false;` | ||
| 92 | err = database.DB.QueryRow(sql).Scan(&rankings.Singleplayer.CompletionTotal, &rankings.Cooperative.CompletionTotal) | ||
| 93 | if err != nil { | ||
| 94 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 95 | return | ||
| 96 | } | ||
| 97 | rankings.Overall.CompletionTotal = rankings.Singleplayer.CompletionTotal + rankings.Cooperative.CompletionTotal | ||
| 98 | // Get user completion count | ||
| 99 | sql = `SELECT 'records_sp' AS table_name, COUNT(rs.id) AS total_user_scores | ||
| 100 | FROM public.records_sp rs JOIN ( | ||
| 101 | SELECT mr.map_id, MIN(mr.score_count) AS min_score_count | ||
| 102 | FROM public.map_routes mr WHERE mr.category_id = 1 GROUP BY mr.map_id | ||
| 103 | ) AS subquery_sp ON rs.map_id = subquery_sp.map_id AND rs.score_count = subquery_sp.min_score_count | ||
| 104 | WHERE rs.user_id = $1 | ||
| 105 | UNION ALL | ||
| 106 | SELECT 'records_mp' AS table_name, COUNT(rm.id) AS total_user_scores | ||
| 107 | FROM public.records_mp rm JOIN ( | ||
| 108 | SELECT mr.map_id, MIN(mr.score_count) AS min_score_count | ||
| 109 | FROM public.map_routes mr WHERE mr.category_id = 1 GROUP BY mr.map_id | ||
| 110 | ) AS subquery_mp ON rm.map_id = subquery_mp.map_id AND rm.score_count = subquery_mp.min_score_count | ||
| 111 | WHERE rm.host_id = $1 OR rm.partner_id = $1;` | ||
| 112 | rows, err := database.DB.Query(sql, user.(models.User).SteamID) | ||
| 113 | if err != nil { | ||
| 114 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 115 | return | ||
| 116 | } | ||
| 117 | for rows.Next() { | ||
| 118 | var tableName string | ||
| 119 | var completionCount int | ||
| 120 | err = rows.Scan(&tableName, &completionCount) | ||
| 121 | if err != nil { | ||
| 122 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 123 | return | ||
| 124 | } | ||
| 125 | if tableName == "records_sp" { | ||
| 126 | rankings.Singleplayer.CompletionCount = completionCount | ||
| 127 | continue | ||
| 128 | } | ||
| 129 | if tableName == "records_mp" { | ||
| 130 | rankings.Cooperative.CompletionCount = completionCount | ||
| 131 | continue | ||
| 132 | } | ||
| 133 | } | ||
| 134 | rankings.Overall.CompletionCount = rankings.Singleplayer.CompletionCount + rankings.Cooperative.CompletionCount | ||
| 135 | // Get user ranking placement for singleplayer | ||
| 136 | sql = `SELECT u.steam_id, COUNT(DISTINCT map_id), | ||
| 137 | (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false), | ||
| 138 | (SELECT SUM(min_score_count) AS total_min_score_count FROM ( | ||
| 139 | SELECT user_id, MIN(score_count) AS min_score_count FROM records_sp GROUP BY user_id, map_id) AS subquery WHERE user_id = u.steam_id) | ||
| 140 | FROM records_sp sp JOIN users u ON u.steam_id = sp.user_id GROUP BY u.steam_id, u.user_name | ||
| 141 | HAVING COUNT(DISTINCT map_id) = (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false) | ||
| 142 | ORDER BY total_min_score_count ASC;` | ||
| 143 | rows, err = database.DB.Query(sql) | ||
| 144 | if err != nil { | ||
| 145 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 146 | return | ||
| 147 | } | ||
| 148 | placement := 1 | ||
| 149 | for rows.Next() { | ||
| 150 | var steamID string | ||
| 151 | var completionCount int | ||
| 152 | var totalCount int | ||
| 153 | var userPortalCount int | ||
| 154 | err = rows.Scan(&steamID, &completionCount, &totalCount, &userPortalCount) | ||
| 155 | if err != nil { | ||
| 156 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 157 | return | ||
| 158 | } | ||
| 159 | if completionCount != totalCount { | ||
| 160 | placement++ | ||
| 161 | continue | ||
| 162 | } | ||
| 163 | if steamID != user.(models.User).SteamID { | ||
| 164 | placement++ | ||
| 165 | continue | ||
| 166 | } | ||
| 167 | rankings.Singleplayer.Rank = placement | ||
| 168 | } | ||
| 169 | // Get user ranking placement for multiplayer | ||
| 170 | sql = `SELECT u.steam_id, COUNT(DISTINCT map_id), | ||
| 171 | (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false), | ||
| 172 | (SELECT SUM(min_score_count) AS total_min_score_count FROM ( | ||
| 173 | SELECT host_id, partner_id, MIN(score_count) AS min_score_count FROM records_mp GROUP BY host_id, partner_id, map_id) AS subquery WHERE host_id = u.steam_id OR partner_id = u.steam_id) | ||
| 174 | FROM records_mp mp JOIN users u ON u.steam_id = mp.host_id OR u.steam_id = mp.partner_id GROUP BY u.steam_id, u.user_name | ||
| 175 | HAVING COUNT(DISTINCT map_id) = (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false) | ||
| 176 | ORDER BY total_min_score_count ASC;` | ||
| 177 | rows, err = database.DB.Query(sql) | ||
| 178 | if err != nil { | ||
| 179 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 180 | return | ||
| 181 | } | ||
| 182 | placement = 1 | ||
| 183 | for rows.Next() { | ||
| 184 | var steamID string | ||
| 185 | var completionCount int | ||
| 186 | var totalCount int | ||
| 187 | var userPortalCount int | ||
| 188 | err = rows.Scan(&steamID, &completionCount, &totalCount, &userPortalCount) | ||
| 189 | if err != nil { | ||
| 190 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 191 | return | ||
| 192 | } | ||
| 193 | if completionCount != totalCount { | ||
| 194 | placement++ | ||
| 195 | continue | ||
| 196 | } | ||
| 197 | if steamID != user.(models.User).SteamID { | ||
| 198 | placement++ | ||
| 199 | continue | ||
| 200 | } | ||
| 201 | rankings.Cooperative.Rank = placement | ||
| 202 | } | ||
| 203 | // TODO: Get user ranking placement for overall if they qualify | ||
| 204 | // if (rankings.Singleplayer.Rank != 0) && (rankings.Cooperative.Rank != 0) { | ||
| 205 | // sql = `SELECT steam_id, SUM(total_min_score_count) AS total_score | ||
| 206 | // FROM ( | ||
| 207 | // SELECT u.steam_id, | ||
| 208 | // (SELECT SUM(min_score_count) AS total_min_score_count FROM ( | ||
| 209 | // SELECT | ||
| 210 | // user_id, | ||
| 211 | // MIN(score_count) AS min_score_count | ||
| 212 | // FROM records_sp | ||
| 213 | // GROUP BY user_id, map_id | ||
| 214 | // ) AS subquery | ||
| 215 | // WHERE user_id = u.steam_id) AS total_min_score_count | ||
| 216 | // FROM records_sp sp | ||
| 217 | // JOIN users u ON u.steam_id = sp.user_id | ||
| 218 | // UNION ALL | ||
| 219 | // SELECT u.steam_id, | ||
| 220 | // (SELECT SUM(min_score_count) AS total_min_score_count FROM ( | ||
| 221 | // SELECT | ||
| 222 | // host_id, | ||
| 223 | // partner_id, | ||
| 224 | // MIN(score_count) AS min_score_count | ||
| 225 | // FROM records_mp | ||
| 226 | // GROUP BY host_id, partner_id, map_id | ||
| 227 | // ) AS subquery | ||
| 228 | // WHERE host_id = u.steam_id OR partner_id = u.steam_id) AS total_min_score_count | ||
| 229 | // FROM records_mp mp | ||
| 230 | // JOIN users u ON u.steam_id = mp.host_id OR u.steam_id = mp.partner_id | ||
| 231 | // ) AS combined_scores | ||
| 232 | // GROUP BY steam_id ORDER BY total_score ASC;` | ||
| 233 | // rows, err = database.DB.Query(sql) | ||
| 234 | // if err != nil { | ||
| 235 | // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 236 | // return | ||
| 237 | // } | ||
| 238 | // placement = 1 | ||
| 239 | // for rows.Next() { | ||
| 240 | // var steamID string | ||
| 241 | // var userPortalCount int | ||
| 242 | // err = rows.Scan(&steamID, &userPortalCount) | ||
| 243 | // if err != nil { | ||
| 244 | // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 245 | // return | ||
| 246 | // } | ||
| 247 | // if completionCount != totalCount { | ||
| 248 | // placement++ | ||
| 249 | // continue | ||
| 250 | // } | ||
| 251 | // if steamID != user.(models.User).SteamID { | ||
| 252 | // placement++ | ||
| 253 | // continue | ||
| 254 | // } | ||
| 255 | // rankings.Cooperative.Rank = placement | ||
| 256 | // } | ||
| 257 | // } | ||
| 258 | records := []ProfileRecords{} | ||
| 259 | // Get singleplayer records | ||
| 260 | sql = `SELECT m.game_id, m.chapter_id, sp.map_id, m."name", (SELECT mr.score_count FROM map_routes mr WHERE mr.map_id = sp.map_id ORDER BY mr.score_count ASC LIMIT 1) AS wr_count, sp.score_count, sp.score_time, sp.demo_id, sp.record_date | ||
| 261 | FROM records_sp sp INNER JOIN maps m ON sp.map_id = m.id WHERE sp.user_id = $1 ORDER BY sp.map_id, sp.score_count, sp.score_time;` | ||
| 262 | rows, err = database.DB.Query(sql, user.(models.User).SteamID) | ||
| 263 | if err != nil { | ||
| 264 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 265 | return | ||
| 266 | } | ||
| 267 | for rows.Next() { | ||
| 268 | var gameID int | ||
| 269 | var categoryID int | ||
| 270 | var mapID int | ||
| 271 | var mapName string | ||
| 272 | var mapWR int | ||
| 273 | score := ProfileScores{} | ||
| 274 | rows.Scan(&gameID, &categoryID, &mapID, &mapName, &mapWR, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) | ||
| 275 | // More than one record in one map | ||
| 276 | if len(records) != 0 && mapID == records[len(records)-1].MapID { | ||
| 277 | records[len(records)-1].Scores = append(records[len(records)-1].Scores, score) | ||
| 278 | continue | ||
| 279 | } | ||
| 280 | // New map | ||
| 281 | records = append(records, ProfileRecords{ | ||
| 282 | GameID: gameID, | ||
| 283 | CategoryID: categoryID, | ||
| 284 | MapID: mapID, | ||
| 285 | MapName: mapName, | ||
| 286 | MapWRCount: mapWR, | ||
| 287 | Scores: []ProfileScores{}, | ||
| 288 | }) | ||
| 289 | records[len(records)-1].Scores = append(records[len(records)-1].Scores, score) | ||
| 290 | } | ||
| 291 | // Get multiplayer records | ||
| 292 | sql = `SELECT m.game_id, m.chapter_id, mp.map_id, m."name", (SELECT mr.score_count FROM map_routes mr WHERE mr.map_id = mp.map_id ORDER BY mr.score_count ASC LIMIT 1) AS wr_count, mp.score_count, mp.score_time, CASE WHEN host_id = $1 THEN mp.host_demo_id WHEN partner_id = $1 THEN mp.partner_demo_id END demo_id, mp.record_date | ||
| 293 | FROM records_mp mp INNER JOIN maps m ON mp.map_id = m.id WHERE mp.host_id = $1 OR mp.partner_id = $1 ORDER BY mp.map_id, mp.score_count, mp.score_time;` | ||
| 294 | rows, err = database.DB.Query(sql, user.(models.User).SteamID) | ||
| 295 | if err != nil { | ||
| 296 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 297 | return | ||
| 298 | } | ||
| 299 | for rows.Next() { | ||
| 300 | var gameID int | ||
| 301 | var categoryID int | ||
| 302 | var mapID int | ||
| 303 | var mapName string | ||
| 304 | var mapWR int | ||
| 305 | score := ProfileScores{} | ||
| 306 | rows.Scan(&gameID, &categoryID, &mapID, &mapName, &mapWR, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) | ||
| 307 | // More than one record in one map | ||
| 308 | if len(records) != 0 && mapID == records[len(records)-1].MapID { | ||
| 309 | records[len(records)-1].Scores = append(records[len(records)-1].Scores, score) | ||
| 310 | continue | ||
| 311 | } | ||
| 312 | // New map | ||
| 313 | records = append(records, ProfileRecords{ | ||
| 314 | GameID: gameID, | ||
| 315 | CategoryID: categoryID, | ||
| 316 | MapID: mapID, | ||
| 317 | MapName: mapName, | ||
| 318 | MapWRCount: mapWR, | ||
| 319 | Scores: []ProfileScores{}, | ||
| 320 | }) | ||
| 321 | records[len(records)-1].Scores = append(records[len(records)-1].Scores, score) | ||
| 322 | } | ||
| 323 | c.JSON(http.StatusOK, models.Response{ | ||
| 324 | Success: true, | ||
| 325 | Message: "Successfully retrieved user scores.", | ||
| 326 | Data: ProfileResponse{ | ||
| 327 | Profile: true, | ||
| 328 | SteamID: user.(models.User).SteamID, | ||
| 329 | UserName: user.(models.User).UserName, | ||
| 330 | AvatarLink: user.(models.User).AvatarLink, | ||
| 331 | CountryCode: user.(models.User).CountryCode, | ||
| 332 | Titles: user.(models.User).Titles, | ||
| 333 | Links: links, | ||
| 334 | Rankings: rankings, | ||
| 335 | Records: records, | ||
| 336 | }, | ||
| 337 | }) | ||
| 338 | } | ||
| 339 | |||
| 340 | // GET User | ||
| 341 | // | ||
| 342 | // @Description Get profile page of another user. | ||
| 343 | // @Tags users | ||
| 344 | // @Accept json | ||
| 345 | // @Produce json | ||
| 346 | // @Param id path int true "User ID" | ||
| 347 | // @Success 200 {object} models.Response{data=ProfileResponse} | ||
| 348 | // @Failure 400 {object} models.Response | ||
| 349 | // @Failure 404 {object} models.Response | ||
| 350 | // @Router /users/{id} [get] | ||
| 351 | func FetchUser(c *gin.Context) { | ||
| 352 | id := c.Param("id") | ||
| 353 | // Check if id is all numbers and 17 length | ||
| 354 | match, _ := regexp.MatchString("^[0-9]{17}$", id) | ||
| 355 | if !match { | ||
| 356 | c.JSON(http.StatusNotFound, models.ErrorResponse("User not found.")) | ||
| 357 | return | ||
| 358 | } | ||
| 359 | // Check if user exists | ||
| 360 | var user models.User | ||
| 361 | links := models.Links{} | ||
| 362 | sql := `SELECT u.steam_id, u.user_name, u.avatar_link, u.country_code, u.created_at, u.updated_at, u.p2sr, u.steam, u.youtube, u.twitch FROM users u WHERE u.steam_id = $1` | ||
| 363 | err := database.DB.QueryRow(sql, id).Scan(&user.SteamID, &user.UserName, &user.AvatarLink, &user.CountryCode, &user.CreatedAt, &user.UpdatedAt, &links.P2SR, &links.Steam, &links.YouTube, &links.Twitch) | ||
| 364 | if err != nil { | ||
| 365 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 366 | return | ||
| 367 | } | ||
| 368 | if user.SteamID == "" { | ||
| 369 | // User does not exist | ||
| 370 | c.JSON(http.StatusNotFound, models.ErrorResponse("User not found.")) | ||
| 371 | return | ||
| 372 | } | ||
| 373 | // Get rankings (all maps done in one game) | ||
| 374 | rankings := ProfileRankings{ | ||
| 375 | Overall: ProfileRankingsDetails{}, | ||
| 376 | Singleplayer: ProfileRankingsDetails{}, | ||
| 377 | Cooperative: ProfileRankingsDetails{}, | ||
| 378 | } | ||
| 379 | // Get total map count | ||
| 380 | sql = `SELECT count(id), (SELECT count(id) FROM maps m WHERE m.game_id = 2 AND m.is_disabled = false) FROM maps m WHERE m.game_id = 1 AND m.is_disabled = false;` | ||
| 381 | err = database.DB.QueryRow(sql).Scan(&rankings.Singleplayer.CompletionTotal, &rankings.Cooperative.CompletionTotal) | ||
| 382 | if err != nil { | ||
| 383 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 384 | return | ||
| 385 | } | ||
| 386 | rankings.Overall.CompletionTotal = rankings.Singleplayer.CompletionTotal + rankings.Cooperative.CompletionTotal | ||
| 387 | // Get user completion count | ||
| 388 | sql = `SELECT 'records_sp' AS table_name, COUNT(rs.id) AS total_user_scores | ||
| 389 | FROM public.records_sp rs JOIN ( | ||
| 390 | SELECT mr.map_id, MIN(mr.score_count) AS min_score_count | ||
| 391 | FROM public.map_routes mr WHERE mr.category_id = 1 GROUP BY mr.map_id | ||
| 392 | ) AS subquery_sp ON rs.map_id = subquery_sp.map_id AND rs.score_count = subquery_sp.min_score_count | ||
| 393 | WHERE rs.user_id = $1 | ||
| 394 | UNION ALL | ||
| 395 | SELECT 'records_mp' AS table_name, COUNT(rm.id) AS total_user_scores | ||
| 396 | FROM public.records_mp rm JOIN ( | ||
| 397 | SELECT mr.map_id, MIN(mr.score_count) AS min_score_count | ||
| 398 | FROM public.map_routes mr WHERE mr.category_id = 1 GROUP BY mr.map_id | ||
| 399 | ) AS subquery_mp ON rm.map_id = subquery_mp.map_id AND rm.score_count = subquery_mp.min_score_count | ||
| 400 | WHERE rm.host_id = $1 OR rm.partner_id = $1;` | ||
| 401 | rows, err := database.DB.Query(sql, user.SteamID) | ||
| 402 | if err != nil { | ||
| 403 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 404 | return | ||
| 405 | } | ||
| 406 | for rows.Next() { | ||
| 407 | var tableName string | ||
| 408 | var completionCount int | ||
| 409 | err = rows.Scan(&tableName, &completionCount) | ||
| 410 | if err != nil { | ||
| 411 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 412 | return | ||
| 413 | } | ||
| 414 | if tableName == "records_sp" { | ||
| 415 | rankings.Singleplayer.CompletionCount = completionCount | ||
| 416 | continue | ||
| 417 | } | ||
| 418 | if tableName == "records_mp" { | ||
| 419 | rankings.Cooperative.CompletionCount = completionCount | ||
| 420 | continue | ||
| 421 | } | ||
| 422 | } | ||
| 423 | rankings.Overall.CompletionCount = rankings.Singleplayer.CompletionCount + rankings.Cooperative.CompletionCount | ||
| 424 | // Get user ranking placement for singleplayer | ||
| 425 | sql = `SELECT u.steam_id, COUNT(DISTINCT map_id), | ||
| 426 | (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false), | ||
| 427 | (SELECT SUM(min_score_count) AS total_min_score_count FROM ( | ||
| 428 | SELECT user_id, MIN(score_count) AS min_score_count FROM records_sp GROUP BY user_id, map_id) AS subquery WHERE user_id = u.steam_id) | ||
| 429 | FROM records_sp sp JOIN users u ON u.steam_id = sp.user_id GROUP BY u.steam_id, u.user_name | ||
| 430 | HAVING COUNT(DISTINCT map_id) = (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false) | ||
| 431 | ORDER BY total_min_score_count ASC;` | ||
| 432 | rows, err = database.DB.Query(sql) | ||
| 433 | if err != nil { | ||
| 434 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 435 | return | ||
| 436 | } | ||
| 437 | placement := 1 | ||
| 438 | for rows.Next() { | ||
| 439 | var steamID string | ||
| 440 | var completionCount int | ||
| 441 | var totalCount int | ||
| 442 | var userPortalCount int | ||
| 443 | err = rows.Scan(&steamID, &completionCount, &totalCount, &userPortalCount) | ||
| 444 | if err != nil { | ||
| 445 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 446 | return | ||
| 447 | } | ||
| 448 | if completionCount != totalCount { | ||
| 449 | placement++ | ||
| 450 | continue | ||
| 451 | } | ||
| 452 | if steamID != user.SteamID { | ||
| 453 | placement++ | ||
| 454 | continue | ||
| 455 | } | ||
| 456 | rankings.Singleplayer.Rank = placement | ||
| 457 | } | ||
| 458 | // Get user ranking placement for multiplayer | ||
| 459 | sql = `SELECT u.steam_id, COUNT(DISTINCT map_id), | ||
| 460 | (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false), | ||
| 461 | (SELECT SUM(min_score_count) AS total_min_score_count FROM ( | ||
| 462 | SELECT host_id, partner_id, MIN(score_count) AS min_score_count FROM records_mp GROUP BY host_id, partner_id, map_id) AS subquery WHERE host_id = u.steam_id OR partner_id = u.steam_id) | ||
| 463 | FROM records_mp mp JOIN users u ON u.steam_id = mp.host_id OR u.steam_id = mp.partner_id GROUP BY u.steam_id, u.user_name | ||
| 464 | HAVING COUNT(DISTINCT map_id) = (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false) | ||
| 465 | ORDER BY total_min_score_count ASC;` | ||
| 466 | rows, err = database.DB.Query(sql) | ||
| 467 | if err != nil { | ||
| 468 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 469 | return | ||
| 470 | } | ||
| 471 | placement = 1 | ||
| 472 | for rows.Next() { | ||
| 473 | var steamID string | ||
| 474 | var completionCount int | ||
| 475 | var totalCount int | ||
| 476 | var userPortalCount int | ||
| 477 | err = rows.Scan(&steamID, &completionCount, &totalCount, &userPortalCount) | ||
| 478 | if err != nil { | ||
| 479 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 480 | return | ||
| 481 | } | ||
| 482 | if completionCount != totalCount { | ||
| 483 | placement++ | ||
| 484 | continue | ||
| 485 | } | ||
| 486 | if steamID != user.SteamID { | ||
| 487 | placement++ | ||
| 488 | continue | ||
| 489 | } | ||
| 490 | rankings.Cooperative.Rank = placement | ||
| 491 | } | ||
| 492 | // TODO: Get user ranking placement for overall if they qualify | ||
| 493 | // if (rankings.Singleplayer.Rank != 0) && (rankings.Cooperative.Rank != 0) { | ||
| 494 | // sql = `SELECT steam_id, SUM(total_min_score_count) AS total_score | ||
| 495 | // FROM ( | ||
| 496 | // SELECT u.steam_id, | ||
| 497 | // (SELECT SUM(min_score_count) AS total_min_score_count FROM ( | ||
| 498 | // SELECT | ||
| 499 | // user_id, | ||
| 500 | // MIN(score_count) AS min_score_count | ||
| 501 | // FROM records_sp | ||
| 502 | // GROUP BY user_id, map_id | ||
| 503 | // ) AS subquery | ||
| 504 | // WHERE user_id = u.steam_id) AS total_min_score_count | ||
| 505 | // FROM records_sp sp | ||
| 506 | // JOIN users u ON u.steam_id = sp.user_id | ||
| 507 | // UNION ALL | ||
| 508 | // SELECT u.steam_id, | ||
| 509 | // (SELECT SUM(min_score_count) AS total_min_score_count FROM ( | ||
| 510 | // SELECT | ||
| 511 | // host_id, | ||
| 512 | // partner_id, | ||
| 513 | // MIN(score_count) AS min_score_count | ||
| 514 | // FROM records_mp | ||
| 515 | // GROUP BY host_id, partner_id, map_id | ||
| 516 | // ) AS subquery | ||
| 517 | // WHERE host_id = u.steam_id OR partner_id = u.steam_id) AS total_min_score_count | ||
| 518 | // FROM records_mp mp | ||
| 519 | // JOIN users u ON u.steam_id = mp.host_id OR u.steam_id = mp.partner_id | ||
| 520 | // ) AS combined_scores | ||
| 521 | // GROUP BY steam_id ORDER BY total_score ASC;` | ||
| 522 | // rows, err = database.DB.Query(sql) | ||
| 523 | // if err != nil { | ||
| 524 | // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 525 | // return | ||
| 526 | // } | ||
| 527 | // placement = 1 | ||
| 528 | // for rows.Next() { | ||
| 529 | // var steamID string | ||
| 530 | // var userPortalCount int | ||
| 531 | // err = rows.Scan(&steamID, &userPortalCount) | ||
| 532 | // if err != nil { | ||
| 533 | // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 534 | // return | ||
| 535 | // } | ||
| 536 | // if completionCount != totalCount { | ||
| 537 | // placement++ | ||
| 538 | // continue | ||
| 539 | // } | ||
| 540 | // if steamID != user.SteamID { | ||
| 541 | // placement++ | ||
| 542 | // continue | ||
| 543 | // } | ||
| 544 | // rankings.Cooperative.Rank = placement | ||
| 545 | // } | ||
| 546 | // } | ||
| 547 | records := []ProfileRecords{} | ||
| 548 | // Get singleplayer records | ||
| 549 | sql = `SELECT m.game_id, m.chapter_id, sp.map_id, m."name", (SELECT mr.score_count FROM map_routes mr WHERE mr.map_id = sp.map_id ORDER BY mr.score_count ASC LIMIT 1) AS wr_count, sp.score_count, sp.score_time, sp.demo_id, sp.record_date | ||
| 550 | FROM records_sp sp INNER JOIN maps m ON sp.map_id = m.id WHERE sp.user_id = $1 ORDER BY sp.map_id, sp.score_count, sp.score_time;` | ||
| 551 | rows, err = database.DB.Query(sql, user.SteamID) | ||
| 552 | if err != nil { | ||
| 553 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 554 | return | ||
| 555 | } | ||
| 556 | for rows.Next() { | ||
| 557 | var gameID int | ||
| 558 | var categoryID int | ||
| 559 | var mapID int | ||
| 560 | var mapName string | ||
| 561 | var mapWR int | ||
| 562 | score := ProfileScores{} | ||
| 563 | rows.Scan(&gameID, &categoryID, &mapID, &mapName, &mapWR, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) | ||
| 564 | // More than one record in one map | ||
| 565 | if len(records) != 0 && mapID == records[len(records)-1].MapID { | ||
| 566 | records[len(records)-1].Scores = append(records[len(records)-1].Scores, score) | ||
| 567 | continue | ||
| 568 | } | ||
| 569 | // New map | ||
| 570 | records = append(records, ProfileRecords{ | ||
| 571 | GameID: gameID, | ||
| 572 | CategoryID: categoryID, | ||
| 573 | MapID: mapID, | ||
| 574 | MapName: mapName, | ||
| 575 | MapWRCount: mapWR, | ||
| 576 | Scores: []ProfileScores{}, | ||
| 577 | }) | ||
| 578 | records[len(records)-1].Scores = append(records[len(records)-1].Scores, score) | ||
| 579 | } | ||
| 580 | // Get multiplayer records | ||
| 581 | sql = `SELECT m.game_id, m.chapter_id, mp.map_id, m."name", (SELECT mr.score_count FROM map_routes mr WHERE mr.map_id = mp.map_id ORDER BY mr.score_count ASC LIMIT 1) AS wr_count, mp.score_count, mp.score_time, CASE WHEN host_id = $1 THEN mp.host_demo_id WHEN partner_id = $1 THEN mp.partner_demo_id END demo_id, mp.record_date | ||
| 582 | FROM records_mp mp INNER JOIN maps m ON mp.map_id = m.id WHERE mp.host_id = $1 OR mp.partner_id = $1 ORDER BY mp.map_id, mp.score_count, mp.score_time;` | ||
| 583 | rows, err = database.DB.Query(sql, user.SteamID) | ||
| 584 | if err != nil { | ||
| 585 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 586 | return | ||
| 587 | } | ||
| 588 | for rows.Next() { | ||
| 589 | var gameID int | ||
| 590 | var categoryID int | ||
| 591 | var mapID int | ||
| 592 | var mapName string | ||
| 593 | var mapWR int | ||
| 594 | score := ProfileScores{} | ||
| 595 | rows.Scan(&gameID, &categoryID, &mapID, &mapName, &mapWR, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) | ||
| 596 | // More than one record in one map | ||
| 597 | if len(records) != 0 && mapID == records[len(records)-1].MapID { | ||
| 598 | records[len(records)-1].Scores = append(records[len(records)-1].Scores, score) | ||
| 599 | continue | ||
| 600 | } | ||
| 601 | // New map | ||
| 602 | records = append(records, ProfileRecords{ | ||
| 603 | GameID: gameID, | ||
| 604 | CategoryID: categoryID, | ||
| 605 | MapID: mapID, | ||
| 606 | MapName: mapName, | ||
| 607 | MapWRCount: mapWR, | ||
| 608 | Scores: []ProfileScores{}, | ||
| 609 | }) | ||
| 610 | records[len(records)-1].Scores = append(records[len(records)-1].Scores, score) | ||
| 611 | } | ||
| 612 | c.JSON(http.StatusOK, models.Response{ | ||
| 613 | Success: true, | ||
| 614 | Message: "Successfully retrieved user scores.", | ||
| 615 | Data: ProfileResponse{ | ||
| 616 | Profile: true, | ||
| 617 | SteamID: user.SteamID, | ||
| 618 | UserName: user.UserName, | ||
| 619 | AvatarLink: user.AvatarLink, | ||
| 620 | CountryCode: user.CountryCode, | ||
| 621 | Titles: user.Titles, | ||
| 622 | Links: links, | ||
| 623 | Rankings: rankings, | ||
| 624 | Records: records, | ||
| 625 | }, | ||
| 626 | }) | ||
| 627 | } | ||
| 628 | |||
| 629 | // PUT Profile | ||
| 630 | // | ||
| 631 | // @Description Update profile page of session user. | ||
| 632 | // @Tags users | ||
| 633 | // @Accept json | ||
| 634 | // @Produce json | ||
| 635 | // @Param Authorization header string true "JWT Token" | ||
| 636 | // @Success 200 {object} models.Response{data=ProfileResponse} | ||
| 637 | // @Failure 400 {object} models.Response | ||
| 638 | // @Failure 401 {object} models.Response | ||
| 639 | // @Router /profile [post] | ||
| 640 | func UpdateUser(c *gin.Context) { | ||
| 641 | // Check if user exists | ||
| 642 | user, exists := c.Get("user") | ||
| 643 | if !exists { | ||
| 644 | c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) | ||
| 645 | return | ||
| 646 | } | ||
| 647 | profile, err := GetPlayerSummaries(user.(models.User).SteamID, os.Getenv("API_KEY")) | ||
| 648 | if err != nil { | ||
| 649 | CreateLog(user.(models.User).SteamID, LogTypeUser, LogDescriptionUserUpdateSummaryFail) | ||
| 650 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 651 | return | ||
| 652 | } | ||
| 653 | // Update profile | ||
| 654 | _, err = database.DB.Exec(`UPDATE users SET username = $1, avatar_link = $2, country_code = $3, updated_at = $4 | ||
| 655 | WHERE steam_id = $5`, profile.PersonaName, profile.AvatarFull, profile.LocCountryCode, time.Now().UTC(), user.(models.User).SteamID) | ||
| 656 | if err != nil { | ||
| 657 | CreateLog(user.(models.User).SteamID, LogTypeUser, LogDescriptionUserUpdateFail) | ||
| 658 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 659 | return | ||
| 660 | } | ||
| 661 | CreateLog(user.(models.User).SteamID, LogTypeUser, LogDescriptionUserUpdateSuccess) | ||
| 662 | c.JSON(http.StatusOK, models.Response{ | ||
| 663 | Success: true, | ||
| 664 | Message: "Successfully updated user.", | ||
| 665 | Data: ProfileResponse{ | ||
| 666 | Profile: true, | ||
| 667 | SteamID: user.(models.User).SteamID, | ||
| 668 | UserName: profile.PersonaName, | ||
| 669 | AvatarLink: profile.AvatarFull, | ||
| 670 | CountryCode: profile.LocCountryCode, | ||
| 671 | }, | ||
| 672 | }) | ||
| 673 | } | ||
| 674 | |||
| 675 | // PUT Profile/CountryCode | ||
| 676 | // | ||
| 677 | // @Description Update country code of session user. | ||
| 678 | // @Tags users | ||
| 679 | // @Accept json | ||
| 680 | // @Produce json | ||
| 681 | // @Param Authorization header string true "JWT Token" | ||
| 682 | // @Param country_code query string true "Country Code [XX]" | ||
| 683 | // @Success 200 {object} models.Response | ||
| 684 | // @Failure 400 {object} models.Response | ||
| 685 | // @Failure 401 {object} models.Response | ||
| 686 | // @Router /profile [put] | ||
| 687 | func UpdateCountryCode(c *gin.Context) { | ||
| 688 | // Check if user exists | ||
| 689 | user, exists := c.Get("user") | ||
| 690 | if !exists { | ||
| 691 | c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) | ||
| 692 | return | ||
| 693 | } | ||
| 694 | code := c.Query("country_code") | ||
| 695 | if code == "" { | ||
| 696 | CreateLog(user.(models.User).SteamID, LogTypeUser, LogDescriptionUserUpdateCountryFail) | ||
| 697 | c.JSON(http.StatusNotFound, models.ErrorResponse("Enter a valid country code.")) | ||
| 698 | return | ||
| 699 | } | ||
| 700 | var validCode string | ||
| 701 | err := database.DB.QueryRow(`SELECT country_code FROM countries WHERE country_code = $1`, code).Scan(&validCode) | ||
| 702 | if err != nil { | ||
| 703 | CreateLog(user.(models.User).SteamID, LogTypeUser, LogDescriptionUserUpdateCountryFail) | ||
| 704 | c.JSON(http.StatusNotFound, models.ErrorResponse(err.Error())) | ||
| 705 | return | ||
| 706 | } | ||
| 707 | // Valid code, update profile | ||
| 708 | _, err = database.DB.Exec(`UPDATE users SET country_code = $1 WHERE steam_id = $2`, validCode, user.(models.User).SteamID) | ||
| 709 | if err != nil { | ||
| 710 | CreateLog(user.(models.User).SteamID, LogTypeUser, LogDescriptionUserUpdateCountryFail) | ||
| 711 | c.JSON(http.StatusNotFound, models.ErrorResponse(err.Error())) | ||
| 712 | return | ||
| 713 | } | ||
| 714 | CreateLog(user.(models.User).SteamID, LogTypeUser, LogDescriptionUserUpdateCountrySuccess) | ||
| 715 | c.JSON(http.StatusOK, models.Response{ | ||
| 716 | Success: true, | ||
| 717 | Message: "Successfully updated country code.", | ||
| 718 | }) | ||
| 719 | } | ||
diff --git a/backend/models/models.go b/backend/models/models.go index 1231cb1..2d54295 100644 --- a/backend/models/models.go +++ b/backend/models/models.go | |||
| @@ -4,6 +4,20 @@ import ( | |||
| 4 | "time" | 4 | "time" |
| 5 | ) | 5 | ) |
| 6 | 6 | ||
| 7 | type Response struct { | ||
| 8 | Success bool `json:"success"` | ||
| 9 | Message string `json:"message"` | ||
| 10 | Data any `json:"data"` | ||
| 11 | } | ||
| 12 | |||
| 13 | func ErrorResponse(message string) Response { | ||
| 14 | return Response{ | ||
| 15 | Success: false, | ||
| 16 | Message: message, | ||
| 17 | Data: nil, | ||
| 18 | } | ||
| 19 | } | ||
| 20 | |||
| 7 | type User struct { | 21 | type User struct { |
| 8 | SteamID string `json:"steam_id"` | 22 | SteamID string `json:"steam_id"` |
| 9 | UserName string `json:"user_name"` | 23 | UserName string `json:"user_name"` |
| @@ -11,7 +25,7 @@ type User struct { | |||
| 11 | CountryCode string `json:"country_code"` | 25 | CountryCode string `json:"country_code"` |
| 12 | CreatedAt time.Time `json:"created_at"` | 26 | CreatedAt time.Time `json:"created_at"` |
| 13 | UpdatedAt time.Time `json:"updated_at"` | 27 | UpdatedAt time.Time `json:"updated_at"` |
| 14 | Titles []string `json:"titles"` | 28 | Titles []Title `json:"titles"` |
| 15 | } | 29 | } |
| 16 | 30 | ||
| 17 | type UserShort struct { | 31 | type UserShort struct { |
| @@ -19,6 +33,12 @@ type UserShort struct { | |||
| 19 | UserName string `json:"user_name"` | 33 | UserName string `json:"user_name"` |
| 20 | } | 34 | } |
| 21 | 35 | ||
| 36 | type UserShortWithAvatar struct { | ||
| 37 | SteamID string `json:"steam_id"` | ||
| 38 | UserName string `json:"user_name"` | ||
| 39 | AvatarLink string `json:"avatar_link"` | ||
| 40 | } | ||
| 41 | |||
| 22 | type Map struct { | 42 | type Map struct { |
| 23 | ID int `json:"id"` | 43 | ID int `json:"id"` |
| 24 | GameName string `json:"game_name"` | 44 | GameName string `json:"game_name"` |
| @@ -57,9 +77,8 @@ type MapRecords struct { | |||
| 57 | } | 77 | } |
| 58 | 78 | ||
| 59 | type UserRanking struct { | 79 | type UserRanking struct { |
| 60 | UserID string `json:"user_id"` | 80 | User UserShort `json:"user"` |
| 61 | UserName string `json:"user_name"` | 81 | TotalScore int `json:"total_score"` |
| 62 | TotalScore int `json:"total_score"` | ||
| 63 | } | 82 | } |
| 64 | 83 | ||
| 65 | type Game struct { | 84 | type Game struct { |
| @@ -78,32 +97,16 @@ type Category struct { | |||
| 78 | Name string `json:"name"` | 97 | Name string `json:"name"` |
| 79 | } | 98 | } |
| 80 | 99 | ||
| 81 | type RecordSP struct { | 100 | type Title struct { |
| 82 | RecordID int `json:"record_id"` | 101 | Name string `json:"name"` |
| 83 | Placement int `json:"placement"` | 102 | Color string `json:"color"` |
| 84 | UserID string `json:"user_id"` | 103 | } |
| 85 | UserName string `json:"user_name"` | 104 | |
| 86 | UserAvatar string `json:"user_avatar"` | 105 | type Links struct { |
| 87 | ScoreCount int `json:"score_count"` | 106 | P2SR string `json:"p2sr"` |
| 88 | ScoreTime int `json:"score_time"` | 107 | Steam string `json:"stream"` |
| 89 | DemoID string `json:"demo_id"` | 108 | YouTube string `json:"youtube"` |
| 90 | RecordDate time.Time `json:"record_date"` | 109 | Twitch string `json:"twitch"` |
| 91 | } | ||
| 92 | |||
| 93 | type RecordMP struct { | ||
| 94 | RecordID int `json:"record_id"` | ||
| 95 | Placement int `json:"placement"` | ||
| 96 | HostID string `json:"host_id"` | ||
| 97 | HostName string `json:"host_name"` | ||
| 98 | HostAvatar string `json:"host_avatar"` | ||
| 99 | PartnerID string `json:"partner_id"` | ||
| 100 | PartnerName string `json:"partner_name"` | ||
| 101 | PartnerAvatar string `json:"partner_avatar"` | ||
| 102 | ScoreCount int `json:"score_count"` | ||
| 103 | ScoreTime int `json:"score_time"` | ||
| 104 | HostDemoID string `json:"host_demo_id"` | ||
| 105 | PartnerDemoID string `json:"partner_demo_id"` | ||
| 106 | RecordDate time.Time `json:"record_date"` | ||
| 107 | } | 110 | } |
| 108 | 111 | ||
| 109 | type PlayerSummaries struct { | 112 | type PlayerSummaries struct { |
diff --git a/backend/models/requests.go b/backend/models/requests.go deleted file mode 100644 index 0113597..0000000 --- a/backend/models/requests.go +++ /dev/null | |||
| @@ -1,39 +0,0 @@ | |||
| 1 | package models | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "mime/multipart" | ||
| 5 | "time" | ||
| 6 | ) | ||
| 7 | |||
| 8 | type CreateMapSummaryRequest struct { | ||
| 9 | CategoryID int `json:"category_id" binding:"required"` | ||
| 10 | Description string `json:"description" binding:"required"` | ||
| 11 | Showcase string `json:"showcase"` | ||
| 12 | UserName string `json:"user_name" binding:"required"` | ||
| 13 | ScoreCount *int `json:"score_count" binding:"required"` | ||
| 14 | RecordDate time.Time `json:"record_date" binding:"required"` | ||
| 15 | } | ||
| 16 | |||
| 17 | type EditMapSummaryRequest struct { | ||
| 18 | RouteID int `json:"route_id" binding:"required"` | ||
| 19 | Description string `json:"description" binding:"required"` | ||
| 20 | Showcase string `json:"showcase"` | ||
| 21 | UserName string `json:"user_name" binding:"required"` | ||
| 22 | ScoreCount *int `json:"score_count" binding:"required"` | ||
| 23 | RecordDate time.Time `json:"record_date" binding:"required"` | ||
| 24 | } | ||
| 25 | |||
| 26 | type DeleteMapSummaryRequest struct { | ||
| 27 | RouteID int `json:"route_id" binding:"required"` | ||
| 28 | } | ||
| 29 | |||
| 30 | type EditMapImageRequest struct { | ||
| 31 | Image string `json:"image" binding:"required"` | ||
| 32 | } | ||
| 33 | |||
| 34 | type RecordRequest struct { | ||
| 35 | HostDemo *multipart.FileHeader `json:"host_demo" form:"host_demo" binding:"required" swaggerignore:"true"` | ||
| 36 | PartnerDemo *multipart.FileHeader `json:"partner_demo" form:"partner_demo" swaggerignore:"true"` | ||
| 37 | IsPartnerOrange bool `json:"is_partner_orange" form:"is_partner_orange"` | ||
| 38 | PartnerID string `json:"partner_id" form:"partner_id"` | ||
| 39 | } | ||
diff --git a/backend/models/responses.go b/backend/models/responses.go deleted file mode 100644 index 459911c..0000000 --- a/backend/models/responses.go +++ /dev/null | |||
| @@ -1,64 +0,0 @@ | |||
| 1 | package models | ||
| 2 | |||
| 3 | type Response struct { | ||
| 4 | Success bool `json:"success"` | ||
| 5 | Message string `json:"message"` | ||
| 6 | Data any `json:"data"` | ||
| 7 | } | ||
| 8 | |||
| 9 | type LoginResponse struct { | ||
| 10 | Token string `json:"token"` | ||
| 11 | } | ||
| 12 | |||
| 13 | type RankingsResponse struct { | ||
| 14 | RankingsSP []UserRanking `json:"rankings_sp"` | ||
| 15 | RankingsMP []UserRanking `json:"rankings_mp"` | ||
| 16 | } | ||
| 17 | |||
| 18 | type ProfileResponse struct { | ||
| 19 | Profile bool `json:"profile"` | ||
| 20 | SteamID string `json:"steam_id"` | ||
| 21 | UserName string `json:"user_name"` | ||
| 22 | AvatarLink string `json:"avatar_link"` | ||
| 23 | CountryCode string `json:"country_code"` | ||
| 24 | ScoresSP []ScoreResponse `json:"scores_sp"` | ||
| 25 | ScoresMP []ScoreResponse `json:"scores_mp"` | ||
| 26 | } | ||
| 27 | |||
| 28 | type ScoreResponse struct { | ||
| 29 | MapID int `json:"map_id"` | ||
| 30 | Records any `json:"records"` | ||
| 31 | } | ||
| 32 | |||
| 33 | type MapSummaryResponse struct { | ||
| 34 | Map Map `json:"map"` | ||
| 35 | Summary MapSummary `json:"summary"` | ||
| 36 | } | ||
| 37 | |||
| 38 | type SearchResponse struct { | ||
| 39 | Players []UserShort `json:"players"` | ||
| 40 | Maps []MapShort `json:"maps"` | ||
| 41 | } | ||
| 42 | |||
| 43 | type ChaptersResponse struct { | ||
| 44 | Game Game `json:"game"` | ||
| 45 | Chapters []Chapter `json:"chapters"` | ||
| 46 | } | ||
| 47 | |||
| 48 | type ChapterMapsResponse struct { | ||
| 49 | Chapter Chapter `json:"chapter"` | ||
| 50 | Maps []MapShort `json:"maps"` | ||
| 51 | } | ||
| 52 | |||
| 53 | type RecordResponse struct { | ||
| 54 | ScoreCount int `json:"score_count"` | ||
| 55 | ScoreTime int `json:"score_time"` | ||
| 56 | } | ||
| 57 | |||
| 58 | func ErrorResponse(message string) Response { | ||
| 59 | return Response{ | ||
| 60 | Success: false, | ||
| 61 | Message: message, | ||
| 62 | Data: nil, | ||
| 63 | } | ||
| 64 | } | ||
diff --git a/backend/routes/routes.go b/backend/routes/routes.go deleted file mode 100644 index 0b80678..0000000 --- a/backend/routes/routes.go +++ /dev/null | |||
| @@ -1,41 +0,0 @@ | |||
| 1 | package routes | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "github.com/gin-gonic/gin" | ||
| 5 | "github.com/pektezol/leastportalshub/backend/controllers" | ||
| 6 | "github.com/pektezol/leastportalshub/backend/middleware" | ||
| 7 | swaggerfiles "github.com/swaggo/files" | ||
| 8 | ginSwagger "github.com/swaggo/gin-swagger" | ||
| 9 | ) | ||
| 10 | |||
| 11 | func InitRoutes(router *gin.Engine) { | ||
| 12 | api := router.Group("/api") | ||
| 13 | { | ||
| 14 | v1 := api.Group("/v1") | ||
| 15 | v1.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) | ||
| 16 | v1.GET("/", func(c *gin.Context) { | ||
| 17 | c.File("docs/index.html") | ||
| 18 | }) | ||
| 19 | v1.GET("/token", controllers.GetCookie) | ||
| 20 | v1.DELETE("/token", controllers.DeleteCookie) | ||
| 21 | v1.GET("/home", middleware.CheckAuth, controllers.Home) | ||
| 22 | v1.GET("/login", controllers.Login) | ||
| 23 | v1.GET("/profile", middleware.CheckAuth, controllers.Profile) | ||
| 24 | v1.PUT("/profile", middleware.CheckAuth, controllers.UpdateCountryCode) | ||
| 25 | v1.POST("/profile", middleware.CheckAuth, controllers.UpdateUser) | ||
| 26 | v1.GET("/users/:id", middleware.CheckAuth, controllers.FetchUser) | ||
| 27 | v1.GET("/demos", controllers.DownloadDemoWithID) | ||
| 28 | v1.GET("/maps/:id/summary", controllers.FetchMapSummary) | ||
| 29 | v1.POST("/maps/:id/summary", middleware.CheckAuth, controllers.CreateMapSummary) | ||
| 30 | v1.PUT("/maps/:id/summary", middleware.CheckAuth, controllers.EditMapSummary) | ||
| 31 | v1.DELETE("/maps/:id/summary", middleware.CheckAuth, controllers.DeleteMapSummary) | ||
| 32 | v1.PUT("/maps/:id/image", middleware.CheckAuth, controllers.EditMapImage) | ||
| 33 | v1.GET("/maps/:id/leaderboards", controllers.FetchMapLeaderboards) | ||
| 34 | v1.POST("/maps/:id/record", middleware.CheckAuth, controllers.CreateRecordWithDemo) | ||
| 35 | v1.GET("/rankings", controllers.Rankings) | ||
| 36 | v1.GET("/search", controllers.SearchWithQuery) | ||
| 37 | v1.GET("/games", controllers.FetchGames) | ||
| 38 | v1.GET("/games/:id", controllers.FetchChapters) | ||
| 39 | v1.GET("/chapters/:id", controllers.FetchChapterMaps) | ||
| 40 | } | ||
| 41 | } | ||