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