aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNidboj132 <lol2s@vp.plm>2023-09-05 18:23:11 +0200
committerNidboj132 <lol2s@vp.plm>2023-09-05 18:23:11 +0200
commit3869cb67351ccf3bc45b076f31afdc7133292c39 (patch)
treedc03341e147dde0964bf6be84b14e13424c647b7
parentadded graph and fixed some css (diff)
parentfix: create map summary, why the fuck does this have to be a pointer integer?? (diff)
downloadlphub-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--.gitignore3
-rw-r--r--README.md15
-rw-r--r--backend/api/auth.go (renamed from backend/middleware/auth.go)17
-rw-r--r--backend/api/routes.go41
-rw-r--r--backend/controllers/userController.go286
-rw-r--r--backend/database/history.sql522
-rw-r--r--backend/database/init.sql27
-rw-r--r--backend/database/maps.sql4
-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.go189
-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.go719
-rw-r--r--backend/models/models.go63
-rw-r--r--backend/models/requests.go39
-rw-r--r--backend/models/responses.go64
-rw-r--r--backend/routes/routes.go41
-rw-r--r--docs/docs.go549
-rw-r--r--docs/swagger.json549
-rw-r--r--docs/swagger.yaml383
-rw-r--r--main.go4
23 files changed, 2644 insertions, 1355 deletions
diff --git a/.gitignore b/.gitignore
index 1434a43..10d4fda 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/README.md b/README.md
index e1d8c80..e1bc0b7 100644
--- a/README.md
+++ b/README.md
@@ -36,6 +36,15 @@ If you have any questions or feedback, please feel free to contact us at our [Di
36 36
37If you want to support the creator, you can do it via using GitHub sponsorships by clicking [here](https://github.com/sponsors/pektezol). 37If 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
41This 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. 50This 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
49Your 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. 58Your 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
51By using LPHUB, you acknowledge that you have read and understood this disclaimer and agree to its terms. \ No newline at end of file 60By using LPHUB, you acknowledge that you have read and understood this disclaimer and agree to its terms.
61
62## Last Update
63
642023-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 @@
1package middleware 1package api
2 2
3import ( 3import (
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 @@
1package api
2
3import (
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
10func 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 @@
1package controllers
2
3import (
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]
25func 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]
112func 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]
214func 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]
258func 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 @@
1INSERT INTO map_history(map_id,category_id,user_name,score_count,record_date) VALUES 1INSERT 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
119CREATE TABLE titles ( 123CREATE 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
130CREATE 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
143CREATE 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 @@
1package controllers 1package handlers
2 2
3import ( 3import (
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
13func Home(c *gin.Context) { 14type 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{ 19type 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]
32func Rankings(c *gin.Context) { 33func 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, &currentCount, &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, &currentCount, &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]
135func SearchWithQuery(c *gin.Context) { 140func 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 @@
1package controllers 1package handlers
2 2
3import ( 3import (
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
18type 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]
27func Login(c *gin.Context) { 31func 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]
105func GetCookie(c *gin.Context) { 109func 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]
129func DeleteCookie(c *gin.Context) { 133func 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 @@
1package handlers
2
3import (
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
13const (
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
43type 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
50type LogsResponse struct {
51 Logs []LogsResponseDetails `json:"logs"`
52}
53
54type LogsResponseDetails struct {
55 User models.UserShort `json:"user"`
56 Log string `json:"detail"`
57 Date time.Time `json:"date"`
58}
59
60type ScoreLogsResponse struct {
61 Logs []ScoreLogsResponseDetails `json:"scores"`
62}
63
64type 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]
83func 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]
130func 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
182func 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 @@
1package controllers 1package handlers
2 2
3import ( 3import (
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
13type MapSummaryResponse struct {
14 Map models.Map `json:"map"`
15 Summary models.MapSummary `json:"summary"`
16}
17
18type MapLeaderboardsResponse struct {
19 Map models.Map `json:"map"`
20 Records any `json:"records"`
21}
22
23type ChaptersResponse struct {
24 Game models.Game `json:"game"`
25 Chapters []models.Chapter `json:"chapters"`
26}
27
28type ChapterMapsResponse struct {
29 Chapter models.Chapter `json:"chapter"`
30 Maps []models.MapShort `json:"maps"`
31}
32
33type 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
43type 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]
21func FetchMapSummary(c *gin.Context) { 64func 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]
79func FetchMapLeaderboards(c *gin.Context) { 122func 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]
226func FetchChapters(c *gin.Context) { 289func 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]
268func FetchChapterMaps(c *gin.Context) { 331func 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 @@
1package controllers 1package handlers
2 2
3import ( 3import (
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
13type 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
22type 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
31type DeleteMapSummaryRequest struct {
32 RouteID int `json:"route_id" binding:"required"`
33}
34
35type 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]
23func CreateMapSummary(c *gin.Context) { 50func 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]
109func EditMapSummary(c *gin.Context) { 131func 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]
195func DeleteMapSummary(c *gin.Context) { 212func 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]
285func EditMapImage(c *gin.Context) { 297func 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 @@
1package controllers 1package handlers
2 2
3import ( 3import (
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
22type 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
29type 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
256func createFile(service *drive.Service, name string, mimeType string, content io.Reader, parentId string) (*drive.File, error) { 284func 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
272func deleteFile(service *drive.Service, fileId string) { 301func 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 @@
1package handlers
2
3import (
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
14type 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
26type ProfileRankings struct {
27 Overall ProfileRankingsDetails `json:"overall"`
28 Singleplayer ProfileRankingsDetails `json:"singleplayer"`
29 Cooperative ProfileRankingsDetails `json:"cooperative"`
30}
31
32type ProfileRankingsDetails struct {
33 Rank int `json:"rank"`
34 CompletionCount int `json:"completion_count"`
35 CompletionTotal int `json:"completion_total"`
36}
37type 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
46type 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
53type 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]
69func 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]
351func 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]
640func 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]
687func 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
7type Response struct {
8 Success bool `json:"success"`
9 Message string `json:"message"`
10 Data any `json:"data"`
11}
12
13func ErrorResponse(message string) Response {
14 return Response{
15 Success: false,
16 Message: message,
17 Data: nil,
18 }
19}
20
7type User struct { 21type 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
17type UserShort struct { 31type 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
36type UserShortWithAvatar struct {
37 SteamID string `json:"steam_id"`
38 UserName string `json:"user_name"`
39 AvatarLink string `json:"avatar_link"`
40}
41
22type Map struct { 42type 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
59type UserRanking struct { 79type 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
65type Game struct { 84type Game struct {
@@ -78,32 +97,16 @@ type Category struct {
78 Name string `json:"name"` 97 Name string `json:"name"`
79} 98}
80 99
81type RecordSP struct { 100type 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"` 105type 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
93type 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
109type PlayerSummaries struct { 112type 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 @@
1package models
2
3import (
4 "mime/multipart"
5 "time"
6)
7
8type 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
17type 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
26type DeleteMapSummaryRequest struct {
27 RouteID int `json:"route_id" binding:"required"`
28}
29
30type EditMapImageRequest struct {
31 Image string `json:"image" binding:"required"`
32}
33
34type 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 @@
1package models
2
3type Response struct {
4 Success bool `json:"success"`
5 Message string `json:"message"`
6 Data any `json:"data"`
7}
8
9type LoginResponse struct {
10 Token string `json:"token"`
11}
12
13type RankingsResponse struct {
14 RankingsSP []UserRanking `json:"rankings_sp"`
15 RankingsMP []UserRanking `json:"rankings_mp"`
16}
17
18type 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
28type ScoreResponse struct {
29 MapID int `json:"map_id"`
30 Records any `json:"records"`
31}
32
33type MapSummaryResponse struct {
34 Map Map `json:"map"`
35 Summary MapSummary `json:"summary"`
36}
37
38type SearchResponse struct {
39 Players []UserShort `json:"players"`
40 Maps []MapShort `json:"maps"`
41}
42
43type ChaptersResponse struct {
44 Game Game `json:"game"`
45 Chapters []Chapter `json:"chapters"`
46}
47
48type ChapterMapsResponse struct {
49 Chapter Chapter `json:"chapter"`
50 Maps []MapShort `json:"maps"`
51}
52
53type RecordResponse struct {
54 ScoreCount int `json:"score_count"`
55 ScoreTime int `json:"score_time"`
56}
57
58func 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 @@
1package routes
2
3import (
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
11func 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 @@
1basePath: /v1 1basePath: /v1
2definitions: 2definitions:
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
diff --git a/main.go b/main.go
index 9b531fe..868db1e 100644
--- a/main.go
+++ b/main.go
@@ -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}