aboutsummaryrefslogtreecommitdiff
path: root/backend
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 /backend
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
Diffstat (limited to 'backend')
-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
17 files changed, 1576 insertions, 920 deletions
diff --git a/backend/middleware/auth.go b/backend/api/auth.go
index 0744b3d..91ef80c 100644
--- a/backend/middleware/auth.go
+++ b/backend/api/auth.go
@@ -1,4 +1,4 @@
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}