aboutsummaryrefslogtreecommitdiff
path: root/backend/handlers
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--backend/handlers/home.go (renamed from backend/controllers/homeController.go)169
-rw-r--r--backend/handlers/login.go (renamed from backend/controllers/loginController.go)34
-rw-r--r--backend/handlers/logs.go189
-rw-r--r--backend/handlers/map.go (renamed from backend/controllers/mapController.go)131
-rw-r--r--backend/handlers/mod.go (renamed from backend/controllers/modController.go)113
-rw-r--r--backend/handlers/record.go (renamed from backend/controllers/recordController.go)37
-rw-r--r--backend/handlers/user.go719
7 files changed, 1204 insertions, 188 deletions
diff --git a/backend/controllers/homeController.go b/backend/handlers/home.go
index c94590a..2095a74 100644
--- a/backend/controllers/homeController.go
+++ b/backend/handlers/home.go
@@ -1,8 +1,9 @@
1package controllers 1package handlers
2 2
3import ( 3import (
4 "log" 4 "log"
5 "net/http" 5 "net/http"
6 "sort"
6 "strings" 7 "strings"
7 8
8 "github.com/gin-gonic/gin" 9 "github.com/gin-gonic/gin"
@@ -10,15 +11,15 @@ import (
10 "github.com/pektezol/leastportalshub/backend/models" 11 "github.com/pektezol/leastportalshub/backend/models"
11) 12)
12 13
13func Home(c *gin.Context) { 14type SearchResponse struct {
14 user, exists := c.Get("user") 15 Players []models.UserShort `json:"players"`
15 if !exists { 16 Maps []models.MapShort `json:"maps"`
16 c.JSON(200, "no id, not auth") 17}
17 } else { 18
18 c.JSON(200, gin.H{ 19type RankingsResponse struct {
19 "output": user, 20 Overall []models.UserRanking `json:"rankings_overall"`
20 }) 21 Singleplayer []models.UserRanking `json:"rankings_singleplayer"`
21 } 22 Multiplayer []models.UserRanking `json:"rankings_multiplayer"`
22} 23}
23 24
24// GET Rankings 25// GET Rankings
@@ -26,100 +27,104 @@ func Home(c *gin.Context) {
26// @Description Get rankings of every player. 27// @Description Get rankings of every player.
27// @Tags rankings 28// @Tags rankings
28// @Produce json 29// @Produce json
29// @Success 200 {object} models.Response{data=models.RankingsResponse} 30// @Success 200 {object} models.Response{data=RankingsResponse}
30// @Failure 400 {object} models.Response 31// @Failure 400 {object} models.Response
31// @Router /rankings [get] 32// @Router /rankings [get]
32func Rankings(c *gin.Context) { 33func Rankings(c *gin.Context) {
33 rows, err := database.DB.Query(`SELECT steam_id, user_name FROM users`) 34 response := RankingsResponse{
35 Overall: []models.UserRanking{},
36 Singleplayer: []models.UserRanking{},
37 Multiplayer: []models.UserRanking{},
38 }
39 // Singleplayer rankings
40 sql := `SELECT u.steam_id, u.user_name, COUNT(DISTINCT map_id),
41 (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false),
42 (SELECT SUM(min_score_count) AS total_min_score_count FROM (
43 SELECT
44 user_id,
45 MIN(score_count) AS min_score_count
46 FROM records_sp
47 GROUP BY user_id, map_id
48 ) AS subquery
49 WHERE user_id = u.steam_id)
50 FROM records_sp sp JOIN users u ON u.steam_id = sp.user_id GROUP BY u.steam_id, u.user_name`
51 rows, err := database.DB.Query(sql)
34 if err != nil { 52 if err != nil {
35 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 53 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
36 return 54 return
37 } 55 }
38 var spRankings []models.UserRanking
39 var mpRankings []models.UserRanking
40 for rows.Next() { 56 for rows.Next() {
41 var userID, username string 57 ranking := models.UserRanking{}
42 err := rows.Scan(&userID, &username) 58 var currentCount int
43 if err != nil { 59 var totalCount int
44 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 60 err = rows.Scan(&ranking.User.SteamID, &ranking.User.UserName, &currentCount, &totalCount, &ranking.TotalScore)
45 return
46 }
47 // Getting all sp records for each user
48 var uniqueSingleUserRecords, totalSingleMaps int
49 sql := `SELECT COUNT(DISTINCT map_id), (SELECT COUNT(map_name) FROM maps
50 WHERE is_coop = FALSE AND is_disabled = false) FROM records_sp WHERE user_id = $1`
51 err = database.DB.QueryRow(sql, userID).Scan(&uniqueSingleUserRecords, &totalSingleMaps)
52 if err != nil { 61 if err != nil {
53 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 62 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
54 return 63 return
55 } 64 }
56 // Has all singleplayer records 65 if currentCount != totalCount {
57 if uniqueSingleUserRecords == totalSingleMaps { 66 continue
58 var ranking models.UserRanking
59 ranking.UserID = userID
60 ranking.UserName = username
61 sql := `SELECT DISTINCT map_id, score_count FROM records_sp WHERE user_id = $1 ORDER BY map_id, score_count`
62 rows, err := database.DB.Query(sql, userID)
63 if err != nil {
64 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
65 return
66 }
67 totalScore := 0
68 var maps []int
69 for rows.Next() {
70 var mapID, scoreCount int
71 rows.Scan(&mapID, &scoreCount)
72 if len(maps) != 0 && maps[len(maps)-1] == mapID {
73 continue
74 }
75 totalScore += scoreCount
76 maps = append(maps, mapID)
77 }
78 ranking.TotalScore = totalScore
79 spRankings = append(spRankings, ranking)
80 } 67 }
81 // Getting all mp records for each user 68 response.Singleplayer = append(response.Singleplayer, ranking)
82 var uniqueMultiUserRecords, totalMultiMaps int 69 }
83 sql = `SELECT COUNT(DISTINCT map_id), (SELECT COUNT(map_name) FROM maps 70 // Multiplayer rankings
84 WHERE is_coop = TRUE AND is_disabled = false) FROM records_mp WHERE host_id = $1 OR partner_id = $2` 71 sql = `SELECT u.steam_id, u.user_name, COUNT(DISTINCT map_id),
85 err = database.DB.QueryRow(sql, userID, userID).Scan(&uniqueMultiUserRecords, &totalMultiMaps) 72 (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false),
73 (SELECT SUM(min_score_count) AS total_min_score_count FROM (
74 SELECT
75 host_id,
76 partner_id,
77 MIN(score_count) AS min_score_count
78 FROM records_mp
79 GROUP BY host_id, partner_id, map_id
80 ) AS subquery
81 WHERE host_id = u.steam_id OR partner_id = u.steam_id)
82 FROM records_mp mp JOIN users u ON u.steam_id = mp.host_id OR u.steam_id = mp.partner_id GROUP BY u.steam_id, u.user_name`
83 rows, err = database.DB.Query(sql)
84 if err != nil {
85 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
86 return
87 }
88 for rows.Next() {
89 ranking := models.UserRanking{}
90 var currentCount int
91 var totalCount int
92 err = rows.Scan(&ranking.User.SteamID, &ranking.User.UserName, &currentCount, &totalCount, &ranking.TotalScore)
86 if err != nil { 93 if err != nil {
87 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 94 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
88 return 95 return
89 } 96 }
90 // Has all singleplayer records 97 if currentCount != totalCount {
91 if uniqueMultiUserRecords == totalMultiMaps { 98 continue
92 var ranking models.UserRanking 99 }
93 ranking.UserID = userID 100 response.Multiplayer = append(response.Multiplayer, ranking)
94 ranking.UserName = username 101 }
95 sql := `SELECT DISTINCT map_id, score_count FROM records_mp WHERE host_id = $1 OR partner_id = $2 ORDER BY map_id, score_count` 102 // Has both so they are qualified for overall ranking
96 rows, err := database.DB.Query(sql, userID, userID) 103 for _, spRanking := range response.Singleplayer {
97 if err != nil { 104 for _, mpRanking := range response.Multiplayer {
98 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 105 if spRanking.User.SteamID == mpRanking.User.SteamID {
99 return 106 totalScore := spRanking.TotalScore + mpRanking.TotalScore
100 } 107 overallRanking := models.UserRanking{
101 totalScore := 0 108 User: spRanking.User,
102 var maps []int 109 TotalScore: totalScore,
103 for rows.Next() {
104 var mapID, scoreCount int
105 rows.Scan(&mapID, &scoreCount)
106 if len(maps) != 0 && maps[len(maps)-1] == mapID {
107 continue
108 } 110 }
109 totalScore += scoreCount 111 response.Overall = append(response.Overall, overallRanking)
110 maps = append(maps, mapID)
111 } 112 }
112 ranking.TotalScore = totalScore
113 mpRankings = append(mpRankings, ranking)
114 } 113 }
115 } 114 }
115 sort.Slice(response.Singleplayer, func(i, j int) bool {
116 return response.Singleplayer[i].TotalScore < response.Singleplayer[j].TotalScore
117 })
118 sort.Slice(response.Multiplayer, func(i, j int) bool {
119 return response.Multiplayer[i].TotalScore < response.Multiplayer[j].TotalScore
120 })
121 sort.Slice(response.Overall, func(i, j int) bool {
122 return response.Overall[i].TotalScore < response.Overall[j].TotalScore
123 })
116 c.JSON(http.StatusOK, models.Response{ 124 c.JSON(http.StatusOK, models.Response{
117 Success: true, 125 Success: true,
118 Message: "Successfully retrieved rankings.", 126 Message: "Successfully retrieved rankings.",
119 Data: models.RankingsResponse{ 127 Data: response,
120 RankingsSP: spRankings,
121 RankingsMP: mpRankings,
122 },
123 }) 128 })
124} 129}
125 130
@@ -129,14 +134,14 @@ func Rankings(c *gin.Context) {
129// @Tags search 134// @Tags search
130// @Produce json 135// @Produce json
131// @Param q query string false "Search user or map name." 136// @Param q query string false "Search user or map name."
132// @Success 200 {object} models.Response{data=models.SearchResponse} 137// @Success 200 {object} models.Response{data=SearchResponse}
133// @Failure 400 {object} models.Response 138// @Failure 400 {object} models.Response
134// @Router /search [get] 139// @Router /search [get]
135func SearchWithQuery(c *gin.Context) { 140func SearchWithQuery(c *gin.Context) {
136 query := c.Query("q") 141 query := c.Query("q")
137 query = strings.ToLower(query) 142 query = strings.ToLower(query)
138 log.Println(query) 143 log.Println(query)
139 var response models.SearchResponse 144 var response SearchResponse
140 // Cache all maps for faster response 145 // Cache all maps for faster response
141 var maps = []models.MapShort{ 146 var maps = []models.MapShort{
142 {ID: 1, Name: "Container Ride"}, 147 {ID: 1, Name: "Container Ride"},
diff --git a/backend/controllers/loginController.go b/backend/handlers/login.go
index e907b22..85ffd63 100644
--- a/backend/controllers/loginController.go
+++ b/backend/handlers/login.go
@@ -1,9 +1,9 @@
1package controllers 1package handlers
2 2
3import ( 3import (
4 "encoding/json" 4 "encoding/json"
5 "fmt" 5 "fmt"
6 "io/ioutil" 6 "io"
7 "net/http" 7 "net/http"
8 "os" 8 "os"
9 "time" 9 "time"
@@ -15,13 +15,17 @@ import (
15 "github.com/solovev/steam_go" 15 "github.com/solovev/steam_go"
16) 16)
17 17
18type LoginResponse struct {
19 Token string `json:"token"`
20}
21
18// Login 22// Login
19// 23//
20// @Description Get (redirect) login page for Steam auth. 24// @Description Get (redirect) login page for Steam auth.
21// @Tags login 25// @Tags login
22// @Accept json 26// @Accept json
23// @Produce json 27// @Produce json
24// @Success 200 {object} models.Response{data=models.LoginResponse} 28// @Success 200 {object} models.Response{data=LoginResponse}
25// @Failure 400 {object} models.Response 29// @Failure 400 {object} models.Response
26// @Router /login [get] 30// @Router /login [get]
27func Login(c *gin.Context) { 31func Login(c *gin.Context) {
@@ -34,20 +38,18 @@ func Login(c *gin.Context) {
34 default: 38 default:
35 steamID, err := openID.ValidateAndGetId() 39 steamID, err := openID.ValidateAndGetId()
36 if err != nil { 40 if err != nil {
41 CreateLog(steamID, LogTypeUser, LogDescriptionUserLoginFailValidate)
37 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 42 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
38 return 43 return
39 } 44 }
40 // Create user if new 45 // Create user if new
41 var checkSteamID int64 46 var checkSteamID int64
42 err = database.DB.QueryRow("SELECT steam_id FROM users WHERE steam_id = $1", steamID).Scan(&checkSteamID) 47 database.DB.QueryRow("SELECT steam_id FROM users WHERE steam_id = $1", steamID).Scan(&checkSteamID)
43 // if err != nil {
44 // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
45 // return
46 // }
47 // User does not exist 48 // User does not exist
48 if checkSteamID == 0 { 49 if checkSteamID == 0 {
49 user, err := GetPlayerSummaries(steamID, os.Getenv("API_KEY")) 50 user, err := GetPlayerSummaries(steamID, os.Getenv("API_KEY"))
50 if err != nil { 51 if err != nil {
52 CreateLog(steamID, LogTypeUser, LogDescriptionUserLoginFailSummary)
51 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 53 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
52 return 54 return
53 } 55 }
@@ -60,7 +62,7 @@ func Login(c *gin.Context) {
60 VALUES ($1, $2, $3, $4)`, steamID, user.PersonaName, user.AvatarFull, user.LocCountryCode) 62 VALUES ($1, $2, $3, $4)`, steamID, user.PersonaName, user.AvatarFull, user.LocCountryCode)
61 } 63 }
62 moderator := false 64 moderator := false
63 rows, _ := database.DB.Query("SELECT title_name FROM titles WHERE user_id = $1", steamID) 65 rows, _ := database.DB.Query("SELECT title_name FROM titles t INNER JOIN user_titles ut ON t.id=ut.title_id WHERE ut.user_id = $1", steamID)
64 for rows.Next() { 66 for rows.Next() {
65 var title string 67 var title string
66 rows.Scan(&title) 68 rows.Scan(&title)
@@ -77,15 +79,17 @@ func Login(c *gin.Context) {
77 // Sign and get the complete encoded token as a string using the secret 79 // Sign and get the complete encoded token as a string using the secret
78 tokenString, err := token.SignedString([]byte(os.Getenv("SECRET_KEY"))) 80 tokenString, err := token.SignedString([]byte(os.Getenv("SECRET_KEY")))
79 if err != nil { 81 if err != nil {
82 CreateLog(steamID, LogTypeUser, LogDescriptionUserLoginFailToken)
80 c.JSON(http.StatusBadRequest, models.ErrorResponse("Failed to generate token.")) 83 c.JSON(http.StatusBadRequest, models.ErrorResponse("Failed to generate token."))
81 return 84 return
82 } 85 }
83 c.SetCookie("token", tokenString, 3600*24*30, "/", "", true, true) 86 c.SetCookie("token", tokenString, 3600*24*30, "/", "", true, true)
87 CreateLog(steamID, LogTypeUser, LogDescriptionUserLoginSuccess)
84 c.Redirect(http.StatusTemporaryRedirect, "/") 88 c.Redirect(http.StatusTemporaryRedirect, "/")
85 // c.JSON(http.StatusOK, models.Response{ 89 // c.JSON(http.StatusOK, models.Response{
86 // Success: true, 90 // Success: true,
87 // Message: "Successfully generated token.", 91 // Message: "Successfully generated token.",
88 // Data: models.LoginResponse{ 92 // Data: LoginResponse{
89 // Token: tokenString, 93 // Token: tokenString,
90 // }, 94 // },
91 // }) 95 // })
@@ -99,7 +103,7 @@ func Login(c *gin.Context) {
99// @Tags auth 103// @Tags auth
100// @Produce json 104// @Produce json
101// 105//
102// @Success 200 {object} models.Response{data=models.LoginResponse} 106// @Success 200 {object} models.Response{data=LoginResponse}
103// @Failure 404 {object} models.Response 107// @Failure 404 {object} models.Response
104// @Router /token [get] 108// @Router /token [get]
105func GetCookie(c *gin.Context) { 109func GetCookie(c *gin.Context) {
@@ -111,7 +115,7 @@ func GetCookie(c *gin.Context) {
111 c.JSON(http.StatusOK, models.Response{ 115 c.JSON(http.StatusOK, models.Response{
112 Success: true, 116 Success: true,
113 Message: "Token cookie successfully retrieved.", 117 Message: "Token cookie successfully retrieved.",
114 Data: models.LoginResponse{ 118 Data: LoginResponse{
115 Token: cookie, 119 Token: cookie,
116 }, 120 },
117 }) 121 })
@@ -123,7 +127,7 @@ func GetCookie(c *gin.Context) {
123// @Tags auth 127// @Tags auth
124// @Produce json 128// @Produce json
125// 129//
126// @Success 200 {object} models.Response{data=models.LoginResponse} 130// @Success 200 {object} models.Response{data=LoginResponse}
127// @Failure 404 {object} models.Response 131// @Failure 404 {object} models.Response
128// @Router /token [delete] 132// @Router /token [delete]
129func DeleteCookie(c *gin.Context) { 133func DeleteCookie(c *gin.Context) {
@@ -136,7 +140,7 @@ func DeleteCookie(c *gin.Context) {
136 c.JSON(http.StatusOK, models.Response{ 140 c.JSON(http.StatusOK, models.Response{
137 Success: true, 141 Success: true,
138 Message: "Token cookie successfully deleted.", 142 Message: "Token cookie successfully deleted.",
139 Data: models.LoginResponse{ 143 Data: LoginResponse{
140 Token: cookie, 144 Token: cookie,
141 }, 145 },
142 }) 146 })
@@ -148,7 +152,7 @@ func GetPlayerSummaries(steamId, apiKey string) (*models.PlayerSummaries, error)
148 if err != nil { 152 if err != nil {
149 return nil, err 153 return nil, err
150 } 154 }
151 body, err := ioutil.ReadAll(resp.Body) 155 body, err := io.ReadAll(resp.Body)
152 if err != nil { 156 if err != nil {
153 return nil, err 157 return nil, err
154 } 158 }
diff --git a/backend/handlers/logs.go b/backend/handlers/logs.go
new file mode 100644
index 0000000..2b8223a
--- /dev/null
+++ b/backend/handlers/logs.go
@@ -0,0 +1,189 @@
1package handlers
2
3import (
4 "fmt"
5 "net/http"
6 "time"
7
8 "github.com/gin-gonic/gin"
9 "github.com/pektezol/leastportalshub/backend/database"
10 "github.com/pektezol/leastportalshub/backend/models"
11)
12
13const (
14 LogTypeMod string = "Mod"
15 LogTypeUser string = "User"
16 LogTypeRecord string = "Record"
17
18 LogDescriptionUserLoginSuccess string = "LoginSuccess"
19 LogDescriptionUserLoginFailToken string = "LoginTokenFail"
20 LogDescriptionUserLoginFailValidate string = "LoginValidateFail"
21 LogDescriptionUserLoginFailSummary string = "LoginSummaryFail"
22 LogDescriptionUserUpdateSuccess string = "UpdateSuccess"
23 LogDescriptionUserUpdateFail string = "UpdateFail"
24 LogDescriptionUserUpdateSummaryFail string = "UpdateSummaryFail"
25 LogDescriptionUserUpdateCountrySuccess string = "UpdateCountrySuccess"
26 LogDescriptionUserUpdateCountryFail string = "UpdateCountryFail"
27
28 LogDescriptionMapSummaryCreate string = "MapSummaryCreate"
29 LogDescriptionMapSummaryEdit string = "MapSummaryEdit"
30 LogDescriptionMapSummaryEditImage string = "MapSummaryEditImage"
31 LogDescriptionMapSummaryDelete string = "MapSummaryDelete"
32
33 LogDescriptionRecordSuccess string = "Success"
34 LogDescriptionRecordFailInsertRecord string = "InsertRecordFail"
35 LogDescriptionRecordFailInsertDemo string = "InsertDemoFail"
36 LogDescriptionRecordFailProcessDemo string = "ProcessDemoFail"
37 LogDescriptionRecordFailCreateDemo string = "CreateDemoFail"
38 LogDescriptionRecordFailOpenDemo string = "OpenDemoFail"
39 LogDescriptionRecordFailSaveDemo string = "SaveDemoFail"
40 LogDescriptionRecordFailInvalidRequest string = "InvalidRequestFail"
41)
42
43type Log struct {
44 User models.UserShort `json:"user"`
45 Type string `json:"type"`
46 Description string `json:"description"`
47 Date time.Time `json:"date"`
48}
49
50type LogsResponse struct {
51 Logs []LogsResponseDetails `json:"logs"`
52}
53
54type LogsResponseDetails struct {
55 User models.UserShort `json:"user"`
56 Log string `json:"detail"`
57 Date time.Time `json:"date"`
58}
59
60type ScoreLogsResponse struct {
61 Logs []ScoreLogsResponseDetails `json:"scores"`
62}
63
64type ScoreLogsResponseDetails struct {
65 Game models.Game `json:"game"`
66 User models.UserShort `json:"user"`
67 Map models.MapShort `json:"map"`
68 ScoreCount int `json:"score_count"`
69 ScoreTime int `json:"score_time"`
70 DemoID string `json:"demo_id"`
71 Date time.Time `json:"date"`
72}
73
74// GET Mod Logs
75//
76// @Description Get mod logs.
77// @Tags logs
78// @Produce json
79// @Param Authorization header string true "JWT Token"
80// @Success 200 {object} models.Response{data=LogsResponse}
81// @Failure 400 {object} models.Response
82// @Router /logs/mod [get]
83func ModLogs(c *gin.Context) {
84 mod, exists := c.Get("mod")
85 if !exists || !mod.(bool) {
86 c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions."))
87 return
88 }
89 response := LogsResponse{Logs: []LogsResponseDetails{}}
90 sql := `SELECT u.user_name, l.user_id, l.type, l.description, l.date
91 FROM logs l INNER JOIN users u ON l.user_id = u.steam_id WHERE type != 'Score'
92 ORDER BY l.date DESC LIMIT 100;`
93 rows, err := database.DB.Query(sql)
94 if err != nil {
95 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
96 return
97 }
98 for rows.Next() {
99 log := Log{}
100 err = rows.Scan(&log.User.UserName, &log.User.SteamID, &log.Type, &log.Description, &log.Date)
101 if err != nil {
102 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
103 return
104 }
105 detail := fmt.Sprintf("%s.%s", log.Type, log.Description)
106 response.Logs = append(response.Logs, LogsResponseDetails{
107 User: models.UserShort{
108 SteamID: log.User.SteamID,
109 UserName: log.User.UserName,
110 },
111 Log: detail,
112 Date: log.Date,
113 })
114 }
115 c.JSON(http.StatusOK, models.Response{
116 Success: true,
117 Message: "Successfully retrieved logs.",
118 Data: response,
119 })
120}
121
122// GET Score Logs
123//
124// @Description Get score logs of every player.
125// @Tags logs
126// @Produce json
127// @Success 200 {object} models.Response{data=ScoreLogsResponse}
128// @Failure 400 {object} models.Response
129// @Router /logs/score [get]
130func ScoreLogs(c *gin.Context) {
131 response := ScoreLogsResponse{Logs: []ScoreLogsResponseDetails{}}
132 sql := `SELECT g.id,
133 g."name",
134 g.is_coop,
135 rs.map_id,
136 m.name AS map_name,
137 u.steam_id,
138 u.user_name,
139 rs.score_count,
140 rs.score_time,
141 rs.demo_id,
142 rs.record_date
143 FROM (
144 SELECT id, map_id, user_id, score_count, score_time, demo_id, record_date
145 FROM records_sp
146
147 UNION ALL
148
149 SELECT id, map_id, host_id AS user_id, score_count, score_time, host_demo_id AS demo_id, record_date
150 FROM records_mp
151
152 UNION ALL
153
154 SELECT id, map_id, partner_id AS user_id, score_count, score_time, partner_demo_id AS demo_id, record_date
155 FROM records_mp
156 ) AS rs
157 JOIN users u ON rs.user_id = u.steam_id
158 JOIN maps m ON rs.map_id = m.id
159 JOIN games g ON m.game_id = g.id
160 ORDER BY rs.record_date DESC LIMIT 100;`
161 rows, err := database.DB.Query(sql)
162 if err != nil {
163 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
164 return
165 }
166 for rows.Next() {
167 score := ScoreLogsResponseDetails{}
168 err = rows.Scan(&score.Game.ID, &score.Game.Name, &score.Game.IsCoop, &score.Map.ID, &score.Map.Name, &score.User.SteamID, &score.User.UserName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date)
169 if err != nil {
170 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
171 return
172 }
173 response.Logs = append(response.Logs, score)
174 }
175 c.JSON(http.StatusOK, models.Response{
176 Success: true,
177 Message: "Successfully retrieved score logs.",
178 Data: response,
179 })
180}
181
182func CreateLog(user_id string, log_type string, log_description string) (err error) {
183 sql := `INSERT INTO logs (user_id, "type", description) VALUES($1, $2, $3)`
184 _, err = database.DB.Exec(sql, user_id, log_type, log_description)
185 if err != nil {
186 return err
187 }
188 return nil
189}
diff --git a/backend/controllers/mapController.go b/backend/handlers/map.go
index ebd65dd..1d9cee8 100644
--- a/backend/controllers/mapController.go
+++ b/backend/handlers/map.go
@@ -1,26 +1,69 @@
1package controllers 1package handlers
2 2
3import ( 3import (
4 "net/http" 4 "net/http"
5 "strconv" 5 "strconv"
6 "time"
6 7
7 "github.com/gin-gonic/gin" 8 "github.com/gin-gonic/gin"
8 "github.com/pektezol/leastportalshub/backend/database" 9 "github.com/pektezol/leastportalshub/backend/database"
9 "github.com/pektezol/leastportalshub/backend/models" 10 "github.com/pektezol/leastportalshub/backend/models"
10) 11)
11 12
13type MapSummaryResponse struct {
14 Map models.Map `json:"map"`
15 Summary models.MapSummary `json:"summary"`
16}
17
18type MapLeaderboardsResponse struct {
19 Map models.Map `json:"map"`
20 Records any `json:"records"`
21}
22
23type ChaptersResponse struct {
24 Game models.Game `json:"game"`
25 Chapters []models.Chapter `json:"chapters"`
26}
27
28type ChapterMapsResponse struct {
29 Chapter models.Chapter `json:"chapter"`
30 Maps []models.MapShort `json:"maps"`
31}
32
33type RecordSingleplayer struct {
34 Placement int `json:"placement"`
35 RecordID int `json:"record_id"`
36 ScoreCount int `json:"score_count"`
37 ScoreTime int `json:"score_time"`
38 User models.UserShortWithAvatar `json:"user"`
39 DemoID string `json:"demo_id"`
40 RecordDate time.Time `json:"record_date"`
41}
42
43type RecordMultiplayer struct {
44 Placement int `json:"placement"`
45 RecordID int `json:"record_id"`
46 ScoreCount int `json:"score_count"`
47 ScoreTime int `json:"score_time"`
48 Host models.UserShortWithAvatar `json:"host"`
49 Partner models.UserShortWithAvatar `json:"partner"`
50 HostDemoID string `json:"host_demo_id"`
51 PartnerDemoID string `json:"partner_demo_id"`
52 RecordDate time.Time `json:"record_date"`
53}
54
12// GET Map Summary 55// GET Map Summary
13// 56//
14// @Description Get map summary with specified id. 57// @Description Get map summary with specified id.
15// @Tags maps 58// @Tags maps
16// @Produce json 59// @Produce json
17// @Param id path int true "Map ID" 60// @Param id path int true "Map ID"
18// @Success 200 {object} models.Response{data=models.MapSummaryResponse} 61// @Success 200 {object} models.Response{data=MapSummaryResponse}
19// @Failure 400 {object} models.Response 62// @Failure 400 {object} models.Response
20// @Router /maps/{id}/summary [get] 63// @Router /maps/{id}/summary [get]
21func FetchMapSummary(c *gin.Context) { 64func FetchMapSummary(c *gin.Context) {
22 id := c.Param("id") 65 id := c.Param("id")
23 response := models.MapSummaryResponse{Map: models.Map{}, Summary: models.MapSummary{Routes: []models.MapRoute{}}} 66 response := MapSummaryResponse{Map: models.Map{}, Summary: models.MapSummary{Routes: []models.MapRoute{}}}
24 intID, err := strconv.Atoi(id) 67 intID, err := strconv.Atoi(id)
25 if err != nil { 68 if err != nil {
26 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 69 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
@@ -73,28 +116,29 @@ func FetchMapSummary(c *gin.Context) {
73// @Tags maps 116// @Tags maps
74// @Produce json 117// @Produce json
75// @Param id path int true "Map ID" 118// @Param id path int true "Map ID"
76// @Success 200 {object} models.Response{data=models.Map{data=models.MapRecords}} 119// @Success 200 {object} models.Response{data=MapLeaderboardsResponse}
77// @Failure 400 {object} models.Response 120// @Failure 400 {object} models.Response
78// @Router /maps/{id}/leaderboards [get] 121// @Router /maps/{id}/leaderboards [get]
79func FetchMapLeaderboards(c *gin.Context) { 122func FetchMapLeaderboards(c *gin.Context) {
80 // TODO: make new response type 123 // TODO: make new response type
81 id := c.Param("id") 124 id := c.Param("id")
82 // Get map data 125 // Get map data
83 var mapData models.Map 126 response := MapLeaderboardsResponse{Map: models.Map{}, Records: nil}
84 var mapRecordsData models.MapRecords 127 // var mapData models.Map
128 // var mapRecordsData models.MapRecords
85 var isDisabled bool 129 var isDisabled bool
86 intID, err := strconv.Atoi(id) 130 intID, err := strconv.Atoi(id)
87 if err != nil { 131 if err != nil {
88 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 132 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
89 return 133 return
90 } 134 }
91 mapData.ID = intID 135 response.Map.ID = intID
92 sql := `SELECT g.name, c.name, m.name, is_disabled, m.image 136 sql := `SELECT g.name, c.name, m.name, is_disabled, m.image, g.is_coop
93 FROM maps m 137 FROM maps m
94 INNER JOIN games g ON m.game_id = g.id 138 INNER JOIN games g ON m.game_id = g.id
95 INNER JOIN chapters c ON m.chapter_id = c.id 139 INNER JOIN chapters c ON m.chapter_id = c.id
96 WHERE m.id = $1` 140 WHERE m.id = $1`
97 err = database.DB.QueryRow(sql, id).Scan(&mapData.GameName, &mapData.ChapterName, &mapData.MapName, &isDisabled, &mapData.Image) 141 err = database.DB.QueryRow(sql, id).Scan(&response.Map.GameName, &response.Map.ChapterName, &response.Map.MapName, &isDisabled, &response.Map.Image, &response.Map.IsCoop)
98 if err != nil { 142 if err != nil {
99 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 143 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
100 return 144 return
@@ -104,17 +148,38 @@ func FetchMapLeaderboards(c *gin.Context) {
104 return 148 return
105 } 149 }
106 // TODO: avatar and names for host & partner 150 // TODO: avatar and names for host & partner
107 // Get records from the map 151 if response.Map.GameName == "Portal 2 - Cooperative" {
108 if mapData.GameName == "Portal 2 - Cooperative" { 152 records := []RecordMultiplayer{}
109 var records []models.RecordMP 153 sql = `SELECT
110 sql = `SELECT id, host_id, partner_id, score_count, score_time, host_demo_id, partner_demo_id, record_date 154 sub.id,
111 FROM ( 155 sub.host_id,
112 SELECT id, host_id, partner_id, score_count, score_time, host_demo_id, partner_demo_id, record_date, 156 host.user_name AS host_user_name,
113 ROW_NUMBER() OVER (PARTITION BY host_id, partner_id ORDER BY score_count, score_time) AS rn 157 host.avatar_link AS host_avatar_link,
114 FROM records_mp 158 sub.partner_id,
115 WHERE map_id = $1 159 partner.user_name AS partner_user_name,
116 ) sub 160 partner.avatar_link AS partner_avatar_link,
117 WHERE rn = 1` 161 sub.score_count,
162 sub.score_time,
163 sub.host_demo_id,
164 sub.partner_demo_id,
165 sub.record_date
166 FROM (
167 SELECT
168 id,
169 host_id,
170 partner_id,
171 score_count,
172 score_time,
173 host_demo_id,
174 partner_demo_id,
175 record_date,
176 ROW_NUMBER() OVER (PARTITION BY host_id, partner_id ORDER BY score_count, score_time) AS rn
177 FROM records_mp
178 WHERE map_id = $1
179 ) sub
180 JOIN users AS host ON sub.host_id = host.steam_id
181 JOIN users AS partner ON sub.partner_id = partner.steam_id
182 WHERE sub.rn = 1;`
118 rows, err := database.DB.Query(sql, id) 183 rows, err := database.DB.Query(sql, id)
119 if err != nil { 184 if err != nil {
120 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 185 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
@@ -123,8 +188,8 @@ func FetchMapLeaderboards(c *gin.Context) {
123 placement := 1 188 placement := 1
124 ties := 0 189 ties := 0
125 for rows.Next() { 190 for rows.Next() {
126 var record models.RecordMP 191 var record RecordMultiplayer
127 err := rows.Scan(&record.RecordID, &record.HostID, &record.PartnerID, &record.ScoreCount, &record.ScoreTime, &record.HostDemoID, &record.PartnerDemoID, &record.RecordDate) 192 err := rows.Scan(&record.RecordID, &record.Host.SteamID, &record.Host.UserName, &record.Host.AvatarLink, &record.Partner.SteamID, &record.Partner.UserName, &record.Partner.AvatarLink, &record.ScoreCount, &record.ScoreTime, &record.HostDemoID, &record.PartnerDemoID, &record.RecordDate)
128 if err != nil { 193 if err != nil {
129 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 194 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
130 return 195 return
@@ -138,9 +203,9 @@ func FetchMapLeaderboards(c *gin.Context) {
138 records = append(records, record) 203 records = append(records, record)
139 placement++ 204 placement++
140 } 205 }
141 mapRecordsData.Records = records 206 response.Records = records
142 } else { 207 } else {
143 var records []models.RecordSP 208 records := []RecordSingleplayer{}
144 sql = `SELECT id, user_id, users.user_name, users.avatar_link, score_count, score_time, demo_id, record_date 209 sql = `SELECT id, user_id, users.user_name, users.avatar_link, score_count, score_time, demo_id, record_date
145 FROM ( 210 FROM (
146 SELECT id, user_id, score_count, score_time, demo_id, record_date, 211 SELECT id, user_id, score_count, score_time, demo_id, record_date,
@@ -158,8 +223,8 @@ func FetchMapLeaderboards(c *gin.Context) {
158 placement := 1 223 placement := 1
159 ties := 0 224 ties := 0
160 for rows.Next() { 225 for rows.Next() {
161 var record models.RecordSP 226 var record RecordSingleplayer
162 err := rows.Scan(&record.RecordID, &record.UserID, &record.UserName, &record.UserAvatar, &record.ScoreCount, &record.ScoreTime, &record.DemoID, &record.RecordDate) 227 err := rows.Scan(&record.RecordID, &record.User.SteamID, &record.User.UserName, &record.User.AvatarLink, &record.ScoreCount, &record.ScoreTime, &record.DemoID, &record.RecordDate)
163 if err != nil { 228 if err != nil {
164 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 229 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
165 return 230 return
@@ -173,14 +238,12 @@ func FetchMapLeaderboards(c *gin.Context) {
173 records = append(records, record) 238 records = append(records, record)
174 placement++ 239 placement++
175 } 240 }
176 mapRecordsData.Records = records 241 response.Records = records
177 } 242 }
178 // mapData.Data = mapRecordsData
179 // Return response
180 c.JSON(http.StatusOK, models.Response{ 243 c.JSON(http.StatusOK, models.Response{
181 Success: true, 244 Success: true,
182 Message: "Successfully retrieved map leaderboards.", 245 Message: "Successfully retrieved map leaderboards.",
183 Data: mapData, 246 Data: response,
184 }) 247 })
185} 248}
186 249
@@ -220,7 +283,7 @@ func FetchGames(c *gin.Context) {
220// @Tags games & chapters 283// @Tags games & chapters
221// @Produce json 284// @Produce json
222// @Param id path int true "Game ID" 285// @Param id path int true "Game ID"
223// @Success 200 {object} models.Response{data=models.ChaptersResponse} 286// @Success 200 {object} models.Response{data=ChaptersResponse}
224// @Failure 400 {object} models.Response 287// @Failure 400 {object} models.Response
225// @Router /games/{id} [get] 288// @Router /games/{id} [get]
226func FetchChapters(c *gin.Context) { 289func FetchChapters(c *gin.Context) {
@@ -230,7 +293,7 @@ func FetchChapters(c *gin.Context) {
230 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 293 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
231 return 294 return
232 } 295 }
233 var response models.ChaptersResponse 296 var response ChaptersResponse
234 rows, err := database.DB.Query(`SELECT c.id, c.name, g.name FROM chapters c INNER JOIN games g ON c.game_id = g.id WHERE game_id = $1`, gameID) 297 rows, err := database.DB.Query(`SELECT c.id, c.name, g.name FROM chapters c INNER JOIN games g ON c.game_id = g.id WHERE game_id = $1`, gameID)
235 if err != nil { 298 if err != nil {
236 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 299 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
@@ -262,7 +325,7 @@ func FetchChapters(c *gin.Context) {
262// @Tags games & chapters 325// @Tags games & chapters
263// @Produce json 326// @Produce json
264// @Param id path int true "Chapter ID" 327// @Param id path int true "Chapter ID"
265// @Success 200 {object} models.Response{data=models.ChapterMapsResponse} 328// @Success 200 {object} models.Response{data=ChapterMapsResponse}
266// @Failure 400 {object} models.Response 329// @Failure 400 {object} models.Response
267// @Router /chapters/{id} [get] 330// @Router /chapters/{id} [get]
268func FetchChapterMaps(c *gin.Context) { 331func FetchChapterMaps(c *gin.Context) {
@@ -272,7 +335,7 @@ func FetchChapterMaps(c *gin.Context) {
272 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 335 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
273 return 336 return
274 } 337 }
275 var response models.ChapterMapsResponse 338 var response ChapterMapsResponse
276 rows, err := database.DB.Query(`SELECT m.id, m.name, c.name FROM maps m INNER JOIN chapters c ON m.chapter_id = c.id WHERE chapter_id = $1`, chapterID) 339 rows, err := database.DB.Query(`SELECT m.id, m.name, c.name FROM maps m INNER JOIN chapters c ON m.chapter_id = c.id WHERE chapter_id = $1`, chapterID)
277 if err != nil { 340 if err != nil {
278 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 341 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
diff --git a/backend/controllers/modController.go b/backend/handlers/mod.go
index e2add1f..9e93395 100644
--- a/backend/controllers/modController.go
+++ b/backend/handlers/mod.go
@@ -1,23 +1,50 @@
1package controllers 1package handlers
2 2
3import ( 3import (
4 "net/http" 4 "net/http"
5 "strconv" 5 "strconv"
6 "time"
6 7
7 "github.com/gin-gonic/gin" 8 "github.com/gin-gonic/gin"
8 "github.com/pektezol/leastportalshub/backend/database" 9 "github.com/pektezol/leastportalshub/backend/database"
9 "github.com/pektezol/leastportalshub/backend/models" 10 "github.com/pektezol/leastportalshub/backend/models"
10) 11)
11 12
13type CreateMapSummaryRequest struct {
14 CategoryID int `json:"category_id" binding:"required"`
15 Description string `json:"description" binding:"required"`
16 Showcase string `json:"showcase"`
17 UserName string `json:"user_name" binding:"required"`
18 ScoreCount *int `json:"score_count" binding:"required"`
19 RecordDate time.Time `json:"record_date" binding:"required"`
20}
21
22type EditMapSummaryRequest struct {
23 RouteID int `json:"route_id" binding:"required"`
24 Description string `json:"description" binding:"required"`
25 Showcase string `json:"showcase"`
26 UserName string `json:"user_name" binding:"required"`
27 ScoreCount int `json:"score_count" binding:"required"`
28 RecordDate time.Time `json:"record_date" binding:"required"`
29}
30
31type DeleteMapSummaryRequest struct {
32 RouteID int `json:"route_id" binding:"required"`
33}
34
35type EditMapImageRequest struct {
36 Image string `json:"image" binding:"required"`
37}
38
12// POST Map Summary 39// POST Map Summary
13// 40//
14// @Description Create map summary with specified map id. 41// @Description Create map summary with specified map id.
15// @Tags maps 42// @Tags maps
16// @Produce json 43// @Produce json
17// @Param Authorization header string true "JWT Token" 44// @Param Authorization header string true "JWT Token"
18// @Param id path int true "Map ID" 45// @Param id path int true "Map ID"
19// @Param request body models.CreateMapSummaryRequest true "Body" 46// @Param request body CreateMapSummaryRequest true "Body"
20// @Success 200 {object} models.Response{data=models.CreateMapSummaryRequest} 47// @Success 200 {object} models.Response{data=CreateMapSummaryRequest}
21// @Failure 400 {object} models.Response 48// @Failure 400 {object} models.Response
22// @Router /maps/{id}/summary [post] 49// @Router /maps/{id}/summary [post]
23func CreateMapSummary(c *gin.Context) { 50func CreateMapSummary(c *gin.Context) {
@@ -27,13 +54,8 @@ func CreateMapSummary(c *gin.Context) {
27 c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) 54 c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in."))
28 return 55 return
29 } 56 }
30 var moderator bool 57 mod, exists := c.Get("mod")
31 for _, title := range user.(models.User).Titles { 58 if !exists || !mod.(bool) {
32 if title == "Moderator" {
33 moderator = true
34 }
35 }
36 if !moderator {
37 c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) 59 c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions."))
38 return 60 return
39 } 61 }
@@ -44,7 +66,7 @@ func CreateMapSummary(c *gin.Context) {
44 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 66 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
45 return 67 return
46 } 68 }
47 var request models.CreateMapSummaryRequest 69 var request CreateMapSummaryRequest
48 if err := c.BindJSON(&request); err != nil { 70 if err := c.BindJSON(&request); err != nil {
49 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 71 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
50 return 72 return
@@ -87,7 +109,7 @@ func CreateMapSummary(c *gin.Context) {
87 c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) 109 c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error()))
88 return 110 return
89 } 111 }
90 // Return response 112 CreateLog(user.(models.User).SteamID, LogTypeMod, LogDescriptionMapSummaryCreate)
91 c.JSON(http.StatusOK, models.Response{ 113 c.JSON(http.StatusOK, models.Response{
92 Success: true, 114 Success: true,
93 Message: "Successfully created map summary.", 115 Message: "Successfully created map summary.",
@@ -100,10 +122,10 @@ func CreateMapSummary(c *gin.Context) {
100// @Description Edit map summary with specified map id. 122// @Description Edit map summary with specified map id.
101// @Tags maps 123// @Tags maps
102// @Produce json 124// @Produce json
103// @Param Authorization header string true "JWT Token" 125// @Param Authorization header string true "JWT Token"
104// @Param id path int true "Map ID" 126// @Param id path int true "Map ID"
105// @Param request body models.EditMapSummaryRequest true "Body" 127// @Param request body EditMapSummaryRequest true "Body"
106// @Success 200 {object} models.Response{data=models.EditMapSummaryRequest} 128// @Success 200 {object} models.Response{data=EditMapSummaryRequest}
107// @Failure 400 {object} models.Response 129// @Failure 400 {object} models.Response
108// @Router /maps/{id}/summary [put] 130// @Router /maps/{id}/summary [put]
109func EditMapSummary(c *gin.Context) { 131func EditMapSummary(c *gin.Context) {
@@ -113,13 +135,8 @@ func EditMapSummary(c *gin.Context) {
113 c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) 135 c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in."))
114 return 136 return
115 } 137 }
116 var moderator bool 138 mod, exists := c.Get("mod")
117 for _, title := range user.(models.User).Titles { 139 if !exists || !mod.(bool) {
118 if title == "Moderator" {
119 moderator = true
120 }
121 }
122 if !moderator {
123 c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) 140 c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions."))
124 return 141 return
125 } 142 }
@@ -130,7 +147,7 @@ func EditMapSummary(c *gin.Context) {
130 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 147 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
131 return 148 return
132 } 149 }
133 var request models.EditMapSummaryRequest 150 var request EditMapSummaryRequest
134 if err := c.BindJSON(&request); err != nil { 151 if err := c.BindJSON(&request); err != nil {
135 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 152 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
136 return 153 return
@@ -173,7 +190,7 @@ func EditMapSummary(c *gin.Context) {
173 c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) 190 c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error()))
174 return 191 return
175 } 192 }
176 // Return response 193 CreateLog(user.(models.User).SteamID, LogTypeMod, LogDescriptionMapSummaryEdit)
177 c.JSON(http.StatusOK, models.Response{ 194 c.JSON(http.StatusOK, models.Response{
178 Success: true, 195 Success: true,
179 Message: "Successfully updated map summary.", 196 Message: "Successfully updated map summary.",
@@ -186,10 +203,10 @@ func EditMapSummary(c *gin.Context) {
186// @Description Delete map summary with specified map id. 203// @Description Delete map summary with specified map id.
187// @Tags maps 204// @Tags maps
188// @Produce json 205// @Produce json
189// @Param Authorization header string true "JWT Token" 206// @Param Authorization header string true "JWT Token"
190// @Param id path int true "Map ID" 207// @Param id path int true "Map ID"
191// @Param request body models.DeleteMapSummaryRequest true "Body" 208// @Param request body DeleteMapSummaryRequest true "Body"
192// @Success 200 {object} models.Response{data=models.DeleteMapSummaryRequest} 209// @Success 200 {object} models.Response{data=DeleteMapSummaryRequest}
193// @Failure 400 {object} models.Response 210// @Failure 400 {object} models.Response
194// @Router /maps/{id}/summary [delete] 211// @Router /maps/{id}/summary [delete]
195func DeleteMapSummary(c *gin.Context) { 212func DeleteMapSummary(c *gin.Context) {
@@ -199,13 +216,8 @@ func DeleteMapSummary(c *gin.Context) {
199 c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) 216 c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in."))
200 return 217 return
201 } 218 }
202 var moderator bool 219 mod, exists := c.Get("mod")
203 for _, title := range user.(models.User).Titles { 220 if !exists || !mod.(bool) {
204 if title == "Moderator" {
205 moderator = true
206 }
207 }
208 if !moderator {
209 c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) 221 c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions."))
210 return 222 return
211 } 223 }
@@ -216,7 +228,7 @@ func DeleteMapSummary(c *gin.Context) {
216 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 228 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
217 return 229 return
218 } 230 }
219 var request models.DeleteMapSummaryRequest 231 var request DeleteMapSummaryRequest
220 if err := c.BindJSON(&request); err != nil { 232 if err := c.BindJSON(&request); err != nil {
221 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 233 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
222 return 234 return
@@ -263,7 +275,7 @@ func DeleteMapSummary(c *gin.Context) {
263 c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) 275 c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error()))
264 return 276 return
265 } 277 }
266 // Return response 278 CreateLog(user.(models.User).SteamID, LogTypeMod, LogDescriptionMapSummaryDelete)
267 c.JSON(http.StatusOK, models.Response{ 279 c.JSON(http.StatusOK, models.Response{
268 Success: true, 280 Success: true,
269 Message: "Successfully delete map summary.", 281 Message: "Successfully delete map summary.",
@@ -276,10 +288,10 @@ func DeleteMapSummary(c *gin.Context) {
276// @Description Edit map image with specified map id. 288// @Description Edit map image with specified map id.
277// @Tags maps 289// @Tags maps
278// @Produce json 290// @Produce json
279// @Param Authorization header string true "JWT Token" 291// @Param Authorization header string true "JWT Token"
280// @Param id path int true "Map ID" 292// @Param id path int true "Map ID"
281// @Param request body models.EditMapImageRequest true "Body" 293// @Param request body EditMapImageRequest true "Body"
282// @Success 200 {object} models.Response{data=models.EditMapImageRequest} 294// @Success 200 {object} models.Response{data=EditMapImageRequest}
283// @Failure 400 {object} models.Response 295// @Failure 400 {object} models.Response
284// @Router /maps/{id}/image [put] 296// @Router /maps/{id}/image [put]
285func EditMapImage(c *gin.Context) { 297func EditMapImage(c *gin.Context) {
@@ -289,13 +301,8 @@ func EditMapImage(c *gin.Context) {
289 c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) 301 c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in."))
290 return 302 return
291 } 303 }
292 var moderator bool 304 mod, exists := c.Get("mod")
293 for _, title := range user.(models.User).Titles { 305 if !exists || !mod.(bool) {
294 if title == "Moderator" {
295 moderator = true
296 }
297 }
298 if !moderator {
299 c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) 306 c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions."))
300 return 307 return
301 } 308 }
@@ -306,7 +313,7 @@ func EditMapImage(c *gin.Context) {
306 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 313 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
307 return 314 return
308 } 315 }
309 var request models.EditMapImageRequest 316 var request EditMapImageRequest
310 if err := c.BindJSON(&request); err != nil { 317 if err := c.BindJSON(&request); err != nil {
311 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 318 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
312 return 319 return
@@ -318,7 +325,7 @@ func EditMapImage(c *gin.Context) {
318 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 325 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
319 return 326 return
320 } 327 }
321 // Return response 328 CreateLog(user.(models.User).SteamID, LogTypeMod, LogDescriptionMapSummaryEditImage)
322 c.JSON(http.StatusOK, models.Response{ 329 c.JSON(http.StatusOK, models.Response{
323 Success: true, 330 Success: true,
324 Message: "Successfully updated map image.", 331 Message: "Successfully updated map image.",
diff --git a/backend/controllers/recordController.go b/backend/handlers/record.go
index 951be41..3d29eb8 100644
--- a/backend/controllers/recordController.go
+++ b/backend/handlers/record.go
@@ -1,4 +1,4 @@
1package controllers 1package handlers
2 2
3import ( 3import (
4 "context" 4 "context"
@@ -19,6 +19,18 @@ import (
19 "google.golang.org/api/drive/v3" 19 "google.golang.org/api/drive/v3"
20) 20)
21 21
22type RecordRequest struct {
23 HostDemo *multipart.FileHeader `json:"host_demo" form:"host_demo" binding:"required" swaggerignore:"true"`
24 PartnerDemo *multipart.FileHeader `json:"partner_demo" form:"partner_demo" swaggerignore:"true"`
25 IsPartnerOrange bool `json:"is_partner_orange" form:"is_partner_orange"`
26 PartnerID string `json:"partner_id" form:"partner_id"`
27}
28
29type RecordResponse struct {
30 ScoreCount int `json:"score_count"`
31 ScoreTime int `json:"score_time"`
32}
33
22// POST Record 34// POST Record
23// 35//
24// @Description Post record with demo of a specific map. 36// @Description Post record with demo of a specific map.
@@ -31,7 +43,7 @@ import (
31// @Param partner_demo formData file false "Partner Demo" 43// @Param partner_demo formData file false "Partner Demo"
32// @Param is_partner_orange formData boolean false "Is Partner Orange" 44// @Param is_partner_orange formData boolean false "Is Partner Orange"
33// @Param partner_id formData string false "Partner ID" 45// @Param partner_id formData string false "Partner ID"
34// @Success 200 {object} models.Response{data=models.RecordResponse} 46// @Success 200 {object} models.Response{data=RecordResponse}
35// @Failure 400 {object} models.Response 47// @Failure 400 {object} models.Response
36// @Failure 401 {object} models.Response 48// @Failure 401 {object} models.Response
37// @Router /maps/{id}/record [post] 49// @Router /maps/{id}/record [post]
@@ -54,6 +66,7 @@ func CreateRecordWithDemo(c *gin.Context) {
54 return 66 return
55 } 67 }
56 if isDisabled { 68 if isDisabled {
69 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInvalidRequest)
57 c.JSON(http.StatusBadRequest, models.ErrorResponse("Map is not available for competitive boards.")) 70 c.JSON(http.StatusBadRequest, models.ErrorResponse("Map is not available for competitive boards."))
58 return 71 return
59 } 72 }
@@ -61,12 +74,14 @@ func CreateRecordWithDemo(c *gin.Context) {
61 isCoop = true 74 isCoop = true
62 } 75 }
63 // Get record request 76 // Get record request
64 var record models.RecordRequest 77 var record RecordRequest
65 if err := c.ShouldBind(&record); err != nil { 78 if err := c.ShouldBind(&record); err != nil {
79 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInvalidRequest)
66 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 80 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
67 return 81 return
68 } 82 }
69 if isCoop && (record.PartnerDemo == nil || record.PartnerID == "") { 83 if isCoop && (record.PartnerDemo == nil || record.PartnerID == "") {
84 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInvalidRequest)
70 c.JSON(http.StatusBadRequest, models.ErrorResponse("Invalid entry for coop record submission.")) 85 c.JSON(http.StatusBadRequest, models.ErrorResponse("Invalid entry for coop record submission."))
71 return 86 return
72 } 87 }
@@ -96,23 +111,27 @@ func CreateRecordWithDemo(c *gin.Context) {
96 // Upload & insert into demos 111 // Upload & insert into demos
97 err = c.SaveUploadedFile(header, "backend/parser/"+uuid+".dem") 112 err = c.SaveUploadedFile(header, "backend/parser/"+uuid+".dem")
98 if err != nil { 113 if err != nil {
114 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailSaveDemo)
99 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 115 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
100 return 116 return
101 } 117 }
102 defer os.Remove("backend/parser/" + uuid + ".dem") 118 defer os.Remove("backend/parser/" + uuid + ".dem")
103 f, err := os.Open("backend/parser/" + uuid + ".dem") 119 f, err := os.Open("backend/parser/" + uuid + ".dem")
104 if err != nil { 120 if err != nil {
121 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailOpenDemo)
105 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 122 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
106 return 123 return
107 } 124 }
108 defer f.Close() 125 defer f.Close()
109 file, err := createFile(srv, uuid+".dem", "application/octet-stream", f, os.Getenv("GOOGLE_FOLDER_ID")) 126 file, err := createFile(srv, uuid+".dem", "application/octet-stream", f, os.Getenv("GOOGLE_FOLDER_ID"))
110 if err != nil { 127 if err != nil {
128 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailCreateDemo)
111 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 129 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
112 return 130 return
113 } 131 }
114 hostDemoScoreCount, hostDemoScoreTime, err = parser.ProcessDemo("backend/parser/" + uuid + ".dem") 132 hostDemoScoreCount, hostDemoScoreTime, err = parser.ProcessDemo("backend/parser/" + uuid + ".dem")
115 if err != nil { 133 if err != nil {
134 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailProcessDemo)
116 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 135 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
117 return 136 return
118 } 137 }
@@ -126,6 +145,7 @@ func CreateRecordWithDemo(c *gin.Context) {
126 _, err = tx.Exec(`INSERT INTO demos (id,location_id) VALUES ($1,$2)`, uuid, file.Id) 145 _, err = tx.Exec(`INSERT INTO demos (id,location_id) VALUES ($1,$2)`, uuid, file.Id)
127 if err != nil { 146 if err != nil {
128 deleteFile(srv, file.Id) 147 deleteFile(srv, file.Id)
148 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInsertDemo)
129 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 149 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
130 return 150 return
131 } 151 }
@@ -147,6 +167,7 @@ func CreateRecordWithDemo(c *gin.Context) {
147 if err != nil { 167 if err != nil {
148 deleteFile(srv, hostDemoFileID) 168 deleteFile(srv, hostDemoFileID)
149 deleteFile(srv, partnerDemoFileID) 169 deleteFile(srv, partnerDemoFileID)
170 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInsertRecord)
150 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 171 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
151 return 172 return
152 } 173 }
@@ -164,6 +185,7 @@ func CreateRecordWithDemo(c *gin.Context) {
164 _, err := tx.Exec(sql, mapId, hostDemoScoreCount, hostDemoScoreTime, user.(models.User).SteamID, hostDemoUUID) 185 _, err := tx.Exec(sql, mapId, hostDemoScoreCount, hostDemoScoreTime, user.(models.User).SteamID, hostDemoUUID)
165 if err != nil { 186 if err != nil {
166 deleteFile(srv, hostDemoFileID) 187 deleteFile(srv, hostDemoFileID)
188 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInsertRecord)
167 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) 189 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
168 return 190 return
169 } 191 }
@@ -180,10 +202,11 @@ func CreateRecordWithDemo(c *gin.Context) {
180 c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) 202 c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error()))
181 return 203 return
182 } 204 }
205 CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordSuccess)
183 c.JSON(http.StatusOK, models.Response{ 206 c.JSON(http.StatusOK, models.Response{
184 Success: true, 207 Success: true,
185 Message: "Successfully created record.", 208 Message: "Successfully created record.",
186 Data: models.RecordResponse{ScoreCount: hostDemoScoreCount, ScoreTime: hostDemoScoreTime}, 209 Data: RecordResponse{ScoreCount: hostDemoScoreCount, ScoreTime: hostDemoScoreTime},
187 }) 210 })
188} 211}
189 212
@@ -216,6 +239,10 @@ func DownloadDemoWithID(c *gin.Context) {
216 url := "https://drive.google.com/uc?export=download&id=" + locationID 239 url := "https://drive.google.com/uc?export=download&id=" + locationID
217 fileName := uuid + ".dem" 240 fileName := uuid + ".dem"
218 output, err := os.Create(fileName) 241 output, err := os.Create(fileName)
242 if err != nil {
243 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
244 return
245 }
219 defer os.Remove(fileName) 246 defer os.Remove(fileName)
220 defer output.Close() 247 defer output.Close()
221 response, err := http.Get(url) 248 response, err := http.Get(url)
@@ -253,6 +280,7 @@ func serviceAccount() *http.Client {
253 return client 280 return client
254} 281}
255 282
283// Create Gdrive file
256func createFile(service *drive.Service, name string, mimeType string, content io.Reader, parentId string) (*drive.File, error) { 284func createFile(service *drive.Service, name string, mimeType string, content io.Reader, parentId string) (*drive.File, error) {
257 f := &drive.File{ 285 f := &drive.File{
258 MimeType: mimeType, 286 MimeType: mimeType,
@@ -269,6 +297,7 @@ func createFile(service *drive.Service, name string, mimeType string, content io
269 return file, nil 297 return file, nil
270} 298}
271 299
300// Delete Gdrive file
272func deleteFile(service *drive.Service, fileId string) { 301func deleteFile(service *drive.Service, fileId string) {
273 service.Files.Delete(fileId) 302 service.Files.Delete(fileId)
274} 303}
diff --git a/backend/handlers/user.go b/backend/handlers/user.go
new file mode 100644
index 0000000..742a57c
--- /dev/null
+++ b/backend/handlers/user.go
@@ -0,0 +1,719 @@
1package handlers
2
3import (
4 "net/http"
5 "os"
6 "regexp"
7 "time"
8
9 "github.com/gin-gonic/gin"
10 "github.com/pektezol/leastportalshub/backend/database"
11 "github.com/pektezol/leastportalshub/backend/models"
12)
13
14type ProfileResponse struct {
15 Profile bool `json:"profile"`
16 SteamID string `json:"steam_id"`
17 UserName string `json:"user_name"`
18 AvatarLink string `json:"avatar_link"`
19 CountryCode string `json:"country_code"`
20 Titles []models.Title `json:"titles"`
21 Links models.Links `json:"links"`
22 Rankings ProfileRankings `json:"rankings"`
23 Records []ProfileRecords `json:"records"`
24}
25
26type ProfileRankings struct {
27 Overall ProfileRankingsDetails `json:"overall"`
28 Singleplayer ProfileRankingsDetails `json:"singleplayer"`
29 Cooperative ProfileRankingsDetails `json:"cooperative"`
30}
31
32type ProfileRankingsDetails struct {
33 Rank int `json:"rank"`
34 CompletionCount int `json:"completion_count"`
35 CompletionTotal int `json:"completion_total"`
36}
37type ProfileRecords struct {
38 GameID int `json:"game_id"`
39 CategoryID int `json:"category_id"`
40 MapID int `json:"map_id"`
41 MapName string `json:"map_name"`
42 MapWRCount int `json:"map_wr_count"`
43 Scores []ProfileScores `json:"scores"`
44}
45
46type ProfileScores struct {
47 DemoID string `json:"demo_id"`
48 ScoreCount int `json:"score_count"`
49 ScoreTime int `json:"score_time"`
50 Date time.Time `json:"date"`
51}
52
53type ScoreResponse struct {
54 MapID int `json:"map_id"`
55 Records any `json:"records"`
56}
57
58// GET Profile
59//
60// @Description Get profile page of session user.
61// @Tags users
62// @Accept json
63// @Produce json
64// @Param Authorization header string true "JWT Token"
65// @Success 200 {object} models.Response{data=ProfileResponse}
66// @Failure 400 {object} models.Response
67// @Failure 401 {object} models.Response
68// @Router /profile [get]
69func Profile(c *gin.Context) {
70 // Check if user exists
71 user, exists := c.Get("user")
72 if !exists {
73 c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in."))
74 return
75 }
76 // Get user links
77 links := models.Links{}
78 sql := `SELECT u.p2sr, u.steam, u.youtube, u.twitch FROM users u WHERE u.steam_id = $1`
79 err := database.DB.QueryRow(sql, user.(models.User).SteamID).Scan(&links.P2SR, &links.Steam, &links.YouTube, &links.Twitch)
80 if err != nil {
81 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
82 return
83 }
84 // Get rankings (all maps done in one game)
85 rankings := ProfileRankings{
86 Overall: ProfileRankingsDetails{},
87 Singleplayer: ProfileRankingsDetails{},
88 Cooperative: ProfileRankingsDetails{},
89 }
90 // Get total map count
91 sql = `SELECT count(id), (SELECT count(id) FROM maps m WHERE m.game_id = 2 AND m.is_disabled = false) FROM maps m WHERE m.game_id = 1 AND m.is_disabled = false;`
92 err = database.DB.QueryRow(sql).Scan(&rankings.Singleplayer.CompletionTotal, &rankings.Cooperative.CompletionTotal)
93 if err != nil {
94 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
95 return
96 }
97 rankings.Overall.CompletionTotal = rankings.Singleplayer.CompletionTotal + rankings.Cooperative.CompletionTotal
98 // Get user completion count
99 sql = `SELECT 'records_sp' AS table_name, COUNT(rs.id) AS total_user_scores
100 FROM public.records_sp rs JOIN (
101 SELECT mr.map_id, MIN(mr.score_count) AS min_score_count
102 FROM public.map_routes mr WHERE mr.category_id = 1 GROUP BY mr.map_id
103 ) AS subquery_sp ON rs.map_id = subquery_sp.map_id AND rs.score_count = subquery_sp.min_score_count
104 WHERE rs.user_id = $1
105 UNION ALL
106 SELECT 'records_mp' AS table_name, COUNT(rm.id) AS total_user_scores
107 FROM public.records_mp rm JOIN (
108 SELECT mr.map_id, MIN(mr.score_count) AS min_score_count
109 FROM public.map_routes mr WHERE mr.category_id = 1 GROUP BY mr.map_id
110 ) AS subquery_mp ON rm.map_id = subquery_mp.map_id AND rm.score_count = subquery_mp.min_score_count
111 WHERE rm.host_id = $1 OR rm.partner_id = $1;`
112 rows, err := database.DB.Query(sql, user.(models.User).SteamID)
113 if err != nil {
114 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
115 return
116 }
117 for rows.Next() {
118 var tableName string
119 var completionCount int
120 err = rows.Scan(&tableName, &completionCount)
121 if err != nil {
122 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
123 return
124 }
125 if tableName == "records_sp" {
126 rankings.Singleplayer.CompletionCount = completionCount
127 continue
128 }
129 if tableName == "records_mp" {
130 rankings.Cooperative.CompletionCount = completionCount
131 continue
132 }
133 }
134 rankings.Overall.CompletionCount = rankings.Singleplayer.CompletionCount + rankings.Cooperative.CompletionCount
135 // Get user ranking placement for singleplayer
136 sql = `SELECT u.steam_id, COUNT(DISTINCT map_id),
137 (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false),
138 (SELECT SUM(min_score_count) AS total_min_score_count FROM (
139 SELECT user_id, MIN(score_count) AS min_score_count FROM records_sp GROUP BY user_id, map_id) AS subquery WHERE user_id = u.steam_id)
140 FROM records_sp sp JOIN users u ON u.steam_id = sp.user_id GROUP BY u.steam_id, u.user_name
141 HAVING COUNT(DISTINCT map_id) = (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false)
142 ORDER BY total_min_score_count ASC;`
143 rows, err = database.DB.Query(sql)
144 if err != nil {
145 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
146 return
147 }
148 placement := 1
149 for rows.Next() {
150 var steamID string
151 var completionCount int
152 var totalCount int
153 var userPortalCount int
154 err = rows.Scan(&steamID, &completionCount, &totalCount, &userPortalCount)
155 if err != nil {
156 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
157 return
158 }
159 if completionCount != totalCount {
160 placement++
161 continue
162 }
163 if steamID != user.(models.User).SteamID {
164 placement++
165 continue
166 }
167 rankings.Singleplayer.Rank = placement
168 }
169 // Get user ranking placement for multiplayer
170 sql = `SELECT u.steam_id, COUNT(DISTINCT map_id),
171 (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false),
172 (SELECT SUM(min_score_count) AS total_min_score_count FROM (
173 SELECT host_id, partner_id, MIN(score_count) AS min_score_count FROM records_mp GROUP BY host_id, partner_id, map_id) AS subquery WHERE host_id = u.steam_id OR partner_id = u.steam_id)
174 FROM records_mp mp JOIN users u ON u.steam_id = mp.host_id OR u.steam_id = mp.partner_id GROUP BY u.steam_id, u.user_name
175 HAVING COUNT(DISTINCT map_id) = (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false)
176 ORDER BY total_min_score_count ASC;`
177 rows, err = database.DB.Query(sql)
178 if err != nil {
179 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
180 return
181 }
182 placement = 1
183 for rows.Next() {
184 var steamID string
185 var completionCount int
186 var totalCount int
187 var userPortalCount int
188 err = rows.Scan(&steamID, &completionCount, &totalCount, &userPortalCount)
189 if err != nil {
190 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
191 return
192 }
193 if completionCount != totalCount {
194 placement++
195 continue
196 }
197 if steamID != user.(models.User).SteamID {
198 placement++
199 continue
200 }
201 rankings.Cooperative.Rank = placement
202 }
203 // TODO: Get user ranking placement for overall if they qualify
204 // if (rankings.Singleplayer.Rank != 0) && (rankings.Cooperative.Rank != 0) {
205 // sql = `SELECT steam_id, SUM(total_min_score_count) AS total_score
206 // FROM (
207 // SELECT u.steam_id,
208 // (SELECT SUM(min_score_count) AS total_min_score_count FROM (
209 // SELECT
210 // user_id,
211 // MIN(score_count) AS min_score_count
212 // FROM records_sp
213 // GROUP BY user_id, map_id
214 // ) AS subquery
215 // WHERE user_id = u.steam_id) AS total_min_score_count
216 // FROM records_sp sp
217 // JOIN users u ON u.steam_id = sp.user_id
218 // UNION ALL
219 // SELECT u.steam_id,
220 // (SELECT SUM(min_score_count) AS total_min_score_count FROM (
221 // SELECT
222 // host_id,
223 // partner_id,
224 // MIN(score_count) AS min_score_count
225 // FROM records_mp
226 // GROUP BY host_id, partner_id, map_id
227 // ) AS subquery
228 // WHERE host_id = u.steam_id OR partner_id = u.steam_id) AS total_min_score_count
229 // FROM records_mp mp
230 // JOIN users u ON u.steam_id = mp.host_id OR u.steam_id = mp.partner_id
231 // ) AS combined_scores
232 // GROUP BY steam_id ORDER BY total_score ASC;`
233 // rows, err = database.DB.Query(sql)
234 // if err != nil {
235 // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
236 // return
237 // }
238 // placement = 1
239 // for rows.Next() {
240 // var steamID string
241 // var userPortalCount int
242 // err = rows.Scan(&steamID, &userPortalCount)
243 // if err != nil {
244 // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
245 // return
246 // }
247 // if completionCount != totalCount {
248 // placement++
249 // continue
250 // }
251 // if steamID != user.(models.User).SteamID {
252 // placement++
253 // continue
254 // }
255 // rankings.Cooperative.Rank = placement
256 // }
257 // }
258 records := []ProfileRecords{}
259 // Get singleplayer records
260 sql = `SELECT m.game_id, m.chapter_id, sp.map_id, m."name", (SELECT mr.score_count FROM map_routes mr WHERE mr.map_id = sp.map_id ORDER BY mr.score_count ASC LIMIT 1) AS wr_count, sp.score_count, sp.score_time, sp.demo_id, sp.record_date
261 FROM records_sp sp INNER JOIN maps m ON sp.map_id = m.id WHERE sp.user_id = $1 ORDER BY sp.map_id, sp.score_count, sp.score_time;`
262 rows, err = database.DB.Query(sql, user.(models.User).SteamID)
263 if err != nil {
264 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
265 return
266 }
267 for rows.Next() {
268 var gameID int
269 var categoryID int
270 var mapID int
271 var mapName string
272 var mapWR int
273 score := ProfileScores{}
274 rows.Scan(&gameID, &categoryID, &mapID, &mapName, &mapWR, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date)
275 // More than one record in one map
276 if len(records) != 0 && mapID == records[len(records)-1].MapID {
277 records[len(records)-1].Scores = append(records[len(records)-1].Scores, score)
278 continue
279 }
280 // New map
281 records = append(records, ProfileRecords{
282 GameID: gameID,
283 CategoryID: categoryID,
284 MapID: mapID,
285 MapName: mapName,
286 MapWRCount: mapWR,
287 Scores: []ProfileScores{},
288 })
289 records[len(records)-1].Scores = append(records[len(records)-1].Scores, score)
290 }
291 // Get multiplayer records
292 sql = `SELECT m.game_id, m.chapter_id, mp.map_id, m."name", (SELECT mr.score_count FROM map_routes mr WHERE mr.map_id = mp.map_id ORDER BY mr.score_count ASC LIMIT 1) AS wr_count, mp.score_count, mp.score_time, CASE WHEN host_id = $1 THEN mp.host_demo_id WHEN partner_id = $1 THEN mp.partner_demo_id END demo_id, mp.record_date
293 FROM records_mp mp INNER JOIN maps m ON mp.map_id = m.id WHERE mp.host_id = $1 OR mp.partner_id = $1 ORDER BY mp.map_id, mp.score_count, mp.score_time;`
294 rows, err = database.DB.Query(sql, user.(models.User).SteamID)
295 if err != nil {
296 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
297 return
298 }
299 for rows.Next() {
300 var gameID int
301 var categoryID int
302 var mapID int
303 var mapName string
304 var mapWR int
305 score := ProfileScores{}
306 rows.Scan(&gameID, &categoryID, &mapID, &mapName, &mapWR, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date)
307 // More than one record in one map
308 if len(records) != 0 && mapID == records[len(records)-1].MapID {
309 records[len(records)-1].Scores = append(records[len(records)-1].Scores, score)
310 continue
311 }
312 // New map
313 records = append(records, ProfileRecords{
314 GameID: gameID,
315 CategoryID: categoryID,
316 MapID: mapID,
317 MapName: mapName,
318 MapWRCount: mapWR,
319 Scores: []ProfileScores{},
320 })
321 records[len(records)-1].Scores = append(records[len(records)-1].Scores, score)
322 }
323 c.JSON(http.StatusOK, models.Response{
324 Success: true,
325 Message: "Successfully retrieved user scores.",
326 Data: ProfileResponse{
327 Profile: true,
328 SteamID: user.(models.User).SteamID,
329 UserName: user.(models.User).UserName,
330 AvatarLink: user.(models.User).AvatarLink,
331 CountryCode: user.(models.User).CountryCode,
332 Titles: user.(models.User).Titles,
333 Links: links,
334 Rankings: rankings,
335 Records: records,
336 },
337 })
338}
339
340// GET User
341//
342// @Description Get profile page of another user.
343// @Tags users
344// @Accept json
345// @Produce json
346// @Param id path int true "User ID"
347// @Success 200 {object} models.Response{data=ProfileResponse}
348// @Failure 400 {object} models.Response
349// @Failure 404 {object} models.Response
350// @Router /users/{id} [get]
351func FetchUser(c *gin.Context) {
352 id := c.Param("id")
353 // Check if id is all numbers and 17 length
354 match, _ := regexp.MatchString("^[0-9]{17}$", id)
355 if !match {
356 c.JSON(http.StatusNotFound, models.ErrorResponse("User not found."))
357 return
358 }
359 // Check if user exists
360 var user models.User
361 links := models.Links{}
362 sql := `SELECT u.steam_id, u.user_name, u.avatar_link, u.country_code, u.created_at, u.updated_at, u.p2sr, u.steam, u.youtube, u.twitch FROM users u WHERE u.steam_id = $1`
363 err := database.DB.QueryRow(sql, id).Scan(&user.SteamID, &user.UserName, &user.AvatarLink, &user.CountryCode, &user.CreatedAt, &user.UpdatedAt, &links.P2SR, &links.Steam, &links.YouTube, &links.Twitch)
364 if err != nil {
365 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
366 return
367 }
368 if user.SteamID == "" {
369 // User does not exist
370 c.JSON(http.StatusNotFound, models.ErrorResponse("User not found."))
371 return
372 }
373 // Get rankings (all maps done in one game)
374 rankings := ProfileRankings{
375 Overall: ProfileRankingsDetails{},
376 Singleplayer: ProfileRankingsDetails{},
377 Cooperative: ProfileRankingsDetails{},
378 }
379 // Get total map count
380 sql = `SELECT count(id), (SELECT count(id) FROM maps m WHERE m.game_id = 2 AND m.is_disabled = false) FROM maps m WHERE m.game_id = 1 AND m.is_disabled = false;`
381 err = database.DB.QueryRow(sql).Scan(&rankings.Singleplayer.CompletionTotal, &rankings.Cooperative.CompletionTotal)
382 if err != nil {
383 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
384 return
385 }
386 rankings.Overall.CompletionTotal = rankings.Singleplayer.CompletionTotal + rankings.Cooperative.CompletionTotal
387 // Get user completion count
388 sql = `SELECT 'records_sp' AS table_name, COUNT(rs.id) AS total_user_scores
389 FROM public.records_sp rs JOIN (
390 SELECT mr.map_id, MIN(mr.score_count) AS min_score_count
391 FROM public.map_routes mr WHERE mr.category_id = 1 GROUP BY mr.map_id
392 ) AS subquery_sp ON rs.map_id = subquery_sp.map_id AND rs.score_count = subquery_sp.min_score_count
393 WHERE rs.user_id = $1
394 UNION ALL
395 SELECT 'records_mp' AS table_name, COUNT(rm.id) AS total_user_scores
396 FROM public.records_mp rm JOIN (
397 SELECT mr.map_id, MIN(mr.score_count) AS min_score_count
398 FROM public.map_routes mr WHERE mr.category_id = 1 GROUP BY mr.map_id
399 ) AS subquery_mp ON rm.map_id = subquery_mp.map_id AND rm.score_count = subquery_mp.min_score_count
400 WHERE rm.host_id = $1 OR rm.partner_id = $1;`
401 rows, err := database.DB.Query(sql, user.SteamID)
402 if err != nil {
403 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
404 return
405 }
406 for rows.Next() {
407 var tableName string
408 var completionCount int
409 err = rows.Scan(&tableName, &completionCount)
410 if err != nil {
411 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
412 return
413 }
414 if tableName == "records_sp" {
415 rankings.Singleplayer.CompletionCount = completionCount
416 continue
417 }
418 if tableName == "records_mp" {
419 rankings.Cooperative.CompletionCount = completionCount
420 continue
421 }
422 }
423 rankings.Overall.CompletionCount = rankings.Singleplayer.CompletionCount + rankings.Cooperative.CompletionCount
424 // Get user ranking placement for singleplayer
425 sql = `SELECT u.steam_id, COUNT(DISTINCT map_id),
426 (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false),
427 (SELECT SUM(min_score_count) AS total_min_score_count FROM (
428 SELECT user_id, MIN(score_count) AS min_score_count FROM records_sp GROUP BY user_id, map_id) AS subquery WHERE user_id = u.steam_id)
429 FROM records_sp sp JOIN users u ON u.steam_id = sp.user_id GROUP BY u.steam_id, u.user_name
430 HAVING COUNT(DISTINCT map_id) = (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false)
431 ORDER BY total_min_score_count ASC;`
432 rows, err = database.DB.Query(sql)
433 if err != nil {
434 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
435 return
436 }
437 placement := 1
438 for rows.Next() {
439 var steamID string
440 var completionCount int
441 var totalCount int
442 var userPortalCount int
443 err = rows.Scan(&steamID, &completionCount, &totalCount, &userPortalCount)
444 if err != nil {
445 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
446 return
447 }
448 if completionCount != totalCount {
449 placement++
450 continue
451 }
452 if steamID != user.SteamID {
453 placement++
454 continue
455 }
456 rankings.Singleplayer.Rank = placement
457 }
458 // Get user ranking placement for multiplayer
459 sql = `SELECT u.steam_id, COUNT(DISTINCT map_id),
460 (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false),
461 (SELECT SUM(min_score_count) AS total_min_score_count FROM (
462 SELECT host_id, partner_id, MIN(score_count) AS min_score_count FROM records_mp GROUP BY host_id, partner_id, map_id) AS subquery WHERE host_id = u.steam_id OR partner_id = u.steam_id)
463 FROM records_mp mp JOIN users u ON u.steam_id = mp.host_id OR u.steam_id = mp.partner_id GROUP BY u.steam_id, u.user_name
464 HAVING COUNT(DISTINCT map_id) = (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false)
465 ORDER BY total_min_score_count ASC;`
466 rows, err = database.DB.Query(sql)
467 if err != nil {
468 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
469 return
470 }
471 placement = 1
472 for rows.Next() {
473 var steamID string
474 var completionCount int
475 var totalCount int
476 var userPortalCount int
477 err = rows.Scan(&steamID, &completionCount, &totalCount, &userPortalCount)
478 if err != nil {
479 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
480 return
481 }
482 if completionCount != totalCount {
483 placement++
484 continue
485 }
486 if steamID != user.SteamID {
487 placement++
488 continue
489 }
490 rankings.Cooperative.Rank = placement
491 }
492 // TODO: Get user ranking placement for overall if they qualify
493 // if (rankings.Singleplayer.Rank != 0) && (rankings.Cooperative.Rank != 0) {
494 // sql = `SELECT steam_id, SUM(total_min_score_count) AS total_score
495 // FROM (
496 // SELECT u.steam_id,
497 // (SELECT SUM(min_score_count) AS total_min_score_count FROM (
498 // SELECT
499 // user_id,
500 // MIN(score_count) AS min_score_count
501 // FROM records_sp
502 // GROUP BY user_id, map_id
503 // ) AS subquery
504 // WHERE user_id = u.steam_id) AS total_min_score_count
505 // FROM records_sp sp
506 // JOIN users u ON u.steam_id = sp.user_id
507 // UNION ALL
508 // SELECT u.steam_id,
509 // (SELECT SUM(min_score_count) AS total_min_score_count FROM (
510 // SELECT
511 // host_id,
512 // partner_id,
513 // MIN(score_count) AS min_score_count
514 // FROM records_mp
515 // GROUP BY host_id, partner_id, map_id
516 // ) AS subquery
517 // WHERE host_id = u.steam_id OR partner_id = u.steam_id) AS total_min_score_count
518 // FROM records_mp mp
519 // JOIN users u ON u.steam_id = mp.host_id OR u.steam_id = mp.partner_id
520 // ) AS combined_scores
521 // GROUP BY steam_id ORDER BY total_score ASC;`
522 // rows, err = database.DB.Query(sql)
523 // if err != nil {
524 // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
525 // return
526 // }
527 // placement = 1
528 // for rows.Next() {
529 // var steamID string
530 // var userPortalCount int
531 // err = rows.Scan(&steamID, &userPortalCount)
532 // if err != nil {
533 // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
534 // return
535 // }
536 // if completionCount != totalCount {
537 // placement++
538 // continue
539 // }
540 // if steamID != user.SteamID {
541 // placement++
542 // continue
543 // }
544 // rankings.Cooperative.Rank = placement
545 // }
546 // }
547 records := []ProfileRecords{}
548 // Get singleplayer records
549 sql = `SELECT m.game_id, m.chapter_id, sp.map_id, m."name", (SELECT mr.score_count FROM map_routes mr WHERE mr.map_id = sp.map_id ORDER BY mr.score_count ASC LIMIT 1) AS wr_count, sp.score_count, sp.score_time, sp.demo_id, sp.record_date
550 FROM records_sp sp INNER JOIN maps m ON sp.map_id = m.id WHERE sp.user_id = $1 ORDER BY sp.map_id, sp.score_count, sp.score_time;`
551 rows, err = database.DB.Query(sql, user.SteamID)
552 if err != nil {
553 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
554 return
555 }
556 for rows.Next() {
557 var gameID int
558 var categoryID int
559 var mapID int
560 var mapName string
561 var mapWR int
562 score := ProfileScores{}
563 rows.Scan(&gameID, &categoryID, &mapID, &mapName, &mapWR, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date)
564 // More than one record in one map
565 if len(records) != 0 && mapID == records[len(records)-1].MapID {
566 records[len(records)-1].Scores = append(records[len(records)-1].Scores, score)
567 continue
568 }
569 // New map
570 records = append(records, ProfileRecords{
571 GameID: gameID,
572 CategoryID: categoryID,
573 MapID: mapID,
574 MapName: mapName,
575 MapWRCount: mapWR,
576 Scores: []ProfileScores{},
577 })
578 records[len(records)-1].Scores = append(records[len(records)-1].Scores, score)
579 }
580 // Get multiplayer records
581 sql = `SELECT m.game_id, m.chapter_id, mp.map_id, m."name", (SELECT mr.score_count FROM map_routes mr WHERE mr.map_id = mp.map_id ORDER BY mr.score_count ASC LIMIT 1) AS wr_count, mp.score_count, mp.score_time, CASE WHEN host_id = $1 THEN mp.host_demo_id WHEN partner_id = $1 THEN mp.partner_demo_id END demo_id, mp.record_date
582 FROM records_mp mp INNER JOIN maps m ON mp.map_id = m.id WHERE mp.host_id = $1 OR mp.partner_id = $1 ORDER BY mp.map_id, mp.score_count, mp.score_time;`
583 rows, err = database.DB.Query(sql, user.SteamID)
584 if err != nil {
585 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
586 return
587 }
588 for rows.Next() {
589 var gameID int
590 var categoryID int
591 var mapID int
592 var mapName string
593 var mapWR int
594 score := ProfileScores{}
595 rows.Scan(&gameID, &categoryID, &mapID, &mapName, &mapWR, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date)
596 // More than one record in one map
597 if len(records) != 0 && mapID == records[len(records)-1].MapID {
598 records[len(records)-1].Scores = append(records[len(records)-1].Scores, score)
599 continue
600 }
601 // New map
602 records = append(records, ProfileRecords{
603 GameID: gameID,
604 CategoryID: categoryID,
605 MapID: mapID,
606 MapName: mapName,
607 MapWRCount: mapWR,
608 Scores: []ProfileScores{},
609 })
610 records[len(records)-1].Scores = append(records[len(records)-1].Scores, score)
611 }
612 c.JSON(http.StatusOK, models.Response{
613 Success: true,
614 Message: "Successfully retrieved user scores.",
615 Data: ProfileResponse{
616 Profile: true,
617 SteamID: user.SteamID,
618 UserName: user.UserName,
619 AvatarLink: user.AvatarLink,
620 CountryCode: user.CountryCode,
621 Titles: user.Titles,
622 Links: links,
623 Rankings: rankings,
624 Records: records,
625 },
626 })
627}
628
629// PUT Profile
630//
631// @Description Update profile page of session user.
632// @Tags users
633// @Accept json
634// @Produce json
635// @Param Authorization header string true "JWT Token"
636// @Success 200 {object} models.Response{data=ProfileResponse}
637// @Failure 400 {object} models.Response
638// @Failure 401 {object} models.Response
639// @Router /profile [post]
640func UpdateUser(c *gin.Context) {
641 // Check if user exists
642 user, exists := c.Get("user")
643 if !exists {
644 c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in."))
645 return
646 }
647 profile, err := GetPlayerSummaries(user.(models.User).SteamID, os.Getenv("API_KEY"))
648 if err != nil {
649 CreateLog(user.(models.User).SteamID, LogTypeUser, LogDescriptionUserUpdateSummaryFail)
650 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
651 return
652 }
653 // Update profile
654 _, err = database.DB.Exec(`UPDATE users SET username = $1, avatar_link = $2, country_code = $3, updated_at = $4
655 WHERE steam_id = $5`, profile.PersonaName, profile.AvatarFull, profile.LocCountryCode, time.Now().UTC(), user.(models.User).SteamID)
656 if err != nil {
657 CreateLog(user.(models.User).SteamID, LogTypeUser, LogDescriptionUserUpdateFail)
658 c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error()))
659 return
660 }
661 CreateLog(user.(models.User).SteamID, LogTypeUser, LogDescriptionUserUpdateSuccess)
662 c.JSON(http.StatusOK, models.Response{
663 Success: true,
664 Message: "Successfully updated user.",
665 Data: ProfileResponse{
666 Profile: true,
667 SteamID: user.(models.User).SteamID,
668 UserName: profile.PersonaName,
669 AvatarLink: profile.AvatarFull,
670 CountryCode: profile.LocCountryCode,
671 },
672 })
673}
674
675// PUT Profile/CountryCode
676//
677// @Description Update country code of session user.
678// @Tags users
679// @Accept json
680// @Produce json
681// @Param Authorization header string true "JWT Token"
682// @Param country_code query string true "Country Code [XX]"
683// @Success 200 {object} models.Response
684// @Failure 400 {object} models.Response
685// @Failure 401 {object} models.Response
686// @Router /profile [put]
687func UpdateCountryCode(c *gin.Context) {
688 // Check if user exists
689 user, exists := c.Get("user")
690 if !exists {
691 c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in."))
692 return
693 }
694 code := c.Query("country_code")
695 if code == "" {
696 CreateLog(user.(models.User).SteamID, LogTypeUser, LogDescriptionUserUpdateCountryFail)
697 c.JSON(http.StatusNotFound, models.ErrorResponse("Enter a valid country code."))
698 return
699 }
700 var validCode string
701 err := database.DB.QueryRow(`SELECT country_code FROM countries WHERE country_code = $1`, code).Scan(&validCode)
702 if err != nil {
703 CreateLog(user.(models.User).SteamID, LogTypeUser, LogDescriptionUserUpdateCountryFail)
704 c.JSON(http.StatusNotFound, models.ErrorResponse(err.Error()))
705 return
706 }
707 // Valid code, update profile
708 _, err = database.DB.Exec(`UPDATE users SET country_code = $1 WHERE steam_id = $2`, validCode, user.(models.User).SteamID)
709 if err != nil {
710 CreateLog(user.(models.User).SteamID, LogTypeUser, LogDescriptionUserUpdateCountryFail)
711 c.JSON(http.StatusNotFound, models.ErrorResponse(err.Error()))
712 return
713 }
714 CreateLog(user.(models.User).SteamID, LogTypeUser, LogDescriptionUserUpdateCountrySuccess)
715 c.JSON(http.StatusOK, models.Response{
716 Success: true,
717 Message: "Successfully updated country code.",
718 })
719}