aboutsummaryrefslogtreecommitdiff
path: root/backend/handlers/user.go
diff options
context:
space:
mode:
Diffstat (limited to 'backend/handlers/user.go')
-rw-r--r--backend/handlers/user.go383
1 files changed, 383 insertions, 0 deletions
diff --git a/backend/handlers/user.go b/backend/handlers/user.go
new file mode 100644
index 0000000..51eadb4
--- /dev/null
+++ b/backend/handlers/user.go
@@ -0,0 +1,383 @@
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}
37
38type ProfileRecords struct {
39 P2Singleplayer []ProfileRecordsDetails `json:"portal2_singleplayer"`
40 P2Cooperative []ProfileRecordsDetails `json:"portal2_cooperative"`
41}
42
43type ProfileRecordsDetails struct {
44 MapID int `json:"map_id"`
45 MapName string `json:"map_name"`
46 Scores []ProfileScores `json:"scores"`
47}
48
49type ProfileScores struct {
50 DemoID string `json:"demo_id"`
51 ScoreCount int `json:"score_count"`
52 ScoreTime int `json:"score_time"`
53 Date time.Time `json:"date"`
54}
55
56type ScoreResponse struct {
57 MapID int `json:"map_id"`
58 Records any `json:"records"`
59}
60
61// GET Profile
62//
63// @Description Get profile page of session user.
64// @Tags users
65// @Accept json
66// @Produce json
67// @Param Authorization header string true "JWT Token"
68// @Success 200 {object} models.Response{data=ProfileResponse}
69// @Failure 400 {object} models.Response
70// @Failure 401 {object} models.Response
71// @Router /profile [get]
72func Profile(c *gin.Context) {
73 // Check if user exists
74 user, exists := c.Get("user")
75 if !exists {
76 c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in."))
77 return
78 }
79 // Get user links
80 links := models.Links{}
81 sql := `SELECT u.p2sr, u.steam, u.youtube, u.twitch FROM users u WHERE u.steam_id = $1`
82 err := database.DB.QueryRow(sql, user.(models.User).SteamID).Scan(&links.P2SR, &links.Steam, &links.YouTube, &links.Twitch)
83 if err != nil {
84 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
85 return
86 }
87 // TODO: Get rankings (all maps done in one game)
88 records := ProfileRecords{
89 P2Singleplayer: []ProfileRecordsDetails{},
90 P2Cooperative: []ProfileRecordsDetails{},
91 }
92 // Get singleplayer records
93 sql = `SELECT m.game_id, sp.map_id, m."name", sp.score_count, sp.score_time, sp.demo_id, sp.record_date
94 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;`
95 rows, err := database.DB.Query(sql, user.(models.User).SteamID)
96 if err != nil {
97 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
98 return
99 }
100 for rows.Next() {
101 var mapID int
102 var mapName string
103 var gameID int
104 score := ProfileScores{}
105 rows.Scan(&gameID, &mapID, &mapName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date)
106 if gameID != 1 {
107 continue
108 }
109 // More than one record in one map
110 if len(records.P2Singleplayer) != 0 && mapID == records.P2Singleplayer[len(records.P2Singleplayer)-1].MapID {
111 records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores = append(records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores, score)
112 continue
113 }
114 // New map
115 records.P2Singleplayer = append(records.P2Singleplayer, ProfileRecordsDetails{
116 MapID: mapID,
117 MapName: mapName,
118 Scores: []ProfileScores{},
119 })
120 records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores = append(records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores, score)
121 }
122 // Get multiplayer records
123 sql = `SELECT m.game_id, mp.map_id, m."name", 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
124 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;`
125 rows, err = database.DB.Query(sql, user.(models.User).SteamID)
126 if err != nil {
127 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
128 return
129 }
130 for rows.Next() {
131 var mapID int
132 var mapName string
133 var gameID int
134 score := ProfileScores{}
135 rows.Scan(&gameID, &mapID, &mapName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date)
136 if gameID != 1 {
137 continue
138 }
139 // More than one record in one map
140 if len(records.P2Cooperative) != 0 && mapID == records.P2Cooperative[len(records.P2Cooperative)-1].MapID {
141 records.P2Cooperative[len(records.P2Cooperative)-1].Scores = append(records.P2Cooperative[len(records.P2Cooperative)-1].Scores, score)
142 continue
143 }
144 // New map
145 records.P2Cooperative = append(records.P2Cooperative, ProfileRecordsDetails{
146 MapID: mapID,
147 MapName: mapName,
148 Scores: []ProfileScores{},
149 })
150 records.P2Cooperative[len(records.P2Cooperative)-1].Scores = append(records.P2Cooperative[len(records.P2Cooperative)-1].Scores, score)
151 }
152 c.JSON(http.StatusOK, models.Response{
153 Success: true,
154 Message: "Successfully retrieved user scores.",
155 Data: ProfileResponse{
156 Profile: true,
157 SteamID: user.(models.User).SteamID,
158 UserName: user.(models.User).UserName,
159 AvatarLink: user.(models.User).AvatarLink,
160 CountryCode: user.(models.User).CountryCode,
161 Titles: user.(models.User).Titles,
162 Links: links,
163 Rankings: ProfileRankings{},
164 Records: records,
165 },
166 })
167}
168
169// GET User
170//
171// @Description Get profile page of another user.
172// @Tags users
173// @Accept json
174// @Produce json
175// @Param id path int true "User ID"
176// @Success 200 {object} models.Response{data=ProfileResponse}
177// @Failure 400 {object} models.Response
178// @Failure 404 {object} models.Response
179// @Router /users/{id} [get]
180func FetchUser(c *gin.Context) {
181 id := c.Param("id")
182 // Check if id is all numbers and 17 length
183 match, _ := regexp.MatchString("^[0-9]{17}$", id)
184 if !match {
185 c.JSON(http.StatusNotFound, models.ErrorResponse("User not found."))
186 return
187 }
188 // Check if user exists
189 var user models.User
190 links := models.Links{}
191 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`
192 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)
193 if err != nil {
194 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
195 return
196 }
197 if user.SteamID == "" {
198 // User does not exist
199 c.JSON(http.StatusNotFound, models.ErrorResponse("User not found."))
200 return
201 }
202 // Get user titles
203 sql = `SELECT t.title_name, t.title_color FROM titles t
204 INNER JOIN user_titles ut ON t.id=ut.title_id WHERE ut.user_id = $1`
205 rows, err := database.DB.Query(sql, user.SteamID)
206 if err != nil {
207 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
208 return
209 }
210 for rows.Next() {
211 var title models.Title
212 if err := rows.Scan(&title.Name, &title.Color); err != nil {
213 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
214 return
215 }
216 user.Titles = append(user.Titles, title)
217 }
218 // TODO: Get rankings (all maps done in one game)
219 records := ProfileRecords{
220 P2Singleplayer: []ProfileRecordsDetails{},
221 P2Cooperative: []ProfileRecordsDetails{},
222 }
223 // Get singleplayer records
224 sql = `SELECT m.game_id, sp.map_id, m."name", sp.score_count, sp.score_time, sp.demo_id, sp.record_date
225 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;`
226 rows, err = database.DB.Query(sql, user.SteamID)
227 if err != nil {
228 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
229 return
230 }
231 for rows.Next() {
232 var mapID int
233 var mapName string
234 var gameID int
235 score := ProfileScores{}
236 rows.Scan(&gameID, &mapID, &mapName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date)
237 if gameID != 1 {
238 continue
239 }
240 // More than one record in one map
241 if len(records.P2Singleplayer) != 0 && mapID == records.P2Singleplayer[len(records.P2Singleplayer)-1].MapID {
242 records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores = append(records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores, score)
243 continue
244 }
245 // New map
246 records.P2Singleplayer = append(records.P2Singleplayer, ProfileRecordsDetails{
247 MapID: mapID,
248 MapName: mapName,
249 Scores: []ProfileScores{},
250 })
251 records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores = append(records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores, score)
252 }
253 // Get multiplayer records
254 sql = `SELECT m.game_id, mp.map_id, m."name", 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
255 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;`
256 rows, err = database.DB.Query(sql, user.SteamID)
257 if err != nil {
258 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
259 return
260 }
261 for rows.Next() {
262 var mapID int
263 var mapName string
264 var gameID int
265 score := ProfileScores{}
266 rows.Scan(&gameID, &mapID, &mapName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date)
267 if gameID != 1 {
268 continue
269 }
270 // More than one record in one map
271 if len(records.P2Cooperative) != 0 && mapID == records.P2Cooperative[len(records.P2Cooperative)-1].MapID {
272 records.P2Cooperative[len(records.P2Cooperative)-1].Scores = append(records.P2Cooperative[len(records.P2Cooperative)-1].Scores, score)
273 continue
274 }
275 // New map
276 records.P2Cooperative = append(records.P2Cooperative, ProfileRecordsDetails{
277 MapID: mapID,
278 MapName: mapName,
279 Scores: []ProfileScores{},
280 })
281 records.P2Cooperative[len(records.P2Cooperative)-1].Scores = append(records.P2Cooperative[len(records.P2Cooperative)-1].Scores, score)
282 }
283 c.JSON(http.StatusOK, models.Response{
284 Success: true,
285 Message: "Successfully retrieved user scores.",
286 Data: ProfileResponse{
287 Profile: true,
288 SteamID: user.SteamID,
289 UserName: user.UserName,
290 AvatarLink: user.AvatarLink,
291 CountryCode: user.CountryCode,
292 Titles: user.Titles,
293 Links: links,
294 Rankings: ProfileRankings{},
295 Records: records,
296 },
297 })
298}
299
300// PUT Profile
301//
302// @Description Update profile page of session user.
303// @Tags users
304// @Accept json
305// @Produce json
306// @Param Authorization header string true "JWT Token"
307// @Success 200 {object} models.Response{data=ProfileResponse}
308// @Failure 400 {object} models.Response
309// @Failure 401 {object} models.Response
310// @Router /profile [post]
311func UpdateUser(c *gin.Context) {
312 // Check if user exists
313 user, exists := c.Get("user")
314 if !exists {
315 c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in."))
316 return
317 }
318 profile, err := GetPlayerSummaries(user.(models.User).SteamID, os.Getenv("API_KEY"))
319 if err != nil {
320 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
321 return
322 }
323 // Update profile
324 _, err = database.DB.Exec(`UPDATE users SET username = $1, avatar_link = $2, country_code = $3, updated_at = $4
325 WHERE steam_id = $5`, profile.PersonaName, profile.AvatarFull, profile.LocCountryCode, time.Now().UTC(), user.(models.User).SteamID)
326 if err != nil {
327 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
328 return
329 }
330 c.JSON(http.StatusOK, models.Response{
331 Success: true,
332 Message: "Successfully updated user.",
333 Data: ProfileResponse{
334 Profile: true,
335 SteamID: user.(models.User).SteamID,
336 UserName: profile.PersonaName,
337 AvatarLink: profile.AvatarFull,
338 CountryCode: profile.LocCountryCode,
339 },
340 })
341}
342
343// PUT Profile/CountryCode
344//
345// @Description Update country code of session user.
346// @Tags users
347// @Accept json
348// @Produce json
349// @Param Authorization header string true "JWT Token"
350// @Param country_code query string true "Country Code [XX]"
351// @Success 200 {object} models.Response
352// @Failure 400 {object} models.Response
353// @Failure 401 {object} models.Response
354// @Router /profile [put]
355func UpdateCountryCode(c *gin.Context) {
356 // Check if user exists
357 user, exists := c.Get("user")
358 if !exists {
359 c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in."))
360 return
361 }
362 code := c.Query("country_code")
363 if code == "" {
364 c.JSON(http.StatusNotFound, models.ErrorResponse("Enter a valid country code."))
365 return
366 }
367 var validCode string
368 err := database.DB.QueryRow(`SELECT country_code FROM countries WHERE country_code = $1`, code).Scan(&validCode)
369 if err != nil {
370 c.JSON(http.StatusNotFound, models.ErrorResponse(err.Error()))
371 return
372 }
373 // Valid code, update profile
374 _, err = database.DB.Exec(`UPDATE users SET country_code = $1 WHERE steam_id = $2`, validCode, user.(models.User).SteamID)
375 if err != nil {
376 c.JSON(http.StatusNotFound, models.ErrorResponse(err.Error()))
377 return
378 }
379 c.JSON(http.StatusOK, models.Response{
380 Success: true,
381 Message: "Successfully updated country code.",
382 })
383}