diff options
Diffstat (limited to 'backend')
| -rw-r--r-- | backend/controllers/homeController.go | 59 | ||||
| -rw-r--r-- | backend/controllers/loginController.go | 48 | ||||
| -rw-r--r-- | backend/controllers/mapController.go | 146 | ||||
| -rw-r--r-- | backend/controllers/modController.go | 327 | ||||
| -rw-r--r-- | backend/controllers/recordController.go | 134 | ||||
| -rw-r--r-- | backend/controllers/userController.go | 74 | ||||
| -rw-r--r-- | backend/database/chapters.sql | 32 | ||||
| -rw-r--r-- | backend/database/games.sql | 6 | ||||
| -rw-r--r-- | backend/database/history.sql | 524 | ||||
| -rw-r--r-- | backend/database/init.sql | 44 | ||||
| -rw-r--r-- | backend/database/route.sql | 279 | ||||
| -rw-r--r-- | backend/middleware/auth.go | 10 | ||||
| -rw-r--r-- | backend/models/models.go | 150 | ||||
| -rw-r--r-- | backend/models/requests.go | 39 | ||||
| -rw-r--r-- | backend/models/responses.go | 64 | ||||
| -rw-r--r-- | backend/parser/parser.REMOVED.git-id | 1 | ||||
| -rw-r--r-- | backend/parser/parser.go | 43 | ||||
| -rw-r--r-- | backend/routes/routes.go | 12 |
18 files changed, 1339 insertions, 653 deletions
diff --git a/backend/controllers/homeController.go b/backend/controllers/homeController.go index edb770f..2780e63 100644 --- a/backend/controllers/homeController.go +++ b/backend/controllers/homeController.go | |||
| @@ -3,6 +3,7 @@ package controllers | |||
| 3 | import ( | 3 | import ( |
| 4 | "log" | 4 | "log" |
| 5 | "net/http" | 5 | "net/http" |
| 6 | "strings" | ||
| 6 | 7 | ||
| 7 | "github.com/gin-gonic/gin" | 8 | "github.com/gin-gonic/gin" |
| 8 | "github.com/pektezol/leastportals/backend/database" | 9 | "github.com/pektezol/leastportals/backend/database" |
| @@ -22,12 +23,12 @@ func Home(c *gin.Context) { | |||
| 22 | 23 | ||
| 23 | // GET Rankings | 24 | // GET Rankings |
| 24 | // | 25 | // |
| 25 | // @Summary Get rankings of every player. | 26 | // @Description Get rankings of every player. |
| 26 | // @Tags rankings | 27 | // @Tags rankings |
| 27 | // @Produce json | 28 | // @Produce json |
| 28 | // @Success 200 {object} models.Response{data=models.RankingsResponse} | 29 | // @Success 200 {object} models.Response{data=models.RankingsResponse} |
| 29 | // @Failure 400 {object} models.Response | 30 | // @Failure 400 {object} models.Response |
| 30 | // @Router /demo [get] | 31 | // @Router /rankings [get] |
| 31 | func Rankings(c *gin.Context) { | 32 | func Rankings(c *gin.Context) { |
| 32 | rows, err := database.DB.Query(`SELECT steam_id, user_name FROM users`) | 33 | rows, err := database.DB.Query(`SELECT steam_id, user_name FROM users`) |
| 33 | if err != nil { | 34 | if err != nil { |
| @@ -122,21 +123,22 @@ func Rankings(c *gin.Context) { | |||
| 122 | }) | 123 | }) |
| 123 | } | 124 | } |
| 124 | 125 | ||
| 125 | // GET Search | 126 | // GET Search With Query |
| 126 | // | 127 | // |
| 127 | // @Summary Get all user and map data. | 128 | // @Description Get all user and map data matching to the query. |
| 128 | // @Tags search | 129 | // @Tags search |
| 129 | // @Produce json | 130 | // @Produce json |
| 130 | // @Success 200 {object} models.Response{data=models.SearchResponse} | 131 | // @Param q query string false "Search user or map name." |
| 131 | // @Failure 400 {object} models.Response | 132 | // @Success 200 {object} models.Response{data=models.SearchResponse} |
| 132 | // @Router /search [get] | 133 | // @Failure 400 {object} models.Response |
| 133 | func Search(c *gin.Context) { | 134 | // @Router /search [get] |
| 135 | func SearchWithQuery(c *gin.Context) { | ||
| 136 | query := c.Query("q") | ||
| 137 | query = strings.ToLower(query) | ||
| 138 | log.Println(query) | ||
| 134 | var response models.SearchResponse | 139 | var response models.SearchResponse |
| 135 | // Cache all maps for faster response | 140 | // Cache all maps for faster response |
| 136 | var maps = []struct { | 141 | var maps = []models.MapShort{ |
| 137 | ID int `json:"id"` | ||
| 138 | Name string `json:"name"` | ||
| 139 | }{ | ||
| 140 | {ID: 1, Name: "Container Ride"}, | 142 | {ID: 1, Name: "Container Ride"}, |
| 141 | {ID: 2, Name: "Portal Carousel"}, | 143 | {ID: 2, Name: "Portal Carousel"}, |
| 142 | {ID: 3, Name: "Portal Gun"}, | 144 | {ID: 3, Name: "Portal Gun"}, |
| @@ -248,23 +250,32 @@ func Search(c *gin.Context) { | |||
| 248 | {ID: 109, Name: "Gel Maze"}, | 250 | {ID: 109, Name: "Gel Maze"}, |
| 249 | {ID: 110, Name: "Crazier Box"}, | 251 | {ID: 110, Name: "Crazier Box"}, |
| 250 | } | 252 | } |
| 251 | response.Maps = maps | 253 | var filteredMaps []models.MapShort |
| 252 | rows, err := database.DB.Query("SELECT steam_id, user_name FROM users") //WHERE player_name LIKE ?", "%"+query+"%") | 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+"%") | ||
| 253 | if err != nil { | 264 | if err != nil { |
| 254 | log.Fatal(err) | 265 | log.Fatal(err) |
| 255 | } | 266 | } |
| 256 | defer rows.Close() | 267 | defer rows.Close() |
| 257 | for rows.Next() { | 268 | for rows.Next() { |
| 258 | var user struct { | 269 | var user models.UserShort |
| 259 | SteamID string `json:"steam_id"` | ||
| 260 | UserName string `json:"user_name"` | ||
| 261 | } | ||
| 262 | if err := rows.Scan(&user.SteamID, &user.UserName); err != nil { | 270 | if err := rows.Scan(&user.SteamID, &user.UserName); err != nil { |
| 263 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 271 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 264 | return | 272 | return |
| 265 | } | 273 | } |
| 266 | response.Players = append(response.Players, user) | 274 | response.Players = append(response.Players, user) |
| 267 | } | 275 | } |
| 276 | if len(response.Players) == 0 { | ||
| 277 | response.Players = []models.UserShort{} | ||
| 278 | } | ||
| 268 | c.JSON(http.StatusOK, models.Response{ | 279 | c.JSON(http.StatusOK, models.Response{ |
| 269 | Success: true, | 280 | Success: true, |
| 270 | Message: "Search successfully retrieved.", | 281 | Message: "Search successfully retrieved.", |
diff --git a/backend/controllers/loginController.go b/backend/controllers/loginController.go index cfe086d..ae6e957 100644 --- a/backend/controllers/loginController.go +++ b/backend/controllers/loginController.go | |||
| @@ -17,13 +17,13 @@ import ( | |||
| 17 | 17 | ||
| 18 | // Login | 18 | // Login |
| 19 | // | 19 | // |
| 20 | // @Summary Get (redirect) login page for Steam auth. | 20 | // @Description Get (redirect) login page for Steam auth. |
| 21 | // @Tags login | 21 | // @Tags login |
| 22 | // @Accept json | 22 | // @Accept json |
| 23 | // @Produce json | 23 | // @Produce json |
| 24 | // @Success 200 {object} models.Response{data=models.LoginResponse} | 24 | // @Success 200 {object} models.Response{data=models.LoginResponse} |
| 25 | // @Failure 400 {object} models.Response | 25 | // @Failure 400 {object} models.Response |
| 26 | // @Router /login [get] | 26 | // @Router /login [get] |
| 27 | func Login(c *gin.Context) { | 27 | func Login(c *gin.Context) { |
| 28 | openID := steam_go.NewOpenId(c.Request) | 28 | openID := steam_go.NewOpenId(c.Request) |
| 29 | switch openID.Mode() { | 29 | switch openID.Mode() { |
| @@ -59,10 +59,20 @@ func Login(c *gin.Context) { | |||
| 59 | database.DB.Exec(`INSERT INTO users (steam_id, user_name, avatar_link, country_code) | 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) | 60 | VALUES ($1, $2, $3, $4)`, steamID, user.PersonaName, user.AvatarFull, user.LocCountryCode) |
| 61 | } | 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 | } | ||
| 62 | // Generate JWT token | 71 | // Generate JWT token |
| 63 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ | 72 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ |
| 64 | "sub": steamID, | 73 | "sub": steamID, |
| 65 | "exp": time.Now().Add(time.Hour * 24 * 30).Unix(), | 74 | "exp": time.Now().Add(time.Hour * 24 * 30).Unix(), |
| 75 | "mod": moderator, | ||
| 66 | }) | 76 | }) |
| 67 | // Sign and get the complete encoded token as a string using the secret | 77 | // Sign and get the complete encoded token as a string using the secret |
| 68 | tokenString, err := token.SignedString([]byte(os.Getenv("SECRET_KEY"))) | 78 | tokenString, err := token.SignedString([]byte(os.Getenv("SECRET_KEY"))) |
| @@ -85,13 +95,13 @@ func Login(c *gin.Context) { | |||
| 85 | 95 | ||
| 86 | // GET Token | 96 | // GET Token |
| 87 | // | 97 | // |
| 88 | // @Summary Gets the token cookie value from the user. | 98 | // @Description Gets the token cookie value from the user. |
| 89 | // @Tags auth | 99 | // @Tags auth |
| 90 | // @Produce json | 100 | // @Produce json |
| 91 | // | 101 | // |
| 92 | // @Success 200 {object} models.Response{data=models.LoginResponse} | 102 | // @Success 200 {object} models.Response{data=models.LoginResponse} |
| 93 | // @Failure 404 {object} models.Response | 103 | // @Failure 404 {object} models.Response |
| 94 | // @Router /token [get] | 104 | // @Router /token [get] |
| 95 | func GetCookie(c *gin.Context) { | 105 | func GetCookie(c *gin.Context) { |
| 96 | cookie, err := c.Cookie("token") | 106 | cookie, err := c.Cookie("token") |
| 97 | if err != nil { | 107 | if err != nil { |
| @@ -109,13 +119,13 @@ func GetCookie(c *gin.Context) { | |||
| 109 | 119 | ||
| 110 | // DELETE Token | 120 | // DELETE Token |
| 111 | // | 121 | // |
| 112 | // @Summary Deletes the token cookie from the user. | 122 | // @Description Deletes the token cookie from the user. |
| 113 | // @Tags auth | 123 | // @Tags auth |
| 114 | // @Produce json | 124 | // @Produce json |
| 115 | // | 125 | // |
| 116 | // @Success 200 {object} models.Response{data=models.LoginResponse} | 126 | // @Success 200 {object} models.Response{data=models.LoginResponse} |
| 117 | // @Failure 404 {object} models.Response | 127 | // @Failure 404 {object} models.Response |
| 118 | // @Router /token [delete] | 128 | // @Router /token [delete] |
| 119 | func DeleteCookie(c *gin.Context) { | 129 | func DeleteCookie(c *gin.Context) { |
| 120 | cookie, err := c.Cookie("token") | 130 | cookie, err := c.Cookie("token") |
| 121 | if err != nil { | 131 | if err != nil { |
diff --git a/backend/controllers/mapController.go b/backend/controllers/mapController.go index d8783b7..e7c5566 100644 --- a/backend/controllers/mapController.go +++ b/backend/controllers/mapController.go | |||
| @@ -3,111 +3,81 @@ package controllers | |||
| 3 | import ( | 3 | import ( |
| 4 | "net/http" | 4 | "net/http" |
| 5 | "strconv" | 5 | "strconv" |
| 6 | "time" | ||
| 7 | 6 | ||
| 8 | "github.com/gin-gonic/gin" | 7 | "github.com/gin-gonic/gin" |
| 9 | "github.com/lib/pq" | ||
| 10 | "github.com/pektezol/leastportals/backend/database" | 8 | "github.com/pektezol/leastportals/backend/database" |
| 11 | "github.com/pektezol/leastportals/backend/models" | 9 | "github.com/pektezol/leastportals/backend/models" |
| 12 | ) | 10 | ) |
| 13 | 11 | ||
| 14 | // GET Map Summary | 12 | // GET Map Summary |
| 15 | // | 13 | // |
| 16 | // @Summary Get map summary with specified id. | 14 | // @Description Get map summary with specified id. |
| 17 | // @Tags maps | 15 | // @Tags maps |
| 18 | // @Produce json | 16 | // @Produce json |
| 19 | // @Param id path int true "Map ID" | 17 | // @Param id path int true "Map ID" |
| 20 | // @Success 200 {object} models.Response{data=models.Map{data=models.MapSummary}} | 18 | // @Success 200 {object} models.Response{data=models.MapSummaryResponse} |
| 21 | // @Failure 400 {object} models.Response | 19 | // @Failure 400 {object} models.Response |
| 22 | // @Router /maps/{id}/summary [get] | 20 | // @Router /maps/{id}/summary [get] |
| 23 | func FetchMapSummary(c *gin.Context) { | 21 | func FetchMapSummary(c *gin.Context) { |
| 24 | id := c.Param("id") | 22 | id := c.Param("id") |
| 25 | // Get map data | 23 | response := models.MapSummaryResponse{Map: models.Map{}, Summary: models.MapSummary{Routes: []models.MapRoute{}}} |
| 26 | var mapData models.Map | ||
| 27 | var mapSummaryData models.MapSummary | ||
| 28 | var mapHistoryData []models.MapHistory | ||
| 29 | intID, err := strconv.Atoi(id) | 24 | intID, err := strconv.Atoi(id) |
| 30 | if err != nil { | 25 | if err != nil { |
| 31 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 26 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 32 | return | 27 | return |
| 33 | } | 28 | } |
| 34 | mapData.ID = intID | 29 | // Get map data |
| 35 | var routers pq.StringArray | 30 | response.Map.ID = intID |
| 36 | sql := `SELECT g.name, c.name, m.name, m.description, m.showcase, | 31 | sql := `SELECT m.id, g.name, c.name, m.name, m.image, g.is_coop |
| 37 | ( | ||
| 38 | SELECT array_agg(user_name) | ||
| 39 | FROM map_routers | ||
| 40 | WHERE map_id = $1 | ||
| 41 | AND score_count = ( | ||
| 42 | SELECT score_count | ||
| 43 | FROM map_history | ||
| 44 | WHERE map_id = $1 | ||
| 45 | ORDER BY score_count | ||
| 46 | LIMIT 1 | ||
| 47 | ) | ||
| 48 | GROUP BY map_routers.user_name | ||
| 49 | ORDER BY user_name | ||
| 50 | ), | ||
| 51 | ( | ||
| 52 | SELECT COALESCE(avg(rating), 0.0) | ||
| 53 | FROM map_ratings | ||
| 54 | WHERE map_id = $1 | ||
| 55 | ) | ||
| 56 | FROM maps m | 32 | FROM maps m |
| 57 | INNER JOIN games g ON m.game_id = g.id | 33 | INNER JOIN games g ON m.game_id = g.id |
| 58 | INNER JOIN chapters c ON m.chapter_id = c.id | 34 | INNER JOIN chapters c ON m.chapter_id = c.id |
| 59 | WHERE m.id = $1` | 35 | WHERE m.id = $1` |
| 60 | // TODO: CategoryScores | 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) |
| 61 | err = database.DB.QueryRow(sql, id).Scan(&mapData.GameName, &mapData.ChapterName, &mapData.MapName, &mapSummaryData.Description, &mapSummaryData.Showcase, &routers, &mapSummaryData.Rating) | ||
| 62 | if err != nil { | 37 | if err != nil { |
| 63 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 38 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 64 | return | 39 | return |
| 65 | } | 40 | } |
| 66 | var historyNames pq.StringArray | 41 | // Get map routes and histories |
| 67 | var historyScores pq.Int32Array | 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 |
| 68 | var historyDates pq.StringArray | 43 | INNER JOIN categories c ON r.category_id = c.id |
| 69 | sql = `SELECT array_agg(user_name), array_agg(score_count), array_agg(record_date) | 44 | INNER JOIN map_history h ON r.map_id = h.map_id AND r.category_id = h.category_id |
| 70 | FROM map_history | 45 | LEFT JOIN map_ratings rt ON r.map_id = rt.map_id AND r.category_id = rt.category_id |
| 71 | WHERE map_id = $1` | 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 |
| 72 | err = database.DB.QueryRow(sql, id).Scan(&historyNames, &historyScores, &historyDates) | 47 | ORDER BY h.record_date ASC;` |
| 48 | rows, err := database.DB.Query(sql, id) | ||
| 73 | if err != nil { | 49 | if err != nil { |
| 74 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 50 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 75 | return | 51 | return |
| 76 | } | 52 | } |
| 77 | for i := 0; i < len(historyNames); i++ { | 53 | for rows.Next() { |
| 78 | var history models.MapHistory | 54 | route := models.MapRoute{Category: models.Category{}, History: models.MapHistory{}} |
| 79 | history.RunnerName = historyNames[i] | 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) |
| 80 | history.ScoreCount = int(historyScores[i]) | ||
| 81 | layout := "2006-01-02 15:04:05" | ||
| 82 | date, err := time.Parse(layout, historyDates[i]) | ||
| 83 | if err != nil { | 56 | if err != nil { |
| 84 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 57 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 85 | return | 58 | return |
| 86 | } | 59 | } |
| 87 | history.Date = date | 60 | response.Summary.Routes = append(response.Summary.Routes, route) |
| 88 | mapHistoryData = append(mapHistoryData, history) | ||
| 89 | } | 61 | } |
| 90 | mapSummaryData.History = mapHistoryData | ||
| 91 | mapSummaryData.Routers = routers | ||
| 92 | mapData.Data = mapSummaryData | ||
| 93 | // Return response | 62 | // Return response |
| 94 | c.JSON(http.StatusOK, models.Response{ | 63 | c.JSON(http.StatusOK, models.Response{ |
| 95 | Success: true, | 64 | Success: true, |
| 96 | Message: "Successfully retrieved map summary.", | 65 | Message: "Successfully retrieved map summary.", |
| 97 | Data: mapData, | 66 | Data: response, |
| 98 | }) | 67 | }) |
| 99 | } | 68 | } |
| 100 | 69 | ||
| 101 | // GET Map Leaderboards | 70 | // GET Map Leaderboards |
| 102 | // | 71 | // |
| 103 | // @Summary Get map leaderboards with specified id. | 72 | // @Description Get map leaderboards with specified id. |
| 104 | // @Tags maps | 73 | // @Tags maps |
| 105 | // @Produce json | 74 | // @Produce json |
| 106 | // @Param id path int true "Map ID" | 75 | // @Param id path int true "Map ID" |
| 107 | // @Success 200 {object} models.Response{data=models.Map{data=models.MapRecords}} | 76 | // @Success 200 {object} models.Response{data=models.Map{data=models.MapRecords}} |
| 108 | // @Failure 400 {object} models.Response | 77 | // @Failure 400 {object} models.Response |
| 109 | // @Router /maps/{id}/leaderboards [get] | 78 | // @Router /maps/{id}/leaderboards [get] |
| 110 | func FetchMapLeaderboards(c *gin.Context) { | 79 | func FetchMapLeaderboards(c *gin.Context) { |
| 80 | // TODO: make new response type | ||
| 111 | id := c.Param("id") | 81 | id := c.Param("id") |
| 112 | // Get map data | 82 | // Get map data |
| 113 | var mapData models.Map | 83 | var mapData models.Map |
| @@ -119,12 +89,12 @@ func FetchMapLeaderboards(c *gin.Context) { | |||
| 119 | return | 89 | return |
| 120 | } | 90 | } |
| 121 | mapData.ID = intID | 91 | mapData.ID = intID |
| 122 | sql := `SELECT g.name, c.name, m.name, is_disabled | 92 | sql := `SELECT g.name, c.name, m.name, is_disabled, m.image |
| 123 | FROM maps m | 93 | FROM maps m |
| 124 | INNER JOIN games g ON m.game_id = g.id | 94 | INNER JOIN games g ON m.game_id = g.id |
| 125 | INNER JOIN chapters c ON m.chapter_id = c.id | 95 | INNER JOIN chapters c ON m.chapter_id = c.id |
| 126 | WHERE m.id = $1` | 96 | WHERE m.id = $1` |
| 127 | err = database.DB.QueryRow(sql, id).Scan(&mapData.GameName, &mapData.ChapterName, &mapData.MapName, &isDisabled) | 97 | err = database.DB.QueryRow(sql, id).Scan(&mapData.GameName, &mapData.ChapterName, &mapData.MapName, &isDisabled, &mapData.Image) |
| 128 | if err != nil { | 98 | if err != nil { |
| 129 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 99 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 130 | return | 100 | return |
| @@ -205,7 +175,7 @@ func FetchMapLeaderboards(c *gin.Context) { | |||
| 205 | } | 175 | } |
| 206 | mapRecordsData.Records = records | 176 | mapRecordsData.Records = records |
| 207 | } | 177 | } |
| 208 | mapData.Data = mapRecordsData | 178 | // mapData.Data = mapRecordsData |
| 209 | // Return response | 179 | // Return response |
| 210 | c.JSON(http.StatusOK, models.Response{ | 180 | c.JSON(http.StatusOK, models.Response{ |
| 211 | Success: true, | 181 | Success: true, |
| @@ -216,14 +186,14 @@ func FetchMapLeaderboards(c *gin.Context) { | |||
| 216 | 186 | ||
| 217 | // GET Games | 187 | // GET Games |
| 218 | // | 188 | // |
| 219 | // @Summary Get games from the leaderboards. | 189 | // @Description Get games from the leaderboards. |
| 220 | // @Tags games & chapters | 190 | // @Tags games & chapters |
| 221 | // @Produce json | 191 | // @Produce json |
| 222 | // @Success 200 {object} models.Response{data=[]models.Game} | 192 | // @Success 200 {object} models.Response{data=[]models.Game} |
| 223 | // @Failure 400 {object} models.Response | 193 | // @Failure 400 {object} models.Response |
| 224 | // @Router /games [get] | 194 | // @Router /games [get] |
| 225 | func FetchGames(c *gin.Context) { | 195 | func FetchGames(c *gin.Context) { |
| 226 | rows, err := database.DB.Query(`SELECT id, name FROM games`) | 196 | rows, err := database.DB.Query(`SELECT id, name, is_coop FROM games`) |
| 227 | if err != nil { | 197 | if err != nil { |
| 228 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 198 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 229 | return | 199 | return |
| @@ -231,7 +201,7 @@ func FetchGames(c *gin.Context) { | |||
| 231 | var games []models.Game | 201 | var games []models.Game |
| 232 | for rows.Next() { | 202 | for rows.Next() { |
| 233 | var game models.Game | 203 | var game models.Game |
| 234 | if err := rows.Scan(&game.ID, &game.Name); err != nil { | 204 | if err := rows.Scan(&game.ID, &game.Name, &game.IsCoop); err != nil { |
| 235 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 205 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 236 | return | 206 | return |
| 237 | } | 207 | } |
| @@ -246,13 +216,13 @@ func FetchGames(c *gin.Context) { | |||
| 246 | 216 | ||
| 247 | // GET Chapters of a Game | 217 | // GET Chapters of a Game |
| 248 | // | 218 | // |
| 249 | // @Summary Get chapters from the specified game id. | 219 | // @Description Get chapters from the specified game id. |
| 250 | // @Tags games & chapters | 220 | // @Tags games & chapters |
| 251 | // @Produce json | 221 | // @Produce json |
| 252 | // @Param id path int true "Game ID" | 222 | // @Param id path int true "Game ID" |
| 253 | // @Success 200 {object} models.Response{data=models.ChaptersResponse} | 223 | // @Success 200 {object} models.Response{data=models.ChaptersResponse} |
| 254 | // @Failure 400 {object} models.Response | 224 | // @Failure 400 {object} models.Response |
| 255 | // @Router /games/{id} [get] | 225 | // @Router /games/{id} [get] |
| 256 | func FetchChapters(c *gin.Context) { | 226 | func FetchChapters(c *gin.Context) { |
| 257 | gameID := c.Param("id") | 227 | gameID := c.Param("id") |
| 258 | intID, err := strconv.Atoi(gameID) | 228 | intID, err := strconv.Atoi(gameID) |
| @@ -288,13 +258,13 @@ func FetchChapters(c *gin.Context) { | |||
| 288 | 258 | ||
| 289 | // GET Maps of a Chapter | 259 | // GET Maps of a Chapter |
| 290 | // | 260 | // |
| 291 | // @Summary Get maps from the specified chapter id. | 261 | // @Description Get maps from the specified chapter id. |
| 292 | // @Tags games & chapters | 262 | // @Tags games & chapters |
| 293 | // @Produce json | 263 | // @Produce json |
| 294 | // @Param id path int true "Chapter ID" | 264 | // @Param id path int true "Chapter ID" |
| 295 | // @Success 200 {object} models.Response{data=models.ChapterMapsResponse} | 265 | // @Success 200 {object} models.Response{data=models.ChapterMapsResponse} |
| 296 | // @Failure 400 {object} models.Response | 266 | // @Failure 400 {object} models.Response |
| 297 | // @Router /chapters/{id} [get] | 267 | // @Router /chapters/{id} [get] |
| 298 | func FetchChapterMaps(c *gin.Context) { | 268 | func FetchChapterMaps(c *gin.Context) { |
| 299 | chapterID := c.Param("id") | 269 | chapterID := c.Param("id") |
| 300 | intID, err := strconv.Atoi(chapterID) | 270 | intID, err := strconv.Atoi(chapterID) |
diff --git a/backend/controllers/modController.go b/backend/controllers/modController.go new file mode 100644 index 0000000..07edff5 --- /dev/null +++ b/backend/controllers/modController.go | |||
| @@ -0,0 +1,327 @@ | |||
| 1 | package controllers | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "net/http" | ||
| 5 | "strconv" | ||
| 6 | |||
| 7 | "github.com/gin-gonic/gin" | ||
| 8 | "github.com/pektezol/leastportals/backend/database" | ||
| 9 | "github.com/pektezol/leastportals/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] | ||
| 23 | func 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] | ||
| 109 | func 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] | ||
| 195 | func 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] | ||
| 285 | func 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 index 627be57..d1404f4 100644 --- a/backend/controllers/recordController.go +++ b/backend/controllers/recordController.go | |||
| @@ -2,17 +2,18 @@ package controllers | |||
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "context" | 4 | "context" |
| 5 | b64 "encoding/base64" | 5 | "encoding/base64" |
| 6 | "io" | 6 | "io" |
| 7 | "log" | 7 | "log" |
| 8 | "mime/multipart" | ||
| 8 | "net/http" | 9 | "net/http" |
| 9 | "os" | 10 | "os" |
| 10 | "strconv" | ||
| 11 | 11 | ||
| 12 | "github.com/gin-gonic/gin" | 12 | "github.com/gin-gonic/gin" |
| 13 | "github.com/google/uuid" | 13 | "github.com/google/uuid" |
| 14 | "github.com/pektezol/leastportals/backend/database" | 14 | "github.com/pektezol/leastportals/backend/database" |
| 15 | "github.com/pektezol/leastportals/backend/models" | 15 | "github.com/pektezol/leastportals/backend/models" |
| 16 | "github.com/pektezol/leastportals/backend/parser" | ||
| 16 | "golang.org/x/oauth2/google" | 17 | "golang.org/x/oauth2/google" |
| 17 | "golang.org/x/oauth2/jwt" | 18 | "golang.org/x/oauth2/jwt" |
| 18 | "google.golang.org/api/drive/v3" | 19 | "google.golang.org/api/drive/v3" |
| @@ -20,20 +21,20 @@ import ( | |||
| 20 | 21 | ||
| 21 | // POST Record | 22 | // POST Record |
| 22 | // | 23 | // |
| 23 | // @Summary Post record with demo of a specific map. | 24 | // @Description Post record with demo of a specific map. |
| 24 | // @Tags maps | 25 | // @Tags maps |
| 25 | // @Accept mpfd | 26 | // @Accept mpfd |
| 26 | // @Produce json | 27 | // @Produce json |
| 27 | // @Param Authorization header string true "JWT Token" | 28 | // @Param id path int true "Map ID" |
| 28 | // @Param demos formData []file true "Demos" | 29 | // @Param Authorization header string true "JWT Token" |
| 29 | // @Param score_count formData int true "Score Count" | 30 | // @Param host_demo formData file true "Host Demo" |
| 30 | // @Param score_time formData int true "Score Time" | 31 | // @Param partner_demo formData file false "Partner Demo" |
| 31 | // @Param is_partner_orange formData boolean true "Is Partner Orange" | 32 | // @Param is_partner_orange formData boolean false "Is Partner Orange" |
| 32 | // @Param partner_id formData string true "Partner ID" | 33 | // @Param partner_id formData string false "Partner ID" |
| 33 | // @Success 200 {object} models.Response{data=models.RecordRequest} | 34 | // @Success 200 {object} models.Response{data=models.RecordResponse} |
| 34 | // @Failure 400 {object} models.Response | 35 | // @Failure 400 {object} models.Response |
| 35 | // @Failure 401 {object} models.Response | 36 | // @Failure 401 {object} models.Response |
| 36 | // @Router /maps/{id}/record [post] | 37 | // @Router /maps/{id}/record [post] |
| 37 | func CreateRecordWithDemo(c *gin.Context) { | 38 | func CreateRecordWithDemo(c *gin.Context) { |
| 38 | mapId := c.Param("id") | 39 | mapId := c.Param("id") |
| 39 | // Check if user exists | 40 | // Check if user exists |
| @@ -43,11 +44,11 @@ func CreateRecordWithDemo(c *gin.Context) { | |||
| 43 | return | 44 | return |
| 44 | } | 45 | } |
| 45 | // Check if map is sp or mp | 46 | // Check if map is sp or mp |
| 46 | var gameID int | 47 | var gameName string |
| 47 | var isCoop bool | 48 | var isCoop bool |
| 48 | var isDisabled bool | 49 | var isDisabled bool |
| 49 | sql := `SELECT game_id, is_disabled FROM maps WHERE id = $1` | 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` |
| 50 | err := database.DB.QueryRow(sql, mapId).Scan(&gameID, &isDisabled) | 51 | err := database.DB.QueryRow(sql, mapId).Scan(&gameName, &isDisabled) |
| 51 | if err != nil { | 52 | if err != nil { |
| 52 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 53 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 53 | return | 54 | return |
| @@ -56,51 +57,26 @@ func CreateRecordWithDemo(c *gin.Context) { | |||
| 56 | c.JSON(http.StatusBadRequest, models.ErrorResponse("Map is not available for competitive boards.")) | 57 | c.JSON(http.StatusBadRequest, models.ErrorResponse("Map is not available for competitive boards.")) |
| 57 | return | 58 | return |
| 58 | } | 59 | } |
| 59 | if gameID == 2 { | 60 | if gameName == "Portal 2 - Cooperative" { |
| 60 | isCoop = true | 61 | isCoop = true |
| 61 | } | 62 | } |
| 62 | // Get record request | 63 | // Get record request |
| 63 | var record models.RecordRequest | 64 | var record models.RecordRequest |
| 64 | score_count, err := strconv.Atoi(c.PostForm("score_count")) | 65 | if err := c.ShouldBind(&record); err != nil { |
| 65 | if err != nil { | ||
| 66 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 67 | return | ||
| 68 | } | ||
| 69 | score_time, err := strconv.Atoi(c.PostForm("score_time")) | ||
| 70 | if err != nil { | ||
| 71 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 72 | return | ||
| 73 | } | ||
| 74 | is_partner_orange, err := strconv.ParseBool(c.PostForm("is_partner_orange")) | ||
| 75 | if err != nil { | ||
| 76 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | ||
| 77 | return | ||
| 78 | } | ||
| 79 | record.ScoreCount = score_count | ||
| 80 | record.ScoreTime = score_time | ||
| 81 | record.PartnerID = c.PostForm("partner_id") | ||
| 82 | record.IsPartnerOrange = is_partner_orange | ||
| 83 | if record.PartnerID == "" { | ||
| 84 | c.JSON(http.StatusBadRequest, models.ErrorResponse("No partner id given.")) | ||
| 85 | return | ||
| 86 | } | ||
| 87 | // Multipart form | ||
| 88 | form, err := c.MultipartForm() | ||
| 89 | if err != nil { | ||
| 90 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 66 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 91 | return | 67 | return |
| 92 | } | 68 | } |
| 93 | files := form.File["demos"] | 69 | if isCoop && (record.PartnerDemo == nil || record.PartnerID == "") { |
| 94 | if len(files) != 2 && isCoop { | 70 | c.JSON(http.StatusBadRequest, models.ErrorResponse("Invalid entry for coop record submission.")) |
| 95 | c.JSON(http.StatusBadRequest, models.ErrorResponse("Not enough demos for coop submission.")) | ||
| 96 | return | 71 | return |
| 97 | } | 72 | } |
| 98 | if len(files) != 1 && !isCoop { | 73 | // Demo files |
| 99 | c.JSON(http.StatusBadRequest, models.ErrorResponse("Too many demos for singleplayer submission.")) | 74 | demoFiles := []*multipart.FileHeader{record.HostDemo} |
| 100 | return | 75 | if isCoop { |
| 76 | demoFiles = append(demoFiles, record.PartnerDemo) | ||
| 101 | } | 77 | } |
| 102 | var hostDemoUUID string | 78 | var hostDemoUUID, hostDemoFileID, partnerDemoUUID, partnerDemoFileID string |
| 103 | var partnerDemoUUID string | 79 | var hostDemoScoreCount, hostDemoScoreTime int |
| 104 | client := serviceAccount() | 80 | client := serviceAccount() |
| 105 | srv, err := drive.New(client) | 81 | srv, err := drive.New(client) |
| 106 | if err != nil { | 82 | if err != nil { |
| @@ -115,16 +91,16 @@ func CreateRecordWithDemo(c *gin.Context) { | |||
| 115 | } | 91 | } |
| 116 | // Defer to a rollback in case anything fails | 92 | // Defer to a rollback in case anything fails |
| 117 | defer tx.Rollback() | 93 | defer tx.Rollback() |
| 118 | fileID := "" | 94 | for i, header := range demoFiles { |
| 119 | for i, header := range files { | ||
| 120 | uuid := uuid.New().String() | 95 | uuid := uuid.New().String() |
| 121 | // Upload & insert into demos | 96 | // Upload & insert into demos |
| 122 | err = c.SaveUploadedFile(header, "docs/"+header.Filename) | 97 | err = c.SaveUploadedFile(header, "backend/parser/"+uuid+".dem") |
| 123 | if err != nil { | 98 | if err != nil { |
| 124 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 99 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 125 | return | 100 | return |
| 126 | } | 101 | } |
| 127 | f, err := os.Open("docs/" + header.Filename) | 102 | defer os.Remove("backend/parser/" + uuid + ".dem") |
| 103 | f, err := os.Open("backend/parser/" + uuid + ".dem") | ||
| 128 | if err != nil { | 104 | if err != nil { |
| 129 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 105 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 130 | return | 106 | return |
| @@ -135,11 +111,16 @@ func CreateRecordWithDemo(c *gin.Context) { | |||
| 135 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 111 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 136 | return | 112 | return |
| 137 | } | 113 | } |
| 138 | fileID = file.Id | 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 | } | ||
| 139 | if i == 0 { | 119 | if i == 0 { |
| 120 | hostDemoFileID = file.Id | ||
| 140 | hostDemoUUID = uuid | 121 | hostDemoUUID = uuid |
| 141 | } | 122 | } else if i == 1 { |
| 142 | if i == 1 { | 123 | partnerDemoFileID = file.Id |
| 143 | partnerDemoUUID = uuid | 124 | partnerDemoUUID = uuid |
| 144 | } | 125 | } |
| 145 | _, err = tx.Exec(`INSERT INTO demos (id,location_id) VALUES ($1,$2)`, uuid, file.Id) | 126 | _, err = tx.Exec(`INSERT INTO demos (id,location_id) VALUES ($1,$2)`, uuid, file.Id) |
| @@ -148,7 +129,6 @@ func CreateRecordWithDemo(c *gin.Context) { | |||
| 148 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 129 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 149 | return | 130 | return |
| 150 | } | 131 | } |
| 151 | os.Remove("docs/" + header.Filename) | ||
| 152 | } | 132 | } |
| 153 | // Insert into records | 133 | // Insert into records |
| 154 | if isCoop { | 134 | if isCoop { |
| @@ -163,9 +143,10 @@ func CreateRecordWithDemo(c *gin.Context) { | |||
| 163 | partnerID = user.(models.User).SteamID | 143 | partnerID = user.(models.User).SteamID |
| 164 | hostID = record.PartnerID | 144 | hostID = record.PartnerID |
| 165 | } | 145 | } |
| 166 | _, err := tx.Exec(sql, mapId, record.ScoreCount, record.ScoreTime, hostID, partnerID, hostDemoUUID, partnerDemoUUID) | 146 | _, err := tx.Exec(sql, mapId, hostDemoScoreCount, hostDemoScoreTime, hostID, partnerID, hostDemoUUID, partnerDemoUUID) |
| 167 | if err != nil { | 147 | if err != nil { |
| 168 | deleteFile(srv, fileID) | 148 | deleteFile(srv, hostDemoFileID) |
| 149 | deleteFile(srv, partnerDemoFileID) | ||
| 169 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 150 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 170 | return | 151 | return |
| 171 | } | 152 | } |
| @@ -180,9 +161,9 @@ func CreateRecordWithDemo(c *gin.Context) { | |||
| 180 | } else { | 161 | } else { |
| 181 | sql := `INSERT INTO records_sp(map_id,score_count,score_time,user_id,demo_id) | 162 | sql := `INSERT INTO records_sp(map_id,score_count,score_time,user_id,demo_id) |
| 182 | VALUES($1, $2, $3, $4, $5)` | 163 | VALUES($1, $2, $3, $4, $5)` |
| 183 | _, err := tx.Exec(sql, mapId, record.ScoreCount, record.ScoreTime, user.(models.User).SteamID, hostDemoUUID) | 164 | _, err := tx.Exec(sql, mapId, hostDemoScoreCount, hostDemoScoreTime, user.(models.User).SteamID, hostDemoUUID) |
| 184 | if err != nil { | 165 | if err != nil { |
| 185 | deleteFile(srv, fileID) | 166 | deleteFile(srv, hostDemoFileID) |
| 186 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) | 167 | c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) |
| 187 | return | 168 | return |
| 188 | } | 169 | } |
| @@ -202,21 +183,20 @@ func CreateRecordWithDemo(c *gin.Context) { | |||
| 202 | c.JSON(http.StatusOK, models.Response{ | 183 | c.JSON(http.StatusOK, models.Response{ |
| 203 | Success: true, | 184 | Success: true, |
| 204 | Message: "Successfully created record.", | 185 | Message: "Successfully created record.", |
| 205 | Data: record, | 186 | Data: models.RecordResponse{ScoreCount: hostDemoScoreCount, ScoreTime: hostDemoScoreTime}, |
| 206 | }) | 187 | }) |
| 207 | return | ||
| 208 | } | 188 | } |
| 209 | 189 | ||
| 210 | // GET Demo | 190 | // GET Demo |
| 211 | // | 191 | // |
| 212 | // @Summary Get demo with specified demo uuid. | 192 | // @Description Get demo with specified demo uuid. |
| 213 | // @Tags demo | 193 | // @Tags demo |
| 214 | // @Accept json | 194 | // @Accept json |
| 215 | // @Produce octet-stream | 195 | // @Produce octet-stream |
| 216 | // @Param uuid query int true "Demo UUID" | 196 | // @Param uuid query string true "Demo UUID" |
| 217 | // @Success 200 {file} binary "Demo File" | 197 | // @Success 200 {file} binary "Demo File" |
| 218 | // @Failure 400 {object} models.Response | 198 | // @Failure 400 {object} models.Response |
| 219 | // @Router /demos [get] | 199 | // @Router /demos [get] |
| 220 | func DownloadDemoWithID(c *gin.Context) { | 200 | func DownloadDemoWithID(c *gin.Context) { |
| 221 | uuid := c.Query("uuid") | 201 | uuid := c.Query("uuid") |
| 222 | var locationID string | 202 | var locationID string |
| @@ -260,7 +240,7 @@ func DownloadDemoWithID(c *gin.Context) { | |||
| 260 | 240 | ||
| 261 | // Use Service account | 241 | // Use Service account |
| 262 | func serviceAccount() *http.Client { | 242 | func serviceAccount() *http.Client { |
| 263 | privateKey, _ := b64.StdEncoding.DecodeString(os.Getenv("GOOGLE_PRIVATE_KEY_BASE64")) | 243 | privateKey, _ := base64.StdEncoding.DecodeString(os.Getenv("GOOGLE_PRIVATE_KEY_BASE64")) |
| 264 | config := &jwt.Config{ | 244 | config := &jwt.Config{ |
| 265 | Email: os.Getenv("GOOGLE_CLIENT_EMAIL"), | 245 | Email: os.Getenv("GOOGLE_CLIENT_EMAIL"), |
| 266 | PrivateKey: []byte(privateKey), | 246 | PrivateKey: []byte(privateKey), |
diff --git a/backend/controllers/userController.go b/backend/controllers/userController.go index adf936b..e73b1fe 100644 --- a/backend/controllers/userController.go +++ b/backend/controllers/userController.go | |||
| @@ -13,15 +13,15 @@ import ( | |||
| 13 | 13 | ||
| 14 | // GET Profile | 14 | // GET Profile |
| 15 | // | 15 | // |
| 16 | // @Summary Get profile page of session user. | 16 | // @Description Get profile page of session user. |
| 17 | // @Tags users | 17 | // @Tags users |
| 18 | // @Accept json | 18 | // @Accept json |
| 19 | // @Produce json | 19 | // @Produce json |
| 20 | // @Param Authorization header string true "JWT Token" | 20 | // @Param Authorization header string true "JWT Token" |
| 21 | // @Success 200 {object} models.Response{data=models.ProfileResponse} | 21 | // @Success 200 {object} models.Response{data=models.ProfileResponse} |
| 22 | // @Failure 400 {object} models.Response | 22 | // @Failure 400 {object} models.Response |
| 23 | // @Failure 401 {object} models.Response | 23 | // @Failure 401 {object} models.Response |
| 24 | // @Router /profile [get] | 24 | // @Router /profile [get] |
| 25 | func Profile(c *gin.Context) { | 25 | func Profile(c *gin.Context) { |
| 26 | // Check if user exists | 26 | // Check if user exists |
| 27 | user, exists := c.Get("user") | 27 | user, exists := c.Get("user") |
| @@ -100,15 +100,15 @@ func Profile(c *gin.Context) { | |||
| 100 | 100 | ||
| 101 | // GET User | 101 | // GET User |
| 102 | // | 102 | // |
| 103 | // @Summary Get profile page of another user. | 103 | // @Description Get profile page of another user. |
| 104 | // @Tags users | 104 | // @Tags users |
| 105 | // @Accept json | 105 | // @Accept json |
| 106 | // @Produce json | 106 | // @Produce json |
| 107 | // @Param id path int true "User ID" | 107 | // @Param id path int true "User ID" |
| 108 | // @Success 200 {object} models.Response{data=models.ProfileResponse} | 108 | // @Success 200 {object} models.Response{data=models.ProfileResponse} |
| 109 | // @Failure 400 {object} models.Response | 109 | // @Failure 400 {object} models.Response |
| 110 | // @Failure 404 {object} models.Response | 110 | // @Failure 404 {object} models.Response |
| 111 | // @Router /users/{id} [get] | 111 | // @Router /users/{id} [get] |
| 112 | func FetchUser(c *gin.Context) { | 112 | func FetchUser(c *gin.Context) { |
| 113 | id := c.Param("id") | 113 | id := c.Param("id") |
| 114 | // Check if id is all numbers and 17 length | 114 | // Check if id is all numbers and 17 length |
| @@ -202,15 +202,15 @@ func FetchUser(c *gin.Context) { | |||
| 202 | 202 | ||
| 203 | // PUT Profile | 203 | // PUT Profile |
| 204 | // | 204 | // |
| 205 | // @Summary Update profile page of session user. | 205 | // @Description Update profile page of session user. |
| 206 | // @Tags users | 206 | // @Tags users |
| 207 | // @Accept json | 207 | // @Accept json |
| 208 | // @Produce json | 208 | // @Produce json |
| 209 | // @Param Authorization header string true "JWT Token" | 209 | // @Param Authorization header string true "JWT Token" |
| 210 | // @Success 200 {object} models.Response{data=models.ProfileResponse} | 210 | // @Success 200 {object} models.Response{data=models.ProfileResponse} |
| 211 | // @Failure 400 {object} models.Response | 211 | // @Failure 400 {object} models.Response |
| 212 | // @Failure 401 {object} models.Response | 212 | // @Failure 401 {object} models.Response |
| 213 | // @Router /profile [post] | 213 | // @Router /profile [post] |
| 214 | func UpdateUser(c *gin.Context) { | 214 | func UpdateUser(c *gin.Context) { |
| 215 | // Check if user exists | 215 | // Check if user exists |
| 216 | user, exists := c.Get("user") | 216 | user, exists := c.Get("user") |
| @@ -245,16 +245,16 @@ func UpdateUser(c *gin.Context) { | |||
| 245 | 245 | ||
| 246 | // PUT Profile/CountryCode | 246 | // PUT Profile/CountryCode |
| 247 | // | 247 | // |
| 248 | // @Summary Update country code of session user. | 248 | // @Description Update country code of session user. |
| 249 | // @Tags users | 249 | // @Tags users |
| 250 | // @Accept json | 250 | // @Accept json |
| 251 | // @Produce json | 251 | // @Produce json |
| 252 | // @Param Authorization header string true "JWT Token" | 252 | // @Param Authorization header string true "JWT Token" |
| 253 | // @Param country_code query string true "Country Code [XX]" | 253 | // @Param country_code query string true "Country Code [XX]" |
| 254 | // @Success 200 {object} models.Response{data=models.ProfileResponse} | 254 | // @Success 200 {object} models.Response |
| 255 | // @Failure 400 {object} models.Response | 255 | // @Failure 400 {object} models.Response |
| 256 | // @Failure 401 {object} models.Response | 256 | // @Failure 401 {object} models.Response |
| 257 | // @Router /profile [put] | 257 | // @Router /profile [put] |
| 258 | func UpdateCountryCode(c *gin.Context) { | 258 | func UpdateCountryCode(c *gin.Context) { |
| 259 | // Check if user exists | 259 | // Check if user exists |
| 260 | user, exists := c.Get("user") | 260 | user, exists := c.Get("user") |
diff --git a/backend/database/chapters.sql b/backend/database/chapters.sql index f01ae9c..9538bed 100644 --- a/backend/database/chapters.sql +++ b/backend/database/chapters.sql | |||
| @@ -1,17 +1,17 @@ | |||
| 1 | INSERT INTO chapters(id, game_id, name) VALUES | 1 | INSERT INTO chapters(id, game_id, name) VALUES |
| 2 | (1, 1, 'The Coutesy Call'), | 2 | (1, 1, 'Chapter 1 - The Coutesy Call'), |
| 3 | (2, 1, 'The Cold Boot'), | 3 | (2, 1, 'Chapter 2 - The Cold Boot'), |
| 4 | (3, 1, 'The Return'), | 4 | (3, 1, 'Chapter 3 - The Return'), |
| 5 | (4, 1, 'The Surprise'), | 5 | (4, 1, 'Chapter 4 - The Surprise'), |
| 6 | (5, 1, 'The Escape'), | 6 | (5, 1, 'Chapter 5 - The Escape'), |
| 7 | (6, 1, 'The Fall'), | 7 | (6, 1, 'Chapter 6 - The Fall'), |
| 8 | (7, 1, 'The Reunion'), | 8 | (7, 1, 'Chapter 7 - The Reunion'), |
| 9 | (8, 1, 'The Itch'), | 9 | (8, 1, 'Chapter 8 - The Itch'), |
| 10 | (9, 1, 'The Part Where He Kills You'), | 10 | (9, 1, 'Chapter 9 - The Part Where He Kills You'), |
| 11 | (10, 2, 'Introduction'), | 11 | (10, 2, 'Course 0 - Introduction'), |
| 12 | (11, 2, 'Team Building'), | 12 | (11, 2, 'Course 1 - Team Building'), |
| 13 | (12, 2, 'Mass And Velocity'), | 13 | (12, 2, 'Course 2 - Mass And Velocity'), |
| 14 | (13, 2, 'Hard-Light Surfaces'), | 14 | (13, 2, 'Course 3 - Hard-Light Surfaces'), |
| 15 | (14, 2, 'Excursion Funnels'), | 15 | (14, 2, 'Course 4 - Excursion Funnels'), |
| 16 | (15, 2, 'Mobility Gels'), | 16 | (15, 2, 'Course 5 - Mobility Gels'), |
| 17 | (16, 2, 'Art Therapy'); \ No newline at end of file | 17 | (16, 2, 'Course 6 - Art Therapy'); \ No newline at end of file |
diff --git a/backend/database/games.sql b/backend/database/games.sql index 5e2f4ee..0c2374c 100644 --- a/backend/database/games.sql +++ b/backend/database/games.sql | |||
| @@ -1,3 +1,3 @@ | |||
| 1 | INSERT INTO games(id, name) VALUES | 1 | INSERT INTO games(id, name, is_coop) VALUES |
| 2 | (1, 'Portal 2 - Singleplayer'), | 2 | (1, 'Portal 2 - Singleplayer', false), |
| 3 | (2, 'Portal 2 - Cooperative'); \ No newline at end of file | 3 | (2, 'Portal 2 - Cooperative', true); \ No newline at end of file |
diff --git a/backend/database/history.sql b/backend/database/history.sql index 0840bf3..320d72f 100644 --- a/backend/database/history.sql +++ b/backend/database/history.sql | |||
| @@ -1,279 +1,279 @@ | |||
| 1 | INSERT INTO map_history(map_id,user_name,score_count,record_date) VALUES | 1 | INSERT INTO map_history(map_id,category_id,user_name,score_count,record_date) VALUES |
| 2 | -- Portal 2 Singleplayer | 2 | -- Portal 2 Singleplayer |
| 3 | -- 1 | 3 | -- 1 |
| 4 | (3,'slmid1995',3,'2011-10-05 00:00:00'), | 4 | (3,1,'slmid1995',3,'2011-10-05 00:00:00'), |
| 5 | (3,'LookLikeAKango',1,'2011-10-06 00:00:00'), | 5 | (3,1,'LookLikeAKango',1,'2011-10-06 00:00:00'), |
| 6 | (3,'Bananasaurus Rex',0,'2011-10-24 00:00:00'), | 6 | (3,1,'Bananasaurus Rex',0,'2011-10-24 00:00:00'), |
| 7 | (4,'Tyronis',1,'2011-10-05 00:00:00'), | 7 | (4,1,'Tyronis',1,'2011-10-05 00:00:00'), |
| 8 | (4,'Krzyhau',0,'2019-05-10 00:00:00'), | 8 | (4,1,'Krzyhau',0,'2019-05-10 00:00:00'), |
| 9 | (5,'LookLikeAKango',2,'2011-10-05 00:00:00'), | 9 | (5,1,'LookLikeAKango',2,'2011-10-05 00:00:00'), |
| 10 | (5,'Jetwash',1,'2013-12-03 00:00:00'), | 10 | (5,1,'Jetwash',1,'2013-12-03 00:00:00'), |
| 11 | (6,'Stimich',4,'2011-10-08 00:00:00'), | 11 | (6,1,'Stimich',4,'2011-10-08 00:00:00'), |
| 12 | (6,'aepaePolakrn',3,'2011-10-19 00:00:00'), | 12 | (6,1,'aepaePolakrn',3,'2011-10-19 00:00:00'), |
| 13 | (6,'Krzyhau',2,'2020-10-10 00:00:00'), | 13 | (6,1,'Krzyhau',2,'2020-10-10 00:00:00'), |
| 14 | (9,'slmid1995',4,'2011-10-05 00:00:00'), | 14 | (9,1,'slmid1995',4,'2011-10-05 00:00:00'), |
| 15 | (9,'Jokie',3,'2011-10-05 00:00:00'), | 15 | (9,1,'Jokie',3,'2011-10-05 00:00:00'), |
| 16 | (9,'Tyronis',2,'2011-10-05 00:00:00'), | 16 | (9,1,'Tyronis',2,'2011-10-05 00:00:00'), |
| 17 | (9,'sicklebrick',0,'2013-03-13 00:00:00'), | 17 | (9,1,'sicklebrick',0,'2013-03-13 00:00:00'), |
| 18 | -- 2 | 18 | -- 2 |
| 19 | (10,'Paraxade0',2,'2011-04-21 00:00:00'), | 19 | (10,1,'Paraxade0',2,'2011-04-21 00:00:00'), |
| 20 | (10,'PerOculos',0,'2011-04-21 00:00:00'), | 20 | (10,1,'PerOculos',0,'2011-04-21 00:00:00'), |
| 21 | (11,'Tyronis',2,'2011-10-05 00:00:00'), | 21 | (11,1,'Tyronis',2,'2011-10-05 00:00:00'), |
| 22 | (11,'Krzyhau',0,'2018-06-09 00:00:00'), | 22 | (11,1,'Krzyhau',0,'2018-06-09 00:00:00'), |
| 23 | (12,'slmid1995',2,'2011-10-04 00:00:00'), | 23 | (12,1,'slmid1995',2,'2011-10-04 00:00:00'), |
| 24 | (13,'LookLikeAKango',3,'2011-10-05 00:00:00'), | 24 | (13,1,'LookLikeAKango',3,'2011-10-05 00:00:00'), |
| 25 | (13,'Imanex',2,'2011-12-08 00:00:00'), | 25 | (13,1,'Imanex',2,'2011-12-08 00:00:00'), |
| 26 | (13,'jyjey',0,'2012-08-22 00:00:00'), | 26 | (13,1,'jyjey',0,'2012-08-22 00:00:00'), |
| 27 | (15,'Tyronis',2,'2011-10-05 00:00:00'), | 27 | (15,1,'Tyronis',2,'2011-10-05 00:00:00'), |
| 28 | (16,'LookLikeAKango',2,'2011-10-05 00:00:00'), | 28 | (16,1,'LookLikeAKango',2,'2011-10-05 00:00:00'), |
| 29 | (16,'jyjey',0,'2012-08-25 00:00:00'), | 29 | (16,1,'jyjey',0,'2012-08-25 00:00:00'), |
| 30 | (17,'rocoty',0,'2011-10-05 00:00:00'), | 30 | (17,1,'rocoty',2,'2011-10-05 00:00:00'), |
| 31 | (17,'Nidboj132',0,'2023-02-05 00:00:00'), | 31 | (17,1,'Nidboj132',0,'2023-02-05 00:00:00'), |
| 32 | -- 3 | 32 | -- 3 |
| 33 | (18,'The Last Tofus',5,'2011-05-08 00:00:00'), | 33 | (18,1,'The Last Tofus',5,'2011-05-08 00:00:00'), |
| 34 | (18,'Schlepian',4,'2011-10-08 00:00:00'), | 34 | (18,1,'Schlepian',4,'2011-10-08 00:00:00'), |
| 35 | (18,'szeimartin',3,'2013-10-08 00:00:00'), | 35 | (18,1,'szeimartin',3,'2013-10-08 00:00:00'), |
| 36 | (18,'Krzyhau',2,'2020-05-15 00:00:00'), | 36 | (18,1,'Krzyhau',2,'2020-05-15 00:00:00'), |
| 37 | (18,'Krzyhau',0,'2022-07-02 00:00:00'), | 37 | (18,1,'Krzyhau',0,'2022-07-02 00:00:00'), |
| 38 | (19,'LookLikeAKango',2,'2011-10-06 00:00:00'), | 38 | (19,1,'LookLikeAKango',2,'2011-10-06 00:00:00'), |
| 39 | (20,'Djinndrache',5,'2011-10-20 00:00:00'), | 39 | (20,1,'Djinndrache',5,'2011-10-20 00:00:00'), |
| 40 | (20,'Schlepian',4,'2011-10-30 00:00:00'), | 40 | (20,1,'Schlepian',4,'2011-10-30 00:00:00'), |
| 41 | (20,'Jetwash',3,'2014-09-04 00:00:00'), | 41 | (20,1,'Jetwash',3,'2014-09-04 00:00:00'), |
| 42 | (20,'Krzyhau',2,'2022-04-24 00:00:00'), | 42 | (20,1,'Krzyhau',2,'2022-04-24 00:00:00'), |
| 43 | (21,'LookLikeAKango',4,'2011-10-06 00:00:00'), | 43 | (21,1,'LookLikeAKango',4,'2011-10-06 00:00:00'), |
| 44 | (21,'ncla',2,'2011-10-30 00:00:00'), | 44 | (21,1,'ncla',2,'2011-10-30 00:00:00'), |
| 45 | (21,'PerOculos',0,'2019-07-08 00:00:00'), | 45 | (21,1,'PerOculos',0,'2019-07-08 00:00:00'), |
| 46 | (22,'Tyronis',0,'2011-10-05 00:00:00'), | 46 | (22,1,'Tyronis',0,'2011-10-05 00:00:00'), |
| 47 | (23,'LookLikeAKango',2,'2011-10-06 00:00:00'), | 47 | (23,1,'LookLikeAKango',2,'2011-10-06 00:00:00'), |
| 48 | (23,'Krzyhau',0,'2018-08-01 00:00:00'), | 48 | (23,1,'Krzyhau',0,'2018-08-01 00:00:00'), |
| 49 | (24,'LeviHB',0,'2011-04-30 00:00:00'), | 49 | (24,1,'LeviHB',0,'2011-04-30 00:00:00'), |
| 50 | (25,'Tyronis',0,'2011-10-06 00:00:00'), | 50 | (25,1,'Tyronis',0,'2011-10-06 00:00:00'), |
| 51 | (26,'Schlepian',3,'2011-10-30 00:00:00'), | 51 | (26,1,'Schlepian',3,'2011-10-30 00:00:00'), |
| 52 | (26,'Tyronis',2,'2012-01-08 00:00:00'), | 52 | (26,1,'Tyronis',2,'2012-01-08 00:00:00'), |
| 53 | (26,'PerOculos',0,'2016-06-08 00:00:00'), | 53 | (26,1,'PerOculos',0,'2016-06-08 00:00:00'), |
| 54 | -- 4 | 54 | -- 4 |
| 55 | (27,'LeviHB',2,'2011-05-01 00:00:00'), | 55 | (27,1,'LeviHB',2,'2011-05-01 00:00:00'), |
| 56 | (27,'PerOculos',0,'2020-07-13 00:00:00'), | 56 | (27,1,'PerOculos',0,'2020-07-13 00:00:00'), |
| 57 | (28,'LeviHB',7,'2011-05-01 00:00:00'), | 57 | (28,1,'LeviHB',7,'2011-05-01 00:00:00'), |
| 58 | (28,'Andy M.J.',2,'2011-10-07 00:00:00'), | 58 | (28,1,'Andy M.J.',2,'2011-10-07 00:00:00'), |
| 59 | (28,'Krzyhau',0,'2018-05-19 00:00:00'), | 59 | (28,1,'Krzyhau',0,'2018-05-19 00:00:00'), |
| 60 | (29,'LeviHB',0,'2011-05-01 00:00:00'), | 60 | (29,1,'LeviHB',0,'2011-05-01 00:00:00'), |
| 61 | (30,'Schlepian',2,'2011-10-30 00:00:00'), | 61 | (30,1,'Schlepian',2,'2011-10-30 00:00:00'), |
| 62 | (31,'Tyronis',0,'2011-10-06 00:00:00'), | 62 | (31,1,'Tyronis',0,'2011-10-06 00:00:00'), |
| 63 | -- 5 | 63 | -- 5 |
| 64 | (32,'Tyronis',6,'2011-10-21 00:00:00'), | 64 | (32,1,'Tyronis',6,'2011-10-21 00:00:00'), |
| 65 | (32,'Nidboj132',5,'2022-04-24 00:00:00'), | 65 | (32,1,'Nidboj132',5,'2022-04-24 00:00:00'), |
| 66 | (33,'Tyronis',7,'2011-10-06 00:00:00'), | 66 | (33,1,'Tyronis',7,'2011-10-06 00:00:00'), |
| 67 | (33,'ISimmo',5,'2011-11-02 00:00:00'), | 67 | (33,1,'ISimmo',5,'2011-11-02 00:00:00'), |
| 68 | (33,'PerOculos',4,'2017-05-30 00:00:00'), | 68 | (33,1,'PerOculos',4,'2017-05-30 00:00:00'), |
| 69 | (34,'Schlepian',3,'2011-11-01 00:00:00'), | 69 | (34,1,'Schlepian',3,'2011-11-01 00:00:00'), |
| 70 | (34,'Krzyhau',2,'2020-10-14 00:00:00'), | 70 | (34,1,'Krzyhau',2,'2020-10-14 00:00:00'), |
| 71 | (34,'zach',0,'2022-11-02 00:00:00'), | 71 | (34,1,'zach',0,'2022-11-02 00:00:00'), |
| 72 | (35,'Krank',2,'2012-07-28 00:00:00'), | 72 | (35,1,'Krank',2,'2012-07-28 00:00:00'), |
| 73 | -- 6 | 73 | -- 6 |
| 74 | (36,'Tyronis',6,'2011-10-06 00:00:00'), | 74 | (36,1,'Tyronis',6,'2011-10-06 00:00:00'), |
| 75 | (36,'CalmlyFrenetic',5,'2011-10-09 00:00:00'), | 75 | (36,1,'CalmlyFrenetic',5,'2011-10-09 00:00:00'), |
| 76 | (36,'sicklebrick',4,'2012-09-13 00:00:00'), | 76 | (36,1,'sicklebrick',4,'2012-09-13 00:00:00'), |
| 77 | (36,'Nidboj132',2,'2023-03-04 00:00:00'), | 77 | (36,1,'Nidboj132',2,'2023-03-04 00:00:00'), |
| 78 | (37,'LookLikeAKango',7,'2011-10-06 00:00:00'), | 78 | (37,1,'LookLikeAKango',7,'2011-10-06 00:00:00'), |
| 79 | (37,'Schlepian',6,'2011-11-01 00:00:00'), | 79 | (37,1,'Schlepian',6,'2011-11-01 00:00:00'), |
| 80 | (37,'Tyronis',5,'2012-01-28 00:00:00'), | 80 | (37,1,'Tyronis',5,'2012-01-28 00:00:00'), |
| 81 | (37,'Nidboj132',4,'2021-08-22 00:00:00'), | 81 | (37,1,'Nidboj132',4,'2021-08-22 00:00:00'), |
| 82 | (38,'Andy M.J.',2,'2011-10-06 00:00:00'), | 82 | (38,1,'Andy M.J.',2,'2011-10-06 00:00:00'), |
| 83 | (38,'Sanguine Dagger',0,'2012-03-19 00:00:00'), | 83 | (38,1,'Sanguine Dagger',0,'2012-03-19 00:00:00'), |
| 84 | (39,'Lambda Core',6,'2011-05-13 00:00:00'), | 84 | (39,1,'Lambda Core',6,'2011-05-13 00:00:00'), |
| 85 | (39,'The Last Tofus',5,'2011-05-13 00:00:00'), | 85 | (39,1,'The Last Tofus',5,'2011-05-13 00:00:00'), |
| 86 | (39,'LookLikeAKango',4,'2011-10-16 00:00:00'), | 86 | (39,1,'LookLikeAKango',4,'2011-10-16 00:00:00'), |
| 87 | (39,'Kittaye',3,'2013-03-25 00:00:00'), | 87 | (39,1,'Kittaye',3,'2013-03-25 00:00:00'), |
| 88 | (40,'LookLikeAKango',7,'2011-10-07 00:00:00'), | 88 | (40,1,'LookLikeAKango',7,'2011-10-07 00:00:00'), |
| 89 | (40,'Schlepian',6,'2011-11-05 00:00:00'), | 89 | (40,1,'Schlepian',6,'2011-11-05 00:00:00'), |
| 90 | (40,'Kittaye',4,'2013-04-01 00:00:00'), | 90 | (40,1,'Kittaye',4,'2013-04-01 00:00:00'), |
| 91 | (40,'Kittaye',3,'2014-09-13 00:00:00'), | 91 | (40,1,'Kittaye',3,'2014-09-13 00:00:00'), |
| 92 | (40,'szeimartin',2,'2014-09-13 00:00:00'), | 92 | (40,1,'szeimartin',2,'2014-09-13 00:00:00'), |
| 93 | (40,'Kittaye',0,'2014-09-15 00:00:00'), | 93 | (40,1,'Kittaye',0,'2014-09-15 00:00:00'), |
| 94 | (41,'CalmlyFrenetic',7,'2011-10-09 00:00:00'), | 94 | (41,1,'CalmlyFrenetic',7,'2011-10-09 00:00:00'), |
| 95 | (41,'Jaso',6,'2011-10-11 00:00:00'), | 95 | (41,1,'Jaso',6,'2011-10-11 00:00:00'), |
| 96 | (41,'Krank',5,'2012-07-17 00:00:00'), | 96 | (41,1,'Krank',5,'2012-07-17 00:00:00'), |
| 97 | -- 7 | 97 | -- 7 |
| 98 | (42,'LookLikeAKango',4,'2011-05-17 00:00:00'), | 98 | (42,1,'LookLikeAKango',4,'2011-05-17 00:00:00'), |
| 99 | (42,'ISimmo',2,'2011-11-07 00:00:00'), | 99 | (42,1,'ISimmo',2,'2011-11-07 00:00:00'), |
| 100 | (43,'lmao4ever',5,'2011-10-30 00:00:00'), | 100 | (43,1,'lmao4ever',5,'2011-10-30 00:00:00'), |
| 101 | (43,'Jaso',2,'2011-11-09 00:00:00'), | 101 | (43,1,'Jaso',2,'2011-11-09 00:00:00'), |
| 102 | (43,'feliser',0,'2022-06-26 00:00:00'), | 102 | (43,1,'feliser',0,'2022-06-26 00:00:00'), |
| 103 | (44,'LookLikeAKango',18,'2011-10-07 00:00:00'), | 103 | (44,1,'LookLikeAKango',18,'2011-10-07 00:00:00'), |
| 104 | (44,'Tyronis',13,'2011-10-30 00:00:00'), | 104 | (44,1,'Tyronis',13,'2011-10-30 00:00:00'), |
| 105 | (44,'Tyronis',12,'2011-11-10 00:00:00'), | 105 | (44,1,'Tyronis',12,'2011-11-10 00:00:00'), |
| 106 | (44,'Jetwash',11,'2017-06-12 00:00:00'), | 106 | (44,1,'Jetwash',11,'2017-06-12 00:00:00'), |
| 107 | (44,'Krzyhau',9,'2022-01-02 00:00:00'), | 107 | (44,1,'Krzyhau',9,'2022-01-02 00:00:00'), |
| 108 | (45,'LookLikeAKango',23,'2011-10-08 00:00:00'), | 108 | (45,1,'LookLikeAKango',23,'2011-10-08 00:00:00'), |
| 109 | (45,'CalmlyFrenetic',22,'2011-10-09 00:00:00'), | 109 | (45,1,'CalmlyFrenetic',22,'2011-10-09 00:00:00'), |
| 110 | (45,'cgreactor',17,'2011-10-09 00:00:00'), | 110 | (45,1,'cgreactor',17,'2011-10-09 00:00:00'), |
| 111 | (45,'CalmlyFrenetic',16,'2011-10-10 00:00:00'), | 111 | (45,1,'CalmlyFrenetic',16,'2011-10-10 00:00:00'), |
| 112 | (45,'LookLikeAKango',15,'2011-10-19 00:00:00'), | 112 | (45,1,'LookLikeAKango',15,'2011-10-19 00:00:00'), |
| 113 | (45,'Jaso',12,'2012-07-19 00:00:00'), | 113 | (45,1,'Jaso',12,'2012-07-19 00:00:00'), |
| 114 | (45,'Krank',10,'2013-01-31 00:00:00'), | 114 | (45,1,'Krank',10,'2013-01-31 00:00:00'), |
| 115 | (45,'Kittaye',7,'2013-04-04 00:00:00'), | 115 | (45,1,'Kittaye',7,'2013-04-04 00:00:00'), |
| 116 | (45,'PerOculos',4,'2014-09-13 00:00:00'), | 116 | (45,1,'PerOculos',4,'2014-09-13 00:00:00'), |
| 117 | -- 8 | 117 | -- 8 |
| 118 | (46,'sparkle1princess',6,'2012-03-24 00:00:00'), | 118 | (46,1,'sparkle1princess',6,'2012-03-24 00:00:00'), |
| 119 | (46,'Krzyhau',2,'2019-11-21 00:00:00'), | 119 | (46,1,'Krzyhau',2,'2019-11-21 00:00:00'), |
| 120 | (47,'holydevel',2,'2011-10-06 00:00:00'), | 120 | (47,1,'holydevel',2,'2011-10-06 00:00:00'), |
| 121 | (47,'JesusCatFace',0,'2015-01-16 00:00:00'), | 121 | (47,1,'JesusCatFace',0,'2015-01-16 00:00:00'), |
| 122 | (48,'LookLikeAKango',5,'2011-10-08 00:00:00'), | 122 | (48,1,'LookLikeAKango',5,'2011-10-08 00:00:00'), |
| 123 | (48,'Tyronis',2,'2011-10-08 00:00:00'), | 123 | (48,1,'Tyronis',2,'2011-10-08 00:00:00'), |
| 124 | (48,'adzicents',0,'2011-10-09 00:00:00'), | 124 | (48,1,'adzicents',0,'2011-10-09 00:00:00'), |
| 125 | (49,'adzicents',4,'2011-10-07 00:00:00'), | 125 | (49,1,'adzicents',4,'2011-10-07 00:00:00'), |
| 126 | (49,'Schlepian',2,'2011-10-08 00:00:00'), | 126 | (49,1,'Schlepian',2,'2011-10-08 00:00:00'), |
| 127 | (49,'Nidboj132',0,'2022-09-26 00:00:00'), | 127 | (49,1,'Nidboj132',0,'2022-09-26 00:00:00'), |
| 128 | (50,'LookLikeAKango',4,'2011-10-08 00:00:00'), | 128 | (50,1,'LookLikeAKango',4,'2011-10-08 00:00:00'), |
| 129 | (50,'Tyronis',2,'2011-10-11 00:00:00'), | 129 | (50,1,'Tyronis',2,'2011-10-11 00:00:00'), |
| 130 | (50,'sicklebrick',0,'2013-03-20 00:00:00'), | 130 | (50,1,'sicklebrick',0,'2013-03-20 00:00:00'), |
| 131 | (51,'Andy M.J.',3,'2011-10-08 00:00:00'), | 131 | (51,1,'Andy M.J.',3,'2011-10-08 00:00:00'), |
| 132 | (51,'LookLikeAKango',2,'2011-10-20 00:00:00'), | 132 | (51,1,'LookLikeAKango',2,'2011-10-20 00:00:00'), |
| 133 | (52,'Jaso',0,'2011-10-10 00:00:00'), | 133 | (52,1,'Jaso',0,'2011-10-10 00:00:00'), |
| 134 | (53,'LookLikeAKango',9,'2011-10-08 00:00:00'), | 134 | (53,1,'LookLikeAKango',9,'2011-10-08 00:00:00'), |
| 135 | (53,'LookLikeAKango',2,'2011-10-20 00:00:00'), | 135 | (53,1,'LookLikeAKango',2,'2011-10-20 00:00:00'), |
| 136 | (53,'Schlepian',0,'2011-11-06 00:00:00'), | 136 | (53,1,'Schlepian',0,'2011-11-06 00:00:00'), |
| 137 | (54,'LookLikeAKango',7,'2011-06-01 00:00:00'), | 137 | (54,1,'LookLikeAKango',7,'2011-06-01 00:00:00'), |
| 138 | (54,'Jaso',6,'2011-10-09 00:00:00'), | 138 | (54,1,'Jaso',6,'2011-10-09 00:00:00'), |
| 139 | (54,'Schlepian',5,'2011-11-06 00:00:00'), | 139 | (54,1,'Schlepian',5,'2011-11-06 00:00:00'), |
| 140 | (54,'Spyrunite',4,'2012-08-30 00:00:00'), | 140 | (54,1,'Spyrunite',4,'2012-08-30 00:00:00'), |
| 141 | (54,'Krzyhau',3,'2019-04-22 00:00:00'), | 141 | (54,1,'Krzyhau',3,'2019-04-22 00:00:00'), |
| 142 | (55,'LookLikeAKango',7,'2011-10-08 00:00:00'), | 142 | (55,1,'LookLikeAKango',7,'2011-10-08 00:00:00'), |
| 143 | (55,'CalmlyFrenetic',3,'2011-10-09 00:00:00'), | 143 | (55,1,'CalmlyFrenetic',3,'2011-10-09 00:00:00'), |
| 144 | (55,'Jaso',2,'2011-11-26 00:00:00'), | 144 | (55,1,'Jaso',2,'2011-11-26 00:00:00'), |
| 145 | (55,'PerOculos',0,'2021-02-06 00:00:00'), | 145 | (55,1,'PerOculos',0,'2021-02-06 00:00:00'), |
| 146 | (56,'CalmlyFrenetic',9,'2011-10-08 00:00:00'), | 146 | (56,1,'CalmlyFrenetic',9,'2011-10-08 00:00:00'), |
| 147 | (56,'LookLikeAKango',5,'2011-10-09 00:00:00'), | 147 | (56,1,'LookLikeAKango',5,'2011-10-09 00:00:00'), |
| 148 | (56,'CalmlyFrenetic',4,'2011-10-09 00:00:00'), | 148 | (56,1,'CalmlyFrenetic',4,'2011-10-09 00:00:00'), |
| 149 | (56,'Jetwash',2,'2014-09-05 00:00:00'), | 149 | (56,1,'Jetwash',2,'2014-09-05 00:00:00'), |
| 150 | -- 9 | 150 | -- 9 |
| 151 | (57,'JNS',7,'2011-07-21 00:00:00'), | 151 | (57,1,'JNS',7,'2011-07-21 00:00:00'), |
| 152 | (57,'Krank',5,'2012-07-29 00:00:00'), | 152 | (57,1,'Krank',5,'2012-07-29 00:00:00'), |
| 153 | (57,'Krzyhau',0,'2017-10-29 00:00:00'), | 153 | (57,1,'Krzyhau',0,'2017-10-29 00:00:00'), |
| 154 | (58,'Stimich',2,'2011-10-11 00:00:00'), | 154 | (58,1,'Stimich',2,'2011-10-11 00:00:00'), |
| 155 | (59,'Isimmo',7,'2011-11-04 00:00:00'), | 155 | (59,1,'Isimmo',7,'2011-11-04 00:00:00'), |
| 156 | (59,'sicklebrick',6,'2013-03-20 00:00:00'), | 156 | (59,1,'sicklebrick',6,'2013-03-20 00:00:00'), |
| 157 | (60,'CalmlyFrenetic',7,'2011-10-19 00:00:00'), | 157 | (60,1,'CalmlyFrenetic',7,'2011-10-19 00:00:00'), |
| 158 | (60,'Tyronis',6,'2011-11-01 00:00:00'), | 158 | (60,1,'Tyronis',6,'2011-11-01 00:00:00'), |
| 159 | -- Portal 2 Cooperative | 159 | -- Portal 2 Cooperative |
| 160 | -- 1 | 160 | -- 1 |
| 161 | (63,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01 00:00:00'), | 161 | (63,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01 00:00:00'), |
| 162 | (64,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), | 162 | (64,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), |
| 163 | (64,'Chubfish & Exhale',2,'2011-11-01 00:00:00'), | 163 | (64,1,'Chubfish & Exhale',2,'2011-11-01 00:00:00'), |
| 164 | (65,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), | 164 | (65,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), |
| 165 | (65,'Nidboj132 & Oryn',3,'2022-02-03 00:00:00'), | 165 | (65,1,'Nidboj132 & Oryn',3,'2022-02-03 00:00:00'), |
| 166 | (66,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), | 166 | (66,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), |
| 167 | (66,'Schlepian & Chubfish',2,'2011-10-01 00:00:00'), | 167 | (66,1,'Schlepian & Chubfish',2,'2011-10-01 00:00:00'), |
| 168 | (67,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01 00:00:00'), | 168 | (67,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01 00:00:00'), |
| 169 | (68,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01 00:00:00'), | 169 | (68,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01 00:00:00'), |
| 170 | -- 2 | 170 | -- 2 |
| 171 | (69,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), | 171 | (69,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), |
| 172 | (70,'Mathias123961 & Sir Spawn Alot',6,'2011-08-01 00:00:00'), | 172 | (70,1,'Mathias123961 & Sir Spawn Alot',6,'2011-08-01 00:00:00'), |
| 173 | (70,'Schlepian & Chubfish',4,'2011-10-01 00:00:00'), | 173 | (70,1,'Schlepian & Chubfish',4,'2011-10-01 00:00:00'), |
| 174 | (70,'Gocnak & z1mb0bw4y',2,'2012-08-03 00:00:00'), | 174 | (70,1,'Gocnak & z1mb0bw4y',2,'2012-08-03 00:00:00'), |
| 175 | (70,'DM_ & VEGA',0,'2017-10-01 00:00:00'), | 175 | (70,1,'DM_ & VEGA',0,'2017-10-01 00:00:00'), |
| 176 | (71,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), | 176 | (71,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), |
| 177 | (71,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01 00:00:00'), | 177 | (71,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01 00:00:00'), |
| 178 | (72,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), | 178 | (72,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), |
| 179 | (72,'Schlepian & LongJohnDickWeed',2,'2011-10-01 00:00:00'), | 179 | (72,1,'Schlepian & LongJohnDickWeed',2,'2011-10-01 00:00:00'), |
| 180 | (73,'Stimich & HiTMaRkS',9,'2011-05-09 00:00:00'), | 180 | (73,1,'Stimich & HiTMaRkS',9,'2011-05-09 00:00:00'), |
| 181 | (73,'Mathias123961 & Sir Spawn Alot',8,'2011-08-01 00:00:00'), | 181 | (73,1,'Mathias123961 & Sir Spawn Alot',8,'2011-08-01 00:00:00'), |
| 182 | (73,'Schlepian & Lemonsunshine',7,'2011-11-01 00:00:00'), | 182 | (73,1,'Schlepian & Lemonsunshine',7,'2011-11-01 00:00:00'), |
| 183 | (73,'DM_ & LsDK_',6,'2018-01-01 00:00:00'), | 183 | (73,1,'DM_ & LsDK_',6,'2018-01-01 00:00:00'), |
| 184 | (73,'Krzyhau & Klooger',4,'2018-11-01 00:00:00'), | 184 | (73,1,'Krzyhau & Klooger',4,'2018-11-01 00:00:00'), |
| 185 | (74,'Mathias123961 & Sir Spawn Alot',5,'2011-08-01 00:00:00'), | 185 | (74,1,'Mathias123961 & Sir Spawn Alot',5,'2011-08-01 00:00:00'), |
| 186 | (74,'Stimich & Pitkakorva',7,'2011-10-11 00:00:00'), | 186 | (74,1,'Stimich & Pitkakorva',7,'2011-10-11 00:00:00'), |
| 187 | (74,'Schlepian & Isimmo',3,'2011-10-28 00:00:00'), | 187 | (74,1,'Schlepian & Isimmo',3,'2011-10-28 00:00:00'), |
| 188 | (74,'Zypeh & szeimartin',2,'2013-11-01 00:00:00'), | 188 | (74,1,'Zypeh & szeimartin',2,'2013-11-01 00:00:00'), |
| 189 | (75,'Mathias123961 & Sir Spawn Alot',5,'2011-08-01 00:00:00'), | 189 | (75,1,'Mathias123961 & Sir Spawn Alot',5,'2011-08-01 00:00:00'), |
| 190 | (75,'Schlepian & Urination',4,'2011-10-01 00:00:00'), | 190 | (75,1,'Schlepian & Urination',4,'2011-10-01 00:00:00'), |
| 191 | (75,'Schlepian & Lemonsunshine',2,'2012-02-01 00:00:00'), | 191 | (75,1,'Schlepian & Lemonsunshine',2,'2012-02-01 00:00:00'), |
| 192 | (75,'DM_ & follon',0,'2015-04-01 00:00:00'), | 192 | (75,1,'DM_ & follon',0,'2015-04-01 00:00:00'), |
| 193 | (76,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), | 193 | (76,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), |
| 194 | (76,'Chubfish & Exhale',0,'2011-12-01 00:00:00'), | 194 | (76,1,'Chubfish & Exhale',0,'2011-12-01 00:00:00'), |
| 195 | -- 3 | 195 | -- 3 |
| 196 | (77,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), | 196 | (77,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), |
| 197 | (78,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), | 197 | (78,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), |
| 198 | (78,'DM_ & marK',3,'2016-11-01 00:00:00'), | 198 | (78,1,'DM_ & marK',3,'2016-11-01 00:00:00'), |
| 199 | (78,'Nidboj132 & Oryn',2,'2021-09-04 00:00:00'), | 199 | (78,1,'Nidboj132 & Oryn',2,'2021-09-04 00:00:00'), |
| 200 | (79,'ganonscrub & ?',5,'2011-04-01 00:00:00'), | 200 | (79,1,'ganonscrub & ?',5,'2011-04-01 00:00:00'), |
| 201 | (79,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), | 201 | (79,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), |
| 202 | (79,'Chubfish & Exhale',2,'2012-08-04 00:00:00'), | 202 | (79,1,'Chubfish & Exhale',2,'2012-08-04 00:00:00'), |
| 203 | (80,'Mathias123961 & Sir Spawn Alot',5,'2011-08-01 00:00:00'), | 203 | (80,1,'Mathias123961 & Sir Spawn Alot',5,'2011-08-01 00:00:00'), |
| 204 | (80,'Chubfish & Exhale',4,'2011-12-01 00:00:00'), | 204 | (80,1,'Chubfish & Exhale',4,'2011-12-01 00:00:00'), |
| 205 | (81,'Mathias123961 & Sir Spawn Alot',7,'2011-08-01 00:00:00'), | 205 | (81,1,'Mathias123961 & Sir Spawn Alot',7,'2011-08-01 00:00:00'), |
| 206 | (81,'Schlepian & Lemonsunshine',6,'2011-10-01 00:00:00'), | 206 | (81,1,'Schlepian & Lemonsunshine',6,'2011-10-01 00:00:00'), |
| 207 | (81,'takz & dawn',5,'2011-11-01 00:00:00'), | 207 | (81,1,'takz & dawn',5,'2011-11-01 00:00:00'), |
| 208 | (81,'Nidboj132 & Oryn',4,'2021-03-25 00:00:00'), | 208 | (81,1,'Nidboj132 & Oryn',4,'2021-03-25 00:00:00'), |
| 209 | (82,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), | 209 | (82,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), |
| 210 | (83,'Mathias123961 & Sir Spawn Alot',5,'2011-08-01 00:00:00'), | 210 | (83,1,'Mathias123961 & Sir Spawn Alot',5,'2011-08-01 00:00:00'), |
| 211 | (83,'Schlepian & Lemonsunshine',2,'2011-10-01 00:00:00'), | 211 | (83,1,'Schlepian & Lemonsunshine',2,'2011-10-01 00:00:00'), |
| 212 | (83,'Chubfish & Exhale',0,'2011-12-01 00:00:00'), | 212 | (83,1,'Chubfish & Exhale',0,'2011-12-01 00:00:00'), |
| 213 | (84,'Mathias123961 & Sir Spawn Alot',6,'2011-08-01 00:00:00'), | 213 | (84,1,'Mathias123961 & Sir Spawn Alot',6,'2011-08-01 00:00:00'), |
| 214 | (84,'Schlepian & Chubfish',4,'2011-10-01 00:00:00'), | 214 | (84,1,'Schlepian & Chubfish',4,'2011-10-01 00:00:00'), |
| 215 | (84,'Chubfish & Exhale',2,'2012-01-01 00:00:00'), | 215 | (84,1,'Chubfish & Exhale',2,'2012-01-01 00:00:00'), |
| 216 | (84,'DM_ & wS',0,'2015-05-01 00:00:00'), | 216 | (84,1,'DM_ & wS',0,'2015-05-01 00:00:00'), |
| 217 | -- 4 | 217 | -- 4 |
| 218 | (85,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), | 218 | (85,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), |
| 219 | (85,'Chubfish & Exhale',0,'2011-10-01 00:00:00'), | 219 | (85,1,'Chubfish & Exhale',0,'2011-10-01 00:00:00'), |
| 220 | (86,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), | 220 | (86,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), |
| 221 | (86,'Chubfish & Exhale',0,'2011-12-01 00:00:00'), | 221 | (86,1,'Chubfish & Exhale',0,'2011-12-01 00:00:00'), |
| 222 | (87,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), | 222 | (87,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), |
| 223 | (87,'Schlepian & Gopherdude',2,'2011-10-01 00:00:00'), | 223 | (87,1,'Schlepian & Gopherdude',2,'2011-10-01 00:00:00'), |
| 224 | (87,'DM_ & follon',0,'2015-04-01 00:00:00'), | 224 | (87,1,'DM_ & follon',0,'2015-04-01 00:00:00'), |
| 225 | (88,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), | 225 | (88,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), |
| 226 | (88,'Schlepian & Gopherdude',0,'2011-10-01 00:00:00'), | 226 | (88,1,'Schlepian & Gopherdude',0,'2011-10-01 00:00:00'), |
| 227 | (89,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01 00:00:00'), | 227 | (89,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01 00:00:00'), |
| 228 | (90,'Mathias123961 & Sir Spawn Alot',4,'2011-09-01 00:00:00'), | 228 | (90,1,'Mathias123961 & Sir Spawn Alot',4,'2011-09-01 00:00:00'), |
| 229 | (90,'Schlepian & Urination',2,'2011-10-01 00:00:00'), | 229 | (90,1,'Schlepian & Urination',2,'2011-10-01 00:00:00'), |
| 230 | (90,'Klooger & Jetwash',0,'2016-08-01 00:00:00'), | 230 | (90,1,'Klooger & Jetwash',0,'2016-08-01 00:00:00'), |
| 231 | (91,'Mathias123961 & Sir Spawn Alot',2,'2011-08-01 00:00:00'), | 231 | (91,1,'Mathias123961 & Sir Spawn Alot',2,'2011-08-01 00:00:00'), |
| 232 | (91,'Undead & Zypeh',0,'2013-05-19 00:00:00'), | 232 | (91,1,'Undead & Zypeh',0,'2013-05-19 00:00:00'), |
| 233 | (92,'txx478 & ?',5,'2011-05-01 00:00:00'), | 233 | (92,1,'txx478 & ?',5,'2011-05-01 00:00:00'), |
| 234 | (92,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), | 234 | (92,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), |
| 235 | (92,'Schlepian & Gopherdude',2,'2011-10-01 00:00:00'), | 235 | (92,1,'Schlepian & Gopherdude',2,'2011-10-01 00:00:00'), |
| 236 | (92,'ncla & takz',0,'2012-02-01 00:00:00'), | 236 | (92,1,'ncla & takz',0,'2012-02-01 00:00:00'), |
| 237 | (93,'Mathias123961 & Sir Spawn Alot',2,'2011-08-01 00:00:00'), | 237 | (93,1,'Mathias123961 & Sir Spawn Alot',2,'2011-08-01 00:00:00'), |
| 238 | (93,'Schlepian & Gopherdude',0,'2011-10-01 00:00:00'), | 238 | (93,1,'Schlepian & Gopherdude',0,'2011-10-01 00:00:00'), |
| 239 | -- 5 | 239 | -- 5 |
| 240 | (94,'Chubfish & Exhale',2,'2011-10-01 00:00:00'), | 240 | (94,1,'Chubfish & Exhale',2,'2011-10-01 00:00:00'), |
| 241 | (94,'Klooger & Imanex',0,'2013-08-01 00:00:00'), | 241 | (94,1,'Klooger & Imanex',0,'2013-08-01 00:00:00'), |
| 242 | (95,'Schlepian & Issimoi',2,'2011-10-01 00:00:00'), | 242 | (95,1,'Schlepian & Issimoi',2,'2011-10-01 00:00:00'), |
| 243 | (96,'ThePortalPatrol & ?',4,'2011-04-01 00:00:00'), | 243 | (96,1,'ThePortalPatrol & ?',4,'2011-04-01 00:00:00'), |
| 244 | (96,'sparkle1princess & Zypeh',2,'2014-01-01 00:00:00'), | 244 | (96,1,'sparkle1princess & Zypeh',2,'2014-01-01 00:00:00'), |
| 245 | (97,'Stimich & HiTMaRkS',7,'2011-05-13 00:00:00'), | 245 | (97,1,'Stimich & HiTMaRkS',7,'2011-05-13 00:00:00'), |
| 246 | (97,'Schlepian & Lemonsunshine',4,'2011-10-01 00:00:00'), | 246 | (97,1,'Schlepian & Lemonsunshine',4,'2011-10-01 00:00:00'), |
| 247 | (97,'DM_ & wS',2,'2014-05-01 00:00:00'), | 247 | (97,1,'DM_ & wS',2,'2014-05-01 00:00:00'), |
| 248 | (98,'Imanex & 00svo',0,'2011-11-01 00:00:00'), | 248 | (98,1,'Imanex & 00svo',0,'2011-11-01 00:00:00'), |
| 249 | (99,'Schlepian & Gopherdude',3,'2011-10-01 00:00:00'), | 249 | (99,1,'Schlepian & Gopherdude',3,'2011-10-01 00:00:00'), |
| 250 | (99,'Imanex & Klooger',2,'2013-08-01 00:00:00'), | 250 | (99,1,'Imanex & Klooger',2,'2013-08-01 00:00:00'), |
| 251 | (99,'DM_ & wS',0,'2015-05-01 00:00:00'), | 251 | (99,1,'DM_ & wS',0,'2015-05-01 00:00:00'), |
| 252 | (100,'Schlepian & Bananasaurus Rex',0,'2011-10-01 00:00:00'), | 252 | (100,1,'Schlepian & Bananasaurus Rex',0,'2011-10-01 00:00:00'), |
| 253 | (101,'Chubfish & Exhale',2,'2011-12-01 00:00:00'), | 253 | (101,1,'Chubfish & Exhale',2,'2011-12-01 00:00:00'), |
| 254 | (101,'DM_ & follon',0,'2015-04-01 00:00:00'), | 254 | (101,1,'DM_ & follon',0,'2015-04-01 00:00:00'), |
| 255 | -- 6 | 255 | -- 6 |
| 256 | (102,'dawn & takz',3,'2011-11-18 00:00:00'), | 256 | (102,1,'dawn & takz',3,'2011-11-18 00:00:00'), |
| 257 | (102,'Chubfish & Exhale',2,'2012-01-01 00:00:00'), | 257 | (102,1,'Chubfish & Exhale',2,'2012-01-01 00:00:00'), |
| 258 | (102,'Imanex & Klooger',0,'2013-08-01 00:00:00'), | 258 | (102,1,'Imanex & Klooger',0,'2013-08-01 00:00:00'), |
| 259 | (103,'Schlepian & Lemonsunshine',0,'2011-10-01 00:00:00'), | 259 | (103,1,'Schlepian & Lemonsunshine',0,'2011-10-01 00:00:00'), |
| 260 | (104,'Schlepian & Lemonsunshine',0,'2011-10-01 00:00:00'), | 260 | (104,1,'Schlepian & Lemonsunshine',0,'2011-10-01 00:00:00'), |
| 261 | (105,'Blaizerazer & ?',8,'2011-10-01 00:00:00'), | 261 | (105,1,'Blaizerazer & ?',8,'2011-10-01 00:00:00'), |
| 262 | (105,'Schlepian & Lemonsunshine',5,'2011-11-01 00:00:00'), | 262 | (105,1,'Schlepian & Lemonsunshine',5,'2011-11-01 00:00:00'), |
| 263 | (105,'Imanex & Klooger',4,'2013-08-01 00:00:00'), | 263 | (105,1,'Imanex & Klooger',4,'2013-08-01 00:00:00'), |
| 264 | (105,'DM_ & wS',3,'2014-05-01 00:00:00'), | 264 | (105,1,'DM_ & wS',3,'2014-05-01 00:00:00'), |
| 265 | (105,'DM_ & follon',2,'2015-04-01 00:00:00'), | 265 | (105,1,'DM_ & follon',2,'2015-04-01 00:00:00'), |
| 266 | (106,'Schlepian & Bananasaurus Rex',4,'2011-10-01 00:00:00'), | 266 | (106,1,'Schlepian & Bananasaurus Rex',4,'2011-10-01 00:00:00'), |
| 267 | (106,'Gig & takz',3,'2012-06-01 00:00:00'), | 267 | (106,1,'Gig & takz',3,'2012-06-01 00:00:00'), |
| 268 | (106,'Imanex & Klooger',0,'2013-06-01 00:00:00'), | 268 | (106,1,'Imanex & Klooger',0,'2013-06-01 00:00:00'), |
| 269 | (107,'Chubfish & Exhale',2,'2011-10-01 00:00:00'), | 269 | (107,1,'Chubfish & Exhale',2,'2011-10-01 00:00:00'), |
| 270 | (107,'DM_ & follon',0,'2015-04-01 00:00:00'), | 270 | (107,1,'DM_ & follon',0,'2015-04-01 00:00:00'), |
| 271 | (108,'DaFox & P',0,'2011-12-01 00:00:00'), | 271 | (108,1,'DaFox & P',0,'2011-12-01 00:00:00'), |
| 272 | (109,'Schlepian & Tyronis',5,'2011-10-01 00:00:00'), | 272 | (109,1,'Schlepian & Tyronis',5,'2011-10-01 00:00:00'), |
| 273 | (109,'Chubfish & Exhale',0,'2011-12-01 00:00:00'), | 273 | (109,1,'Chubfish & Exhale',0,'2011-12-01 00:00:00'), |
| 274 | (110,'Tyronis & mr.bob806',15,'2011-10-01 00:00:00'), | 274 | (110,1,'Tyronis & mr.bob806',15,'2011-10-01 00:00:00'), |
| 275 | (110,'Schlepian & Chubfish',6,'2011-11-01 00:00:00'), | 275 | (110,1,'Schlepian & Chubfish',6,'2011-11-01 00:00:00'), |
| 276 | (110,'00svo & z1mb0bw4y',5,'2012-08-08 00:00:00'), | 276 | (110,1,'00svo & z1mb0bw4y',5,'2012-08-08 00:00:00'), |
| 277 | (110,'00svo & z1mb0bw4y',4,'2012-08-10 00:00:00'), | 277 | (110,1,'00svo & z1mb0bw4y',4,'2012-08-10 00:00:00'), |
| 278 | (110,'Klooger & z1mb0bw4y',2,'2014-02-01 00:00:00'), | 278 | (110,1,'Klooger & z1mb0bw4y',2,'2014-02-01 00:00:00'), |
| 279 | (110,'DM_ & follon',0,'2015-04-01 00:00:00'); \ No newline at end of file | 279 | (110,1,'DM_ & follon',0,'2015-04-01 00:00:00'); \ No newline at end of file |
diff --git a/backend/database/init.sql b/backend/database/init.sql index 51a4881..50e7c15 100644 --- a/backend/database/init.sql +++ b/backend/database/init.sql | |||
| @@ -9,13 +9,14 @@ CREATE TABLE users ( | |||
| 9 | ); | 9 | ); |
| 10 | 10 | ||
| 11 | CREATE TABLE games ( | 11 | CREATE TABLE games ( |
| 12 | id SMALLSERIAL, | 12 | id SERIAL, |
| 13 | name TEXT NOT NULL, | 13 | name TEXT NOT NULL, |
| 14 | is_coop BOOLEAN NOT NULL, | ||
| 14 | PRIMARY KEY (id) | 15 | PRIMARY KEY (id) |
| 15 | ); | 16 | ); |
| 16 | 17 | ||
| 17 | CREATE TABLE chapters ( | 18 | CREATE TABLE chapters ( |
| 18 | id SMALLSERIAL, | 19 | id SERIAL, |
| 19 | game_id SMALLINT NOT NULL, | 20 | game_id SMALLINT NOT NULL, |
| 20 | name TEXT NOT NULL, | 21 | name TEXT NOT NULL, |
| 21 | PRIMARY KEY (id), | 22 | PRIMARY KEY (id), |
| @@ -23,52 +24,57 @@ CREATE TABLE chapters ( | |||
| 23 | ); | 24 | ); |
| 24 | 25 | ||
| 25 | CREATE TABLE categories ( | 26 | CREATE TABLE categories ( |
| 26 | id SMALLSERIAL, | 27 | id SERIAL, |
| 27 | name TEXT NOT NULL, | 28 | name TEXT NOT NULL, |
| 28 | PRIMARY KEY (id) | 29 | PRIMARY KEY (id) |
| 29 | ); | 30 | ); |
| 30 | 31 | ||
| 31 | CREATE TABLE maps ( | 32 | CREATE TABLE maps ( |
| 32 | id SMALLSERIAL, | 33 | id SERIAL, |
| 33 | game_id SMALLINT NOT NULL, | 34 | game_id SMALLINT NOT NULL, |
| 34 | chapter_id SMALLINT NOT NULL, | 35 | chapter_id SMALLINT NOT NULL, |
| 35 | name TEXT NOT NULL, | 36 | name TEXT NOT NULL, |
| 36 | description TEXT NOT NULL, | ||
| 37 | showcase TEXT NOT NULL, | ||
| 38 | is_disabled BOOLEAN NOT NULL DEFAULT false, | 37 | is_disabled BOOLEAN NOT NULL DEFAULT false, |
| 39 | PRIMARY KEY (id), | 38 | PRIMARY KEY (id), |
| 40 | FOREIGN KEY (game_id) REFERENCES games(id), | 39 | FOREIGN KEY (game_id) REFERENCES games(id), |
| 41 | FOREIGN KEY (chapter_id) REFERENCES chapters(id) | 40 | FOREIGN KEY (chapter_id) REFERENCES chapters(id) |
| 42 | ); | 41 | ); |
| 43 | 42 | ||
| 43 | CREATE TABLE map_routes ( | ||
| 44 | id SERIAL, | ||
| 45 | map_id SMALLINT NOT NULL, | ||
| 46 | category_id SMALLINT NOT NULL, | ||
| 47 | score_count SMALLINT NOT NULL, | ||
| 48 | description TEXT NOT NULL, | ||
| 49 | showcase TEXT NOT NULL, | ||
| 50 | PRIMARY KEY (id), | ||
| 51 | FOREIGN KEY (map_id) REFERENCES maps(id), | ||
| 52 | FOREIGN KEY (category_id) REFERENCES categories(id), | ||
| 53 | UNIQUE (map_id, category_id, score_count) | ||
| 54 | ); | ||
| 55 | |||
| 44 | CREATE TABLE map_history ( | 56 | CREATE TABLE map_history ( |
| 45 | id SMALLSERIAL, | 57 | id SERIAL, |
| 46 | map_id SMALLINT NOT NULL, | 58 | map_id SMALLINT NOT NULL, |
| 59 | category_id SMALLINT NOT NULL, | ||
| 47 | user_name TEXT NOT NULL, | 60 | user_name TEXT NOT NULL, |
| 48 | score_count SMALLINT NOT NULL, | 61 | score_count SMALLINT NOT NULL, |
| 49 | record_date TIMESTAMP NOT NULL, | 62 | record_date TIMESTAMP NOT NULL, |
| 50 | PRIMARY KEY (id), | 63 | PRIMARY KEY (id), |
| 51 | FOREIGN KEY (map_id) REFERENCES maps(id) | 64 | FOREIGN KEY (category_id) REFERENCES categories(id), |
| 65 | FOREIGN KEY (map_id) REFERENCES maps(id), | ||
| 66 | UNIQUE (map_id, category_id, score_count) | ||
| 52 | ); | 67 | ); |
| 53 | 68 | ||
| 54 | CREATE TABLE map_ratings ( | 69 | CREATE TABLE map_ratings ( |
| 55 | id SERIAL, | 70 | id SERIAL, |
| 56 | map_id SMALLINT NOT NULL, | 71 | map_id SMALLINT NOT NULL, |
| 72 | category_id SMALLINT NOT NULL, | ||
| 57 | user_id TEXT NOT NULL, | 73 | user_id TEXT NOT NULL, |
| 58 | rating SMALLINT NOT NULL, | 74 | rating SMALLINT NOT NULL, |
| 59 | PRIMARY KEY (id), | 75 | PRIMARY KEY (id), |
| 60 | FOREIGN KEY (map_id) REFERENCES maps(id), | 76 | FOREIGN KEY (map_id) REFERENCES maps(id), |
| 61 | FOREIGN KEY (user_id) REFERENCES users(steam_id) | 77 | FOREIGN KEY (category_id) REFERENCES categories(id), |
| 62 | ); | ||
| 63 | |||
| 64 | CREATE TABLE map_routers ( | ||
| 65 | id SMALLSERIAL, | ||
| 66 | map_id SMALLINT NOT NULL, | ||
| 67 | user_id TEXT, | ||
| 68 | user_name TEXT NOT NULL, | ||
| 69 | score_count SMALLINT NOT NULL, | ||
| 70 | PRIMARY KEY (id), | ||
| 71 | FOREIGN KEY (map_id) REFERENCES maps(id), | ||
| 72 | FOREIGN KEY (user_id) REFERENCES users(steam_id) | 78 | FOREIGN KEY (user_id) REFERENCES users(steam_id) |
| 73 | ); | 79 | ); |
| 74 | 80 | ||
diff --git a/backend/database/route.sql b/backend/database/route.sql new file mode 100644 index 0000000..f26180e --- /dev/null +++ b/backend/database/route.sql | |||
| @@ -0,0 +1,279 @@ | |||
| 1 | INSERT INTO map_routes(map_id,category_id,score_count,description,showcase) VALUES | ||
| 2 | -- Portal 2 Singleplayer | ||
| 3 | -- 1 | ||
| 4 | (3,1,3,'',''), | ||
| 5 | (3,1,1,'',''), | ||
| 6 | (3,1,0,'',''), | ||
| 7 | (4,1,1,'',''), | ||
| 8 | (4,1,0,'',''), | ||
| 9 | (5,1,2,'',''), | ||
| 10 | (5,1,1,'',''), | ||
| 11 | (6,1,4,'',''), | ||
| 12 | (6,1,3,'',''), | ||
| 13 | (6,1,2,'',''), | ||
| 14 | (9,1,4,'',''), | ||
| 15 | (9,1,3,'',''), | ||
| 16 | (9,1,2,'',''), | ||
| 17 | (9,1,0,'',''), | ||
| 18 | -- 2 | ||
| 19 | (10,1,2,'',''), | ||
| 20 | (10,1,0,'',''), | ||
| 21 | (11,1,2,'',''), | ||
| 22 | (11,1,0,'',''), | ||
| 23 | (12,1,2,'',''), | ||
| 24 | (13,1,3,'',''), | ||
| 25 | (13,1,2,'',''), | ||
| 26 | (13,1,0,'',''), | ||
| 27 | (15,1,2,'',''), | ||
| 28 | (16,1,2,'',''), | ||
| 29 | (16,1,0,'',''), | ||
| 30 | (17,1,2,'',''), | ||
| 31 | (17,1,0,'',''), | ||
| 32 | -- 3 | ||
| 33 | (18,1,5,'',''), | ||
| 34 | (18,1,4,'',''), | ||
| 35 | (18,1,3,'',''), | ||
| 36 | (18,1,2,'',''), | ||
| 37 | (18,1,0,'',''), | ||
| 38 | (19,1,2,'',''), | ||
| 39 | (20,1,5,'',''), | ||
| 40 | (20,1,4,'',''), | ||
| 41 | (20,1,3,'',''), | ||
| 42 | (20,1,2,'',''), | ||
| 43 | (21,1,4,'',''), | ||
| 44 | (21,1,2,'',''), | ||
| 45 | (21,1,0,'',''), | ||
| 46 | (22,1,0,'',''), | ||
| 47 | (23,1,2,'',''), | ||
| 48 | (23,1,0,'',''), | ||
| 49 | (24,1,0,'',''), | ||
| 50 | (25,1,0,'',''), | ||
| 51 | (26,1,3,'',''), | ||
| 52 | (26,1,2,'',''), | ||
| 53 | (26,1,0,'',''), | ||
| 54 | -- 4 | ||
| 55 | (27,1,2,'',''), | ||
| 56 | (27,1,0,'',''), | ||
| 57 | (28,1,7,'',''), | ||
| 58 | (28,1,2,'',''), | ||
| 59 | (28,1,0,'',''), | ||
| 60 | (29,1,0,'',''), | ||
| 61 | (30,1,2,'',''), | ||
| 62 | (31,1,0,'',''), | ||
| 63 | -- 5 | ||
| 64 | (32,1,6,'',''), | ||
| 65 | (32,1,5,'',''), | ||
| 66 | (33,1,7,'',''), | ||
| 67 | (33,1,5,'',''), | ||
| 68 | (33,1,4,'',''), | ||
| 69 | (34,1,3,'',''), | ||
| 70 | (34,1,2,'',''), | ||
| 71 | (34,1,0,'',''), | ||
| 72 | (35,1,2,'',''), | ||
| 73 | -- 6 | ||
| 74 | (36,1,6,'',''), | ||
| 75 | (36,1,5,'',''), | ||
| 76 | (36,1,4,'',''), | ||
| 77 | (36,1,2,'',''), | ||
| 78 | (37,1,7,'',''), | ||
| 79 | (37,1,6,'',''), | ||
| 80 | (37,1,5,'',''), | ||
| 81 | (37,1,4,'',''), | ||
| 82 | (38,1,2,'',''), | ||
| 83 | (38,1,0,'',''), | ||
| 84 | (39,1,6,'',''), | ||
| 85 | (39,1,5,'',''), | ||
| 86 | (39,1,4,'',''), | ||
| 87 | (39,1,3,'',''), | ||
| 88 | (40,1,7,'',''), | ||
| 89 | (40,1,6,'',''), | ||
| 90 | (40,1,4,'',''), | ||
| 91 | (40,1,3,'',''), | ||
| 92 | (40,1,2,'',''), | ||
| 93 | (40,1,0,'',''), | ||
| 94 | (41,1,7,'',''), | ||
| 95 | (41,1,6,'',''), | ||
| 96 | (41,1,5,'',''), | ||
| 97 | -- 7 | ||
| 98 | (42,1,4,'',''), | ||
| 99 | (42,1,2,'',''), | ||
| 100 | (43,1,5,'',''), | ||
| 101 | (43,1,2,'',''), | ||
| 102 | (43,1,0,'',''), | ||
| 103 | (44,1,18,'',''), | ||
| 104 | (44,1,13,'',''), | ||
| 105 | (44,1,12,'',''), | ||
| 106 | (44,1,11,'',''), | ||
| 107 | (44,1,9,'',''), | ||
| 108 | (45,1,23,'',''), | ||
| 109 | (45,1,22,'',''), | ||
| 110 | (45,1,17,'',''), | ||
| 111 | (45,1,16,'',''), | ||
| 112 | (45,1,15,'',''), | ||
| 113 | (45,1,12,'',''), | ||
| 114 | (45,1,10,'',''), | ||
| 115 | (45,1,7,'',''), | ||
| 116 | (45,1,4,'',''), | ||
| 117 | -- 8 | ||
| 118 | (46,1,6,'',''), | ||
| 119 | (46,1,2,'',''), | ||
| 120 | (47,1,2,'',''), | ||
| 121 | (47,1,0,'',''), | ||
| 122 | (48,1,5,'',''), | ||
| 123 | (48,1,2,'',''), | ||
| 124 | (48,1,0,'',''), | ||
| 125 | (49,1,4,'',''), | ||
| 126 | (49,1,2,'',''), | ||
| 127 | (49,1,0,'',''), | ||
| 128 | (50,1,4,'',''), | ||
| 129 | (50,1,2,'',''), | ||
| 130 | (50,1,0,'',''), | ||
| 131 | (51,1,3,'',''), | ||
| 132 | (51,1,2,'',''), | ||
| 133 | (52,1,0,'',''), | ||
| 134 | (53,1,9,'',''), | ||
| 135 | (53,1,2,'',''), | ||
| 136 | (53,1,0,'',''), | ||
| 137 | (54,1,7,'',''), | ||
| 138 | (54,1,6,'',''), | ||
| 139 | (54,1,5,'',''), | ||
| 140 | (54,1,4,'',''), | ||
| 141 | (54,1,3,'',''), | ||
| 142 | (55,1,7,'',''), | ||
| 143 | (55,1,3,'',''), | ||
| 144 | (55,1,2,'',''), | ||
| 145 | (55,1,0,'',''), | ||
| 146 | (56,1,9,'',''), | ||
| 147 | (56,1,5,'',''), | ||
| 148 | (56,1,4,'',''), | ||
| 149 | (56,1,2,'',''), | ||
| 150 | -- 9 | ||
| 151 | (57,1,7,'',''), | ||
| 152 | (57,1,5,'',''), | ||
| 153 | (57,1,0,'',''), | ||
| 154 | (58,1,2,'',''), | ||
| 155 | (59,1,7,'',''), | ||
| 156 | (59,1,6,'',''), | ||
| 157 | (60,1,7,'',''), | ||
| 158 | (60,1,6,'',''), | ||
| 159 | -- Portal 2 Cooperative | ||
| 160 | -- 1 | ||
| 161 | (63,1,0,'',''), | ||
| 162 | (64,1,3,'',''), | ||
| 163 | (64,1,2,'',''), | ||
| 164 | (65,1,4,'',''), | ||
| 165 | (65,1,3,'',''), | ||
| 166 | (66,1,3,'',''), | ||
| 167 | (66,1,2,'',''), | ||
| 168 | (67,1,0,'',''), | ||
| 169 | (68,1,0,'',''), | ||
| 170 | -- 2 | ||
| 171 | (69,1,4,'',''), | ||
| 172 | (70,1,6,'',''), | ||
| 173 | (70,1,4,'',''), | ||
| 174 | (70,1,2,'',''), | ||
| 175 | (70,1,0,'',''), | ||
| 176 | (71,1,3,'',''), | ||
| 177 | (71,1,0,'',''), | ||
| 178 | (72,1,4,'',''), | ||
| 179 | (72,1,2,'',''), | ||
| 180 | (73,1,9,'',''), | ||
| 181 | (73,1,8,'',''), | ||
| 182 | (73,1,7,'',''), | ||
| 183 | (73,1,6,'',''), | ||
| 184 | (73,1,4,'',''), | ||
| 185 | (74,1,5,'',''), | ||
| 186 | (74,1,7,'',''), | ||
| 187 | (74,1,3,'',''), | ||
| 188 | (74,1,2,'',''), | ||
| 189 | (75,1,5,'',''), | ||
| 190 | (75,1,4,'',''), | ||
| 191 | (75,1,2,'',''), | ||
| 192 | (75,1,0,'',''), | ||
| 193 | (76,1,3,'',''), | ||
| 194 | (76,1,0,'',''), | ||
| 195 | -- 3 | ||
| 196 | (77,1,3,'',''), | ||
| 197 | (78,1,4,'',''), | ||
| 198 | (78,1,3,'',''), | ||
| 199 | (78,1,2,'',''), | ||
| 200 | (79,1,5,'',''), | ||
| 201 | (79,1,4,'',''), | ||
| 202 | (79,1,2,'',''), | ||
| 203 | (80,1,5,'',''), | ||
| 204 | (80,1,4,'',''), | ||
| 205 | (81,1,7,'',''), | ||
| 206 | (81,1,6,'',''), | ||
| 207 | (81,1,5,'',''), | ||
| 208 | (81,1,4,'',''), | ||
| 209 | (82,1,4,'',''), | ||
| 210 | (83,1,5,'',''), | ||
| 211 | (83,1,2,'',''), | ||
| 212 | (83,1,0,'',''), | ||
| 213 | (84,1,6,'',''), | ||
| 214 | (84,1,4,'',''), | ||
| 215 | (84,1,2,'',''), | ||
| 216 | (84,1,0,'',''), | ||
| 217 | -- 4 | ||
| 218 | (85,1,3,'',''), | ||
| 219 | (85,1,0,'',''), | ||
| 220 | (86,1,3,'',''), | ||
| 221 | (86,1,0,'',''), | ||
| 222 | (87,1,3,'',''), | ||
| 223 | (87,1,2,'',''), | ||
| 224 | (87,1,0,'',''), | ||
| 225 | (88,1,4,'',''), | ||
| 226 | (88,1,0,'',''), | ||
| 227 | (89,1,0,'',''), | ||
| 228 | (90,1,4,'',''), | ||
| 229 | (90,1,2,'',''), | ||
| 230 | (90,1,0,'',''), | ||
| 231 | (91,1,2,'',''), | ||
| 232 | (91,1,0,'',''), | ||
| 233 | (92,1,5,'',''), | ||
| 234 | (92,1,4,'',''), | ||
| 235 | (92,1,2,'',''), | ||
| 236 | (92,1,0,'',''), | ||
| 237 | (93,1,2,'',''), | ||
| 238 | (93,1,0,'',''), | ||
| 239 | -- 5 | ||
| 240 | (94,1,2,'',''), | ||
| 241 | (94,1,0,'',''), | ||
| 242 | (95,1,2,'',''), | ||
| 243 | (96,1,4,'',''), | ||
| 244 | (96,1,2,'',''), | ||
| 245 | (97,1,7,'',''), | ||
| 246 | (97,1,4,'',''), | ||
| 247 | (97,1,2,'',''), | ||
| 248 | (98,1,0,'',''), | ||
| 249 | (99,1,3,'',''), | ||
| 250 | (99,1,2,'',''), | ||
| 251 | (99,1,0,'',''), | ||
| 252 | (100,1,0,'',''), | ||
| 253 | (101,1,2,'',''), | ||
| 254 | (101,1,0,'',''), | ||
| 255 | -- 6 | ||
| 256 | (102,1,3,'',''), | ||
| 257 | (102,1,2,'',''), | ||
| 258 | (102,1,0,'',''), | ||
| 259 | (103,1,0,'',''), | ||
| 260 | (104,1,0,'',''), | ||
| 261 | (105,1,8,'',''), | ||
| 262 | (105,1,5,'',''), | ||
| 263 | (105,1,4,'',''), | ||
| 264 | (105,1,3,'',''), | ||
| 265 | (105,1,2,'',''), | ||
| 266 | (106,1,4,'',''), | ||
| 267 | (106,1,3,'',''), | ||
| 268 | (106,1,0,'',''), | ||
| 269 | (107,1,2,'',''), | ||
| 270 | (107,1,0,'',''), | ||
| 271 | (108,1,0,'',''), | ||
| 272 | (109,1,5,'',''), | ||
| 273 | (109,1,0,'',''), | ||
| 274 | (110,1,15,'',''), | ||
| 275 | (110,1,6,'',''), | ||
| 276 | (110,1,5,'',''), | ||
| 277 | (110,1,4,'',''), | ||
| 278 | (110,1,2,'',''), | ||
| 279 | (110,1,0,'',''); \ No newline at end of file | ||
diff --git a/backend/middleware/auth.go b/backend/middleware/auth.go index 14a0b78..0698f0a 100644 --- a/backend/middleware/auth.go +++ b/backend/middleware/auth.go | |||
| @@ -36,13 +36,21 @@ func CheckAuth(c *gin.Context) { | |||
| 36 | } | 36 | } |
| 37 | // Get user from DB | 37 | // Get user from DB |
| 38 | var user models.User | 38 | var user models.User |
| 39 | database.DB.QueryRow(`SELECT * FROM users WHERE steam_id = $1`, claims["sub"]).Scan( | 39 | database.DB.QueryRow(`SELECT u.steam_id, u.user_name, u.avatar_link, u.country_code, u.created_at, u.updated_at FROM users u WHERE steam_id = $1`, claims["sub"]).Scan( |
| 40 | &user.SteamID, &user.UserName, &user.AvatarLink, | 40 | &user.SteamID, &user.UserName, &user.AvatarLink, |
| 41 | &user.CountryCode, &user.CreatedAt, &user.UpdatedAt) | 41 | &user.CountryCode, &user.CreatedAt, &user.UpdatedAt) |
| 42 | if user.SteamID == "" { | 42 | if user.SteamID == "" { |
| 43 | c.Next() | 43 | c.Next() |
| 44 | return | 44 | return |
| 45 | } | 45 | } |
| 46 | // Get user titles from DB | ||
| 47 | user.Titles = []string{} | ||
| 48 | rows, _ := database.DB.Query(`SELECT t.title_name FROM titles t WHERE t.user_id = $1`, user.SteamID) | ||
| 49 | for rows.Next() { | ||
| 50 | var title string | ||
| 51 | rows.Scan(&title) | ||
| 52 | user.Titles = append(user.Titles, title) | ||
| 53 | } | ||
| 46 | c.Set("user", user) | 54 | c.Set("user", user) |
| 47 | c.Next() | 55 | c.Next() |
| 48 | } else { | 56 | } else { |
diff --git a/backend/models/models.go b/backend/models/models.go index 6f5173a..1231cb1 100644 --- a/backend/models/models.go +++ b/backend/models/models.go | |||
| @@ -4,16 +4,6 @@ import ( | |||
| 4 | "time" | 4 | "time" |
| 5 | ) | 5 | ) |
| 6 | 6 | ||
| 7 | type Response struct { | ||
| 8 | Success bool `json:"success"` | ||
| 9 | Message string `json:"message"` | ||
| 10 | Data any `json:"data"` | ||
| 11 | } | ||
| 12 | |||
| 13 | type LoginResponse struct { | ||
| 14 | Token string `json:"token"` | ||
| 15 | } | ||
| 16 | |||
| 17 | type User struct { | 7 | type User struct { |
| 18 | SteamID string `json:"steam_id"` | 8 | SteamID string `json:"steam_id"` |
| 19 | UserName string `json:"user_name"` | 9 | UserName string `json:"user_name"` |
| @@ -21,6 +11,12 @@ type User struct { | |||
| 21 | CountryCode string `json:"country_code"` | 11 | CountryCode string `json:"country_code"` |
| 22 | CreatedAt time.Time `json:"created_at"` | 12 | CreatedAt time.Time `json:"created_at"` |
| 23 | UpdatedAt time.Time `json:"updated_at"` | 13 | UpdatedAt time.Time `json:"updated_at"` |
| 14 | Titles []string `json:"titles"` | ||
| 15 | } | ||
| 16 | |||
| 17 | type UserShort struct { | ||
| 18 | SteamID string `json:"steam_id"` | ||
| 19 | UserName string `json:"user_name"` | ||
| 24 | } | 20 | } |
| 25 | 21 | ||
| 26 | type Map struct { | 22 | type Map struct { |
| @@ -28,33 +24,58 @@ type Map struct { | |||
| 28 | GameName string `json:"game_name"` | 24 | GameName string `json:"game_name"` |
| 29 | ChapterName string `json:"chapter_name"` | 25 | ChapterName string `json:"chapter_name"` |
| 30 | MapName string `json:"map_name"` | 26 | MapName string `json:"map_name"` |
| 31 | Data any `json:"data"` | 27 | Image string `json:"image"` |
| 28 | IsCoop bool `json:"is_coop"` | ||
| 29 | } | ||
| 30 | |||
| 31 | type MapShort struct { | ||
| 32 | ID int `json:"id"` | ||
| 33 | Name string `json:"name"` | ||
| 32 | } | 34 | } |
| 33 | 35 | ||
| 34 | type MapSummary struct { | 36 | type MapSummary struct { |
| 35 | Description string `json:"description"` | 37 | Routes []MapRoute `json:"routes"` |
| 36 | Showcase string `json:"showcase"` | ||
| 37 | CategoryScores MapCategoryScores `json:"category_scores"` | ||
| 38 | Rating float32 `json:"rating"` | ||
| 39 | Routers []string `json:"routers"` | ||
| 40 | History []MapHistory `json:"history"` | ||
| 41 | } | 38 | } |
| 42 | 39 | ||
| 43 | type MapCategoryScores struct { | 40 | type MapHistory struct { |
| 44 | CM int `json:"cm"` | 41 | RunnerName string `json:"runner_name"` |
| 45 | NoSLA int `json:"no_sla"` | 42 | ScoreCount int `json:"score_count"` |
| 46 | InboundsSLA int `json:"inbounds_sla"` | 43 | Date time.Time `json:"date"` |
| 47 | Any int `json:"any"` | 44 | } |
| 45 | |||
| 46 | type MapRoute struct { | ||
| 47 | RouteID int `json:"route_id"` | ||
| 48 | Category Category `json:"category"` | ||
| 49 | History MapHistory `json:"history"` | ||
| 50 | Rating float32 `json:"rating"` | ||
| 51 | Description string `json:"description"` | ||
| 52 | Showcase string `json:"showcase"` | ||
| 48 | } | 53 | } |
| 49 | 54 | ||
| 50 | type MapRecords struct { | 55 | type MapRecords struct { |
| 51 | Records any `json:"records"` | 56 | Records any `json:"records"` |
| 52 | } | 57 | } |
| 53 | 58 | ||
| 54 | type MapHistory struct { | 59 | type UserRanking struct { |
| 55 | RunnerName string `json:"runner_name"` | 60 | UserID string `json:"user_id"` |
| 56 | ScoreCount int `json:"score_count"` | 61 | UserName string `json:"user_name"` |
| 57 | Date time.Time `json:"date"` | 62 | TotalScore int `json:"total_score"` |
| 63 | } | ||
| 64 | |||
| 65 | type Game struct { | ||
| 66 | ID int `json:"id"` | ||
| 67 | Name string `json:"name"` | ||
| 68 | IsCoop bool `json:"is_coop"` | ||
| 69 | } | ||
| 70 | |||
| 71 | type Chapter struct { | ||
| 72 | ID int `json:"id"` | ||
| 73 | Name string `json:"name"` | ||
| 74 | } | ||
| 75 | |||
| 76 | type Category struct { | ||
| 77 | ID int `json:"id"` | ||
| 78 | Name string `json:"name"` | ||
| 58 | } | 79 | } |
| 59 | 80 | ||
| 60 | type RecordSP struct { | 81 | type RecordSP struct { |
| @@ -85,50 +106,6 @@ type RecordMP struct { | |||
| 85 | RecordDate time.Time `json:"record_date"` | 106 | RecordDate time.Time `json:"record_date"` |
| 86 | } | 107 | } |
| 87 | 108 | ||
| 88 | type RecordRequest struct { | ||
| 89 | ScoreCount int `json:"score_count" form:"score_count" binding:"required"` | ||
| 90 | ScoreTime int `json:"score_time" form:"score_time" binding:"required"` | ||
| 91 | PartnerID string `json:"partner_id" form:"partner_id" binding:"required"` | ||
| 92 | IsPartnerOrange bool `json:"is_partner_orange" form:"is_partner_orange" binding:"required"` | ||
| 93 | } | ||
| 94 | |||
| 95 | type UserRanking struct { | ||
| 96 | UserID string `json:"user_id"` | ||
| 97 | UserName string `json:"user_name"` | ||
| 98 | TotalScore int `json:"total_score"` | ||
| 99 | } | ||
| 100 | |||
| 101 | type RankingsResponse struct { | ||
| 102 | RankingsSP []UserRanking `json:"rankings_sp"` | ||
| 103 | RankingsMP []UserRanking `json:"rankings_mp"` | ||
| 104 | } | ||
| 105 | |||
| 106 | type ProfileResponse struct { | ||
| 107 | Profile bool `json:"profile"` | ||
| 108 | SteamID string `json:"steam_id"` | ||
| 109 | UserName string `json:"user_name"` | ||
| 110 | AvatarLink string `json:"avatar_link"` | ||
| 111 | CountryCode string `json:"country_code"` | ||
| 112 | ScoresSP []ScoreResponse `json:"scores_sp"` | ||
| 113 | ScoresMP []ScoreResponse `json:"scores_mp"` | ||
| 114 | } | ||
| 115 | |||
| 116 | type ScoreResponse struct { | ||
| 117 | MapID int `json:"map_id"` | ||
| 118 | Records any `json:"records"` | ||
| 119 | } | ||
| 120 | |||
| 121 | type SearchResponse struct { | ||
| 122 | Players []struct { | ||
| 123 | SteamID string `json:"steam_id"` | ||
| 124 | UserName string `json:"user_name"` | ||
| 125 | } `json:"players"` | ||
| 126 | Maps []struct { | ||
| 127 | ID int `json:"id"` | ||
| 128 | Name string `json:"name"` | ||
| 129 | } `json:"maps"` | ||
| 130 | } | ||
| 131 | |||
| 132 | type PlayerSummaries struct { | 109 | type PlayerSummaries struct { |
| 133 | SteamId string `json:"steamid"` | 110 | SteamId string `json:"steamid"` |
| 134 | CommunityVisibilityState int `json:"communityvisibilitystate"` | 111 | CommunityVisibilityState int `json:"communityvisibilitystate"` |
| @@ -152,36 +129,3 @@ type PlayerSummaries struct { | |||
| 152 | GameExtraInfo string `json:"gameextrainfo"` | 129 | GameExtraInfo string `json:"gameextrainfo"` |
| 153 | GameServerIp string `json:"gameserverip"` | 130 | GameServerIp string `json:"gameserverip"` |
| 154 | } | 131 | } |
| 155 | |||
| 156 | type Game struct { | ||
| 157 | ID int `json:"id"` | ||
| 158 | Name string `json:"name"` | ||
| 159 | } | ||
| 160 | |||
| 161 | type ChaptersResponse struct { | ||
| 162 | Game Game `json:"game"` | ||
| 163 | Chapters []Chapter `json:"chapters"` | ||
| 164 | } | ||
| 165 | |||
| 166 | type Chapter struct { | ||
| 167 | ID int `json:"id"` | ||
| 168 | Name string `json:"name"` | ||
| 169 | } | ||
| 170 | |||
| 171 | type MapShort struct { | ||
| 172 | ID int `json:"id"` | ||
| 173 | Name string `json:"name"` | ||
| 174 | } | ||
| 175 | |||
| 176 | type ChapterMapsResponse struct { | ||
| 177 | Chapter Chapter `json:"chapter"` | ||
| 178 | Maps []MapShort `json:"maps"` | ||
| 179 | } | ||
| 180 | |||
| 181 | func ErrorResponse(message string) Response { | ||
| 182 | return Response{ | ||
| 183 | Success: false, | ||
| 184 | Message: message, | ||
| 185 | Data: nil, | ||
| 186 | } | ||
| 187 | } | ||
diff --git a/backend/models/requests.go b/backend/models/requests.go new file mode 100644 index 0000000..0113597 --- /dev/null +++ b/backend/models/requests.go | |||
| @@ -0,0 +1,39 @@ | |||
| 1 | package models | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "mime/multipart" | ||
| 5 | "time" | ||
| 6 | ) | ||
| 7 | |||
| 8 | type CreateMapSummaryRequest struct { | ||
| 9 | CategoryID int `json:"category_id" binding:"required"` | ||
| 10 | Description string `json:"description" binding:"required"` | ||
| 11 | Showcase string `json:"showcase"` | ||
| 12 | UserName string `json:"user_name" binding:"required"` | ||
| 13 | ScoreCount *int `json:"score_count" binding:"required"` | ||
| 14 | RecordDate time.Time `json:"record_date" binding:"required"` | ||
| 15 | } | ||
| 16 | |||
| 17 | type EditMapSummaryRequest struct { | ||
| 18 | RouteID int `json:"route_id" binding:"required"` | ||
| 19 | Description string `json:"description" binding:"required"` | ||
| 20 | Showcase string `json:"showcase"` | ||
| 21 | UserName string `json:"user_name" binding:"required"` | ||
| 22 | ScoreCount *int `json:"score_count" binding:"required"` | ||
| 23 | RecordDate time.Time `json:"record_date" binding:"required"` | ||
| 24 | } | ||
| 25 | |||
| 26 | type DeleteMapSummaryRequest struct { | ||
| 27 | RouteID int `json:"route_id" binding:"required"` | ||
| 28 | } | ||
| 29 | |||
| 30 | type EditMapImageRequest struct { | ||
| 31 | Image string `json:"image" binding:"required"` | ||
| 32 | } | ||
| 33 | |||
| 34 | type RecordRequest struct { | ||
| 35 | HostDemo *multipart.FileHeader `json:"host_demo" form:"host_demo" binding:"required" swaggerignore:"true"` | ||
| 36 | PartnerDemo *multipart.FileHeader `json:"partner_demo" form:"partner_demo" swaggerignore:"true"` | ||
| 37 | IsPartnerOrange bool `json:"is_partner_orange" form:"is_partner_orange"` | ||
| 38 | PartnerID string `json:"partner_id" form:"partner_id"` | ||
| 39 | } | ||
diff --git a/backend/models/responses.go b/backend/models/responses.go new file mode 100644 index 0000000..459911c --- /dev/null +++ b/backend/models/responses.go | |||
| @@ -0,0 +1,64 @@ | |||
| 1 | package models | ||
| 2 | |||
| 3 | type Response struct { | ||
| 4 | Success bool `json:"success"` | ||
| 5 | Message string `json:"message"` | ||
| 6 | Data any `json:"data"` | ||
| 7 | } | ||
| 8 | |||
| 9 | type LoginResponse struct { | ||
| 10 | Token string `json:"token"` | ||
| 11 | } | ||
| 12 | |||
| 13 | type RankingsResponse struct { | ||
| 14 | RankingsSP []UserRanking `json:"rankings_sp"` | ||
| 15 | RankingsMP []UserRanking `json:"rankings_mp"` | ||
| 16 | } | ||
| 17 | |||
| 18 | type ProfileResponse struct { | ||
| 19 | Profile bool `json:"profile"` | ||
| 20 | SteamID string `json:"steam_id"` | ||
| 21 | UserName string `json:"user_name"` | ||
| 22 | AvatarLink string `json:"avatar_link"` | ||
| 23 | CountryCode string `json:"country_code"` | ||
| 24 | ScoresSP []ScoreResponse `json:"scores_sp"` | ||
| 25 | ScoresMP []ScoreResponse `json:"scores_mp"` | ||
| 26 | } | ||
| 27 | |||
| 28 | type ScoreResponse struct { | ||
| 29 | MapID int `json:"map_id"` | ||
| 30 | Records any `json:"records"` | ||
| 31 | } | ||
| 32 | |||
| 33 | type MapSummaryResponse struct { | ||
| 34 | Map Map `json:"map"` | ||
| 35 | Summary MapSummary `json:"summary"` | ||
| 36 | } | ||
| 37 | |||
| 38 | type SearchResponse struct { | ||
| 39 | Players []UserShort `json:"players"` | ||
| 40 | Maps []MapShort `json:"maps"` | ||
| 41 | } | ||
| 42 | |||
| 43 | type ChaptersResponse struct { | ||
| 44 | Game Game `json:"game"` | ||
| 45 | Chapters []Chapter `json:"chapters"` | ||
| 46 | } | ||
| 47 | |||
| 48 | type ChapterMapsResponse struct { | ||
| 49 | Chapter Chapter `json:"chapter"` | ||
| 50 | Maps []MapShort `json:"maps"` | ||
| 51 | } | ||
| 52 | |||
| 53 | type RecordResponse struct { | ||
| 54 | ScoreCount int `json:"score_count"` | ||
| 55 | ScoreTime int `json:"score_time"` | ||
| 56 | } | ||
| 57 | |||
| 58 | func ErrorResponse(message string) Response { | ||
| 59 | return Response{ | ||
| 60 | Success: false, | ||
| 61 | Message: message, | ||
| 62 | Data: nil, | ||
| 63 | } | ||
| 64 | } | ||
diff --git a/backend/parser/parser.REMOVED.git-id b/backend/parser/parser.REMOVED.git-id new file mode 100644 index 0000000..b09611f --- /dev/null +++ b/backend/parser/parser.REMOVED.git-id | |||
| @@ -0,0 +1 @@ | |||
| 72bd63d0667a78d35f643f1f8fcf71d9269d2719 \ No newline at end of file | |||
diff --git a/backend/parser/parser.go b/backend/parser/parser.go new file mode 100644 index 0000000..562b8c0 --- /dev/null +++ b/backend/parser/parser.go | |||
| @@ -0,0 +1,43 @@ | |||
| 1 | package parser | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "bufio" | ||
| 5 | "fmt" | ||
| 6 | "log" | ||
| 7 | "os/exec" | ||
| 8 | "strconv" | ||
| 9 | "strings" | ||
| 10 | ) | ||
| 11 | |||
| 12 | func ProcessDemo(demoPath string) (int, int, error) { | ||
| 13 | cmd := exec.Command("bash", "-c", fmt.Sprintf(`echo "FEXBash" && ./backend/parser/parser %s`, demoPath)) | ||
| 14 | stdout, err := cmd.StdoutPipe() | ||
| 15 | if err != nil { | ||
| 16 | log.Println(err) | ||
| 17 | return 0, 0, err | ||
| 18 | } | ||
| 19 | cmd.Start() | ||
| 20 | scanner := bufio.NewScanner(stdout) | ||
| 21 | var cmTicks, portalCount int | ||
| 22 | for scanner.Scan() { | ||
| 23 | line := scanner.Text() | ||
| 24 | if strings.Contains(line, "CM ticks") { | ||
| 25 | cmTicksStr := strings.TrimSpace(strings.Split(line, ":")[1]) | ||
| 26 | cmTicks, err = strconv.Atoi(cmTicksStr) | ||
| 27 | if err != nil { | ||
| 28 | return 0, 0, err | ||
| 29 | } | ||
| 30 | } | ||
| 31 | if strings.Contains(line, "Portal count") { | ||
| 32 | portalCountStr := strings.TrimSpace(strings.Split(line, ":")[1]) | ||
| 33 | portalCount, err = strconv.Atoi(portalCountStr) | ||
| 34 | if err != nil { | ||
| 35 | return 0, 0, err | ||
| 36 | } | ||
| 37 | } | ||
| 38 | } | ||
| 39 | cmd.Wait() | ||
| 40 | // We don't check for error in wait, since FEXBash always gives segmentation fault | ||
| 41 | // Wanted output is retrieved, so it's okay (i think) | ||
| 42 | return portalCount, cmTicks, nil | ||
| 43 | } | ||
diff --git a/backend/routes/routes.go b/backend/routes/routes.go index 96da1ce..1377d32 100644 --- a/backend/routes/routes.go +++ b/backend/routes/routes.go | |||
| @@ -25,11 +25,15 @@ func InitRoutes(router *gin.Engine) { | |||
| 25 | v1.POST("/profile", middleware.CheckAuth, controllers.UpdateUser) | 25 | v1.POST("/profile", middleware.CheckAuth, controllers.UpdateUser) |
| 26 | v1.GET("/users/:id", middleware.CheckAuth, controllers.FetchUser) | 26 | v1.GET("/users/:id", middleware.CheckAuth, controllers.FetchUser) |
| 27 | v1.GET("/demos", controllers.DownloadDemoWithID) | 27 | v1.GET("/demos", controllers.DownloadDemoWithID) |
| 28 | v1.GET("/maps/:id/summary", middleware.CheckAuth, controllers.FetchMapSummary) | 28 | v1.GET("/maps/:id/summary", controllers.FetchMapSummary) |
| 29 | v1.GET("/maps/:id/leaderboards", middleware.CheckAuth, controllers.FetchMapLeaderboards) | 29 | v1.POST("/maps/:id/summary", middleware.CheckAuth, controllers.CreateMapSummary) |
| 30 | v1.PUT("/maps/:id/summary", middleware.CheckAuth, controllers.EditMapSummary) | ||
| 31 | v1.DELETE("/maps/:id/summary", middleware.CheckAuth, controllers.DeleteMapSummary) | ||
| 32 | v1.PUT("/maps/:id/image", middleware.CheckAuth, controllers.EditMapImage) | ||
| 33 | v1.GET("/maps/:id/leaderboards", controllers.FetchMapLeaderboards) | ||
| 30 | v1.POST("/maps/:id/record", middleware.CheckAuth, controllers.CreateRecordWithDemo) | 34 | v1.POST("/maps/:id/record", middleware.CheckAuth, controllers.CreateRecordWithDemo) |
| 31 | v1.GET("/rankings", middleware.CheckAuth, controllers.Rankings) | 35 | v1.GET("/rankings", controllers.Rankings) |
| 32 | v1.GET("/search", controllers.Search) | 36 | v1.GET("/search", controllers.SearchWithQuery) |
| 33 | v1.GET("/games", controllers.FetchGames) | 37 | v1.GET("/games", controllers.FetchGames) |
| 34 | v1.GET("/games/:id", controllers.FetchChapters) | 38 | v1.GET("/games/:id", controllers.FetchChapters) |
| 35 | v1.GET("/chapters/:id", controllers.FetchChapterMaps) | 39 | v1.GET("/chapters/:id", controllers.FetchChapterMaps) |