aboutsummaryrefslogtreecommitdiff
path: root/backend/handlers
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/handlers
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/handlers')
-rw-r--r--backend/handlers/home.go289
-rw-r--r--backend/handlers/login.go170
-rw-r--r--backend/handlers/logs.go189
-rw-r--r--backend/handlers/map.go362
-rw-r--r--backend/handlers/mod.go334
-rw-r--r--backend/handlers/record.go303
-rw-r--r--backend/handlers/user.go719
7 files changed, 2366 insertions, 0 deletions
diff --git a/backend/handlers/home.go b/backend/handlers/home.go
new file mode 100644
index 0000000..2095a74
--- /dev/null
+++ b/backend/handlers/home.go
@@ -0,0 +1,289 @@
1package handlers
2
3import (
4 "log"
5 "net/http"
6 "sort"
7 "strings"
8
9 "github.com/gin-gonic/gin"
10 "github.com/pektezol/leastportalshub/backend/database"
11 "github.com/pektezol/leastportalshub/backend/models"
12)
13
14type SearchResponse struct {
15 Players []models.UserShort `json:"players"`
16 Maps []models.MapShort `json:"maps"`
17}
18
19type RankingsResponse struct {
20 Overall []models.UserRanking `json:"rankings_overall"`
21 Singleplayer []models.UserRanking `json:"rankings_singleplayer"`
22 Multiplayer []models.UserRanking `json:"rankings_multiplayer"`
23}
24
25// GET Rankings
26//
27// @Description Get rankings of every player.
28// @Tags rankings
29// @Produce json
30// @Success 200 {object} models.Response{data=RankingsResponse}
31// @Failure 400 {object} models.Response
32// @Router /rankings [get]
33func Rankings(c *gin.Context) {
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)
52 if err != nil {
53 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
54 return
55 }
56 for rows.Next() {
57 ranking := models.UserRanking{}
58 var currentCount int
59 var totalCount int
60 err = rows.Scan(&ranking.User.SteamID, &ranking.User.UserName, &currentCount, &totalCount, &ranking.TotalScore)
61 if err != nil {
62 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
63 return
64 }
65 if currentCount != totalCount {
66 continue
67 }
68 response.Singleplayer = append(response.Singleplayer, ranking)
69 }
70 // Multiplayer rankings
71 sql = `SELECT u.steam_id, u.user_name, COUNT(DISTINCT map_id),
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)
93 if err != nil {
94 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
95 return
96 }
97 if currentCount != totalCount {
98 continue
99 }
100 response.Multiplayer = append(response.Multiplayer, ranking)
101 }
102 // Has both so they are qualified for overall ranking
103 for _, spRanking := range response.Singleplayer {
104 for _, mpRanking := range response.Multiplayer {
105 if spRanking.User.SteamID == mpRanking.User.SteamID {
106 totalScore := spRanking.TotalScore + mpRanking.TotalScore
107 overallRanking := models.UserRanking{
108 User: spRanking.User,
109 TotalScore: totalScore,
110 }
111 response.Overall = append(response.Overall, overallRanking)
112 }
113 }
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 })
124 c.JSON(http.StatusOK, models.Response{
125 Success: true,
126 Message: "Successfully retrieved rankings.",
127 Data: response,
128 })
129}
130
131// GET Search With Query
132//
133// @Description Get all user and map data matching to the query.
134// @Tags search
135// @Produce json
136// @Param q query string false "Search user or map name."
137// @Success 200 {object} models.Response{data=SearchResponse}
138// @Failure 400 {object} models.Response
139// @Router /search [get]
140func SearchWithQuery(c *gin.Context) {
141 query := c.Query("q")
142 query = strings.ToLower(query)
143 log.Println(query)
144 var response SearchResponse
145 // Cache all maps for faster response
146 var maps = []models.MapShort{
147 {ID: 1, Name: "Container Ride"},
148 {ID: 2, Name: "Portal Carousel"},
149 {ID: 3, Name: "Portal Gun"},
150 {ID: 4, Name: "Smooth Jazz"},
151 {ID: 5, Name: "Cube Momentum"},
152 {ID: 6, Name: "Future Starter"},
153 {ID: 7, Name: "Secret Panel"},
154 {ID: 8, Name: "Wakeup"},
155 {ID: 9, Name: "Incinerator"},
156 {ID: 10, Name: "Laser Intro"},
157 {ID: 11, Name: "Laser Stairs"},
158 {ID: 12, Name: "Dual Lasers"},
159 {ID: 13, Name: "Laser Over Goo"},
160 {ID: 14, Name: "Catapult Intro"},
161 {ID: 15, Name: "Trust Fling"},
162 {ID: 16, Name: "Pit Flings"},
163 {ID: 17, Name: "Fizzler Intro"},
164 {ID: 18, Name: "Ceiling Catapult"},
165 {ID: 19, Name: "Ricochet"},
166 {ID: 20, Name: "Bridge Intro"},
167 {ID: 21, Name: "Bridge The Gap"},
168 {ID: 22, Name: "Turret Intro"},
169 {ID: 23, Name: "Laser Relays"},
170 {ID: 24, Name: "Turret Blocker"},
171 {ID: 25, Name: "Laser vs Turret"},
172 {ID: 26, Name: "Pull The Rug"},
173 {ID: 27, Name: "Column Blocker"},
174 {ID: 28, Name: "Laser Chaining"},
175 {ID: 29, Name: "Triple Laser"},
176 {ID: 30, Name: "Jail Break"},
177 {ID: 31, Name: "Escape"},
178 {ID: 32, Name: "Turret Factory"},
179 {ID: 33, Name: "Turret Sabotage"},
180 {ID: 34, Name: "Neurotoxin Sabotage"},
181 {ID: 35, Name: "Core"},
182 {ID: 36, Name: "Underground"},
183 {ID: 37, Name: "Cave Johnson"},
184 {ID: 38, Name: "Repulsion Intro"},
185 {ID: 39, Name: "Bomb Flings"},
186 {ID: 40, Name: "Crazy Box"},
187 {ID: 41, Name: "PotatOS"},
188 {ID: 42, Name: "Propulsion Intro"},
189 {ID: 43, Name: "Propulsion Flings"},
190 {ID: 44, Name: "Conversion Intro"},
191 {ID: 45, Name: "Three Gels"},
192 {ID: 46, Name: "Test"},
193 {ID: 47, Name: "Funnel Intro"},
194 {ID: 48, Name: "Ceiling Button"},
195 {ID: 49, Name: "Wall Button"},
196 {ID: 50, Name: "Polarity"},
197 {ID: 51, Name: "Funnel Catch"},
198 {ID: 52, Name: "Stop The Box"},
199 {ID: 53, Name: "Laser Catapult"},
200 {ID: 54, Name: "Laser Platform"},
201 {ID: 55, Name: "Propulsion Catch"},
202 {ID: 56, Name: "Repulsion Polarity"},
203 {ID: 57, Name: "Finale 1"},
204 {ID: 58, Name: "Finale 2"},
205 {ID: 59, Name: "Finale 3"},
206 {ID: 60, Name: "Finale 4"},
207 {ID: 61, Name: "Calibration"},
208 {ID: 62, Name: "Hub"},
209 {ID: 63, Name: "Doors"},
210 {ID: 64, Name: "Buttons"},
211 {ID: 65, Name: "Lasers"},
212 {ID: 66, Name: "Rat Maze"},
213 {ID: 67, Name: "Laser Crusher"},
214 {ID: 68, Name: "Behind The Scenes"},
215 {ID: 69, Name: "Flings"},
216 {ID: 70, Name: "Infinifling"},
217 {ID: 71, Name: "Team Retrieval"},
218 {ID: 72, Name: "Vertical Flings"},
219 {ID: 73, Name: "Catapults"},
220 {ID: 74, Name: "Multifling"},
221 {ID: 75, Name: "Fling Crushers"},
222 {ID: 76, Name: "Industrial Fan"},
223 {ID: 77, Name: "Cooperative Bridges"},
224 {ID: 78, Name: "Bridge Swap"},
225 {ID: 79, Name: "Fling Block"},
226 {ID: 80, Name: "Catapult Block"},
227 {ID: 81, Name: "Bridge Fling"},
228 {ID: 82, Name: "Turret Walls"},
229 {ID: 83, Name: "Turret Assasin"},
230 {ID: 84, Name: "Bridge Testing"},
231 {ID: 85, Name: "Cooperative Funnels"},
232 {ID: 86, Name: "Funnel Drill"},
233 {ID: 87, Name: "Funnel Catch"},
234 {ID: 88, Name: "Funnel Laser"},
235 {ID: 89, Name: "Cooperative Polarity"},
236 {ID: 90, Name: "Funnel Hop"},
237 {ID: 91, Name: "Advanced Polarity"},
238 {ID: 92, Name: "Funnel Maze"},
239 {ID: 93, Name: "Turret Warehouse"},
240 {ID: 94, Name: "Repulsion Jumps"},
241 {ID: 95, Name: "Double Bounce"},
242 {ID: 96, Name: "Bridge Repulsion"},
243 {ID: 97, Name: "Wall Repulsion"},
244 {ID: 98, Name: "Propulsion Crushers"},
245 {ID: 99, Name: "Turret Ninja"},
246 {ID: 100, Name: "Propulsion Retrieval"},
247 {ID: 101, Name: "Vault Entrance"},
248 {ID: 102, Name: "Seperation"},
249 {ID: 103, Name: "Triple Axis"},
250 {ID: 104, Name: "Catapult Catch"},
251 {ID: 105, Name: "Bridge Gels"},
252 {ID: 106, Name: "Maintenance"},
253 {ID: 107, Name: "Bridge Catch"},
254 {ID: 108, Name: "Double Lift"},
255 {ID: 109, Name: "Gel Maze"},
256 {ID: 110, Name: "Crazier Box"},
257 }
258 var filteredMaps []models.MapShort
259 for _, m := range maps {
260 if strings.Contains(strings.ToLower(m.Name), strings.ToLower(query)) {
261 filteredMaps = append(filteredMaps, m)
262 }
263 }
264 response.Maps = filteredMaps
265 if len(response.Maps) == 0 {
266 response.Maps = []models.MapShort{}
267 }
268 rows, err := database.DB.Query("SELECT steam_id, user_name FROM users WHERE lower(user_name) LIKE $1", "%"+query+"%")
269 if err != nil {
270 log.Fatal(err)
271 }
272 defer rows.Close()
273 for rows.Next() {
274 var user models.UserShort
275 if err := rows.Scan(&user.SteamID, &user.UserName); err != nil {
276 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
277 return
278 }
279 response.Players = append(response.Players, user)
280 }
281 if len(response.Players) == 0 {
282 response.Players = []models.UserShort{}
283 }
284 c.JSON(http.StatusOK, models.Response{
285 Success: true,
286 Message: "Search successfully retrieved.",
287 Data: response,
288 })
289}
diff --git a/backend/handlers/login.go b/backend/handlers/login.go
new file mode 100644
index 0000000..85ffd63
--- /dev/null
+++ b/backend/handlers/login.go
@@ -0,0 +1,170 @@
1package handlers
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "net/http"
8 "os"
9 "time"
10
11 "github.com/gin-gonic/gin"
12 "github.com/golang-jwt/jwt/v4"
13 "github.com/pektezol/leastportalshub/backend/database"
14 "github.com/pektezol/leastportalshub/backend/models"
15 "github.com/solovev/steam_go"
16)
17
18type LoginResponse struct {
19 Token string `json:"token"`
20}
21
22// Login
23//
24// @Description Get (redirect) login page for Steam auth.
25// @Tags login
26// @Accept json
27// @Produce json
28// @Success 200 {object} models.Response{data=LoginResponse}
29// @Failure 400 {object} models.Response
30// @Router /login [get]
31func Login(c *gin.Context) {
32 openID := steam_go.NewOpenId(c.Request)
33 switch openID.Mode() {
34 case "":
35 c.Redirect(http.StatusMovedPermanently, openID.AuthUrl())
36 case "cancel":
37 c.Redirect(http.StatusMovedPermanently, "/")
38 default:
39 steamID, err := openID.ValidateAndGetId()
40 if err != nil {
41 CreateLog(steamID, LogTypeUser, LogDescriptionUserLoginFailValidate)
42 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
43 return
44 }
45 // Create user if new
46 var checkSteamID int64
47 database.DB.QueryRow("SELECT steam_id FROM users WHERE steam_id = $1", steamID).Scan(&checkSteamID)
48 // User does not exist
49 if checkSteamID == 0 {
50 user, err := GetPlayerSummaries(steamID, os.Getenv("API_KEY"))
51 if err != nil {
52 CreateLog(steamID, LogTypeUser, LogDescriptionUserLoginFailSummary)
53 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
54 return
55 }
56 // Empty country code check
57 if user.LocCountryCode == "" {
58 user.LocCountryCode = "XX"
59 }
60 // Insert new user to database
61 database.DB.Exec(`INSERT INTO users (steam_id, user_name, avatar_link, country_code)
62 VALUES ($1, $2, $3, $4)`, steamID, user.PersonaName, user.AvatarFull, user.LocCountryCode)
63 }
64 moderator := false
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)
66 for rows.Next() {
67 var title string
68 rows.Scan(&title)
69 if title == "Moderator" {
70 moderator = true
71 }
72 }
73 // Generate JWT token
74 token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
75 "sub": steamID,
76 "exp": time.Now().Add(time.Hour * 24 * 30).Unix(),
77 "mod": moderator,
78 })
79 // Sign and get the complete encoded token as a string using the secret
80 tokenString, err := token.SignedString([]byte(os.Getenv("SECRET_KEY")))
81 if err != nil {
82 CreateLog(steamID, LogTypeUser, LogDescriptionUserLoginFailToken)
83 c.JSON(http.StatusBadRequest, models.ErrorResponse("Failed to generate token."))
84 return
85 }
86 c.SetCookie("token", tokenString, 3600*24*30, "/", "", true, true)
87 CreateLog(steamID, LogTypeUser, LogDescriptionUserLoginSuccess)
88 c.Redirect(http.StatusTemporaryRedirect, "/")
89 // c.JSON(http.StatusOK, models.Response{
90 // Success: true,
91 // Message: "Successfully generated token.",
92 // Data: LoginResponse{
93 // Token: tokenString,
94 // },
95 // })
96 return
97 }
98}
99
100// GET Token
101//
102// @Description Gets the token cookie value from the user.
103// @Tags auth
104// @Produce json
105//
106// @Success 200 {object} models.Response{data=LoginResponse}
107// @Failure 404 {object} models.Response
108// @Router /token [get]
109func GetCookie(c *gin.Context) {
110 cookie, err := c.Cookie("token")
111 if err != nil {
112 c.JSON(http.StatusNotFound, models.ErrorResponse("No token cookie found."))
113 return
114 }
115 c.JSON(http.StatusOK, models.Response{
116 Success: true,
117 Message: "Token cookie successfully retrieved.",
118 Data: LoginResponse{
119 Token: cookie,
120 },
121 })
122}
123
124// DELETE Token
125//
126// @Description Deletes the token cookie from the user.
127// @Tags auth
128// @Produce json
129//
130// @Success 200 {object} models.Response{data=LoginResponse}
131// @Failure 404 {object} models.Response
132// @Router /token [delete]
133func DeleteCookie(c *gin.Context) {
134 cookie, err := c.Cookie("token")
135 if err != nil {
136 c.JSON(http.StatusNotFound, models.ErrorResponse("No token cookie found."))
137 return
138 }
139 c.SetCookie("token", "", -1, "/", "", true, true)
140 c.JSON(http.StatusOK, models.Response{
141 Success: true,
142 Message: "Token cookie successfully deleted.",
143 Data: LoginResponse{
144 Token: cookie,
145 },
146 })
147}
148
149func GetPlayerSummaries(steamId, apiKey string) (*models.PlayerSummaries, error) {
150 url := fmt.Sprintf("http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/?key=%s&steamids=%s", apiKey, steamId)
151 resp, err := http.Get(url)
152 if err != nil {
153 return nil, err
154 }
155 body, err := io.ReadAll(resp.Body)
156 if err != nil {
157 return nil, err
158 }
159
160 type Result struct {
161 Response struct {
162 Players []models.PlayerSummaries `json:"players"`
163 } `json:"response"`
164 }
165 var data Result
166 if err := json.Unmarshal(body, &data); err != nil {
167 return nil, err
168 }
169 return &data.Response.Players[0], err
170}
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/handlers/map.go b/backend/handlers/map.go
new file mode 100644
index 0000000..1d9cee8
--- /dev/null
+++ b/backend/handlers/map.go
@@ -0,0 +1,362 @@
1package handlers
2
3import (
4 "net/http"
5 "strconv"
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
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
55// GET Map Summary
56//
57// @Description Get map summary with specified id.
58// @Tags maps
59// @Produce json
60// @Param id path int true "Map ID"
61// @Success 200 {object} models.Response{data=MapSummaryResponse}
62// @Failure 400 {object} models.Response
63// @Router /maps/{id}/summary [get]
64func FetchMapSummary(c *gin.Context) {
65 id := c.Param("id")
66 response := MapSummaryResponse{Map: models.Map{}, Summary: models.MapSummary{Routes: []models.MapRoute{}}}
67 intID, err := strconv.Atoi(id)
68 if err != nil {
69 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
70 return
71 }
72 // Get map data
73 response.Map.ID = intID
74 sql := `SELECT m.id, g.name, c.name, m.name, m.image, g.is_coop
75 FROM maps m
76 INNER JOIN games g ON m.game_id = g.id
77 INNER JOIN chapters c ON m.chapter_id = c.id
78 WHERE m.id = $1`
79 err = database.DB.QueryRow(sql, id).Scan(&response.Map.ID, &response.Map.GameName, &response.Map.ChapterName, &response.Map.MapName, &response.Map.Image, &response.Map.IsCoop)
80 if err != nil {
81 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
82 return
83 }
84 // Get map routes and histories
85 sql = `SELECT r.id, c.id, c.name, h.user_name, h.score_count, h.record_date, r.description, r.showcase, COALESCE(avg(rating), 0.0) FROM map_routes r
86 INNER JOIN categories c ON r.category_id = c.id
87 INNER JOIN map_history h ON r.map_id = h.map_id AND r.category_id = h.category_id
88 LEFT JOIN map_ratings rt ON r.map_id = rt.map_id AND r.category_id = rt.category_id
89 WHERE r.map_id = $1 AND h.score_count = r.score_count GROUP BY r.id, c.id, h.user_name, h.score_count, h.record_date, r.description, r.showcase
90 ORDER BY h.record_date ASC;`
91 rows, err := database.DB.Query(sql, id)
92 if err != nil {
93 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
94 return
95 }
96 for rows.Next() {
97 route := models.MapRoute{Category: models.Category{}, History: models.MapHistory{}}
98 err = rows.Scan(&route.RouteID, &route.Category.ID, &route.Category.Name, &route.History.RunnerName, &route.History.ScoreCount, &route.History.Date, &route.Description, &route.Showcase, &route.Rating)
99 if err != nil {
100 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
101 return
102 }
103 response.Summary.Routes = append(response.Summary.Routes, route)
104 }
105 // Return response
106 c.JSON(http.StatusOK, models.Response{
107 Success: true,
108 Message: "Successfully retrieved map summary.",
109 Data: response,
110 })
111}
112
113// GET Map Leaderboards
114//
115// @Description Get map leaderboards with specified id.
116// @Tags maps
117// @Produce json
118// @Param id path int true "Map ID"
119// @Success 200 {object} models.Response{data=MapLeaderboardsResponse}
120// @Failure 400 {object} models.Response
121// @Router /maps/{id}/leaderboards [get]
122func FetchMapLeaderboards(c *gin.Context) {
123 // TODO: make new response type
124 id := c.Param("id")
125 // Get map data
126 response := MapLeaderboardsResponse{Map: models.Map{}, Records: nil}
127 // var mapData models.Map
128 // var mapRecordsData models.MapRecords
129 var isDisabled bool
130 intID, err := strconv.Atoi(id)
131 if err != nil {
132 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
133 return
134 }
135 response.Map.ID = intID
136 sql := `SELECT g.name, c.name, m.name, is_disabled, m.image, g.is_coop
137 FROM maps m
138 INNER JOIN games g ON m.game_id = g.id
139 INNER JOIN chapters c ON m.chapter_id = c.id
140 WHERE m.id = $1`
141 err = database.DB.QueryRow(sql, id).Scan(&response.Map.GameName, &response.Map.ChapterName, &response.Map.MapName, &isDisabled, &response.Map.Image, &response.Map.IsCoop)
142 if err != nil {
143 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
144 return
145 }
146 if isDisabled {
147 c.JSON(http.StatusBadRequest, models.ErrorResponse("Map is not available for competitive boards."))
148 return
149 }
150 // TODO: avatar and names for host & partner
151 if response.Map.GameName == "Portal 2 - Cooperative" {
152 records := []RecordMultiplayer{}
153 sql = `SELECT
154 sub.id,
155 sub.host_id,
156 host.user_name AS host_user_name,
157 host.avatar_link AS host_avatar_link,
158 sub.partner_id,
159 partner.user_name AS partner_user_name,
160 partner.avatar_link AS partner_avatar_link,
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;`
183 rows, err := database.DB.Query(sql, id)
184 if err != nil {
185 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
186 return
187 }
188 placement := 1
189 ties := 0
190 for rows.Next() {
191 var record RecordMultiplayer
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)
193 if err != nil {
194 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
195 return
196 }
197 if len(records) != 0 && records[len(records)-1].ScoreTime == record.ScoreTime {
198 ties++
199 record.Placement = placement - ties
200 } else {
201 record.Placement = placement
202 }
203 records = append(records, record)
204 placement++
205 }
206 response.Records = records
207 } else {
208 records := []RecordSingleplayer{}
209 sql = `SELECT id, user_id, users.user_name, users.avatar_link, score_count, score_time, demo_id, record_date
210 FROM (
211 SELECT id, user_id, score_count, score_time, demo_id, record_date,
212 ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY score_count, score_time) AS rn
213 FROM records_sp
214 WHERE map_id = $1
215 ) sub
216 INNER JOIN users ON user_id = users.steam_id
217 WHERE rn = 1`
218 rows, err := database.DB.Query(sql, id)
219 if err != nil {
220 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
221 return
222 }
223 placement := 1
224 ties := 0
225 for rows.Next() {
226 var record RecordSingleplayer
227 err := rows.Scan(&record.RecordID, &record.User.SteamID, &record.User.UserName, &record.User.AvatarLink, &record.ScoreCount, &record.ScoreTime, &record.DemoID, &record.RecordDate)
228 if err != nil {
229 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
230 return
231 }
232 if len(records) != 0 && records[len(records)-1].ScoreTime == record.ScoreTime {
233 ties++
234 record.Placement = placement - ties
235 } else {
236 record.Placement = placement
237 }
238 records = append(records, record)
239 placement++
240 }
241 response.Records = records
242 }
243 c.JSON(http.StatusOK, models.Response{
244 Success: true,
245 Message: "Successfully retrieved map leaderboards.",
246 Data: response,
247 })
248}
249
250// GET Games
251//
252// @Description Get games from the leaderboards.
253// @Tags games & chapters
254// @Produce json
255// @Success 200 {object} models.Response{data=[]models.Game}
256// @Failure 400 {object} models.Response
257// @Router /games [get]
258func FetchGames(c *gin.Context) {
259 rows, err := database.DB.Query(`SELECT id, name, is_coop FROM games`)
260 if err != nil {
261 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
262 return
263 }
264 var games []models.Game
265 for rows.Next() {
266 var game models.Game
267 if err := rows.Scan(&game.ID, &game.Name, &game.IsCoop); err != nil {
268 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
269 return
270 }
271 games = append(games, game)
272 }
273 c.JSON(http.StatusOK, models.Response{
274 Success: true,
275 Message: "Successfully retrieved games.",
276 Data: games,
277 })
278}
279
280// GET Chapters of a Game
281//
282// @Description Get chapters from the specified game id.
283// @Tags games & chapters
284// @Produce json
285// @Param id path int true "Game ID"
286// @Success 200 {object} models.Response{data=ChaptersResponse}
287// @Failure 400 {object} models.Response
288// @Router /games/{id} [get]
289func FetchChapters(c *gin.Context) {
290 gameID := c.Param("id")
291 intID, err := strconv.Atoi(gameID)
292 if err != nil {
293 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
294 return
295 }
296 var response ChaptersResponse
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)
298 if err != nil {
299 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
300 return
301 }
302 var chapters []models.Chapter
303 var gameName string
304 for rows.Next() {
305 var chapter models.Chapter
306 if err := rows.Scan(&chapter.ID, &chapter.Name, &gameName); err != nil {
307 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
308 return
309 }
310 chapters = append(chapters, chapter)
311 }
312 response.Game.ID = intID
313 response.Game.Name = gameName
314 response.Chapters = chapters
315 c.JSON(http.StatusOK, models.Response{
316 Success: true,
317 Message: "Successfully retrieved chapters.",
318 Data: response,
319 })
320}
321
322// GET Maps of a Chapter
323//
324// @Description Get maps from the specified chapter id.
325// @Tags games & chapters
326// @Produce json
327// @Param id path int true "Chapter ID"
328// @Success 200 {object} models.Response{data=ChapterMapsResponse}
329// @Failure 400 {object} models.Response
330// @Router /chapters/{id} [get]
331func FetchChapterMaps(c *gin.Context) {
332 chapterID := c.Param("id")
333 intID, err := strconv.Atoi(chapterID)
334 if err != nil {
335 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
336 return
337 }
338 var response ChapterMapsResponse
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)
340 if err != nil {
341 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
342 return
343 }
344 var maps []models.MapShort
345 var chapterName string
346 for rows.Next() {
347 var mapShort models.MapShort
348 if err := rows.Scan(&mapShort.ID, &mapShort.Name, &chapterName); err != nil {
349 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
350 return
351 }
352 maps = append(maps, mapShort)
353 }
354 response.Chapter.ID = intID
355 response.Chapter.Name = chapterName
356 response.Maps = maps
357 c.JSON(http.StatusOK, models.Response{
358 Success: true,
359 Message: "Successfully retrieved maps.",
360 Data: response,
361 })
362}
diff --git a/backend/handlers/mod.go b/backend/handlers/mod.go
new file mode 100644
index 0000000..9e93395
--- /dev/null
+++ b/backend/handlers/mod.go
@@ -0,0 +1,334 @@
1package handlers
2
3import (
4 "net/http"
5 "strconv"
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
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
39// POST Map Summary
40//
41// @Description Create map summary with specified map id.
42// @Tags maps
43// @Produce json
44// @Param Authorization header string true "JWT Token"
45// @Param id path int true "Map ID"
46// @Param request body CreateMapSummaryRequest true "Body"
47// @Success 200 {object} models.Response{data=CreateMapSummaryRequest}
48// @Failure 400 {object} models.Response
49// @Router /maps/{id}/summary [post]
50func CreateMapSummary(c *gin.Context) {
51 // Check if user exists
52 user, exists := c.Get("user")
53 if !exists {
54 c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in."))
55 return
56 }
57 mod, exists := c.Get("mod")
58 if !exists || !mod.(bool) {
59 c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions."))
60 return
61 }
62 // Bind parameter and body
63 id := c.Param("id")
64 mapID, err := strconv.Atoi(id)
65 if err != nil {
66 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
67 return
68 }
69 var request CreateMapSummaryRequest
70 if err := c.BindJSON(&request); err != nil {
71 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
72 return
73 }
74 // Start database transaction
75 tx, err := database.DB.Begin()
76 if err != nil {
77 c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error()))
78 return
79 }
80 defer tx.Rollback()
81 // Fetch route category and score count
82 var checkMapID int
83 sql := `SELECT m.id FROM maps m WHERE m.id = $1`
84 err = database.DB.QueryRow(sql, mapID).Scan(&checkMapID)
85 if err != nil {
86 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
87 return
88 }
89 if mapID != checkMapID {
90 c.JSON(http.StatusBadRequest, models.ErrorResponse("Map ID does not exist."))
91 return
92 }
93 // Update database with new data
94 sql = `INSERT INTO map_routes (map_id,category_id,score_count,description,showcase)
95 VALUES ($1,$2,$3,$4,$5)`
96 _, err = tx.Exec(sql, mapID, request.CategoryID, request.ScoreCount, request.Description, request.Showcase)
97 if err != nil {
98 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
99 return
100 }
101 sql = `INSERT INTO map_history (map_id,category_id,user_name,score_count,record_date)
102 VALUES ($1,$2,$3,$4,$5)`
103 _, err = tx.Exec(sql, mapID, request.CategoryID, request.UserName, request.ScoreCount, request.RecordDate)
104 if err != nil {
105 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
106 return
107 }
108 if err = tx.Commit(); err != nil {
109 c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error()))
110 return
111 }
112 CreateLog(user.(models.User).SteamID, LogTypeMod, LogDescriptionMapSummaryCreate)
113 c.JSON(http.StatusOK, models.Response{
114 Success: true,
115 Message: "Successfully created map summary.",
116 Data: request,
117 })
118}
119
120// PUT Map Summary
121//
122// @Description Edit map summary with specified map id.
123// @Tags maps
124// @Produce json
125// @Param Authorization header string true "JWT Token"
126// @Param id path int true "Map ID"
127// @Param request body EditMapSummaryRequest true "Body"
128// @Success 200 {object} models.Response{data=EditMapSummaryRequest}
129// @Failure 400 {object} models.Response
130// @Router /maps/{id}/summary [put]
131func EditMapSummary(c *gin.Context) {
132 // Check if user exists
133 user, exists := c.Get("user")
134 if !exists {
135 c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in."))
136 return
137 }
138 mod, exists := c.Get("mod")
139 if !exists || !mod.(bool) {
140 c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions."))
141 return
142 }
143 // Bind parameter and body
144 id := c.Param("id")
145 mapID, err := strconv.Atoi(id)
146 if err != nil {
147 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
148 return
149 }
150 var request EditMapSummaryRequest
151 if err := c.BindJSON(&request); err != nil {
152 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
153 return
154 }
155 // Start database transaction
156 tx, err := database.DB.Begin()
157 if err != nil {
158 c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error()))
159 return
160 }
161 defer tx.Rollback()
162 // Fetch route category and score count
163 var categoryID, scoreCount, historyID int
164 sql := `SELECT mr.category_id, mr.score_count FROM map_routes mr INNER JOIN maps m ON m.id = mr.map_id WHERE m.id = $1 AND mr.id = $2`
165 err = database.DB.QueryRow(sql, mapID, request.RouteID).Scan(&categoryID, &scoreCount)
166 if err != nil {
167 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
168 return
169 }
170 sql = `SELECT mh.id FROM map_history mh WHERE mh.score_count = $1 AND mh.category_id = $2 AND mh.map_id = $3`
171 err = database.DB.QueryRow(sql, scoreCount, categoryID, mapID).Scan(&historyID)
172 if err != nil {
173 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
174 return
175 }
176 // Update database with new data
177 sql = `UPDATE map_routes SET score_count = $2, description = $3, showcase = $4 WHERE id = $1`
178 _, err = tx.Exec(sql, request.RouteID, request.ScoreCount, request.Description, request.Showcase)
179 if err != nil {
180 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
181 return
182 }
183 sql = `UPDATE map_history SET user_name = $2, score_count = $3, record_date = $4 WHERE id = $1`
184 _, err = tx.Exec(sql, historyID, request.UserName, request.ScoreCount, request.RecordDate)
185 if err != nil {
186 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
187 return
188 }
189 if err = tx.Commit(); err != nil {
190 c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error()))
191 return
192 }
193 CreateLog(user.(models.User).SteamID, LogTypeMod, LogDescriptionMapSummaryEdit)
194 c.JSON(http.StatusOK, models.Response{
195 Success: true,
196 Message: "Successfully updated map summary.",
197 Data: request,
198 })
199}
200
201// DELETE Map Summary
202//
203// @Description Delete map summary with specified map id.
204// @Tags maps
205// @Produce json
206// @Param Authorization header string true "JWT Token"
207// @Param id path int true "Map ID"
208// @Param request body DeleteMapSummaryRequest true "Body"
209// @Success 200 {object} models.Response{data=DeleteMapSummaryRequest}
210// @Failure 400 {object} models.Response
211// @Router /maps/{id}/summary [delete]
212func DeleteMapSummary(c *gin.Context) {
213 // Check if user exists
214 user, exists := c.Get("user")
215 if !exists {
216 c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in."))
217 return
218 }
219 mod, exists := c.Get("mod")
220 if !exists || !mod.(bool) {
221 c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions."))
222 return
223 }
224 // Bind parameter and body
225 id := c.Param("id")
226 mapID, err := strconv.Atoi(id)
227 if err != nil {
228 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
229 return
230 }
231 var request DeleteMapSummaryRequest
232 if err := c.BindJSON(&request); err != nil {
233 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
234 return
235 }
236 // Start database transaction
237 tx, err := database.DB.Begin()
238 if err != nil {
239 c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error()))
240 return
241 }
242 defer tx.Rollback()
243 // Fetch route category and score count
244 var checkMapID, scoreCount, mapHistoryID int
245 sql := `SELECT m.id, mr.score_count FROM maps m INNER JOIN map_routes mr ON m.id=mr.map_id WHERE m.id = $1 AND mr.id = $2`
246 err = database.DB.QueryRow(sql, mapID, request.RouteID).Scan(&checkMapID, &scoreCount)
247 if err != nil {
248 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
249 return
250 }
251 if mapID != checkMapID {
252 c.JSON(http.StatusBadRequest, models.ErrorResponse("Map ID does not exist."))
253 return
254 }
255 sql = `SELECT mh.id FROM maps m INNER JOIN map_routes mr ON m.id=mr.map_id INNER JOIN map_history mh ON m.id=mh.map_id WHERE m.id = $1 AND mr.id = $2 AND mh.score_count = $3`
256 err = database.DB.QueryRow(sql, mapID, request.RouteID, scoreCount).Scan(&mapHistoryID)
257 if err != nil {
258 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
259 return
260 }
261 // Update database with new data
262 sql = `DELETE FROM map_routes mr WHERE mr.id = $1 `
263 _, err = tx.Exec(sql, request.RouteID)
264 if err != nil {
265 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
266 return
267 }
268 sql = `DELETE FROM map_history mh WHERE mh.id = $1`
269 _, err = tx.Exec(sql, mapHistoryID)
270 if err != nil {
271 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
272 return
273 }
274 if err = tx.Commit(); err != nil {
275 c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error()))
276 return
277 }
278 CreateLog(user.(models.User).SteamID, LogTypeMod, LogDescriptionMapSummaryDelete)
279 c.JSON(http.StatusOK, models.Response{
280 Success: true,
281 Message: "Successfully delete map summary.",
282 Data: request,
283 })
284}
285
286// PUT Map Image
287//
288// @Description Edit map image with specified map id.
289// @Tags maps
290// @Produce json
291// @Param Authorization header string true "JWT Token"
292// @Param id path int true "Map ID"
293// @Param request body EditMapImageRequest true "Body"
294// @Success 200 {object} models.Response{data=EditMapImageRequest}
295// @Failure 400 {object} models.Response
296// @Router /maps/{id}/image [put]
297func EditMapImage(c *gin.Context) {
298 // Check if user exists
299 user, exists := c.Get("user")
300 if !exists {
301 c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in."))
302 return
303 }
304 mod, exists := c.Get("mod")
305 if !exists || !mod.(bool) {
306 c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions."))
307 return
308 }
309 // Bind parameter and body
310 id := c.Param("id")
311 mapID, err := strconv.Atoi(id)
312 if err != nil {
313 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
314 return
315 }
316 var request EditMapImageRequest
317 if err := c.BindJSON(&request); err != nil {
318 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
319 return
320 }
321 // Update database with new data
322 sql := `UPDATE maps SET image = $2 WHERE id = $1`
323 _, err = database.DB.Exec(sql, mapID, request.Image)
324 if err != nil {
325 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
326 return
327 }
328 CreateLog(user.(models.User).SteamID, LogTypeMod, LogDescriptionMapSummaryEditImage)
329 c.JSON(http.StatusOK, models.Response{
330 Success: true,
331 Message: "Successfully updated map image.",
332 Data: request,
333 })
334}
diff --git a/backend/handlers/record.go b/backend/handlers/record.go
new file mode 100644
index 0000000..3d29eb8
--- /dev/null
+++ b/backend/handlers/record.go
@@ -0,0 +1,303 @@
1package handlers
2
3import (
4 "context"
5 "encoding/base64"
6 "io"
7 "log"
8 "mime/multipart"
9 "net/http"
10 "os"
11
12 "github.com/gin-gonic/gin"
13 "github.com/google/uuid"
14 "github.com/pektezol/leastportalshub/backend/database"
15 "github.com/pektezol/leastportalshub/backend/models"
16 "github.com/pektezol/leastportalshub/backend/parser"
17 "golang.org/x/oauth2/google"
18 "golang.org/x/oauth2/jwt"
19 "google.golang.org/api/drive/v3"
20)
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
34// POST Record
35//
36// @Description Post record with demo of a specific map.
37// @Tags maps
38// @Accept mpfd
39// @Produce json
40// @Param id path int true "Map ID"
41// @Param Authorization header string true "JWT Token"
42// @Param host_demo formData file true "Host Demo"
43// @Param partner_demo formData file false "Partner Demo"
44// @Param is_partner_orange formData boolean false "Is Partner Orange"
45// @Param partner_id formData string false "Partner ID"
46// @Success 200 {object} models.Response{data=RecordResponse}
47// @Failure 400 {object} models.Response
48// @Failure 401 {object} models.Response
49// @Router /maps/{id}/record [post]
50func CreateRecordWithDemo(c *gin.Context) {
51 mapId := c.Param("id")
52 // Check if user exists
53 user, exists := c.Get("user")
54 if !exists {
55 c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in."))
56 return
57 }
58 // Check if map is sp or mp
59 var gameName string
60 var isCoop bool
61 var isDisabled bool
62 sql := `SELECT g.name, m.is_disabled FROM maps m INNER JOIN games g ON m.game_id=g.id WHERE m.id = $1`
63 err := database.DB.QueryRow(sql, mapId).Scan(&gameName, &isDisabled)
64 if err != nil {
65 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
66 return
67 }
68 if isDisabled {
69 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInvalidRequest)
70 c.JSON(http.StatusBadRequest, models.ErrorResponse("Map is not available for competitive boards."))
71 return
72 }
73 if gameName == "Portal 2 - Cooperative" {
74 isCoop = true
75 }
76 // Get record request
77 var record RecordRequest
78 if err := c.ShouldBind(&record); err != nil {
79 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInvalidRequest)
80 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
81 return
82 }
83 if isCoop && (record.PartnerDemo == nil || record.PartnerID == "") {
84 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInvalidRequest)
85 c.JSON(http.StatusBadRequest, models.ErrorResponse("Invalid entry for coop record submission."))
86 return
87 }
88 // Demo files
89 demoFiles := []*multipart.FileHeader{record.HostDemo}
90 if isCoop {
91 demoFiles = append(demoFiles, record.PartnerDemo)
92 }
93 var hostDemoUUID, hostDemoFileID, partnerDemoUUID, partnerDemoFileID string
94 var hostDemoScoreCount, hostDemoScoreTime int
95 client := serviceAccount()
96 srv, err := drive.New(client)
97 if err != nil {
98 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
99 return
100 }
101 // Create database transaction for inserts
102 tx, err := database.DB.Begin()
103 if err != nil {
104 c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error()))
105 return
106 }
107 // Defer to a rollback in case anything fails
108 defer tx.Rollback()
109 for i, header := range demoFiles {
110 uuid := uuid.New().String()
111 // Upload & insert into demos
112 err = c.SaveUploadedFile(header, "backend/parser/"+uuid+".dem")
113 if err != nil {
114 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailSaveDemo)
115 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
116 return
117 }
118 defer os.Remove("backend/parser/" + uuid + ".dem")
119 f, err := os.Open("backend/parser/" + uuid + ".dem")
120 if err != nil {
121 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailOpenDemo)
122 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
123 return
124 }
125 defer f.Close()
126 file, err := createFile(srv, uuid+".dem", "application/octet-stream", f, os.Getenv("GOOGLE_FOLDER_ID"))
127 if err != nil {
128 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailCreateDemo)
129 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
130 return
131 }
132 hostDemoScoreCount, hostDemoScoreTime, err = parser.ProcessDemo("backend/parser/" + uuid + ".dem")
133 if err != nil {
134 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailProcessDemo)
135 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
136 return
137 }
138 if i == 0 {
139 hostDemoFileID = file.Id
140 hostDemoUUID = uuid
141 } else if i == 1 {
142 partnerDemoFileID = file.Id
143 partnerDemoUUID = uuid
144 }
145 _, err = tx.Exec(`INSERT INTO demos (id,location_id) VALUES ($1,$2)`, uuid, file.Id)
146 if err != nil {
147 deleteFile(srv, file.Id)
148 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInsertDemo)
149 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
150 return
151 }
152 }
153 // Insert into records
154 if isCoop {
155 sql := `INSERT INTO records_mp(map_id,score_count,score_time,host_id,partner_id,host_demo_id,partner_demo_id)
156 VALUES($1, $2, $3, $4, $5, $6, $7)`
157 var hostID string
158 var partnerID string
159 if record.IsPartnerOrange {
160 hostID = user.(models.User).SteamID
161 partnerID = record.PartnerID
162 } else {
163 partnerID = user.(models.User).SteamID
164 hostID = record.PartnerID
165 }
166 _, err := tx.Exec(sql, mapId, hostDemoScoreCount, hostDemoScoreTime, hostID, partnerID, hostDemoUUID, partnerDemoUUID)
167 if err != nil {
168 deleteFile(srv, hostDemoFileID)
169 deleteFile(srv, partnerDemoFileID)
170 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInsertRecord)
171 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
172 return
173 }
174 // If a new world record based on portal count
175 // if record.ScoreCount < wrScore {
176 // _, err := tx.Exec(`UPDATE maps SET wr_score = $1, wr_time = $2 WHERE id = $3`, record.ScoreCount, record.ScoreTime, mapId)
177 // if err != nil {
178 // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
179 // return
180 // }
181 // }
182 } else {
183 sql := `INSERT INTO records_sp(map_id,score_count,score_time,user_id,demo_id)
184 VALUES($1, $2, $3, $4, $5)`
185 _, err := tx.Exec(sql, mapId, hostDemoScoreCount, hostDemoScoreTime, user.(models.User).SteamID, hostDemoUUID)
186 if err != nil {
187 deleteFile(srv, hostDemoFileID)
188 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInsertRecord)
189 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
190 return
191 }
192 // If a new world record based on portal count
193 // if record.ScoreCount < wrScore {
194 // _, err := tx.Exec(`UPDATE maps SET wr_score = $1, wr_time = $2 WHERE id = $3`, record.ScoreCount, record.ScoreTime, mapId)
195 // if err != nil {
196 // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
197 // return
198 // }
199 // }
200 }
201 if err = tx.Commit(); err != nil {
202 c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error()))
203 return
204 }
205 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordSuccess)
206 c.JSON(http.StatusOK, models.Response{
207 Success: true,
208 Message: "Successfully created record.",
209 Data: RecordResponse{ScoreCount: hostDemoScoreCount, ScoreTime: hostDemoScoreTime},
210 })
211}
212
213// GET Demo
214//
215// @Description Get demo with specified demo uuid.
216// @Tags demo
217// @Accept json
218// @Produce octet-stream
219// @Param uuid query string true "Demo UUID"
220// @Success 200 {file} binary "Demo File"
221// @Failure 400 {object} models.Response
222// @Router /demos [get]
223func DownloadDemoWithID(c *gin.Context) {
224 uuid := c.Query("uuid")
225 var locationID string
226 if uuid == "" {
227 c.JSON(http.StatusBadRequest, models.ErrorResponse("Invalid id given."))
228 return
229 }
230 err := database.DB.QueryRow(`SELECT location_id FROM demos WHERE id = $1`, uuid).Scan(&locationID)
231 if err != nil {
232 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
233 return
234 }
235 if locationID == "" {
236 c.JSON(http.StatusBadRequest, models.ErrorResponse("Invalid id given."))
237 return
238 }
239 url := "https://drive.google.com/uc?export=download&id=" + locationID
240 fileName := uuid + ".dem"
241 output, err := os.Create(fileName)
242 if err != nil {
243 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
244 return
245 }
246 defer os.Remove(fileName)
247 defer output.Close()
248 response, err := http.Get(url)
249 if err != nil {
250 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
251 return
252 }
253 defer response.Body.Close()
254 _, err = io.Copy(output, response.Body)
255 if err != nil {
256 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
257 return
258 }
259 // Downloaded file
260 c.Header("Content-Description", "File Transfer")
261 c.Header("Content-Transfer-Encoding", "binary")
262 c.Header("Content-Disposition", "attachment; filename="+fileName)
263 c.Header("Content-Type", "application/octet-stream")
264 c.File(fileName)
265 // c.FileAttachment()
266}
267
268// Use Service account
269func serviceAccount() *http.Client {
270 privateKey, _ := base64.StdEncoding.DecodeString(os.Getenv("GOOGLE_PRIVATE_KEY_BASE64"))
271 config := &jwt.Config{
272 Email: os.Getenv("GOOGLE_CLIENT_EMAIL"),
273 PrivateKey: []byte(privateKey),
274 Scopes: []string{
275 drive.DriveScope,
276 },
277 TokenURL: google.JWTTokenURL,
278 }
279 client := config.Client(context.Background())
280 return client
281}
282
283// Create Gdrive file
284func createFile(service *drive.Service, name string, mimeType string, content io.Reader, parentId string) (*drive.File, error) {
285 f := &drive.File{
286 MimeType: mimeType,
287 Name: name,
288 Parents: []string{parentId},
289 }
290 file, err := service.Files.Create(f).Media(content).Do()
291
292 if err != nil {
293 log.Println("Could not create file: " + err.Error())
294 return nil, err
295 }
296
297 return file, nil
298}
299
300// Delete Gdrive file
301func deleteFile(service *drive.Service, fileId string) {
302 service.Files.Delete(fileId)
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}