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 | |
| 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
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | README.md | 15 | ||||
| -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 | ||||
| -rw-r--r-- | docs/docs.go | 549 | ||||
| -rw-r--r-- | docs/swagger.json | 549 | ||||
| -rw-r--r-- | docs/swagger.yaml | 383 | ||||
| -rw-r--r-- | main.go | 4 |
23 files changed, 2644 insertions, 1355 deletions
| @@ -2,4 +2,5 @@ | |||
| 2 | .vscode | 2 | .vscode |
| 3 | *.sh | 3 | *.sh |
| 4 | *.txt | 4 | *.txt |
| 5 | *.dem \ No newline at end of file | 5 | *.dem |
| 6 | *.json \ No newline at end of file | ||
| @@ -36,6 +36,15 @@ If you have any questions or feedback, please feel free to contact us at our [Di | |||
| 36 | 36 | ||
| 37 | If you want to support the creator, you can do it via using GitHub sponsorships by clicking [here](https://github.com/sponsors/pektezol). | 37 | If you want to support the creator, you can do it via using GitHub sponsorships by clicking [here](https://github.com/sponsors/pektezol). |
| 38 | 38 | ||
| 39 | ## Privacy Policy | ||
| 40 | |||
| 41 | * We store a JWT (JSON Web Token) on your device as a cookie to facilitate authentication to LPHUB. | ||
| 42 | * We collect and store your publicly available id, username, avatar link, and country code from Steam during your first login to LPHUB. The exact date and time of your LPHUB account creation is also stored. | ||
| 43 | * In a case of profile update, newly changed values replaces the old data, and the update time is also stored. | ||
| 44 | * All of the demo proof submitted by users are stored in a publicly accessible Google Drive folder. By submitting demo as a proof, you agree that your demo proof becomes available to the public. | ||
| 45 | * Any information, ideas, or solutions shared on LPHUB are intended to be openly accessible and available for collaborative purposes. Users should understand that the content they contribute might be viewed and used by others for the purpose of generating insights and solutions. Sharing content on this platform does not transfer ownership of intellectual property rights to the platform owner or other users. Contributors retain their rights to their own content while granting others the opportunity to engage and collaborate. We do not place emphasis on determining ownership of individual contributions. | ||
| 46 | * At any time, Privacy Policy may get updated to reflect changes in LPHUB. The effective date at the bottom of the README indicates when the most recent changes were made. | ||
| 47 | |||
| 39 | ## Disclaimer | 48 | ## Disclaimer |
| 40 | 49 | ||
| 41 | This project, "Portal 2 Least Portals Hub" (hereafter referred to as "LPHUB"), is an unofficial community-driven resource providing strategies, routes, leaderboards, and other information related to the "Least Portals" category of the game "Portal 2". LPHUB is not affiliated with or endorsed by the creators, developers, or publishers of "Portal 2", including but not limited to Valve Corporation. | 50 | This project, "Portal 2 Least Portals Hub" (hereafter referred to as "LPHUB"), is an unofficial community-driven resource providing strategies, routes, leaderboards, and other information related to the "Least Portals" category of the game "Portal 2". LPHUB is not affiliated with or endorsed by the creators, developers, or publishers of "Portal 2", including but not limited to Valve Corporation. |
| @@ -48,4 +57,8 @@ The source code for LPHUB is licensed under the GNU Affero General Public Licens | |||
| 48 | 57 | ||
| 49 | Your use of LPHUB is at your own risk. LPHUB and its administrators and moderators disclaim all liability for any damages, whether direct, indirect, incidental, or consequential, that may result from your use of LPHUB or the strategies, routes, or other information provided therein. | 58 | Your use of LPHUB is at your own risk. LPHUB and its administrators and moderators disclaim all liability for any damages, whether direct, indirect, incidental, or consequential, that may result from your use of LPHUB or the strategies, routes, or other information provided therein. |
| 50 | 59 | ||
| 51 | By using LPHUB, you acknowledge that you have read and understood this disclaimer and agree to its terms. \ No newline at end of file | 60 | By using LPHUB, you acknowledge that you have read and understood this disclaimer and agree to its terms. |
| 61 | |||
| 62 | ## Last Update | ||
| 63 | |||
| 64 | 2023-08-30 \ No newline at end of file | ||
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 | } | ||
diff --git a/docs/docs.go b/docs/docs.go index 423afad..4129343 100644 --- a/docs/docs.go +++ b/docs/docs.go | |||
| @@ -50,7 +50,7 @@ const docTemplate = `{ | |||
| 50 | "type": "object", | 50 | "type": "object", |
| 51 | "properties": { | 51 | "properties": { |
| 52 | "data": { | 52 | "data": { |
| 53 | "$ref": "#/definitions/models.ChapterMapsResponse" | 53 | "$ref": "#/definitions/handlers.ChapterMapsResponse" |
| 54 | } | 54 | } |
| 55 | } | 55 | } |
| 56 | } | 56 | } |
| @@ -173,7 +173,7 @@ const docTemplate = `{ | |||
| 173 | "type": "object", | 173 | "type": "object", |
| 174 | "properties": { | 174 | "properties": { |
| 175 | "data": { | 175 | "data": { |
| 176 | "$ref": "#/definitions/models.ChaptersResponse" | 176 | "$ref": "#/definitions/handlers.ChaptersResponse" |
| 177 | } | 177 | } |
| 178 | } | 178 | } |
| 179 | } | 179 | } |
| @@ -213,7 +213,90 @@ const docTemplate = `{ | |||
| 213 | "type": "object", | 213 | "type": "object", |
| 214 | "properties": { | 214 | "properties": { |
| 215 | "data": { | 215 | "data": { |
| 216 | "$ref": "#/definitions/models.LoginResponse" | 216 | "$ref": "#/definitions/handlers.LoginResponse" |
| 217 | } | ||
| 218 | } | ||
| 219 | } | ||
| 220 | ] | ||
| 221 | } | ||
| 222 | }, | ||
| 223 | "400": { | ||
| 224 | "description": "Bad Request", | ||
| 225 | "schema": { | ||
| 226 | "$ref": "#/definitions/models.Response" | ||
| 227 | } | ||
| 228 | } | ||
| 229 | } | ||
| 230 | } | ||
| 231 | }, | ||
| 232 | "/logs/mod": { | ||
| 233 | "get": { | ||
| 234 | "description": "Get mod logs.", | ||
| 235 | "produces": [ | ||
| 236 | "application/json" | ||
| 237 | ], | ||
| 238 | "tags": [ | ||
| 239 | "logs" | ||
| 240 | ], | ||
| 241 | "parameters": [ | ||
| 242 | { | ||
| 243 | "type": "string", | ||
| 244 | "description": "JWT Token", | ||
| 245 | "name": "Authorization", | ||
| 246 | "in": "header", | ||
| 247 | "required": true | ||
| 248 | } | ||
| 249 | ], | ||
| 250 | "responses": { | ||
| 251 | "200": { | ||
| 252 | "description": "OK", | ||
| 253 | "schema": { | ||
| 254 | "allOf": [ | ||
| 255 | { | ||
| 256 | "$ref": "#/definitions/models.Response" | ||
| 257 | }, | ||
| 258 | { | ||
| 259 | "type": "object", | ||
| 260 | "properties": { | ||
| 261 | "data": { | ||
| 262 | "$ref": "#/definitions/handlers.LogsResponse" | ||
| 263 | } | ||
| 264 | } | ||
| 265 | } | ||
| 266 | ] | ||
| 267 | } | ||
| 268 | }, | ||
| 269 | "400": { | ||
| 270 | "description": "Bad Request", | ||
| 271 | "schema": { | ||
| 272 | "$ref": "#/definitions/models.Response" | ||
| 273 | } | ||
| 274 | } | ||
| 275 | } | ||
| 276 | } | ||
| 277 | }, | ||
| 278 | "/logs/score": { | ||
| 279 | "get": { | ||
| 280 | "description": "Get score logs of every player.", | ||
| 281 | "produces": [ | ||
| 282 | "application/json" | ||
| 283 | ], | ||
| 284 | "tags": [ | ||
| 285 | "logs" | ||
| 286 | ], | ||
| 287 | "responses": { | ||
| 288 | "200": { | ||
| 289 | "description": "OK", | ||
| 290 | "schema": { | ||
| 291 | "allOf": [ | ||
| 292 | { | ||
| 293 | "$ref": "#/definitions/models.Response" | ||
| 294 | }, | ||
| 295 | { | ||
| 296 | "type": "object", | ||
| 297 | "properties": { | ||
| 298 | "data": { | ||
| 299 | "$ref": "#/definitions/handlers.ScoreLogsResponse" | ||
| 217 | } | 300 | } |
| 218 | } | 301 | } |
| 219 | } | 302 | } |
| @@ -259,7 +342,7 @@ const docTemplate = `{ | |||
| 259 | "in": "body", | 342 | "in": "body", |
| 260 | "required": true, | 343 | "required": true, |
| 261 | "schema": { | 344 | "schema": { |
| 262 | "$ref": "#/definitions/models.EditMapImageRequest" | 345 | "$ref": "#/definitions/handlers.EditMapImageRequest" |
| 263 | } | 346 | } |
| 264 | } | 347 | } |
| 265 | ], | 348 | ], |
| @@ -275,7 +358,7 @@ const docTemplate = `{ | |||
| 275 | "type": "object", | 358 | "type": "object", |
| 276 | "properties": { | 359 | "properties": { |
| 277 | "data": { | 360 | "data": { |
| 278 | "$ref": "#/definitions/models.EditMapImageRequest" | 361 | "$ref": "#/definitions/handlers.EditMapImageRequest" |
| 279 | } | 362 | } |
| 280 | } | 363 | } |
| 281 | } | 364 | } |
| @@ -321,19 +404,7 @@ const docTemplate = `{ | |||
| 321 | "type": "object", | 404 | "type": "object", |
| 322 | "properties": { | 405 | "properties": { |
| 323 | "data": { | 406 | "data": { |
| 324 | "allOf": [ | 407 | "$ref": "#/definitions/handlers.MapLeaderboardsResponse" |
| 325 | { | ||
| 326 | "$ref": "#/definitions/models.Map" | ||
| 327 | }, | ||
| 328 | { | ||
| 329 | "type": "object", | ||
| 330 | "properties": { | ||
| 331 | "data": { | ||
| 332 | "$ref": "#/definitions/models.MapRecords" | ||
| 333 | } | ||
| 334 | } | ||
| 335 | } | ||
| 336 | ] | ||
| 337 | } | 408 | } |
| 338 | } | 409 | } |
| 339 | } | 410 | } |
| @@ -414,7 +485,7 @@ const docTemplate = `{ | |||
| 414 | "type": "object", | 485 | "type": "object", |
| 415 | "properties": { | 486 | "properties": { |
| 416 | "data": { | 487 | "data": { |
| 417 | "$ref": "#/definitions/models.RecordResponse" | 488 | "$ref": "#/definitions/handlers.RecordResponse" |
| 418 | } | 489 | } |
| 419 | } | 490 | } |
| 420 | } | 491 | } |
| @@ -466,7 +537,7 @@ const docTemplate = `{ | |||
| 466 | "type": "object", | 537 | "type": "object", |
| 467 | "properties": { | 538 | "properties": { |
| 468 | "data": { | 539 | "data": { |
| 469 | "$ref": "#/definitions/models.MapSummaryResponse" | 540 | "$ref": "#/definitions/handlers.MapSummaryResponse" |
| 470 | } | 541 | } |
| 471 | } | 542 | } |
| 472 | } | 543 | } |
| @@ -510,7 +581,7 @@ const docTemplate = `{ | |||
| 510 | "in": "body", | 581 | "in": "body", |
| 511 | "required": true, | 582 | "required": true, |
| 512 | "schema": { | 583 | "schema": { |
| 513 | "$ref": "#/definitions/models.EditMapSummaryRequest" | 584 | "$ref": "#/definitions/handlers.EditMapSummaryRequest" |
| 514 | } | 585 | } |
| 515 | } | 586 | } |
| 516 | ], | 587 | ], |
| @@ -526,7 +597,7 @@ const docTemplate = `{ | |||
| 526 | "type": "object", | 597 | "type": "object", |
| 527 | "properties": { | 598 | "properties": { |
| 528 | "data": { | 599 | "data": { |
| 529 | "$ref": "#/definitions/models.EditMapSummaryRequest" | 600 | "$ref": "#/definitions/handlers.EditMapSummaryRequest" |
| 530 | } | 601 | } |
| 531 | } | 602 | } |
| 532 | } | 603 | } |
| @@ -570,7 +641,7 @@ const docTemplate = `{ | |||
| 570 | "in": "body", | 641 | "in": "body", |
| 571 | "required": true, | 642 | "required": true, |
| 572 | "schema": { | 643 | "schema": { |
| 573 | "$ref": "#/definitions/models.CreateMapSummaryRequest" | 644 | "$ref": "#/definitions/handlers.CreateMapSummaryRequest" |
| 574 | } | 645 | } |
| 575 | } | 646 | } |
| 576 | ], | 647 | ], |
| @@ -586,7 +657,7 @@ const docTemplate = `{ | |||
| 586 | "type": "object", | 657 | "type": "object", |
| 587 | "properties": { | 658 | "properties": { |
| 588 | "data": { | 659 | "data": { |
| 589 | "$ref": "#/definitions/models.CreateMapSummaryRequest" | 660 | "$ref": "#/definitions/handlers.CreateMapSummaryRequest" |
| 590 | } | 661 | } |
| 591 | } | 662 | } |
| 592 | } | 663 | } |
| @@ -630,7 +701,7 @@ const docTemplate = `{ | |||
| 630 | "in": "body", | 701 | "in": "body", |
| 631 | "required": true, | 702 | "required": true, |
| 632 | "schema": { | 703 | "schema": { |
| 633 | "$ref": "#/definitions/models.DeleteMapSummaryRequest" | 704 | "$ref": "#/definitions/handlers.DeleteMapSummaryRequest" |
| 634 | } | 705 | } |
| 635 | } | 706 | } |
| 636 | ], | 707 | ], |
| @@ -646,7 +717,7 @@ const docTemplate = `{ | |||
| 646 | "type": "object", | 717 | "type": "object", |
| 647 | "properties": { | 718 | "properties": { |
| 648 | "data": { | 719 | "data": { |
| 649 | "$ref": "#/definitions/models.DeleteMapSummaryRequest" | 720 | "$ref": "#/definitions/handlers.DeleteMapSummaryRequest" |
| 650 | } | 721 | } |
| 651 | } | 722 | } |
| 652 | } | 723 | } |
| @@ -695,7 +766,7 @@ const docTemplate = `{ | |||
| 695 | "type": "object", | 766 | "type": "object", |
| 696 | "properties": { | 767 | "properties": { |
| 697 | "data": { | 768 | "data": { |
| 698 | "$ref": "#/definitions/models.ProfileResponse" | 769 | "$ref": "#/definitions/handlers.ProfileResponse" |
| 699 | } | 770 | } |
| 700 | } | 771 | } |
| 701 | } | 772 | } |
| @@ -796,7 +867,7 @@ const docTemplate = `{ | |||
| 796 | "type": "object", | 867 | "type": "object", |
| 797 | "properties": { | 868 | "properties": { |
| 798 | "data": { | 869 | "data": { |
| 799 | "$ref": "#/definitions/models.ProfileResponse" | 870 | "$ref": "#/definitions/handlers.ProfileResponse" |
| 800 | } | 871 | } |
| 801 | } | 872 | } |
| 802 | } | 873 | } |
| @@ -839,7 +910,7 @@ const docTemplate = `{ | |||
| 839 | "type": "object", | 910 | "type": "object", |
| 840 | "properties": { | 911 | "properties": { |
| 841 | "data": { | 912 | "data": { |
| 842 | "$ref": "#/definitions/models.RankingsResponse" | 913 | "$ref": "#/definitions/handlers.RankingsResponse" |
| 843 | } | 914 | } |
| 844 | } | 915 | } |
| 845 | } | 916 | } |
| @@ -884,7 +955,7 @@ const docTemplate = `{ | |||
| 884 | "type": "object", | 955 | "type": "object", |
| 885 | "properties": { | 956 | "properties": { |
| 886 | "data": { | 957 | "data": { |
| 887 | "$ref": "#/definitions/models.SearchResponse" | 958 | "$ref": "#/definitions/handlers.SearchResponse" |
| 888 | } | 959 | } |
| 889 | } | 960 | } |
| 890 | } | 961 | } |
| @@ -921,7 +992,7 @@ const docTemplate = `{ | |||
| 921 | "type": "object", | 992 | "type": "object", |
| 922 | "properties": { | 993 | "properties": { |
| 923 | "data": { | 994 | "data": { |
| 924 | "$ref": "#/definitions/models.LoginResponse" | 995 | "$ref": "#/definitions/handlers.LoginResponse" |
| 925 | } | 996 | } |
| 926 | } | 997 | } |
| 927 | } | 998 | } |
| @@ -956,7 +1027,7 @@ const docTemplate = `{ | |||
| 956 | "type": "object", | 1027 | "type": "object", |
| 957 | "properties": { | 1028 | "properties": { |
| 958 | "data": { | 1029 | "data": { |
| 959 | "$ref": "#/definitions/models.LoginResponse" | 1030 | "$ref": "#/definitions/handlers.LoginResponse" |
| 960 | } | 1031 | } |
| 961 | } | 1032 | } |
| 962 | } | 1033 | } |
| @@ -1005,7 +1076,7 @@ const docTemplate = `{ | |||
| 1005 | "type": "object", | 1076 | "type": "object", |
| 1006 | "properties": { | 1077 | "properties": { |
| 1007 | "data": { | 1078 | "data": { |
| 1008 | "$ref": "#/definitions/models.ProfileResponse" | 1079 | "$ref": "#/definitions/handlers.ProfileResponse" |
| 1009 | } | 1080 | } |
| 1010 | } | 1081 | } |
| 1011 | } | 1082 | } |
| @@ -1029,29 +1100,7 @@ const docTemplate = `{ | |||
| 1029 | } | 1100 | } |
| 1030 | }, | 1101 | }, |
| 1031 | "definitions": { | 1102 | "definitions": { |
| 1032 | "models.Category": { | 1103 | "handlers.ChapterMapsResponse": { |
| 1033 | "type": "object", | ||
| 1034 | "properties": { | ||
| 1035 | "id": { | ||
| 1036 | "type": "integer" | ||
| 1037 | }, | ||
| 1038 | "name": { | ||
| 1039 | "type": "string" | ||
| 1040 | } | ||
| 1041 | } | ||
| 1042 | }, | ||
| 1043 | "models.Chapter": { | ||
| 1044 | "type": "object", | ||
| 1045 | "properties": { | ||
| 1046 | "id": { | ||
| 1047 | "type": "integer" | ||
| 1048 | }, | ||
| 1049 | "name": { | ||
| 1050 | "type": "string" | ||
| 1051 | } | ||
| 1052 | } | ||
| 1053 | }, | ||
| 1054 | "models.ChapterMapsResponse": { | ||
| 1055 | "type": "object", | 1104 | "type": "object", |
| 1056 | "properties": { | 1105 | "properties": { |
| 1057 | "chapter": { | 1106 | "chapter": { |
| @@ -1065,7 +1114,7 @@ const docTemplate = `{ | |||
| 1065 | } | 1114 | } |
| 1066 | } | 1115 | } |
| 1067 | }, | 1116 | }, |
| 1068 | "models.ChaptersResponse": { | 1117 | "handlers.ChaptersResponse": { |
| 1069 | "type": "object", | 1118 | "type": "object", |
| 1070 | "properties": { | 1119 | "properties": { |
| 1071 | "chapters": { | 1120 | "chapters": { |
| @@ -1079,7 +1128,7 @@ const docTemplate = `{ | |||
| 1079 | } | 1128 | } |
| 1080 | } | 1129 | } |
| 1081 | }, | 1130 | }, |
| 1082 | "models.CreateMapSummaryRequest": { | 1131 | "handlers.CreateMapSummaryRequest": { |
| 1083 | "type": "object", | 1132 | "type": "object", |
| 1084 | "required": [ | 1133 | "required": [ |
| 1085 | "category_id", | 1134 | "category_id", |
| @@ -1109,7 +1158,7 @@ const docTemplate = `{ | |||
| 1109 | } | 1158 | } |
| 1110 | } | 1159 | } |
| 1111 | }, | 1160 | }, |
| 1112 | "models.DeleteMapSummaryRequest": { | 1161 | "handlers.DeleteMapSummaryRequest": { |
| 1113 | "type": "object", | 1162 | "type": "object", |
| 1114 | "required": [ | 1163 | "required": [ |
| 1115 | "route_id" | 1164 | "route_id" |
| @@ -1120,7 +1169,7 @@ const docTemplate = `{ | |||
| 1120 | } | 1169 | } |
| 1121 | } | 1170 | } |
| 1122 | }, | 1171 | }, |
| 1123 | "models.EditMapImageRequest": { | 1172 | "handlers.EditMapImageRequest": { |
| 1124 | "type": "object", | 1173 | "type": "object", |
| 1125 | "required": [ | 1174 | "required": [ |
| 1126 | "image" | 1175 | "image" |
| @@ -1131,7 +1180,7 @@ const docTemplate = `{ | |||
| 1131 | } | 1180 | } |
| 1132 | } | 1181 | } |
| 1133 | }, | 1182 | }, |
| 1134 | "models.EditMapSummaryRequest": { | 1183 | "handlers.EditMapSummaryRequest": { |
| 1135 | "type": "object", | 1184 | "type": "object", |
| 1136 | "required": [ | 1185 | "required": [ |
| 1137 | "description", | 1186 | "description", |
| @@ -1161,128 +1210,114 @@ const docTemplate = `{ | |||
| 1161 | } | 1210 | } |
| 1162 | } | 1211 | } |
| 1163 | }, | 1212 | }, |
| 1164 | "models.Game": { | 1213 | "handlers.LoginResponse": { |
| 1165 | "type": "object", | 1214 | "type": "object", |
| 1166 | "properties": { | 1215 | "properties": { |
| 1167 | "id": { | 1216 | "token": { |
| 1168 | "type": "integer" | ||
| 1169 | }, | ||
| 1170 | "is_coop": { | ||
| 1171 | "type": "boolean" | ||
| 1172 | }, | ||
| 1173 | "name": { | ||
| 1174 | "type": "string" | 1217 | "type": "string" |
| 1175 | } | 1218 | } |
| 1176 | } | 1219 | } |
| 1177 | }, | 1220 | }, |
| 1178 | "models.LoginResponse": { | 1221 | "handlers.LogsResponse": { |
| 1179 | "type": "object", | 1222 | "type": "object", |
| 1180 | "properties": { | 1223 | "properties": { |
| 1181 | "token": { | 1224 | "logs": { |
| 1182 | "type": "string" | 1225 | "type": "array", |
| 1226 | "items": { | ||
| 1227 | "$ref": "#/definitions/handlers.LogsResponseDetails" | ||
| 1228 | } | ||
| 1183 | } | 1229 | } |
| 1184 | } | 1230 | } |
| 1185 | }, | 1231 | }, |
| 1186 | "models.Map": { | 1232 | "handlers.LogsResponseDetails": { |
| 1187 | "type": "object", | 1233 | "type": "object", |
| 1188 | "properties": { | 1234 | "properties": { |
| 1189 | "chapter_name": { | 1235 | "date": { |
| 1190 | "type": "string" | ||
| 1191 | }, | ||
| 1192 | "game_name": { | ||
| 1193 | "type": "string" | 1236 | "type": "string" |
| 1194 | }, | 1237 | }, |
| 1195 | "id": { | 1238 | "detail": { |
| 1196 | "type": "integer" | ||
| 1197 | }, | ||
| 1198 | "image": { | ||
| 1199 | "type": "string" | 1239 | "type": "string" |
| 1200 | }, | 1240 | }, |
| 1201 | "is_coop": { | 1241 | "user": { |
| 1202 | "type": "boolean" | 1242 | "$ref": "#/definitions/models.UserShort" |
| 1203 | }, | ||
| 1204 | "map_name": { | ||
| 1205 | "type": "string" | ||
| 1206 | } | 1243 | } |
| 1207 | } | 1244 | } |
| 1208 | }, | 1245 | }, |
| 1209 | "models.MapHistory": { | 1246 | "handlers.MapLeaderboardsResponse": { |
| 1210 | "type": "object", | 1247 | "type": "object", |
| 1211 | "properties": { | 1248 | "properties": { |
| 1212 | "date": { | 1249 | "map": { |
| 1213 | "type": "string" | 1250 | "$ref": "#/definitions/models.Map" |
| 1214 | }, | ||
| 1215 | "runner_name": { | ||
| 1216 | "type": "string" | ||
| 1217 | }, | 1251 | }, |
| 1218 | "score_count": { | 1252 | "records": {} |
| 1219 | "type": "integer" | ||
| 1220 | } | ||
| 1221 | } | 1253 | } |
| 1222 | }, | 1254 | }, |
| 1223 | "models.MapRecords": { | 1255 | "handlers.MapSummaryResponse": { |
| 1224 | "type": "object", | 1256 | "type": "object", |
| 1225 | "properties": { | 1257 | "properties": { |
| 1226 | "records": {} | 1258 | "map": { |
| 1259 | "$ref": "#/definitions/models.Map" | ||
| 1260 | }, | ||
| 1261 | "summary": { | ||
| 1262 | "$ref": "#/definitions/models.MapSummary" | ||
| 1263 | } | ||
| 1227 | } | 1264 | } |
| 1228 | }, | 1265 | }, |
| 1229 | "models.MapRoute": { | 1266 | "handlers.ProfileRankings": { |
| 1230 | "type": "object", | 1267 | "type": "object", |
| 1231 | "properties": { | 1268 | "properties": { |
| 1232 | "category": { | 1269 | "cooperative": { |
| 1233 | "$ref": "#/definitions/models.Category" | 1270 | "$ref": "#/definitions/handlers.ProfileRankingsDetails" |
| 1234 | }, | ||
| 1235 | "description": { | ||
| 1236 | "type": "string" | ||
| 1237 | }, | 1271 | }, |
| 1238 | "history": { | 1272 | "overall": { |
| 1239 | "$ref": "#/definitions/models.MapHistory" | 1273 | "$ref": "#/definitions/handlers.ProfileRankingsDetails" |
| 1240 | }, | 1274 | }, |
| 1241 | "rating": { | 1275 | "singleplayer": { |
| 1242 | "type": "number" | 1276 | "$ref": "#/definitions/handlers.ProfileRankingsDetails" |
| 1243 | }, | ||
| 1244 | "route_id": { | ||
| 1245 | "type": "integer" | ||
| 1246 | }, | ||
| 1247 | "showcase": { | ||
| 1248 | "type": "string" | ||
| 1249 | } | 1277 | } |
| 1250 | } | 1278 | } |
| 1251 | }, | 1279 | }, |
| 1252 | "models.MapShort": { | 1280 | "handlers.ProfileRankingsDetails": { |
| 1253 | "type": "object", | 1281 | "type": "object", |
| 1254 | "properties": { | 1282 | "properties": { |
| 1255 | "id": { | 1283 | "completion_count": { |
| 1256 | "type": "integer" | 1284 | "type": "integer" |
| 1257 | }, | 1285 | }, |
| 1258 | "name": { | 1286 | "completion_total": { |
| 1259 | "type": "string" | 1287 | "type": "integer" |
| 1288 | }, | ||
| 1289 | "rank": { | ||
| 1290 | "type": "integer" | ||
| 1260 | } | 1291 | } |
| 1261 | } | 1292 | } |
| 1262 | }, | 1293 | }, |
| 1263 | "models.MapSummary": { | 1294 | "handlers.ProfileRecords": { |
| 1264 | "type": "object", | 1295 | "type": "object", |
| 1265 | "properties": { | 1296 | "properties": { |
| 1266 | "routes": { | 1297 | "category_id": { |
| 1298 | "type": "integer" | ||
| 1299 | }, | ||
| 1300 | "game_id": { | ||
| 1301 | "type": "integer" | ||
| 1302 | }, | ||
| 1303 | "map_id": { | ||
| 1304 | "type": "integer" | ||
| 1305 | }, | ||
| 1306 | "map_name": { | ||
| 1307 | "type": "string" | ||
| 1308 | }, | ||
| 1309 | "map_wr_count": { | ||
| 1310 | "type": "integer" | ||
| 1311 | }, | ||
| 1312 | "scores": { | ||
| 1267 | "type": "array", | 1313 | "type": "array", |
| 1268 | "items": { | 1314 | "items": { |
| 1269 | "$ref": "#/definitions/models.MapRoute" | 1315 | "$ref": "#/definitions/handlers.ProfileScores" |
| 1270 | } | 1316 | } |
| 1271 | } | 1317 | } |
| 1272 | } | 1318 | } |
| 1273 | }, | 1319 | }, |
| 1274 | "models.MapSummaryResponse": { | 1320 | "handlers.ProfileResponse": { |
| 1275 | "type": "object", | ||
| 1276 | "properties": { | ||
| 1277 | "map": { | ||
| 1278 | "$ref": "#/definitions/models.Map" | ||
| 1279 | }, | ||
| 1280 | "summary": { | ||
| 1281 | "$ref": "#/definitions/models.MapSummary" | ||
| 1282 | } | ||
| 1283 | } | ||
| 1284 | }, | ||
| 1285 | "models.ProfileResponse": { | ||
| 1286 | "type": "object", | 1321 | "type": "object", |
| 1287 | "properties": { | 1322 | "properties": { |
| 1288 | "avatar_link": { | 1323 | "avatar_link": { |
| @@ -1291,39 +1326,68 @@ const docTemplate = `{ | |||
| 1291 | "country_code": { | 1326 | "country_code": { |
| 1292 | "type": "string" | 1327 | "type": "string" |
| 1293 | }, | 1328 | }, |
| 1329 | "links": { | ||
| 1330 | "$ref": "#/definitions/models.Links" | ||
| 1331 | }, | ||
| 1294 | "profile": { | 1332 | "profile": { |
| 1295 | "type": "boolean" | 1333 | "type": "boolean" |
| 1296 | }, | 1334 | }, |
| 1297 | "scores_mp": { | 1335 | "rankings": { |
| 1336 | "$ref": "#/definitions/handlers.ProfileRankings" | ||
| 1337 | }, | ||
| 1338 | "records": { | ||
| 1298 | "type": "array", | 1339 | "type": "array", |
| 1299 | "items": { | 1340 | "items": { |
| 1300 | "$ref": "#/definitions/models.ScoreResponse" | 1341 | "$ref": "#/definitions/handlers.ProfileRecords" |
| 1301 | } | 1342 | } |
| 1302 | }, | 1343 | }, |
| 1303 | "scores_sp": { | 1344 | "steam_id": { |
| 1345 | "type": "string" | ||
| 1346 | }, | ||
| 1347 | "titles": { | ||
| 1304 | "type": "array", | 1348 | "type": "array", |
| 1305 | "items": { | 1349 | "items": { |
| 1306 | "$ref": "#/definitions/models.ScoreResponse" | 1350 | "$ref": "#/definitions/models.Title" |
| 1307 | } | 1351 | } |
| 1308 | }, | 1352 | }, |
| 1309 | "steam_id": { | 1353 | "user_name": { |
| 1354 | "type": "string" | ||
| 1355 | } | ||
| 1356 | } | ||
| 1357 | }, | ||
| 1358 | "handlers.ProfileScores": { | ||
| 1359 | "type": "object", | ||
| 1360 | "properties": { | ||
| 1361 | "date": { | ||
| 1310 | "type": "string" | 1362 | "type": "string" |
| 1311 | }, | 1363 | }, |
| 1312 | "user_name": { | 1364 | "demo_id": { |
| 1313 | "type": "string" | 1365 | "type": "string" |
| 1366 | }, | ||
| 1367 | "score_count": { | ||
| 1368 | "type": "integer" | ||
| 1369 | }, | ||
| 1370 | "score_time": { | ||
| 1371 | "type": "integer" | ||
| 1314 | } | 1372 | } |
| 1315 | } | 1373 | } |
| 1316 | }, | 1374 | }, |
| 1317 | "models.RankingsResponse": { | 1375 | "handlers.RankingsResponse": { |
| 1318 | "type": "object", | 1376 | "type": "object", |
| 1319 | "properties": { | 1377 | "properties": { |
| 1320 | "rankings_mp": { | 1378 | "rankings_multiplayer": { |
| 1379 | "type": "array", | ||
| 1380 | "items": { | ||
| 1381 | "$ref": "#/definitions/models.UserRanking" | ||
| 1382 | } | ||
| 1383 | }, | ||
| 1384 | "rankings_overall": { | ||
| 1321 | "type": "array", | 1385 | "type": "array", |
| 1322 | "items": { | 1386 | "items": { |
| 1323 | "$ref": "#/definitions/models.UserRanking" | 1387 | "$ref": "#/definitions/models.UserRanking" |
| 1324 | } | 1388 | } |
| 1325 | }, | 1389 | }, |
| 1326 | "rankings_sp": { | 1390 | "rankings_singleplayer": { |
| 1327 | "type": "array", | 1391 | "type": "array", |
| 1328 | "items": { | 1392 | "items": { |
| 1329 | "$ref": "#/definitions/models.UserRanking" | 1393 | "$ref": "#/definitions/models.UserRanking" |
| @@ -1331,7 +1395,7 @@ const docTemplate = `{ | |||
| 1331 | } | 1395 | } |
| 1332 | } | 1396 | } |
| 1333 | }, | 1397 | }, |
| 1334 | "models.RecordResponse": { | 1398 | "handlers.RecordResponse": { |
| 1335 | "type": "object", | 1399 | "type": "object", |
| 1336 | "properties": { | 1400 | "properties": { |
| 1337 | "score_count": { | 1401 | "score_count": { |
| @@ -1342,28 +1406,44 @@ const docTemplate = `{ | |||
| 1342 | } | 1406 | } |
| 1343 | } | 1407 | } |
| 1344 | }, | 1408 | }, |
| 1345 | "models.Response": { | 1409 | "handlers.ScoreLogsResponse": { |
| 1346 | "type": "object", | 1410 | "type": "object", |
| 1347 | "properties": { | 1411 | "properties": { |
| 1348 | "data": {}, | 1412 | "scores": { |
| 1349 | "message": { | 1413 | "type": "array", |
| 1350 | "type": "string" | 1414 | "items": { |
| 1351 | }, | 1415 | "$ref": "#/definitions/handlers.ScoreLogsResponseDetails" |
| 1352 | "success": { | 1416 | } |
| 1353 | "type": "boolean" | ||
| 1354 | } | 1417 | } |
| 1355 | } | 1418 | } |
| 1356 | }, | 1419 | }, |
| 1357 | "models.ScoreResponse": { | 1420 | "handlers.ScoreLogsResponseDetails": { |
| 1358 | "type": "object", | 1421 | "type": "object", |
| 1359 | "properties": { | 1422 | "properties": { |
| 1360 | "map_id": { | 1423 | "date": { |
| 1424 | "type": "string" | ||
| 1425 | }, | ||
| 1426 | "demo_id": { | ||
| 1427 | "type": "string" | ||
| 1428 | }, | ||
| 1429 | "game": { | ||
| 1430 | "$ref": "#/definitions/models.Game" | ||
| 1431 | }, | ||
| 1432 | "map": { | ||
| 1433 | "$ref": "#/definitions/models.MapShort" | ||
| 1434 | }, | ||
| 1435 | "score_count": { | ||
| 1361 | "type": "integer" | 1436 | "type": "integer" |
| 1362 | }, | 1437 | }, |
| 1363 | "records": {} | 1438 | "score_time": { |
| 1439 | "type": "integer" | ||
| 1440 | }, | ||
| 1441 | "user": { | ||
| 1442 | "$ref": "#/definitions/models.UserShort" | ||
| 1443 | } | ||
| 1364 | } | 1444 | } |
| 1365 | }, | 1445 | }, |
| 1366 | "models.SearchResponse": { | 1446 | "handlers.SearchResponse": { |
| 1367 | "type": "object", | 1447 | "type": "object", |
| 1368 | "properties": { | 1448 | "properties": { |
| 1369 | "maps": { | 1449 | "maps": { |
| @@ -1380,17 +1460,172 @@ const docTemplate = `{ | |||
| 1380 | } | 1460 | } |
| 1381 | } | 1461 | } |
| 1382 | }, | 1462 | }, |
| 1383 | "models.UserRanking": { | 1463 | "models.Category": { |
| 1384 | "type": "object", | 1464 | "type": "object", |
| 1385 | "properties": { | 1465 | "properties": { |
| 1386 | "total_score": { | 1466 | "id": { |
| 1387 | "type": "integer" | 1467 | "type": "integer" |
| 1388 | }, | 1468 | }, |
| 1389 | "user_id": { | 1469 | "name": { |
| 1390 | "type": "string" | 1470 | "type": "string" |
| 1471 | } | ||
| 1472 | } | ||
| 1473 | }, | ||
| 1474 | "models.Chapter": { | ||
| 1475 | "type": "object", | ||
| 1476 | "properties": { | ||
| 1477 | "id": { | ||
| 1478 | "type": "integer" | ||
| 1391 | }, | 1479 | }, |
| 1392 | "user_name": { | 1480 | "name": { |
| 1481 | "type": "string" | ||
| 1482 | } | ||
| 1483 | } | ||
| 1484 | }, | ||
| 1485 | "models.Game": { | ||
| 1486 | "type": "object", | ||
| 1487 | "properties": { | ||
| 1488 | "id": { | ||
| 1489 | "type": "integer" | ||
| 1490 | }, | ||
| 1491 | "is_coop": { | ||
| 1492 | "type": "boolean" | ||
| 1493 | }, | ||
| 1494 | "name": { | ||
| 1495 | "type": "string" | ||
| 1496 | } | ||
| 1497 | } | ||
| 1498 | }, | ||
| 1499 | "models.Links": { | ||
| 1500 | "type": "object", | ||
| 1501 | "properties": { | ||
| 1502 | "p2sr": { | ||
| 1503 | "type": "string" | ||
| 1504 | }, | ||
| 1505 | "stream": { | ||
| 1506 | "type": "string" | ||
| 1507 | }, | ||
| 1508 | "twitch": { | ||
| 1509 | "type": "string" | ||
| 1510 | }, | ||
| 1511 | "youtube": { | ||
| 1512 | "type": "string" | ||
| 1513 | } | ||
| 1514 | } | ||
| 1515 | }, | ||
| 1516 | "models.Map": { | ||
| 1517 | "type": "object", | ||
| 1518 | "properties": { | ||
| 1519 | "chapter_name": { | ||
| 1520 | "type": "string" | ||
| 1521 | }, | ||
| 1522 | "game_name": { | ||
| 1523 | "type": "string" | ||
| 1524 | }, | ||
| 1525 | "id": { | ||
| 1526 | "type": "integer" | ||
| 1527 | }, | ||
| 1528 | "image": { | ||
| 1529 | "type": "string" | ||
| 1530 | }, | ||
| 1531 | "is_coop": { | ||
| 1532 | "type": "boolean" | ||
| 1533 | }, | ||
| 1534 | "map_name": { | ||
| 1535 | "type": "string" | ||
| 1536 | } | ||
| 1537 | } | ||
| 1538 | }, | ||
| 1539 | "models.MapHistory": { | ||
| 1540 | "type": "object", | ||
| 1541 | "properties": { | ||
| 1542 | "date": { | ||
| 1543 | "type": "string" | ||
| 1544 | }, | ||
| 1545 | "runner_name": { | ||
| 1546 | "type": "string" | ||
| 1547 | }, | ||
| 1548 | "score_count": { | ||
| 1549 | "type": "integer" | ||
| 1550 | } | ||
| 1551 | } | ||
| 1552 | }, | ||
| 1553 | "models.MapRoute": { | ||
| 1554 | "type": "object", | ||
| 1555 | "properties": { | ||
| 1556 | "category": { | ||
| 1557 | "$ref": "#/definitions/models.Category" | ||
| 1558 | }, | ||
| 1559 | "description": { | ||
| 1560 | "type": "string" | ||
| 1561 | }, | ||
| 1562 | "history": { | ||
| 1563 | "$ref": "#/definitions/models.MapHistory" | ||
| 1564 | }, | ||
| 1565 | "rating": { | ||
| 1566 | "type": "number" | ||
| 1567 | }, | ||
| 1568 | "route_id": { | ||
| 1569 | "type": "integer" | ||
| 1570 | }, | ||
| 1571 | "showcase": { | ||
| 1572 | "type": "string" | ||
| 1573 | } | ||
| 1574 | } | ||
| 1575 | }, | ||
| 1576 | "models.MapShort": { | ||
| 1577 | "type": "object", | ||
| 1578 | "properties": { | ||
| 1579 | "id": { | ||
| 1580 | "type": "integer" | ||
| 1581 | }, | ||
| 1582 | "name": { | ||
| 1583 | "type": "string" | ||
| 1584 | } | ||
| 1585 | } | ||
| 1586 | }, | ||
| 1587 | "models.MapSummary": { | ||
| 1588 | "type": "object", | ||
| 1589 | "properties": { | ||
| 1590 | "routes": { | ||
| 1591 | "type": "array", | ||
| 1592 | "items": { | ||
| 1593 | "$ref": "#/definitions/models.MapRoute" | ||
| 1594 | } | ||
| 1595 | } | ||
| 1596 | } | ||
| 1597 | }, | ||
| 1598 | "models.Response": { | ||
| 1599 | "type": "object", | ||
| 1600 | "properties": { | ||
| 1601 | "data": {}, | ||
| 1602 | "message": { | ||
| 1603 | "type": "string" | ||
| 1604 | }, | ||
| 1605 | "success": { | ||
| 1606 | "type": "boolean" | ||
| 1607 | } | ||
| 1608 | } | ||
| 1609 | }, | ||
| 1610 | "models.Title": { | ||
| 1611 | "type": "object", | ||
| 1612 | "properties": { | ||
| 1613 | "color": { | ||
| 1393 | "type": "string" | 1614 | "type": "string" |
| 1615 | }, | ||
| 1616 | "name": { | ||
| 1617 | "type": "string" | ||
| 1618 | } | ||
| 1619 | } | ||
| 1620 | }, | ||
| 1621 | "models.UserRanking": { | ||
| 1622 | "type": "object", | ||
| 1623 | "properties": { | ||
| 1624 | "total_score": { | ||
| 1625 | "type": "integer" | ||
| 1626 | }, | ||
| 1627 | "user": { | ||
| 1628 | "$ref": "#/definitions/models.UserShort" | ||
| 1394 | } | 1629 | } |
| 1395 | } | 1630 | } |
| 1396 | }, | 1631 | }, |
diff --git a/docs/swagger.json b/docs/swagger.json index 2e1a789..646da0f 100644 --- a/docs/swagger.json +++ b/docs/swagger.json | |||
| @@ -43,7 +43,7 @@ | |||
| 43 | "type": "object", | 43 | "type": "object", |
| 44 | "properties": { | 44 | "properties": { |
| 45 | "data": { | 45 | "data": { |
| 46 | "$ref": "#/definitions/models.ChapterMapsResponse" | 46 | "$ref": "#/definitions/handlers.ChapterMapsResponse" |
| 47 | } | 47 | } |
| 48 | } | 48 | } |
| 49 | } | 49 | } |
| @@ -166,7 +166,7 @@ | |||
| 166 | "type": "object", | 166 | "type": "object", |
| 167 | "properties": { | 167 | "properties": { |
| 168 | "data": { | 168 | "data": { |
| 169 | "$ref": "#/definitions/models.ChaptersResponse" | 169 | "$ref": "#/definitions/handlers.ChaptersResponse" |
| 170 | } | 170 | } |
| 171 | } | 171 | } |
| 172 | } | 172 | } |
| @@ -206,7 +206,90 @@ | |||
| 206 | "type": "object", | 206 | "type": "object", |
| 207 | "properties": { | 207 | "properties": { |
| 208 | "data": { | 208 | "data": { |
| 209 | "$ref": "#/definitions/models.LoginResponse" | 209 | "$ref": "#/definitions/handlers.LoginResponse" |
| 210 | } | ||
| 211 | } | ||
| 212 | } | ||
| 213 | ] | ||
| 214 | } | ||
| 215 | }, | ||
| 216 | "400": { | ||
| 217 | "description": "Bad Request", | ||
| 218 | "schema": { | ||
| 219 | "$ref": "#/definitions/models.Response" | ||
| 220 | } | ||
| 221 | } | ||
| 222 | } | ||
| 223 | } | ||
| 224 | }, | ||
| 225 | "/logs/mod": { | ||
| 226 | "get": { | ||
| 227 | "description": "Get mod logs.", | ||
| 228 | "produces": [ | ||
| 229 | "application/json" | ||
| 230 | ], | ||
| 231 | "tags": [ | ||
| 232 | "logs" | ||
| 233 | ], | ||
| 234 | "parameters": [ | ||
| 235 | { | ||
| 236 | "type": "string", | ||
| 237 | "description": "JWT Token", | ||
| 238 | "name": "Authorization", | ||
| 239 | "in": "header", | ||
| 240 | "required": true | ||
| 241 | } | ||
| 242 | ], | ||
| 243 | "responses": { | ||
| 244 | "200": { | ||
| 245 | "description": "OK", | ||
| 246 | "schema": { | ||
| 247 | "allOf": [ | ||
| 248 | { | ||
| 249 | "$ref": "#/definitions/models.Response" | ||
| 250 | }, | ||
| 251 | { | ||
| 252 | "type": "object", | ||
| 253 | "properties": { | ||
| 254 | "data": { | ||
| 255 | "$ref": "#/definitions/handlers.LogsResponse" | ||
| 256 | } | ||
| 257 | } | ||
| 258 | } | ||
| 259 | ] | ||
| 260 | } | ||
| 261 | }, | ||
| 262 | "400": { | ||
| 263 | "description": "Bad Request", | ||
| 264 | "schema": { | ||
| 265 | "$ref": "#/definitions/models.Response" | ||
| 266 | } | ||
| 267 | } | ||
| 268 | } | ||
| 269 | } | ||
| 270 | }, | ||
| 271 | "/logs/score": { | ||
| 272 | "get": { | ||
| 273 | "description": "Get score logs of every player.", | ||
| 274 | "produces": [ | ||
| 275 | "application/json" | ||
| 276 | ], | ||
| 277 | "tags": [ | ||
| 278 | "logs" | ||
| 279 | ], | ||
| 280 | "responses": { | ||
| 281 | "200": { | ||
| 282 | "description": "OK", | ||
| 283 | "schema": { | ||
| 284 | "allOf": [ | ||
| 285 | { | ||
| 286 | "$ref": "#/definitions/models.Response" | ||
| 287 | }, | ||
| 288 | { | ||
| 289 | "type": "object", | ||
| 290 | "properties": { | ||
| 291 | "data": { | ||
| 292 | "$ref": "#/definitions/handlers.ScoreLogsResponse" | ||
| 210 | } | 293 | } |
| 211 | } | 294 | } |
| 212 | } | 295 | } |
| @@ -252,7 +335,7 @@ | |||
| 252 | "in": "body", | 335 | "in": "body", |
| 253 | "required": true, | 336 | "required": true, |
| 254 | "schema": { | 337 | "schema": { |
| 255 | "$ref": "#/definitions/models.EditMapImageRequest" | 338 | "$ref": "#/definitions/handlers.EditMapImageRequest" |
| 256 | } | 339 | } |
| 257 | } | 340 | } |
| 258 | ], | 341 | ], |
| @@ -268,7 +351,7 @@ | |||
| 268 | "type": "object", | 351 | "type": "object", |
| 269 | "properties": { | 352 | "properties": { |
| 270 | "data": { | 353 | "data": { |
| 271 | "$ref": "#/definitions/models.EditMapImageRequest" | 354 | "$ref": "#/definitions/handlers.EditMapImageRequest" |
| 272 | } | 355 | } |
| 273 | } | 356 | } |
| 274 | } | 357 | } |
| @@ -314,19 +397,7 @@ | |||
| 314 | "type": "object", | 397 | "type": "object", |
| 315 | "properties": { | 398 | "properties": { |
| 316 | "data": { | 399 | "data": { |
| 317 | "allOf": [ | 400 | "$ref": "#/definitions/handlers.MapLeaderboardsResponse" |
| 318 | { | ||
| 319 | "$ref": "#/definitions/models.Map" | ||
| 320 | }, | ||
| 321 | { | ||
| 322 | "type": "object", | ||
| 323 | "properties": { | ||
| 324 | "data": { | ||
| 325 | "$ref": "#/definitions/models.MapRecords" | ||
| 326 | } | ||
| 327 | } | ||
| 328 | } | ||
| 329 | ] | ||
| 330 | } | 401 | } |
| 331 | } | 402 | } |
| 332 | } | 403 | } |
| @@ -407,7 +478,7 @@ | |||
| 407 | "type": "object", | 478 | "type": "object", |
| 408 | "properties": { | 479 | "properties": { |
| 409 | "data": { | 480 | "data": { |
| 410 | "$ref": "#/definitions/models.RecordResponse" | 481 | "$ref": "#/definitions/handlers.RecordResponse" |
| 411 | } | 482 | } |
| 412 | } | 483 | } |
| 413 | } | 484 | } |
| @@ -459,7 +530,7 @@ | |||
| 459 | "type": "object", | 530 | "type": "object", |
| 460 | "properties": { | 531 | "properties": { |
| 461 | "data": { | 532 | "data": { |
| 462 | "$ref": "#/definitions/models.MapSummaryResponse" | 533 | "$ref": "#/definitions/handlers.MapSummaryResponse" |
| 463 | } | 534 | } |
| 464 | } | 535 | } |
| 465 | } | 536 | } |
| @@ -503,7 +574,7 @@ | |||
| 503 | "in": "body", | 574 | "in": "body", |
| 504 | "required": true, | 575 | "required": true, |
| 505 | "schema": { | 576 | "schema": { |
| 506 | "$ref": "#/definitions/models.EditMapSummaryRequest" | 577 | "$ref": "#/definitions/handlers.EditMapSummaryRequest" |
| 507 | } | 578 | } |
| 508 | } | 579 | } |
| 509 | ], | 580 | ], |
| @@ -519,7 +590,7 @@ | |||
| 519 | "type": "object", | 590 | "type": "object", |
| 520 | "properties": { | 591 | "properties": { |
| 521 | "data": { | 592 | "data": { |
| 522 | "$ref": "#/definitions/models.EditMapSummaryRequest" | 593 | "$ref": "#/definitions/handlers.EditMapSummaryRequest" |
| 523 | } | 594 | } |
| 524 | } | 595 | } |
| 525 | } | 596 | } |
| @@ -563,7 +634,7 @@ | |||
| 563 | "in": "body", | 634 | "in": "body", |
| 564 | "required": true, | 635 | "required": true, |
| 565 | "schema": { | 636 | "schema": { |
| 566 | "$ref": "#/definitions/models.CreateMapSummaryRequest" | 637 | "$ref": "#/definitions/handlers.CreateMapSummaryRequest" |
| 567 | } | 638 | } |
| 568 | } | 639 | } |
| 569 | ], | 640 | ], |
| @@ -579,7 +650,7 @@ | |||
| 579 | "type": "object", | 650 | "type": "object", |
| 580 | "properties": { | 651 | "properties": { |
| 581 | "data": { | 652 | "data": { |
| 582 | "$ref": "#/definitions/models.CreateMapSummaryRequest" | 653 | "$ref": "#/definitions/handlers.CreateMapSummaryRequest" |
| 583 | } | 654 | } |
| 584 | } | 655 | } |
| 585 | } | 656 | } |
| @@ -623,7 +694,7 @@ | |||
| 623 | "in": "body", | 694 | "in": "body", |
| 624 | "required": true, | 695 | "required": true, |
| 625 | "schema": { | 696 | "schema": { |
| 626 | "$ref": "#/definitions/models.DeleteMapSummaryRequest" | 697 | "$ref": "#/definitions/handlers.DeleteMapSummaryRequest" |
| 627 | } | 698 | } |
| 628 | } | 699 | } |
| 629 | ], | 700 | ], |
| @@ -639,7 +710,7 @@ | |||
| 639 | "type": "object", | 710 | "type": "object", |
| 640 | "properties": { | 711 | "properties": { |
| 641 | "data": { | 712 | "data": { |
| 642 | "$ref": "#/definitions/models.DeleteMapSummaryRequest" | 713 | "$ref": "#/definitions/handlers.DeleteMapSummaryRequest" |
| 643 | } | 714 | } |
| 644 | } | 715 | } |
| 645 | } | 716 | } |
| @@ -688,7 +759,7 @@ | |||
| 688 | "type": "object", | 759 | "type": "object", |
| 689 | "properties": { | 760 | "properties": { |
| 690 | "data": { | 761 | "data": { |
| 691 | "$ref": "#/definitions/models.ProfileResponse" | 762 | "$ref": "#/definitions/handlers.ProfileResponse" |
| 692 | } | 763 | } |
| 693 | } | 764 | } |
| 694 | } | 765 | } |
| @@ -789,7 +860,7 @@ | |||
| 789 | "type": "object", | 860 | "type": "object", |
| 790 | "properties": { | 861 | "properties": { |
| 791 | "data": { | 862 | "data": { |
| 792 | "$ref": "#/definitions/models.ProfileResponse" | 863 | "$ref": "#/definitions/handlers.ProfileResponse" |
| 793 | } | 864 | } |
| 794 | } | 865 | } |
| 795 | } | 866 | } |
| @@ -832,7 +903,7 @@ | |||
| 832 | "type": "object", | 903 | "type": "object", |
| 833 | "properties": { | 904 | "properties": { |
| 834 | "data": { | 905 | "data": { |
| 835 | "$ref": "#/definitions/models.RankingsResponse" | 906 | "$ref": "#/definitions/handlers.RankingsResponse" |
| 836 | } | 907 | } |
| 837 | } | 908 | } |
| 838 | } | 909 | } |
| @@ -877,7 +948,7 @@ | |||
| 877 | "type": "object", | 948 | "type": "object", |
| 878 | "properties": { | 949 | "properties": { |
| 879 | "data": { | 950 | "data": { |
| 880 | "$ref": "#/definitions/models.SearchResponse" | 951 | "$ref": "#/definitions/handlers.SearchResponse" |
| 881 | } | 952 | } |
| 882 | } | 953 | } |
| 883 | } | 954 | } |
| @@ -914,7 +985,7 @@ | |||
| 914 | "type": "object", | 985 | "type": "object", |
| 915 | "properties": { | 986 | "properties": { |
| 916 | "data": { | 987 | "data": { |
| 917 | "$ref": "#/definitions/models.LoginResponse" | 988 | "$ref": "#/definitions/handlers.LoginResponse" |
| 918 | } | 989 | } |
| 919 | } | 990 | } |
| 920 | } | 991 | } |
| @@ -949,7 +1020,7 @@ | |||
| 949 | "type": "object", | 1020 | "type": "object", |
| 950 | "properties": { | 1021 | "properties": { |
| 951 | "data": { | 1022 | "data": { |
| 952 | "$ref": "#/definitions/models.LoginResponse" | 1023 | "$ref": "#/definitions/handlers.LoginResponse" |
| 953 | } | 1024 | } |
| 954 | } | 1025 | } |
| 955 | } | 1026 | } |
| @@ -998,7 +1069,7 @@ | |||
| 998 | "type": "object", | 1069 | "type": "object", |
| 999 | "properties": { | 1070 | "properties": { |
| 1000 | "data": { | 1071 | "data": { |
| 1001 | "$ref": "#/definitions/models.ProfileResponse" | 1072 | "$ref": "#/definitions/handlers.ProfileResponse" |
| 1002 | } | 1073 | } |
| 1003 | } | 1074 | } |
| 1004 | } | 1075 | } |
| @@ -1022,29 +1093,7 @@ | |||
| 1022 | } | 1093 | } |
| 1023 | }, | 1094 | }, |
| 1024 | "definitions": { | 1095 | "definitions": { |
| 1025 | "models.Category": { | 1096 | "handlers.ChapterMapsResponse": { |
| 1026 | "type": "object", | ||
| 1027 | "properties": { | ||
| 1028 | "id": { | ||
| 1029 | "type": "integer" | ||
| 1030 | }, | ||
| 1031 | "name": { | ||
| 1032 | "type": "string" | ||
| 1033 | } | ||
| 1034 | } | ||
| 1035 | }, | ||
| 1036 | "models.Chapter": { | ||
| 1037 | "type": "object", | ||
| 1038 | "properties": { | ||
| 1039 | "id": { | ||
| 1040 | "type": "integer" | ||
| 1041 | }, | ||
| 1042 | "name": { | ||
| 1043 | "type": "string" | ||
| 1044 | } | ||
| 1045 | } | ||
| 1046 | }, | ||
| 1047 | "models.ChapterMapsResponse": { | ||
| 1048 | "type": "object", | 1097 | "type": "object", |
| 1049 | "properties": { | 1098 | "properties": { |
| 1050 | "chapter": { | 1099 | "chapter": { |
| @@ -1058,7 +1107,7 @@ | |||
| 1058 | } | 1107 | } |
| 1059 | } | 1108 | } |
| 1060 | }, | 1109 | }, |
| 1061 | "models.ChaptersResponse": { | 1110 | "handlers.ChaptersResponse": { |
| 1062 | "type": "object", | 1111 | "type": "object", |
| 1063 | "properties": { | 1112 | "properties": { |
| 1064 | "chapters": { | 1113 | "chapters": { |
| @@ -1072,7 +1121,7 @@ | |||
| 1072 | } | 1121 | } |
| 1073 | } | 1122 | } |
| 1074 | }, | 1123 | }, |
| 1075 | "models.CreateMapSummaryRequest": { | 1124 | "handlers.CreateMapSummaryRequest": { |
| 1076 | "type": "object", | 1125 | "type": "object", |
| 1077 | "required": [ | 1126 | "required": [ |
| 1078 | "category_id", | 1127 | "category_id", |
| @@ -1102,7 +1151,7 @@ | |||
| 1102 | } | 1151 | } |
| 1103 | } | 1152 | } |
| 1104 | }, | 1153 | }, |
| 1105 | "models.DeleteMapSummaryRequest": { | 1154 | "handlers.DeleteMapSummaryRequest": { |
| 1106 | "type": "object", | 1155 | "type": "object", |
| 1107 | "required": [ | 1156 | "required": [ |
| 1108 | "route_id" | 1157 | "route_id" |
| @@ -1113,7 +1162,7 @@ | |||
| 1113 | } | 1162 | } |
| 1114 | } | 1163 | } |
| 1115 | }, | 1164 | }, |
| 1116 | "models.EditMapImageRequest": { | 1165 | "handlers.EditMapImageRequest": { |
| 1117 | "type": "object", | 1166 | "type": "object", |
| 1118 | "required": [ | 1167 | "required": [ |
| 1119 | "image" | 1168 | "image" |
| @@ -1124,7 +1173,7 @@ | |||
| 1124 | } | 1173 | } |
| 1125 | } | 1174 | } |
| 1126 | }, | 1175 | }, |
| 1127 | "models.EditMapSummaryRequest": { | 1176 | "handlers.EditMapSummaryRequest": { |
| 1128 | "type": "object", | 1177 | "type": "object", |
| 1129 | "required": [ | 1178 | "required": [ |
| 1130 | "description", | 1179 | "description", |
| @@ -1154,128 +1203,114 @@ | |||
| 1154 | } | 1203 | } |
| 1155 | } | 1204 | } |
| 1156 | }, | 1205 | }, |
| 1157 | "models.Game": { | 1206 | "handlers.LoginResponse": { |
| 1158 | "type": "object", | 1207 | "type": "object", |
| 1159 | "properties": { | 1208 | "properties": { |
| 1160 | "id": { | 1209 | "token": { |
| 1161 | "type": "integer" | ||
| 1162 | }, | ||
| 1163 | "is_coop": { | ||
| 1164 | "type": "boolean" | ||
| 1165 | }, | ||
| 1166 | "name": { | ||
| 1167 | "type": "string" | 1210 | "type": "string" |
| 1168 | } | 1211 | } |
| 1169 | } | 1212 | } |
| 1170 | }, | 1213 | }, |
| 1171 | "models.LoginResponse": { | 1214 | "handlers.LogsResponse": { |
| 1172 | "type": "object", | 1215 | "type": "object", |
| 1173 | "properties": { | 1216 | "properties": { |
| 1174 | "token": { | 1217 | "logs": { |
| 1175 | "type": "string" | 1218 | "type": "array", |
| 1219 | "items": { | ||
| 1220 | "$ref": "#/definitions/handlers.LogsResponseDetails" | ||
| 1221 | } | ||
| 1176 | } | 1222 | } |
| 1177 | } | 1223 | } |
| 1178 | }, | 1224 | }, |
| 1179 | "models.Map": { | 1225 | "handlers.LogsResponseDetails": { |
| 1180 | "type": "object", | 1226 | "type": "object", |
| 1181 | "properties": { | 1227 | "properties": { |
| 1182 | "chapter_name": { | 1228 | "date": { |
| 1183 | "type": "string" | ||
| 1184 | }, | ||
| 1185 | "game_name": { | ||
| 1186 | "type": "string" | 1229 | "type": "string" |
| 1187 | }, | 1230 | }, |
| 1188 | "id": { | 1231 | "detail": { |
| 1189 | "type": "integer" | ||
| 1190 | }, | ||
| 1191 | "image": { | ||
| 1192 | "type": "string" | 1232 | "type": "string" |
| 1193 | }, | 1233 | }, |
| 1194 | "is_coop": { | 1234 | "user": { |
| 1195 | "type": "boolean" | 1235 | "$ref": "#/definitions/models.UserShort" |
| 1196 | }, | ||
| 1197 | "map_name": { | ||
| 1198 | "type": "string" | ||
| 1199 | } | 1236 | } |
| 1200 | } | 1237 | } |
| 1201 | }, | 1238 | }, |
| 1202 | "models.MapHistory": { | 1239 | "handlers.MapLeaderboardsResponse": { |
| 1203 | "type": "object", | 1240 | "type": "object", |
| 1204 | "properties": { | 1241 | "properties": { |
| 1205 | "date": { | 1242 | "map": { |
| 1206 | "type": "string" | 1243 | "$ref": "#/definitions/models.Map" |
| 1207 | }, | ||
| 1208 | "runner_name": { | ||
| 1209 | "type": "string" | ||
| 1210 | }, | 1244 | }, |
| 1211 | "score_count": { | 1245 | "records": {} |
| 1212 | "type": "integer" | ||
| 1213 | } | ||
| 1214 | } | 1246 | } |
| 1215 | }, | 1247 | }, |
| 1216 | "models.MapRecords": { | 1248 | "handlers.MapSummaryResponse": { |
| 1217 | "type": "object", | 1249 | "type": "object", |
| 1218 | "properties": { | 1250 | "properties": { |
| 1219 | "records": {} | 1251 | "map": { |
| 1252 | "$ref": "#/definitions/models.Map" | ||
| 1253 | }, | ||
| 1254 | "summary": { | ||
| 1255 | "$ref": "#/definitions/models.MapSummary" | ||
| 1256 | } | ||
| 1220 | } | 1257 | } |
| 1221 | }, | 1258 | }, |
| 1222 | "models.MapRoute": { | 1259 | "handlers.ProfileRankings": { |
| 1223 | "type": "object", | 1260 | "type": "object", |
| 1224 | "properties": { | 1261 | "properties": { |
| 1225 | "category": { | 1262 | "cooperative": { |
| 1226 | "$ref": "#/definitions/models.Category" | 1263 | "$ref": "#/definitions/handlers.ProfileRankingsDetails" |
| 1227 | }, | ||
| 1228 | "description": { | ||
| 1229 | "type": "string" | ||
| 1230 | }, | 1264 | }, |
| 1231 | "history": { | 1265 | "overall": { |
| 1232 | "$ref": "#/definitions/models.MapHistory" | 1266 | "$ref": "#/definitions/handlers.ProfileRankingsDetails" |
| 1233 | }, | 1267 | }, |
| 1234 | "rating": { | 1268 | "singleplayer": { |
| 1235 | "type": "number" | 1269 | "$ref": "#/definitions/handlers.ProfileRankingsDetails" |
| 1236 | }, | ||
| 1237 | "route_id": { | ||
| 1238 | "type": "integer" | ||
| 1239 | }, | ||
| 1240 | "showcase": { | ||
| 1241 | "type": "string" | ||
| 1242 | } | 1270 | } |
| 1243 | } | 1271 | } |
| 1244 | }, | 1272 | }, |
| 1245 | "models.MapShort": { | 1273 | "handlers.ProfileRankingsDetails": { |
| 1246 | "type": "object", | 1274 | "type": "object", |
| 1247 | "properties": { | 1275 | "properties": { |
| 1248 | "id": { | 1276 | "completion_count": { |
| 1249 | "type": "integer" | 1277 | "type": "integer" |
| 1250 | }, | 1278 | }, |
| 1251 | "name": { | 1279 | "completion_total": { |
| 1252 | "type": "string" | 1280 | "type": "integer" |
| 1281 | }, | ||
| 1282 | "rank": { | ||
| 1283 | "type": "integer" | ||
| 1253 | } | 1284 | } |
| 1254 | } | 1285 | } |
| 1255 | }, | 1286 | }, |
| 1256 | "models.MapSummary": { | 1287 | "handlers.ProfileRecords": { |
| 1257 | "type": "object", | 1288 | "type": "object", |
| 1258 | "properties": { | 1289 | "properties": { |
| 1259 | "routes": { | 1290 | "category_id": { |
| 1291 | "type": "integer" | ||
| 1292 | }, | ||
| 1293 | "game_id": { | ||
| 1294 | "type": "integer" | ||
| 1295 | }, | ||
| 1296 | "map_id": { | ||
| 1297 | "type": "integer" | ||
| 1298 | }, | ||
| 1299 | "map_name": { | ||
| 1300 | "type": "string" | ||
| 1301 | }, | ||
| 1302 | "map_wr_count": { | ||
| 1303 | "type": "integer" | ||
| 1304 | }, | ||
| 1305 | "scores": { | ||
| 1260 | "type": "array", | 1306 | "type": "array", |
| 1261 | "items": { | 1307 | "items": { |
| 1262 | "$ref": "#/definitions/models.MapRoute" | 1308 | "$ref": "#/definitions/handlers.ProfileScores" |
| 1263 | } | 1309 | } |
| 1264 | } | 1310 | } |
| 1265 | } | 1311 | } |
| 1266 | }, | 1312 | }, |
| 1267 | "models.MapSummaryResponse": { | 1313 | "handlers.ProfileResponse": { |
| 1268 | "type": "object", | ||
| 1269 | "properties": { | ||
| 1270 | "map": { | ||
| 1271 | "$ref": "#/definitions/models.Map" | ||
| 1272 | }, | ||
| 1273 | "summary": { | ||
| 1274 | "$ref": "#/definitions/models.MapSummary" | ||
| 1275 | } | ||
| 1276 | } | ||
| 1277 | }, | ||
| 1278 | "models.ProfileResponse": { | ||
| 1279 | "type": "object", | 1314 | "type": "object", |
| 1280 | "properties": { | 1315 | "properties": { |
| 1281 | "avatar_link": { | 1316 | "avatar_link": { |
| @@ -1284,39 +1319,68 @@ | |||
| 1284 | "country_code": { | 1319 | "country_code": { |
| 1285 | "type": "string" | 1320 | "type": "string" |
| 1286 | }, | 1321 | }, |
| 1322 | "links": { | ||
| 1323 | "$ref": "#/definitions/models.Links" | ||
| 1324 | }, | ||
| 1287 | "profile": { | 1325 | "profile": { |
| 1288 | "type": "boolean" | 1326 | "type": "boolean" |
| 1289 | }, | 1327 | }, |
| 1290 | "scores_mp": { | 1328 | "rankings": { |
| 1329 | "$ref": "#/definitions/handlers.ProfileRankings" | ||
| 1330 | }, | ||
| 1331 | "records": { | ||
| 1291 | "type": "array", | 1332 | "type": "array", |
| 1292 | "items": { | 1333 | "items": { |
| 1293 | "$ref": "#/definitions/models.ScoreResponse" | 1334 | "$ref": "#/definitions/handlers.ProfileRecords" |
| 1294 | } | 1335 | } |
| 1295 | }, | 1336 | }, |
| 1296 | "scores_sp": { | 1337 | "steam_id": { |
| 1338 | "type": "string" | ||
| 1339 | }, | ||
| 1340 | "titles": { | ||
| 1297 | "type": "array", | 1341 | "type": "array", |
| 1298 | "items": { | 1342 | "items": { |
| 1299 | "$ref": "#/definitions/models.ScoreResponse" | 1343 | "$ref": "#/definitions/models.Title" |
| 1300 | } | 1344 | } |
| 1301 | }, | 1345 | }, |
| 1302 | "steam_id": { | 1346 | "user_name": { |
| 1347 | "type": "string" | ||
| 1348 | } | ||
| 1349 | } | ||
| 1350 | }, | ||
| 1351 | "handlers.ProfileScores": { | ||
| 1352 | "type": "object", | ||
| 1353 | "properties": { | ||
| 1354 | "date": { | ||
| 1303 | "type": "string" | 1355 | "type": "string" |
| 1304 | }, | 1356 | }, |
| 1305 | "user_name": { | 1357 | "demo_id": { |
| 1306 | "type": "string" | 1358 | "type": "string" |
| 1359 | }, | ||
| 1360 | "score_count": { | ||
| 1361 | "type": "integer" | ||
| 1362 | }, | ||
| 1363 | "score_time": { | ||
| 1364 | "type": "integer" | ||
| 1307 | } | 1365 | } |
| 1308 | } | 1366 | } |
| 1309 | }, | 1367 | }, |
| 1310 | "models.RankingsResponse": { | 1368 | "handlers.RankingsResponse": { |
| 1311 | "type": "object", | 1369 | "type": "object", |
| 1312 | "properties": { | 1370 | "properties": { |
| 1313 | "rankings_mp": { | 1371 | "rankings_multiplayer": { |
| 1372 | "type": "array", | ||
| 1373 | "items": { | ||
| 1374 | "$ref": "#/definitions/models.UserRanking" | ||
| 1375 | } | ||
| 1376 | }, | ||
| 1377 | "rankings_overall": { | ||
| 1314 | "type": "array", | 1378 | "type": "array", |
| 1315 | "items": { | 1379 | "items": { |
| 1316 | "$ref": "#/definitions/models.UserRanking" | 1380 | "$ref": "#/definitions/models.UserRanking" |
| 1317 | } | 1381 | } |
| 1318 | }, | 1382 | }, |
| 1319 | "rankings_sp": { | 1383 | "rankings_singleplayer": { |
| 1320 | "type": "array", | 1384 | "type": "array", |
| 1321 | "items": { | 1385 | "items": { |
| 1322 | "$ref": "#/definitions/models.UserRanking" | 1386 | "$ref": "#/definitions/models.UserRanking" |
| @@ -1324,7 +1388,7 @@ | |||
| 1324 | } | 1388 | } |
| 1325 | } | 1389 | } |
| 1326 | }, | 1390 | }, |
| 1327 | "models.RecordResponse": { | 1391 | "handlers.RecordResponse": { |
| 1328 | "type": "object", | 1392 | "type": "object", |
| 1329 | "properties": { | 1393 | "properties": { |
| 1330 | "score_count": { | 1394 | "score_count": { |
| @@ -1335,28 +1399,44 @@ | |||
| 1335 | } | 1399 | } |
| 1336 | } | 1400 | } |
| 1337 | }, | 1401 | }, |
| 1338 | "models.Response": { | 1402 | "handlers.ScoreLogsResponse": { |
| 1339 | "type": "object", | 1403 | "type": "object", |
| 1340 | "properties": { | 1404 | "properties": { |
| 1341 | "data": {}, | 1405 | "scores": { |
| 1342 | "message": { | 1406 | "type": "array", |
| 1343 | "type": "string" | 1407 | "items": { |
| 1344 | }, | 1408 | "$ref": "#/definitions/handlers.ScoreLogsResponseDetails" |
| 1345 | "success": { | 1409 | } |
| 1346 | "type": "boolean" | ||
| 1347 | } | 1410 | } |
| 1348 | } | 1411 | } |
| 1349 | }, | 1412 | }, |
| 1350 | "models.ScoreResponse": { | 1413 | "handlers.ScoreLogsResponseDetails": { |
| 1351 | "type": "object", | 1414 | "type": "object", |
| 1352 | "properties": { | 1415 | "properties": { |
| 1353 | "map_id": { | 1416 | "date": { |
| 1417 | "type": "string" | ||
| 1418 | }, | ||
| 1419 | "demo_id": { | ||
| 1420 | "type": "string" | ||
| 1421 | }, | ||
| 1422 | "game": { | ||
| 1423 | "$ref": "#/definitions/models.Game" | ||
| 1424 | }, | ||
| 1425 | "map": { | ||
| 1426 | "$ref": "#/definitions/models.MapShort" | ||
| 1427 | }, | ||
| 1428 | "score_count": { | ||
| 1354 | "type": "integer" | 1429 | "type": "integer" |
| 1355 | }, | 1430 | }, |
| 1356 | "records": {} | 1431 | "score_time": { |
| 1432 | "type": "integer" | ||
| 1433 | }, | ||
| 1434 | "user": { | ||
| 1435 | "$ref": "#/definitions/models.UserShort" | ||
| 1436 | } | ||
| 1357 | } | 1437 | } |
| 1358 | }, | 1438 | }, |
| 1359 | "models.SearchResponse": { | 1439 | "handlers.SearchResponse": { |
| 1360 | "type": "object", | 1440 | "type": "object", |
| 1361 | "properties": { | 1441 | "properties": { |
| 1362 | "maps": { | 1442 | "maps": { |
| @@ -1373,17 +1453,172 @@ | |||
| 1373 | } | 1453 | } |
| 1374 | } | 1454 | } |
| 1375 | }, | 1455 | }, |
| 1376 | "models.UserRanking": { | 1456 | "models.Category": { |
| 1377 | "type": "object", | 1457 | "type": "object", |
| 1378 | "properties": { | 1458 | "properties": { |
| 1379 | "total_score": { | 1459 | "id": { |
| 1380 | "type": "integer" | 1460 | "type": "integer" |
| 1381 | }, | 1461 | }, |
| 1382 | "user_id": { | 1462 | "name": { |
| 1383 | "type": "string" | 1463 | "type": "string" |
| 1464 | } | ||
| 1465 | } | ||
| 1466 | }, | ||
| 1467 | "models.Chapter": { | ||
| 1468 | "type": "object", | ||
| 1469 | "properties": { | ||
| 1470 | "id": { | ||
| 1471 | "type": "integer" | ||
| 1384 | }, | 1472 | }, |
| 1385 | "user_name": { | 1473 | "name": { |
| 1474 | "type": "string" | ||
| 1475 | } | ||
| 1476 | } | ||
| 1477 | }, | ||
| 1478 | "models.Game": { | ||
| 1479 | "type": "object", | ||
| 1480 | "properties": { | ||
| 1481 | "id": { | ||
| 1482 | "type": "integer" | ||
| 1483 | }, | ||
| 1484 | "is_coop": { | ||
| 1485 | "type": "boolean" | ||
| 1486 | }, | ||
| 1487 | "name": { | ||
| 1488 | "type": "string" | ||
| 1489 | } | ||
| 1490 | } | ||
| 1491 | }, | ||
| 1492 | "models.Links": { | ||
| 1493 | "type": "object", | ||
| 1494 | "properties": { | ||
| 1495 | "p2sr": { | ||
| 1496 | "type": "string" | ||
| 1497 | }, | ||
| 1498 | "stream": { | ||
| 1499 | "type": "string" | ||
| 1500 | }, | ||
| 1501 | "twitch": { | ||
| 1502 | "type": "string" | ||
| 1503 | }, | ||
| 1504 | "youtube": { | ||
| 1505 | "type": "string" | ||
| 1506 | } | ||
| 1507 | } | ||
| 1508 | }, | ||
| 1509 | "models.Map": { | ||
| 1510 | "type": "object", | ||
| 1511 | "properties": { | ||
| 1512 | "chapter_name": { | ||
| 1513 | "type": "string" | ||
| 1514 | }, | ||
| 1515 | "game_name": { | ||
| 1516 | "type": "string" | ||
| 1517 | }, | ||
| 1518 | "id": { | ||
| 1519 | "type": "integer" | ||
| 1520 | }, | ||
| 1521 | "image": { | ||
| 1522 | "type": "string" | ||
| 1523 | }, | ||
| 1524 | "is_coop": { | ||
| 1525 | "type": "boolean" | ||
| 1526 | }, | ||
| 1527 | "map_name": { | ||
| 1528 | "type": "string" | ||
| 1529 | } | ||
| 1530 | } | ||
| 1531 | }, | ||
| 1532 | "models.MapHistory": { | ||
| 1533 | "type": "object", | ||
| 1534 | "properties": { | ||
| 1535 | "date": { | ||
| 1536 | "type": "string" | ||
| 1537 | }, | ||
| 1538 | "runner_name": { | ||
| 1539 | "type": "string" | ||
| 1540 | }, | ||
| 1541 | "score_count": { | ||
| 1542 | "type": "integer" | ||
| 1543 | } | ||
| 1544 | } | ||
| 1545 | }, | ||
| 1546 | "models.MapRoute": { | ||
| 1547 | "type": "object", | ||
| 1548 | "properties": { | ||
| 1549 | "category": { | ||
| 1550 | "$ref": "#/definitions/models.Category" | ||
| 1551 | }, | ||
| 1552 | "description": { | ||
| 1553 | "type": "string" | ||
| 1554 | }, | ||
| 1555 | "history": { | ||
| 1556 | "$ref": "#/definitions/models.MapHistory" | ||
| 1557 | }, | ||
| 1558 | "rating": { | ||
| 1559 | "type": "number" | ||
| 1560 | }, | ||
| 1561 | "route_id": { | ||
| 1562 | "type": "integer" | ||
| 1563 | }, | ||
| 1564 | "showcase": { | ||
| 1565 | "type": "string" | ||
| 1566 | } | ||
| 1567 | } | ||
| 1568 | }, | ||
| 1569 | "models.MapShort": { | ||
| 1570 | "type": "object", | ||
| 1571 | "properties": { | ||
| 1572 | "id": { | ||
| 1573 | "type": "integer" | ||
| 1574 | }, | ||
| 1575 | "name": { | ||
| 1576 | "type": "string" | ||
| 1577 | } | ||
| 1578 | } | ||
| 1579 | }, | ||
| 1580 | "models.MapSummary": { | ||
| 1581 | "type": "object", | ||
| 1582 | "properties": { | ||
| 1583 | "routes": { | ||
| 1584 | "type": "array", | ||
| 1585 | "items": { | ||
| 1586 | "$ref": "#/definitions/models.MapRoute" | ||
| 1587 | } | ||
| 1588 | } | ||
| 1589 | } | ||
| 1590 | }, | ||
| 1591 | "models.Response": { | ||
| 1592 | "type": "object", | ||
| 1593 | "properties": { | ||
| 1594 | "data": {}, | ||
| 1595 | "message": { | ||
| 1596 | "type": "string" | ||
| 1597 | }, | ||
| 1598 | "success": { | ||
| 1599 | "type": "boolean" | ||
| 1600 | } | ||
| 1601 | } | ||
| 1602 | }, | ||
| 1603 | "models.Title": { | ||
| 1604 | "type": "object", | ||
| 1605 | "properties": { | ||
| 1606 | "color": { | ||
| 1386 | "type": "string" | 1607 | "type": "string" |
| 1608 | }, | ||
| 1609 | "name": { | ||
| 1610 | "type": "string" | ||
| 1611 | } | ||
| 1612 | } | ||
| 1613 | }, | ||
| 1614 | "models.UserRanking": { | ||
| 1615 | "type": "object", | ||
| 1616 | "properties": { | ||
| 1617 | "total_score": { | ||
| 1618 | "type": "integer" | ||
| 1619 | }, | ||
| 1620 | "user": { | ||
| 1621 | "$ref": "#/definitions/models.UserShort" | ||
| 1387 | } | 1622 | } |
| 1388 | } | 1623 | } |
| 1389 | }, | 1624 | }, |
diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 7571073..6b1e6ea 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml | |||
| @@ -1,20 +1,6 @@ | |||
| 1 | basePath: /v1 | 1 | basePath: /v1 |
| 2 | definitions: | 2 | definitions: |
| 3 | models.Category: | 3 | handlers.ChapterMapsResponse: |
| 4 | properties: | ||
| 5 | id: | ||
| 6 | type: integer | ||
| 7 | name: | ||
| 8 | type: string | ||
| 9 | type: object | ||
| 10 | models.Chapter: | ||
| 11 | properties: | ||
| 12 | id: | ||
| 13 | type: integer | ||
| 14 | name: | ||
| 15 | type: string | ||
| 16 | type: object | ||
| 17 | models.ChapterMapsResponse: | ||
| 18 | properties: | 4 | properties: |
| 19 | chapter: | 5 | chapter: |
| 20 | $ref: '#/definitions/models.Chapter' | 6 | $ref: '#/definitions/models.Chapter' |
| @@ -23,7 +9,7 @@ definitions: | |||
| 23 | $ref: '#/definitions/models.MapShort' | 9 | $ref: '#/definitions/models.MapShort' |
| 24 | type: array | 10 | type: array |
| 25 | type: object | 11 | type: object |
| 26 | models.ChaptersResponse: | 12 | handlers.ChaptersResponse: |
| 27 | properties: | 13 | properties: |
| 28 | chapters: | 14 | chapters: |
| 29 | items: | 15 | items: |
| @@ -32,7 +18,7 @@ definitions: | |||
| 32 | game: | 18 | game: |
| 33 | $ref: '#/definitions/models.Game' | 19 | $ref: '#/definitions/models.Game' |
| 34 | type: object | 20 | type: object |
| 35 | models.CreateMapSummaryRequest: | 21 | handlers.CreateMapSummaryRequest: |
| 36 | properties: | 22 | properties: |
| 37 | category_id: | 23 | category_id: |
| 38 | type: integer | 24 | type: integer |
| @@ -53,21 +39,21 @@ definitions: | |||
| 53 | - score_count | 39 | - score_count |
| 54 | - user_name | 40 | - user_name |
| 55 | type: object | 41 | type: object |
| 56 | models.DeleteMapSummaryRequest: | 42 | handlers.DeleteMapSummaryRequest: |
| 57 | properties: | 43 | properties: |
| 58 | route_id: | 44 | route_id: |
| 59 | type: integer | 45 | type: integer |
| 60 | required: | 46 | required: |
| 61 | - route_id | 47 | - route_id |
| 62 | type: object | 48 | type: object |
| 63 | models.EditMapImageRequest: | 49 | handlers.EditMapImageRequest: |
| 64 | properties: | 50 | properties: |
| 65 | image: | 51 | image: |
| 66 | type: string | 52 | type: string |
| 67 | required: | 53 | required: |
| 68 | - image | 54 | - image |
| 69 | type: object | 55 | type: object |
| 70 | models.EditMapSummaryRequest: | 56 | handlers.EditMapSummaryRequest: |
| 71 | properties: | 57 | properties: |
| 72 | description: | 58 | description: |
| 73 | type: string | 59 | type: string |
| @@ -88,6 +74,182 @@ definitions: | |||
| 88 | - score_count | 74 | - score_count |
| 89 | - user_name | 75 | - user_name |
| 90 | type: object | 76 | type: object |
| 77 | handlers.LoginResponse: | ||
| 78 | properties: | ||
| 79 | token: | ||
| 80 | type: string | ||
| 81 | type: object | ||
| 82 | handlers.LogsResponse: | ||
| 83 | properties: | ||
| 84 | logs: | ||
| 85 | items: | ||
| 86 | $ref: '#/definitions/handlers.LogsResponseDetails' | ||
| 87 | type: array | ||
| 88 | type: object | ||
| 89 | handlers.LogsResponseDetails: | ||
| 90 | properties: | ||
| 91 | date: | ||
| 92 | type: string | ||
| 93 | detail: | ||
| 94 | type: string | ||
| 95 | user: | ||
| 96 | $ref: '#/definitions/models.UserShort' | ||
| 97 | type: object | ||
| 98 | handlers.MapLeaderboardsResponse: | ||
| 99 | properties: | ||
| 100 | map: | ||
| 101 | $ref: '#/definitions/models.Map' | ||
| 102 | records: {} | ||
| 103 | type: object | ||
| 104 | handlers.MapSummaryResponse: | ||
| 105 | properties: | ||
| 106 | map: | ||
| 107 | $ref: '#/definitions/models.Map' | ||
| 108 | summary: | ||
| 109 | $ref: '#/definitions/models.MapSummary' | ||
| 110 | type: object | ||
| 111 | handlers.ProfileRankings: | ||
| 112 | properties: | ||
| 113 | cooperative: | ||
| 114 | $ref: '#/definitions/handlers.ProfileRankingsDetails' | ||
| 115 | overall: | ||
| 116 | $ref: '#/definitions/handlers.ProfileRankingsDetails' | ||
| 117 | singleplayer: | ||
| 118 | $ref: '#/definitions/handlers.ProfileRankingsDetails' | ||
| 119 | type: object | ||
| 120 | handlers.ProfileRankingsDetails: | ||
| 121 | properties: | ||
| 122 | completion_count: | ||
| 123 | type: integer | ||
| 124 | completion_total: | ||
| 125 | type: integer | ||
| 126 | rank: | ||
| 127 | type: integer | ||
| 128 | type: object | ||
| 129 | handlers.ProfileRecords: | ||
| 130 | properties: | ||
| 131 | category_id: | ||
| 132 | type: integer | ||
| 133 | game_id: | ||
| 134 | type: integer | ||
| 135 | map_id: | ||
| 136 | type: integer | ||
| 137 | map_name: | ||
| 138 | type: string | ||
| 139 | map_wr_count: | ||
| 140 | type: integer | ||
| 141 | scores: | ||
| 142 | items: | ||
| 143 | $ref: '#/definitions/handlers.ProfileScores' | ||
| 144 | type: array | ||
| 145 | type: object | ||
| 146 | handlers.ProfileResponse: | ||
| 147 | properties: | ||
| 148 | avatar_link: | ||
| 149 | type: string | ||
| 150 | country_code: | ||
| 151 | type: string | ||
| 152 | links: | ||
| 153 | $ref: '#/definitions/models.Links' | ||
| 154 | profile: | ||
| 155 | type: boolean | ||
| 156 | rankings: | ||
| 157 | $ref: '#/definitions/handlers.ProfileRankings' | ||
| 158 | records: | ||
| 159 | items: | ||
| 160 | $ref: '#/definitions/handlers.ProfileRecords' | ||
| 161 | type: array | ||
| 162 | steam_id: | ||
| 163 | type: string | ||
| 164 | titles: | ||
| 165 | items: | ||
| 166 | $ref: '#/definitions/models.Title' | ||
| 167 | type: array | ||
| 168 | user_name: | ||
| 169 | type: string | ||
| 170 | type: object | ||
| 171 | handlers.ProfileScores: | ||
| 172 | properties: | ||
| 173 | date: | ||
| 174 | type: string | ||
| 175 | demo_id: | ||
| 176 | type: string | ||
| 177 | score_count: | ||
| 178 | type: integer | ||
| 179 | score_time: | ||
| 180 | type: integer | ||
| 181 | type: object | ||
| 182 | handlers.RankingsResponse: | ||
| 183 | properties: | ||
| 184 | rankings_multiplayer: | ||
| 185 | items: | ||
| 186 | $ref: '#/definitions/models.UserRanking' | ||
| 187 | type: array | ||
| 188 | rankings_overall: | ||
| 189 | items: | ||
| 190 | $ref: '#/definitions/models.UserRanking' | ||
| 191 | type: array | ||
| 192 | rankings_singleplayer: | ||
| 193 | items: | ||
| 194 | $ref: '#/definitions/models.UserRanking' | ||
| 195 | type: array | ||
| 196 | type: object | ||
| 197 | handlers.RecordResponse: | ||
| 198 | properties: | ||
| 199 | score_count: | ||
| 200 | type: integer | ||
| 201 | score_time: | ||
| 202 | type: integer | ||
| 203 | type: object | ||
| 204 | handlers.ScoreLogsResponse: | ||
| 205 | properties: | ||
| 206 | scores: | ||
| 207 | items: | ||
| 208 | $ref: '#/definitions/handlers.ScoreLogsResponseDetails' | ||
| 209 | type: array | ||
| 210 | type: object | ||
| 211 | handlers.ScoreLogsResponseDetails: | ||
| 212 | properties: | ||
| 213 | date: | ||
| 214 | type: string | ||
| 215 | demo_id: | ||
| 216 | type: string | ||
| 217 | game: | ||
| 218 | $ref: '#/definitions/models.Game' | ||
| 219 | map: | ||
| 220 | $ref: '#/definitions/models.MapShort' | ||
| 221 | score_count: | ||
| 222 | type: integer | ||
| 223 | score_time: | ||
| 224 | type: integer | ||
| 225 | user: | ||
| 226 | $ref: '#/definitions/models.UserShort' | ||
| 227 | type: object | ||
| 228 | handlers.SearchResponse: | ||
| 229 | properties: | ||
| 230 | maps: | ||
| 231 | items: | ||
| 232 | $ref: '#/definitions/models.MapShort' | ||
| 233 | type: array | ||
| 234 | players: | ||
| 235 | items: | ||
| 236 | $ref: '#/definitions/models.UserShort' | ||
| 237 | type: array | ||
| 238 | type: object | ||
| 239 | models.Category: | ||
| 240 | properties: | ||
| 241 | id: | ||
| 242 | type: integer | ||
| 243 | name: | ||
| 244 | type: string | ||
| 245 | type: object | ||
| 246 | models.Chapter: | ||
| 247 | properties: | ||
| 248 | id: | ||
| 249 | type: integer | ||
| 250 | name: | ||
| 251 | type: string | ||
| 252 | type: object | ||
| 91 | models.Game: | 253 | models.Game: |
| 92 | properties: | 254 | properties: |
| 93 | id: | 255 | id: |
| @@ -97,9 +259,15 @@ definitions: | |||
| 97 | name: | 259 | name: |
| 98 | type: string | 260 | type: string |
| 99 | type: object | 261 | type: object |
| 100 | models.LoginResponse: | 262 | models.Links: |
| 101 | properties: | 263 | properties: |
| 102 | token: | 264 | p2sr: |
| 265 | type: string | ||
| 266 | stream: | ||
| 267 | type: string | ||
| 268 | twitch: | ||
| 269 | type: string | ||
| 270 | youtube: | ||
| 103 | type: string | 271 | type: string |
| 104 | type: object | 272 | type: object |
| 105 | models.Map: | 273 | models.Map: |
| @@ -126,10 +294,6 @@ definitions: | |||
| 126 | score_count: | 294 | score_count: |
| 127 | type: integer | 295 | type: integer |
| 128 | type: object | 296 | type: object |
| 129 | models.MapRecords: | ||
| 130 | properties: | ||
| 131 | records: {} | ||
| 132 | type: object | ||
| 133 | models.MapRoute: | 297 | models.MapRoute: |
| 134 | properties: | 298 | properties: |
| 135 | category: | 299 | category: |
| @@ -159,52 +323,6 @@ definitions: | |||
| 159 | $ref: '#/definitions/models.MapRoute' | 323 | $ref: '#/definitions/models.MapRoute' |
| 160 | type: array | 324 | type: array |
| 161 | type: object | 325 | type: object |
| 162 | models.MapSummaryResponse: | ||
| 163 | properties: | ||
| 164 | map: | ||
| 165 | $ref: '#/definitions/models.Map' | ||
| 166 | summary: | ||
| 167 | $ref: '#/definitions/models.MapSummary' | ||
| 168 | type: object | ||
| 169 | models.ProfileResponse: | ||
| 170 | properties: | ||
| 171 | avatar_link: | ||
| 172 | type: string | ||
| 173 | country_code: | ||
| 174 | type: string | ||
| 175 | profile: | ||
| 176 | type: boolean | ||
| 177 | scores_mp: | ||
| 178 | items: | ||
| 179 | $ref: '#/definitions/models.ScoreResponse' | ||
| 180 | type: array | ||
| 181 | scores_sp: | ||
| 182 | items: | ||
| 183 | $ref: '#/definitions/models.ScoreResponse' | ||
| 184 | type: array | ||
| 185 | steam_id: | ||
| 186 | type: string | ||
| 187 | user_name: | ||
| 188 | type: string | ||
| 189 | type: object | ||
| 190 | models.RankingsResponse: | ||
| 191 | properties: | ||
| 192 | rankings_mp: | ||
| 193 | items: | ||
| 194 | $ref: '#/definitions/models.UserRanking' | ||
| 195 | type: array | ||
| 196 | rankings_sp: | ||
| 197 | items: | ||
| 198 | $ref: '#/definitions/models.UserRanking' | ||
| 199 | type: array | ||
| 200 | type: object | ||
| 201 | models.RecordResponse: | ||
| 202 | properties: | ||
| 203 | score_count: | ||
| 204 | type: integer | ||
| 205 | score_time: | ||
| 206 | type: integer | ||
| 207 | type: object | ||
| 208 | models.Response: | 326 | models.Response: |
| 209 | properties: | 327 | properties: |
| 210 | data: {} | 328 | data: {} |
| @@ -213,31 +331,19 @@ definitions: | |||
| 213 | success: | 331 | success: |
| 214 | type: boolean | 332 | type: boolean |
| 215 | type: object | 333 | type: object |
| 216 | models.ScoreResponse: | 334 | models.Title: |
| 217 | properties: | ||
| 218 | map_id: | ||
| 219 | type: integer | ||
| 220 | records: {} | ||
| 221 | type: object | ||
| 222 | models.SearchResponse: | ||
| 223 | properties: | 335 | properties: |
| 224 | maps: | 336 | color: |
| 225 | items: | 337 | type: string |
| 226 | $ref: '#/definitions/models.MapShort' | 338 | name: |
| 227 | type: array | 339 | type: string |
| 228 | players: | ||
| 229 | items: | ||
| 230 | $ref: '#/definitions/models.UserShort' | ||
| 231 | type: array | ||
| 232 | type: object | 340 | type: object |
| 233 | models.UserRanking: | 341 | models.UserRanking: |
| 234 | properties: | 342 | properties: |
| 235 | total_score: | 343 | total_score: |
| 236 | type: integer | 344 | type: integer |
| 237 | user_id: | 345 | user: |
| 238 | type: string | 346 | $ref: '#/definitions/models.UserShort' |
| 239 | user_name: | ||
| 240 | type: string | ||
| 241 | type: object | 347 | type: object |
| 242 | models.UserShort: | 348 | models.UserShort: |
| 243 | properties: | 349 | properties: |
| @@ -275,7 +381,7 @@ paths: | |||
| 275 | - $ref: '#/definitions/models.Response' | 381 | - $ref: '#/definitions/models.Response' |
| 276 | - properties: | 382 | - properties: |
| 277 | data: | 383 | data: |
| 278 | $ref: '#/definitions/models.ChapterMapsResponse' | 384 | $ref: '#/definitions/handlers.ChapterMapsResponse' |
| 279 | type: object | 385 | type: object |
| 280 | "400": | 386 | "400": |
| 281 | description: Bad Request | 387 | description: Bad Request |
| @@ -349,7 +455,7 @@ paths: | |||
| 349 | - $ref: '#/definitions/models.Response' | 455 | - $ref: '#/definitions/models.Response' |
| 350 | - properties: | 456 | - properties: |
| 351 | data: | 457 | data: |
| 352 | $ref: '#/definitions/models.ChaptersResponse' | 458 | $ref: '#/definitions/handlers.ChaptersResponse' |
| 353 | type: object | 459 | type: object |
| 354 | "400": | 460 | "400": |
| 355 | description: Bad Request | 461 | description: Bad Request |
| @@ -372,7 +478,7 @@ paths: | |||
| 372 | - $ref: '#/definitions/models.Response' | 478 | - $ref: '#/definitions/models.Response' |
| 373 | - properties: | 479 | - properties: |
| 374 | data: | 480 | data: |
| 375 | $ref: '#/definitions/models.LoginResponse' | 481 | $ref: '#/definitions/handlers.LoginResponse' |
| 376 | type: object | 482 | type: object |
| 377 | "400": | 483 | "400": |
| 378 | description: Bad Request | 484 | description: Bad Request |
| @@ -380,6 +486,54 @@ paths: | |||
| 380 | $ref: '#/definitions/models.Response' | 486 | $ref: '#/definitions/models.Response' |
| 381 | tags: | 487 | tags: |
| 382 | - login | 488 | - login |
| 489 | /logs/mod: | ||
| 490 | get: | ||
| 491 | description: Get mod logs. | ||
| 492 | parameters: | ||
| 493 | - description: JWT Token | ||
| 494 | in: header | ||
| 495 | name: Authorization | ||
| 496 | required: true | ||
| 497 | type: string | ||
| 498 | produces: | ||
| 499 | - application/json | ||
| 500 | responses: | ||
| 501 | "200": | ||
| 502 | description: OK | ||
| 503 | schema: | ||
| 504 | allOf: | ||
| 505 | - $ref: '#/definitions/models.Response' | ||
| 506 | - properties: | ||
| 507 | data: | ||
| 508 | $ref: '#/definitions/handlers.LogsResponse' | ||
| 509 | type: object | ||
| 510 | "400": | ||
| 511 | description: Bad Request | ||
| 512 | schema: | ||
| 513 | $ref: '#/definitions/models.Response' | ||
| 514 | tags: | ||
| 515 | - logs | ||
| 516 | /logs/score: | ||
| 517 | get: | ||
| 518 | description: Get score logs of every player. | ||
| 519 | produces: | ||
| 520 | - application/json | ||
| 521 | responses: | ||
| 522 | "200": | ||
| 523 | description: OK | ||
| 524 | schema: | ||
| 525 | allOf: | ||
| 526 | - $ref: '#/definitions/models.Response' | ||
| 527 | - properties: | ||
| 528 | data: | ||
| 529 | $ref: '#/definitions/handlers.ScoreLogsResponse' | ||
| 530 | type: object | ||
| 531 | "400": | ||
| 532 | description: Bad Request | ||
| 533 | schema: | ||
| 534 | $ref: '#/definitions/models.Response' | ||
| 535 | tags: | ||
| 536 | - logs | ||
| 383 | /maps/{id}/image: | 537 | /maps/{id}/image: |
| 384 | put: | 538 | put: |
| 385 | description: Edit map image with specified map id. | 539 | description: Edit map image with specified map id. |
| @@ -399,7 +553,7 @@ paths: | |||
| 399 | name: request | 553 | name: request |
| 400 | required: true | 554 | required: true |
| 401 | schema: | 555 | schema: |
| 402 | $ref: '#/definitions/models.EditMapImageRequest' | 556 | $ref: '#/definitions/handlers.EditMapImageRequest' |
| 403 | produces: | 557 | produces: |
| 404 | - application/json | 558 | - application/json |
| 405 | responses: | 559 | responses: |
| @@ -410,7 +564,7 @@ paths: | |||
| 410 | - $ref: '#/definitions/models.Response' | 564 | - $ref: '#/definitions/models.Response' |
| 411 | - properties: | 565 | - properties: |
| 412 | data: | 566 | data: |
| 413 | $ref: '#/definitions/models.EditMapImageRequest' | 567 | $ref: '#/definitions/handlers.EditMapImageRequest' |
| 414 | type: object | 568 | type: object |
| 415 | "400": | 569 | "400": |
| 416 | description: Bad Request | 570 | description: Bad Request |
| @@ -437,12 +591,7 @@ paths: | |||
| 437 | - $ref: '#/definitions/models.Response' | 591 | - $ref: '#/definitions/models.Response' |
| 438 | - properties: | 592 | - properties: |
| 439 | data: | 593 | data: |
| 440 | allOf: | 594 | $ref: '#/definitions/handlers.MapLeaderboardsResponse' |
| 441 | - $ref: '#/definitions/models.Map' | ||
| 442 | - properties: | ||
| 443 | data: | ||
| 444 | $ref: '#/definitions/models.MapRecords' | ||
| 445 | type: object | ||
| 446 | type: object | 595 | type: object |
| 447 | "400": | 596 | "400": |
| 448 | description: Bad Request | 597 | description: Bad Request |
| @@ -493,7 +642,7 @@ paths: | |||
| 493 | - $ref: '#/definitions/models.Response' | 642 | - $ref: '#/definitions/models.Response' |
| 494 | - properties: | 643 | - properties: |
| 495 | data: | 644 | data: |
| 496 | $ref: '#/definitions/models.RecordResponse' | 645 | $ref: '#/definitions/handlers.RecordResponse' |
| 497 | type: object | 646 | type: object |
| 498 | "400": | 647 | "400": |
| 499 | description: Bad Request | 648 | description: Bad Request |
| @@ -524,7 +673,7 @@ paths: | |||
| 524 | name: request | 673 | name: request |
| 525 | required: true | 674 | required: true |
| 526 | schema: | 675 | schema: |
| 527 | $ref: '#/definitions/models.DeleteMapSummaryRequest' | 676 | $ref: '#/definitions/handlers.DeleteMapSummaryRequest' |
| 528 | produces: | 677 | produces: |
| 529 | - application/json | 678 | - application/json |
| 530 | responses: | 679 | responses: |
| @@ -535,7 +684,7 @@ paths: | |||
| 535 | - $ref: '#/definitions/models.Response' | 684 | - $ref: '#/definitions/models.Response' |
| 536 | - properties: | 685 | - properties: |
| 537 | data: | 686 | data: |
| 538 | $ref: '#/definitions/models.DeleteMapSummaryRequest' | 687 | $ref: '#/definitions/handlers.DeleteMapSummaryRequest' |
| 539 | type: object | 688 | type: object |
| 540 | "400": | 689 | "400": |
| 541 | description: Bad Request | 690 | description: Bad Request |
| @@ -561,7 +710,7 @@ paths: | |||
| 561 | - $ref: '#/definitions/models.Response' | 710 | - $ref: '#/definitions/models.Response' |
| 562 | - properties: | 711 | - properties: |
| 563 | data: | 712 | data: |
| 564 | $ref: '#/definitions/models.MapSummaryResponse' | 713 | $ref: '#/definitions/handlers.MapSummaryResponse' |
| 565 | type: object | 714 | type: object |
| 566 | "400": | 715 | "400": |
| 567 | description: Bad Request | 716 | description: Bad Request |
| @@ -587,7 +736,7 @@ paths: | |||
| 587 | name: request | 736 | name: request |
| 588 | required: true | 737 | required: true |
| 589 | schema: | 738 | schema: |
| 590 | $ref: '#/definitions/models.CreateMapSummaryRequest' | 739 | $ref: '#/definitions/handlers.CreateMapSummaryRequest' |
| 591 | produces: | 740 | produces: |
| 592 | - application/json | 741 | - application/json |
| 593 | responses: | 742 | responses: |
| @@ -598,7 +747,7 @@ paths: | |||
| 598 | - $ref: '#/definitions/models.Response' | 747 | - $ref: '#/definitions/models.Response' |
| 599 | - properties: | 748 | - properties: |
| 600 | data: | 749 | data: |
| 601 | $ref: '#/definitions/models.CreateMapSummaryRequest' | 750 | $ref: '#/definitions/handlers.CreateMapSummaryRequest' |
| 602 | type: object | 751 | type: object |
| 603 | "400": | 752 | "400": |
| 604 | description: Bad Request | 753 | description: Bad Request |
| @@ -624,7 +773,7 @@ paths: | |||
| 624 | name: request | 773 | name: request |
| 625 | required: true | 774 | required: true |
| 626 | schema: | 775 | schema: |
| 627 | $ref: '#/definitions/models.EditMapSummaryRequest' | 776 | $ref: '#/definitions/handlers.EditMapSummaryRequest' |
| 628 | produces: | 777 | produces: |
| 629 | - application/json | 778 | - application/json |
| 630 | responses: | 779 | responses: |
| @@ -635,7 +784,7 @@ paths: | |||
| 635 | - $ref: '#/definitions/models.Response' | 784 | - $ref: '#/definitions/models.Response' |
| 636 | - properties: | 785 | - properties: |
| 637 | data: | 786 | data: |
| 638 | $ref: '#/definitions/models.EditMapSummaryRequest' | 787 | $ref: '#/definitions/handlers.EditMapSummaryRequest' |
| 639 | type: object | 788 | type: object |
| 640 | "400": | 789 | "400": |
| 641 | description: Bad Request | 790 | description: Bad Request |
| @@ -664,7 +813,7 @@ paths: | |||
| 664 | - $ref: '#/definitions/models.Response' | 813 | - $ref: '#/definitions/models.Response' |
| 665 | - properties: | 814 | - properties: |
| 666 | data: | 815 | data: |
| 667 | $ref: '#/definitions/models.ProfileResponse' | 816 | $ref: '#/definitions/handlers.ProfileResponse' |
| 668 | type: object | 817 | type: object |
| 669 | "400": | 818 | "400": |
| 670 | description: Bad Request | 819 | description: Bad Request |
| @@ -696,7 +845,7 @@ paths: | |||
| 696 | - $ref: '#/definitions/models.Response' | 845 | - $ref: '#/definitions/models.Response' |
| 697 | - properties: | 846 | - properties: |
| 698 | data: | 847 | data: |
| 699 | $ref: '#/definitions/models.ProfileResponse' | 848 | $ref: '#/definitions/handlers.ProfileResponse' |
| 700 | type: object | 849 | type: object |
| 701 | "400": | 850 | "400": |
| 702 | description: Bad Request | 851 | description: Bad Request |
| @@ -753,7 +902,7 @@ paths: | |||
| 753 | - $ref: '#/definitions/models.Response' | 902 | - $ref: '#/definitions/models.Response' |
| 754 | - properties: | 903 | - properties: |
| 755 | data: | 904 | data: |
| 756 | $ref: '#/definitions/models.RankingsResponse' | 905 | $ref: '#/definitions/handlers.RankingsResponse' |
| 757 | type: object | 906 | type: object |
| 758 | "400": | 907 | "400": |
| 759 | description: Bad Request | 908 | description: Bad Request |
| @@ -779,7 +928,7 @@ paths: | |||
| 779 | - $ref: '#/definitions/models.Response' | 928 | - $ref: '#/definitions/models.Response' |
| 780 | - properties: | 929 | - properties: |
| 781 | data: | 930 | data: |
| 782 | $ref: '#/definitions/models.SearchResponse' | 931 | $ref: '#/definitions/handlers.SearchResponse' |
| 783 | type: object | 932 | type: object |
| 784 | "400": | 933 | "400": |
| 785 | description: Bad Request | 934 | description: Bad Request |
| @@ -800,7 +949,7 @@ paths: | |||
| 800 | - $ref: '#/definitions/models.Response' | 949 | - $ref: '#/definitions/models.Response' |
| 801 | - properties: | 950 | - properties: |
| 802 | data: | 951 | data: |
| 803 | $ref: '#/definitions/models.LoginResponse' | 952 | $ref: '#/definitions/handlers.LoginResponse' |
| 804 | type: object | 953 | type: object |
| 805 | "404": | 954 | "404": |
| 806 | description: Not Found | 955 | description: Not Found |
| @@ -820,7 +969,7 @@ paths: | |||
| 820 | - $ref: '#/definitions/models.Response' | 969 | - $ref: '#/definitions/models.Response' |
| 821 | - properties: | 970 | - properties: |
| 822 | data: | 971 | data: |
| 823 | $ref: '#/definitions/models.LoginResponse' | 972 | $ref: '#/definitions/handlers.LoginResponse' |
| 824 | type: object | 973 | type: object |
| 825 | "404": | 974 | "404": |
| 826 | description: Not Found | 975 | description: Not Found |
| @@ -849,7 +998,7 @@ paths: | |||
| 849 | - $ref: '#/definitions/models.Response' | 998 | - $ref: '#/definitions/models.Response' |
| 850 | - properties: | 999 | - properties: |
| 851 | data: | 1000 | data: |
| 852 | $ref: '#/definitions/models.ProfileResponse' | 1001 | $ref: '#/definitions/handlers.ProfileResponse' |
| 853 | type: object | 1002 | type: object |
| 854 | "400": | 1003 | "400": |
| 855 | description: Bad Request | 1004 | description: Bad Request |
| @@ -7,8 +7,8 @@ import ( | |||
| 7 | 7 | ||
| 8 | "github.com/gin-gonic/gin" | 8 | "github.com/gin-gonic/gin" |
| 9 | "github.com/joho/godotenv" | 9 | "github.com/joho/godotenv" |
| 10 | "github.com/pektezol/leastportalshub/backend/api" | ||
| 10 | "github.com/pektezol/leastportalshub/backend/database" | 11 | "github.com/pektezol/leastportalshub/backend/database" |
| 11 | "github.com/pektezol/leastportalshub/backend/routes" | ||
| 12 | _ "github.com/pektezol/leastportalshub/docs" | 12 | _ "github.com/pektezol/leastportalshub/docs" |
| 13 | ) | 13 | ) |
| 14 | 14 | ||
| @@ -31,6 +31,6 @@ func main() { | |||
| 31 | } | 31 | } |
| 32 | router := gin.Default() | 32 | router := gin.Default() |
| 33 | database.ConnectDB() | 33 | database.ConnectDB() |
| 34 | routes.InitRoutes(router) | 34 | api.InitRoutes(router) |
| 35 | router.Run(fmt.Sprintf(":%s", os.Getenv("PORT"))) | 35 | router.Run(fmt.Sprintf(":%s", os.Getenv("PORT"))) |
| 36 | } | 36 | } |