aboutsummaryrefslogtreecommitdiff
path: root/backend/controllers
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/controllers
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 '')
-rw-r--r--backend/controllers/userController.go286
-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/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
6 files changed, 296 insertions, 474 deletions
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/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/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}