From 9f7b5a836c8aeb83b66e0b41b8533443b5e99380 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Sun, 20 Aug 2023 12:36:34 +0300 Subject: fix: change map history from timestamp to date Former-commit-id: 8a46950e919891f380dda5502e40434383c91245 --- backend/database/history.sql | 522 +++++++++++++++++++++---------------------- backend/database/init.sql | 2 +- 2 files changed, 262 insertions(+), 262 deletions(-) diff --git a/backend/database/history.sql b/backend/database/history.sql index 320d72f..b30d10d 100644 --- a/backend/database/history.sql +++ b/backend/database/history.sql @@ -1,279 +1,279 @@ INSERT INTO map_history(map_id,category_id,user_name,score_count,record_date) VALUES -- Portal 2 Singleplayer -- 1 -(3,1,'slmid1995',3,'2011-10-05 00:00:00'), -(3,1,'LookLikeAKango',1,'2011-10-06 00:00:00'), -(3,1,'Bananasaurus Rex',0,'2011-10-24 00:00:00'), -(4,1,'Tyronis',1,'2011-10-05 00:00:00'), -(4,1,'Krzyhau',0,'2019-05-10 00:00:00'), -(5,1,'LookLikeAKango',2,'2011-10-05 00:00:00'), -(5,1,'Jetwash',1,'2013-12-03 00:00:00'), -(6,1,'Stimich',4,'2011-10-08 00:00:00'), -(6,1,'aepaePolakrn',3,'2011-10-19 00:00:00'), -(6,1,'Krzyhau',2,'2020-10-10 00:00:00'), -(9,1,'slmid1995',4,'2011-10-05 00:00:00'), -(9,1,'Jokie',3,'2011-10-05 00:00:00'), -(9,1,'Tyronis',2,'2011-10-05 00:00:00'), -(9,1,'sicklebrick',0,'2013-03-13 00:00:00'), +(3,1,'slmid1995',3,'2011-10-05'), +(3,1,'LookLikeAKango',1,'2011-10-06'), +(3,1,'Bananasaurus Rex',0,'2011-10-24'), +(4,1,'Tyronis',1,'2011-10-05'), +(4,1,'Krzyhau',0,'2019-05-10'), +(5,1,'LookLikeAKango',2,'2011-10-05'), +(5,1,'Jetwash',1,'2013-12-03'), +(6,1,'Stimich',4,'2011-10-08'), +(6,1,'aepaePolakrn',3,'2011-10-19'), +(6,1,'Krzyhau',2,'2020-10-10'), +(9,1,'slmid1995',4,'2011-10-05'), +(9,1,'Jokie',3,'2011-10-05'), +(9,1,'Tyronis',2,'2011-10-05'), +(9,1,'sicklebrick',0,'2013-03-13'), -- 2 -(10,1,'Paraxade0',2,'2011-04-21 00:00:00'), -(10,1,'PerOculos',0,'2011-04-21 00:00:00'), -(11,1,'Tyronis',2,'2011-10-05 00:00:00'), -(11,1,'Krzyhau',0,'2018-06-09 00:00:00'), -(12,1,'slmid1995',2,'2011-10-04 00:00:00'), -(13,1,'LookLikeAKango',3,'2011-10-05 00:00:00'), -(13,1,'Imanex',2,'2011-12-08 00:00:00'), -(13,1,'jyjey',0,'2012-08-22 00:00:00'), -(15,1,'Tyronis',2,'2011-10-05 00:00:00'), -(16,1,'LookLikeAKango',2,'2011-10-05 00:00:00'), -(16,1,'jyjey',0,'2012-08-25 00:00:00'), -(17,1,'rocoty',2,'2011-10-05 00:00:00'), -(17,1,'Nidboj132',0,'2023-02-05 00:00:00'), +(10,1,'Paraxade0',2,'2011-04-21'), +(10,1,'PerOculos',0,'2011-04-21'), +(11,1,'Tyronis',2,'2011-10-05'), +(11,1,'Krzyhau',0,'2018-06-09'), +(12,1,'slmid1995',2,'2011-10-04'), +(13,1,'LookLikeAKango',3,'2011-10-05'), +(13,1,'Imanex',2,'2011-12-08'), +(13,1,'jyjey',0,'2012-08-22'), +(15,1,'Tyronis',2,'2011-10-05'), +(16,1,'LookLikeAKango',2,'2011-10-05'), +(16,1,'jyjey',0,'2012-08-25'), +(17,1,'rocoty',2,'2011-10-05'), +(17,1,'Nidboj132',0,'2023-02-05'), -- 3 -(18,1,'The Last Tofus',5,'2011-05-08 00:00:00'), -(18,1,'Schlepian',4,'2011-10-08 00:00:00'), -(18,1,'szeimartin',3,'2013-10-08 00:00:00'), -(18,1,'Krzyhau',2,'2020-05-15 00:00:00'), -(18,1,'Krzyhau',0,'2022-07-02 00:00:00'), -(19,1,'LookLikeAKango',2,'2011-10-06 00:00:00'), -(20,1,'Djinndrache',5,'2011-10-20 00:00:00'), -(20,1,'Schlepian',4,'2011-10-30 00:00:00'), -(20,1,'Jetwash',3,'2014-09-04 00:00:00'), -(20,1,'Krzyhau',2,'2022-04-24 00:00:00'), -(21,1,'LookLikeAKango',4,'2011-10-06 00:00:00'), -(21,1,'ncla',2,'2011-10-30 00:00:00'), -(21,1,'PerOculos',0,'2019-07-08 00:00:00'), -(22,1,'Tyronis',0,'2011-10-05 00:00:00'), -(23,1,'LookLikeAKango',2,'2011-10-06 00:00:00'), -(23,1,'Krzyhau',0,'2018-08-01 00:00:00'), -(24,1,'LeviHB',0,'2011-04-30 00:00:00'), -(25,1,'Tyronis',0,'2011-10-06 00:00:00'), -(26,1,'Schlepian',3,'2011-10-30 00:00:00'), -(26,1,'Tyronis',2,'2012-01-08 00:00:00'), -(26,1,'PerOculos',0,'2016-06-08 00:00:00'), +(18,1,'The Last Tofus',5,'2011-05-08'), +(18,1,'Schlepian',4,'2011-10-08'), +(18,1,'szeimartin',3,'2013-10-08'), +(18,1,'Krzyhau',2,'2020-05-15'), +(18,1,'Krzyhau',0,'2022-07-02'), +(19,1,'LookLikeAKango',2,'2011-10-06'), +(20,1,'Djinndrache',5,'2011-10-20'), +(20,1,'Schlepian',4,'2011-10-30'), +(20,1,'Jetwash',3,'2014-09-04'), +(20,1,'Krzyhau',2,'2022-04-24'), +(21,1,'LookLikeAKango',4,'2011-10-06'), +(21,1,'ncla',2,'2011-10-30'), +(21,1,'PerOculos',0,'2019-07-08'), +(22,1,'Tyronis',0,'2011-10-05'), +(23,1,'LookLikeAKango',2,'2011-10-06'), +(23,1,'Krzyhau',0,'2018-08-01'), +(24,1,'LeviHB',0,'2011-04-30'), +(25,1,'Tyronis',0,'2011-10-06'), +(26,1,'Schlepian',3,'2011-10-30'), +(26,1,'Tyronis',2,'2012-01-08'), +(26,1,'PerOculos',0,'2016-06-08'), -- 4 -(27,1,'LeviHB',2,'2011-05-01 00:00:00'), -(27,1,'PerOculos',0,'2020-07-13 00:00:00'), -(28,1,'LeviHB',7,'2011-05-01 00:00:00'), -(28,1,'Andy M.J.',2,'2011-10-07 00:00:00'), -(28,1,'Krzyhau',0,'2018-05-19 00:00:00'), -(29,1,'LeviHB',0,'2011-05-01 00:00:00'), -(30,1,'Schlepian',2,'2011-10-30 00:00:00'), -(31,1,'Tyronis',0,'2011-10-06 00:00:00'), +(27,1,'LeviHB',2,'2011-05-01'), +(27,1,'PerOculos',0,'2020-07-13'), +(28,1,'LeviHB',7,'2011-05-01'), +(28,1,'Andy M.J.',2,'2011-10-07'), +(28,1,'Krzyhau',0,'2018-05-19'), +(29,1,'LeviHB',0,'2011-05-01'), +(30,1,'Schlepian',2,'2011-10-30'), +(31,1,'Tyronis',0,'2011-10-06'), -- 5 -(32,1,'Tyronis',6,'2011-10-21 00:00:00'), -(32,1,'Nidboj132',5,'2022-04-24 00:00:00'), -(33,1,'Tyronis',7,'2011-10-06 00:00:00'), -(33,1,'ISimmo',5,'2011-11-02 00:00:00'), -(33,1,'PerOculos',4,'2017-05-30 00:00:00'), -(34,1,'Schlepian',3,'2011-11-01 00:00:00'), -(34,1,'Krzyhau',2,'2020-10-14 00:00:00'), -(34,1,'zach',0,'2022-11-02 00:00:00'), -(35,1,'Krank',2,'2012-07-28 00:00:00'), +(32,1,'Tyronis',6,'2011-10-21'), +(32,1,'Nidboj132',5,'2022-04-24'), +(33,1,'Tyronis',7,'2011-10-06'), +(33,1,'ISimmo',5,'2011-11-02'), +(33,1,'PerOculos',4,'2017-05-30'), +(34,1,'Schlepian',3,'2011-11-01'), +(34,1,'Krzyhau',2,'2020-10-14'), +(34,1,'zach',0,'2022-11-02'), +(35,1,'Krank',2,'2012-07-28'), -- 6 -(36,1,'Tyronis',6,'2011-10-06 00:00:00'), -(36,1,'CalmlyFrenetic',5,'2011-10-09 00:00:00'), -(36,1,'sicklebrick',4,'2012-09-13 00:00:00'), -(36,1,'Nidboj132',2,'2023-03-04 00:00:00'), -(37,1,'LookLikeAKango',7,'2011-10-06 00:00:00'), -(37,1,'Schlepian',6,'2011-11-01 00:00:00'), -(37,1,'Tyronis',5,'2012-01-28 00:00:00'), -(37,1,'Nidboj132',4,'2021-08-22 00:00:00'), -(38,1,'Andy M.J.',2,'2011-10-06 00:00:00'), -(38,1,'Sanguine Dagger',0,'2012-03-19 00:00:00'), -(39,1,'Lambda Core',6,'2011-05-13 00:00:00'), -(39,1,'The Last Tofus',5,'2011-05-13 00:00:00'), -(39,1,'LookLikeAKango',4,'2011-10-16 00:00:00'), -(39,1,'Kittaye',3,'2013-03-25 00:00:00'), -(40,1,'LookLikeAKango',7,'2011-10-07 00:00:00'), -(40,1,'Schlepian',6,'2011-11-05 00:00:00'), -(40,1,'Kittaye',4,'2013-04-01 00:00:00'), -(40,1,'Kittaye',3,'2014-09-13 00:00:00'), -(40,1,'szeimartin',2,'2014-09-13 00:00:00'), -(40,1,'Kittaye',0,'2014-09-15 00:00:00'), -(41,1,'CalmlyFrenetic',7,'2011-10-09 00:00:00'), -(41,1,'Jaso',6,'2011-10-11 00:00:00'), -(41,1,'Krank',5,'2012-07-17 00:00:00'), +(36,1,'Tyronis',6,'2011-10-06'), +(36,1,'CalmlyFrenetic',5,'2011-10-09'), +(36,1,'sicklebrick',4,'2012-09-13'), +(36,1,'Nidboj132',2,'2023-03-04'), +(37,1,'LookLikeAKango',7,'2011-10-06'), +(37,1,'Schlepian',6,'2011-11-01'), +(37,1,'Tyronis',5,'2012-01-28'), +(37,1,'Nidboj132',4,'2021-08-22'), +(38,1,'Andy M.J.',2,'2011-10-06'), +(38,1,'Sanguine Dagger',0,'2012-03-19'), +(39,1,'Lambda Core',6,'2011-05-13'), +(39,1,'The Last Tofus',5,'2011-05-13'), +(39,1,'LookLikeAKango',4,'2011-10-16'), +(39,1,'Kittaye',3,'2013-03-25'), +(40,1,'LookLikeAKango',7,'2011-10-07'), +(40,1,'Schlepian',6,'2011-11-05'), +(40,1,'Kittaye',4,'2013-04-01'), +(40,1,'Kittaye',3,'2014-09-13'), +(40,1,'szeimartin',2,'2014-09-13'), +(40,1,'Kittaye',0,'2014-09-15'), +(41,1,'CalmlyFrenetic',7,'2011-10-09'), +(41,1,'Jaso',6,'2011-10-11'), +(41,1,'Krank',5,'2012-07-17'), -- 7 -(42,1,'LookLikeAKango',4,'2011-05-17 00:00:00'), -(42,1,'ISimmo',2,'2011-11-07 00:00:00'), -(43,1,'lmao4ever',5,'2011-10-30 00:00:00'), -(43,1,'Jaso',2,'2011-11-09 00:00:00'), -(43,1,'feliser',0,'2022-06-26 00:00:00'), -(44,1,'LookLikeAKango',18,'2011-10-07 00:00:00'), -(44,1,'Tyronis',13,'2011-10-30 00:00:00'), -(44,1,'Tyronis',12,'2011-11-10 00:00:00'), -(44,1,'Jetwash',11,'2017-06-12 00:00:00'), -(44,1,'Krzyhau',9,'2022-01-02 00:00:00'), -(45,1,'LookLikeAKango',23,'2011-10-08 00:00:00'), -(45,1,'CalmlyFrenetic',22,'2011-10-09 00:00:00'), -(45,1,'cgreactor',17,'2011-10-09 00:00:00'), -(45,1,'CalmlyFrenetic',16,'2011-10-10 00:00:00'), -(45,1,'LookLikeAKango',15,'2011-10-19 00:00:00'), -(45,1,'Jaso',12,'2012-07-19 00:00:00'), -(45,1,'Krank',10,'2013-01-31 00:00:00'), -(45,1,'Kittaye',7,'2013-04-04 00:00:00'), -(45,1,'PerOculos',4,'2014-09-13 00:00:00'), +(42,1,'LookLikeAKango',4,'2011-05-17'), +(42,1,'ISimmo',2,'2011-11-07'), +(43,1,'lmao4ever',5,'2011-10-30'), +(43,1,'Jaso',2,'2011-11-09'), +(43,1,'feliser',0,'2022-06-26'), +(44,1,'LookLikeAKango',18,'2011-10-07'), +(44,1,'Tyronis',13,'2011-10-30'), +(44,1,'Tyronis',12,'2011-11-10'), +(44,1,'Jetwash',11,'2017-06-12'), +(44,1,'Krzyhau',9,'2022-01-02'), +(45,1,'LookLikeAKango',23,'2011-10-08'), +(45,1,'CalmlyFrenetic',22,'2011-10-09'), +(45,1,'cgreactor',17,'2011-10-09'), +(45,1,'CalmlyFrenetic',16,'2011-10-10'), +(45,1,'LookLikeAKango',15,'2011-10-19'), +(45,1,'Jaso',12,'2012-07-19'), +(45,1,'Krank',10,'2013-01-31'), +(45,1,'Kittaye',7,'2013-04-04'), +(45,1,'PerOculos',4,'2014-09-13'), -- 8 -(46,1,'sparkle1princess',6,'2012-03-24 00:00:00'), -(46,1,'Krzyhau',2,'2019-11-21 00:00:00'), -(47,1,'holydevel',2,'2011-10-06 00:00:00'), -(47,1,'JesusCatFace',0,'2015-01-16 00:00:00'), -(48,1,'LookLikeAKango',5,'2011-10-08 00:00:00'), -(48,1,'Tyronis',2,'2011-10-08 00:00:00'), -(48,1,'adzicents',0,'2011-10-09 00:00:00'), -(49,1,'adzicents',4,'2011-10-07 00:00:00'), -(49,1,'Schlepian',2,'2011-10-08 00:00:00'), -(49,1,'Nidboj132',0,'2022-09-26 00:00:00'), -(50,1,'LookLikeAKango',4,'2011-10-08 00:00:00'), -(50,1,'Tyronis',2,'2011-10-11 00:00:00'), -(50,1,'sicklebrick',0,'2013-03-20 00:00:00'), -(51,1,'Andy M.J.',3,'2011-10-08 00:00:00'), -(51,1,'LookLikeAKango',2,'2011-10-20 00:00:00'), -(52,1,'Jaso',0,'2011-10-10 00:00:00'), -(53,1,'LookLikeAKango',9,'2011-10-08 00:00:00'), -(53,1,'LookLikeAKango',2,'2011-10-20 00:00:00'), -(53,1,'Schlepian',0,'2011-11-06 00:00:00'), -(54,1,'LookLikeAKango',7,'2011-06-01 00:00:00'), -(54,1,'Jaso',6,'2011-10-09 00:00:00'), -(54,1,'Schlepian',5,'2011-11-06 00:00:00'), -(54,1,'Spyrunite',4,'2012-08-30 00:00:00'), -(54,1,'Krzyhau',3,'2019-04-22 00:00:00'), -(55,1,'LookLikeAKango',7,'2011-10-08 00:00:00'), -(55,1,'CalmlyFrenetic',3,'2011-10-09 00:00:00'), -(55,1,'Jaso',2,'2011-11-26 00:00:00'), -(55,1,'PerOculos',0,'2021-02-06 00:00:00'), -(56,1,'CalmlyFrenetic',9,'2011-10-08 00:00:00'), -(56,1,'LookLikeAKango',5,'2011-10-09 00:00:00'), -(56,1,'CalmlyFrenetic',4,'2011-10-09 00:00:00'), -(56,1,'Jetwash',2,'2014-09-05 00:00:00'), +(46,1,'sparkle1princess',6,'2012-03-24'), +(46,1,'Krzyhau',2,'2019-11-21'), +(47,1,'holydevel',2,'2011-10-06'), +(47,1,'JesusCatFace',0,'2015-01-16'), +(48,1,'LookLikeAKango',5,'2011-10-08'), +(48,1,'Tyronis',2,'2011-10-08'), +(48,1,'adzicents',0,'2011-10-09'), +(49,1,'adzicents',4,'2011-10-07'), +(49,1,'Schlepian',2,'2011-10-08'), +(49,1,'Nidboj132',0,'2022-09-26'), +(50,1,'LookLikeAKango',4,'2011-10-08'), +(50,1,'Tyronis',2,'2011-10-11'), +(50,1,'sicklebrick',0,'2013-03-20'), +(51,1,'Andy M.J.',3,'2011-10-08'), +(51,1,'LookLikeAKango',2,'2011-10-20'), +(52,1,'Jaso',0,'2011-10-10'), +(53,1,'LookLikeAKango',9,'2011-10-08'), +(53,1,'LookLikeAKango',2,'2011-10-20'), +(53,1,'Schlepian',0,'2011-11-06'), +(54,1,'LookLikeAKango',7,'2011-06-01'), +(54,1,'Jaso',6,'2011-10-09'), +(54,1,'Schlepian',5,'2011-11-06'), +(54,1,'Spyrunite',4,'2012-08-30'), +(54,1,'Krzyhau',3,'2019-04-22'), +(55,1,'LookLikeAKango',7,'2011-10-08'), +(55,1,'CalmlyFrenetic',3,'2011-10-09'), +(55,1,'Jaso',2,'2011-11-26'), +(55,1,'PerOculos',0,'2021-02-06'), +(56,1,'CalmlyFrenetic',9,'2011-10-08'), +(56,1,'LookLikeAKango',5,'2011-10-09'), +(56,1,'CalmlyFrenetic',4,'2011-10-09'), +(56,1,'Jetwash',2,'2014-09-05'), -- 9 -(57,1,'JNS',7,'2011-07-21 00:00:00'), -(57,1,'Krank',5,'2012-07-29 00:00:00'), -(57,1,'Krzyhau',0,'2017-10-29 00:00:00'), -(58,1,'Stimich',2,'2011-10-11 00:00:00'), -(59,1,'Isimmo',7,'2011-11-04 00:00:00'), -(59,1,'sicklebrick',6,'2013-03-20 00:00:00'), -(60,1,'CalmlyFrenetic',7,'2011-10-19 00:00:00'), -(60,1,'Tyronis',6,'2011-11-01 00:00:00'), +(57,1,'JNS',7,'2011-07-21'), +(57,1,'Krank',5,'2012-07-29'), +(57,1,'Krzyhau',0,'2017-10-29'), +(58,1,'Stimich',2,'2011-10-11'), +(59,1,'Isimmo',7,'2011-11-04'), +(59,1,'sicklebrick',6,'2013-03-20'), +(60,1,'CalmlyFrenetic',7,'2011-10-19'), +(60,1,'Tyronis',6,'2011-11-01'), -- Portal 2 Cooperative -- 1 -(63,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01 00:00:00'), -(64,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), -(64,1,'Chubfish & Exhale',2,'2011-11-01 00:00:00'), -(65,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), -(65,1,'Nidboj132 & Oryn',3,'2022-02-03 00:00:00'), -(66,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), -(66,1,'Schlepian & Chubfish',2,'2011-10-01 00:00:00'), -(67,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01 00:00:00'), -(68,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01 00:00:00'), +(63,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01'), +(64,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01'), +(64,1,'Chubfish & Exhale',2,'2011-11-01'), +(65,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01'), +(65,1,'Nidboj132 & Oryn',3,'2022-02-03'), +(66,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01'), +(66,1,'Schlepian & Chubfish',2,'2011-10-01'), +(67,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01'), +(68,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01'), -- 2 -(69,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), -(70,1,'Mathias123961 & Sir Spawn Alot',6,'2011-08-01 00:00:00'), -(70,1,'Schlepian & Chubfish',4,'2011-10-01 00:00:00'), -(70,1,'Gocnak & z1mb0bw4y',2,'2012-08-03 00:00:00'), -(70,1,'DM_ & VEGA',0,'2017-10-01 00:00:00'), -(71,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), -(71,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01 00:00:00'), -(72,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), -(72,1,'Schlepian & LongJohnDickWeed',2,'2011-10-01 00:00:00'), -(73,1,'Stimich & HiTMaRkS',9,'2011-05-09 00:00:00'), -(73,1,'Mathias123961 & Sir Spawn Alot',8,'2011-08-01 00:00:00'), -(73,1,'Schlepian & Lemonsunshine',7,'2011-11-01 00:00:00'), -(73,1,'DM_ & LsDK_',6,'2018-01-01 00:00:00'), -(73,1,'Krzyhau & Klooger',4,'2018-11-01 00:00:00'), -(74,1,'Mathias123961 & Sir Spawn Alot',5,'2011-08-01 00:00:00'), -(74,1,'Stimich & Pitkakorva',7,'2011-10-11 00:00:00'), -(74,1,'Schlepian & Isimmo',3,'2011-10-28 00:00:00'), -(74,1,'Zypeh & szeimartin',2,'2013-11-01 00:00:00'), -(75,1,'Mathias123961 & Sir Spawn Alot',5,'2011-08-01 00:00:00'), -(75,1,'Schlepian & Urination',4,'2011-10-01 00:00:00'), -(75,1,'Schlepian & Lemonsunshine',2,'2012-02-01 00:00:00'), -(75,1,'DM_ & follon',0,'2015-04-01 00:00:00'), -(76,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), -(76,1,'Chubfish & Exhale',0,'2011-12-01 00:00:00'), +(69,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01'), +(70,1,'Mathias123961 & Sir Spawn Alot',6,'2011-08-01'), +(70,1,'Schlepian & Chubfish',4,'2011-10-01'), +(70,1,'Gocnak & z1mb0bw4y',2,'2012-08-03'), +(70,1,'DM_ & VEGA',0,'2017-10-01'), +(71,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01'), +(71,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01'), +(72,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01'), +(72,1,'Schlepian & LongJohnDickWeed',2,'2011-10-01'), +(73,1,'Stimich & HiTMaRkS',9,'2011-05-09'), +(73,1,'Mathias123961 & Sir Spawn Alot',8,'2011-08-01'), +(73,1,'Schlepian & Lemonsunshine',7,'2011-11-01'), +(73,1,'DM_ & LsDK_',6,'2018-01-01'), +(73,1,'Krzyhau & Klooger',4,'2018-11-01'), +(74,1,'Mathias123961 & Sir Spawn Alot',5,'2011-08-01'), +(74,1,'Stimich & Pitkakorva',7,'2011-10-11'), +(74,1,'Schlepian & Isimmo',3,'2011-10-28'), +(74,1,'Zypeh & szeimartin',2,'2013-11-01'), +(75,1,'Mathias123961 & Sir Spawn Alot',5,'2011-08-01'), +(75,1,'Schlepian & Urination',4,'2011-10-01'), +(75,1,'Schlepian & Lemonsunshine',2,'2012-02-01'), +(75,1,'DM_ & follon',0,'2015-04-01'), +(76,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01'), +(76,1,'Chubfish & Exhale',0,'2011-12-01'), -- 3 -(77,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), -(78,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), -(78,1,'DM_ & marK',3,'2016-11-01 00:00:00'), -(78,1,'Nidboj132 & Oryn',2,'2021-09-04 00:00:00'), -(79,1,'ganonscrub & ?',5,'2011-04-01 00:00:00'), -(79,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), -(79,1,'Chubfish & Exhale',2,'2012-08-04 00:00:00'), -(80,1,'Mathias123961 & Sir Spawn Alot',5,'2011-08-01 00:00:00'), -(80,1,'Chubfish & Exhale',4,'2011-12-01 00:00:00'), -(81,1,'Mathias123961 & Sir Spawn Alot',7,'2011-08-01 00:00:00'), -(81,1,'Schlepian & Lemonsunshine',6,'2011-10-01 00:00:00'), -(81,1,'takz & dawn',5,'2011-11-01 00:00:00'), -(81,1,'Nidboj132 & Oryn',4,'2021-03-25 00:00:00'), -(82,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), -(83,1,'Mathias123961 & Sir Spawn Alot',5,'2011-08-01 00:00:00'), -(83,1,'Schlepian & Lemonsunshine',2,'2011-10-01 00:00:00'), -(83,1,'Chubfish & Exhale',0,'2011-12-01 00:00:00'), -(84,1,'Mathias123961 & Sir Spawn Alot',6,'2011-08-01 00:00:00'), -(84,1,'Schlepian & Chubfish',4,'2011-10-01 00:00:00'), -(84,1,'Chubfish & Exhale',2,'2012-01-01 00:00:00'), -(84,1,'DM_ & wS',0,'2015-05-01 00:00:00'), +(77,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01'), +(78,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01'), +(78,1,'DM_ & marK',3,'2016-11-01'), +(78,1,'Nidboj132 & Oryn',2,'2021-09-04'), +(79,1,'ganonscrub & ?',5,'2011-04-01'), +(79,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01'), +(79,1,'Chubfish & Exhale',2,'2012-08-04'), +(80,1,'Mathias123961 & Sir Spawn Alot',5,'2011-08-01'), +(80,1,'Chubfish & Exhale',4,'2011-12-01'), +(81,1,'Mathias123961 & Sir Spawn Alot',7,'2011-08-01'), +(81,1,'Schlepian & Lemonsunshine',6,'2011-10-01'), +(81,1,'takz & dawn',5,'2011-11-01'), +(81,1,'Nidboj132 & Oryn',4,'2021-03-25'), +(82,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01'), +(83,1,'Mathias123961 & Sir Spawn Alot',5,'2011-08-01'), +(83,1,'Schlepian & Lemonsunshine',2,'2011-10-01'), +(83,1,'Chubfish & Exhale',0,'2011-12-01'), +(84,1,'Mathias123961 & Sir Spawn Alot',6,'2011-08-01'), +(84,1,'Schlepian & Chubfish',4,'2011-10-01'), +(84,1,'Chubfish & Exhale',2,'2012-01-01'), +(84,1,'DM_ & wS',0,'2015-05-01'), -- 4 -(85,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), -(85,1,'Chubfish & Exhale',0,'2011-10-01 00:00:00'), -(86,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), -(86,1,'Chubfish & Exhale',0,'2011-12-01 00:00:00'), -(87,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01 00:00:00'), -(87,1,'Schlepian & Gopherdude',2,'2011-10-01 00:00:00'), -(87,1,'DM_ & follon',0,'2015-04-01 00:00:00'), -(88,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), -(88,1,'Schlepian & Gopherdude',0,'2011-10-01 00:00:00'), -(89,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01 00:00:00'), -(90,1,'Mathias123961 & Sir Spawn Alot',4,'2011-09-01 00:00:00'), -(90,1,'Schlepian & Urination',2,'2011-10-01 00:00:00'), -(90,1,'Klooger & Jetwash',0,'2016-08-01 00:00:00'), -(91,1,'Mathias123961 & Sir Spawn Alot',2,'2011-08-01 00:00:00'), -(91,1,'Undead & Zypeh',0,'2013-05-19 00:00:00'), -(92,1,'txx478 & ?',5,'2011-05-01 00:00:00'), -(92,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01 00:00:00'), -(92,1,'Schlepian & Gopherdude',2,'2011-10-01 00:00:00'), -(92,1,'ncla & takz',0,'2012-02-01 00:00:00'), -(93,1,'Mathias123961 & Sir Spawn Alot',2,'2011-08-01 00:00:00'), -(93,1,'Schlepian & Gopherdude',0,'2011-10-01 00:00:00'), +(85,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01'), +(85,1,'Chubfish & Exhale',0,'2011-10-01'), +(86,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01'), +(86,1,'Chubfish & Exhale',0,'2011-12-01'), +(87,1,'Mathias123961 & Sir Spawn Alot',3,'2011-08-01'), +(87,1,'Schlepian & Gopherdude',2,'2011-10-01'), +(87,1,'DM_ & follon',0,'2015-04-01'), +(88,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01'), +(88,1,'Schlepian & Gopherdude',0,'2011-10-01'), +(89,1,'Mathias123961 & Sir Spawn Alot',0,'2011-08-01'), +(90,1,'Mathias123961 & Sir Spawn Alot',4,'2011-09-01'), +(90,1,'Schlepian & Urination',2,'2011-10-01'), +(90,1,'Klooger & Jetwash',0,'2016-08-01'), +(91,1,'Mathias123961 & Sir Spawn Alot',2,'2011-08-01'), +(91,1,'Undead & Zypeh',0,'2013-05-19'), +(92,1,'txx478 & ?',5,'2011-05-01'), +(92,1,'Mathias123961 & Sir Spawn Alot',4,'2011-08-01'), +(92,1,'Schlepian & Gopherdude',2,'2011-10-01'), +(92,1,'ncla & takz',0,'2012-02-01'), +(93,1,'Mathias123961 & Sir Spawn Alot',2,'2011-08-01'), +(93,1,'Schlepian & Gopherdude',0,'2011-10-01'), -- 5 -(94,1,'Chubfish & Exhale',2,'2011-10-01 00:00:00'), -(94,1,'Klooger & Imanex',0,'2013-08-01 00:00:00'), -(95,1,'Schlepian & Issimoi',2,'2011-10-01 00:00:00'), -(96,1,'ThePortalPatrol & ?',4,'2011-04-01 00:00:00'), -(96,1,'sparkle1princess & Zypeh',2,'2014-01-01 00:00:00'), -(97,1,'Stimich & HiTMaRkS',7,'2011-05-13 00:00:00'), -(97,1,'Schlepian & Lemonsunshine',4,'2011-10-01 00:00:00'), -(97,1,'DM_ & wS',2,'2014-05-01 00:00:00'), -(98,1,'Imanex & 00svo',0,'2011-11-01 00:00:00'), -(99,1,'Schlepian & Gopherdude',3,'2011-10-01 00:00:00'), -(99,1,'Imanex & Klooger',2,'2013-08-01 00:00:00'), -(99,1,'DM_ & wS',0,'2015-05-01 00:00:00'), -(100,1,'Schlepian & Bananasaurus Rex',0,'2011-10-01 00:00:00'), -(101,1,'Chubfish & Exhale',2,'2011-12-01 00:00:00'), -(101,1,'DM_ & follon',0,'2015-04-01 00:00:00'), +(94,1,'Chubfish & Exhale',2,'2011-10-01'), +(94,1,'Klooger & Imanex',0,'2013-08-01'), +(95,1,'Schlepian & Issimoi',2,'2011-10-01'), +(96,1,'ThePortalPatrol & ?',4,'2011-04-01'), +(96,1,'sparkle1princess & Zypeh',2,'2014-01-01'), +(97,1,'Stimich & HiTMaRkS',7,'2011-05-13'), +(97,1,'Schlepian & Lemonsunshine',4,'2011-10-01'), +(97,1,'DM_ & wS',2,'2014-05-01'), +(98,1,'Imanex & 00svo',0,'2011-11-01'), +(99,1,'Schlepian & Gopherdude',3,'2011-10-01'), +(99,1,'Imanex & Klooger',2,'2013-08-01'), +(99,1,'DM_ & wS',0,'2015-05-01'), +(100,1,'Schlepian & Bananasaurus Rex',0,'2011-10-01'), +(101,1,'Chubfish & Exhale',2,'2011-12-01'), +(101,1,'DM_ & follon',0,'2015-04-01'), -- 6 -(102,1,'dawn & takz',3,'2011-11-18 00:00:00'), -(102,1,'Chubfish & Exhale',2,'2012-01-01 00:00:00'), -(102,1,'Imanex & Klooger',0,'2013-08-01 00:00:00'), -(103,1,'Schlepian & Lemonsunshine',0,'2011-10-01 00:00:00'), -(104,1,'Schlepian & Lemonsunshine',0,'2011-10-01 00:00:00'), -(105,1,'Blaizerazer & ?',8,'2011-10-01 00:00:00'), -(105,1,'Schlepian & Lemonsunshine',5,'2011-11-01 00:00:00'), -(105,1,'Imanex & Klooger',4,'2013-08-01 00:00:00'), -(105,1,'DM_ & wS',3,'2014-05-01 00:00:00'), -(105,1,'DM_ & follon',2,'2015-04-01 00:00:00'), -(106,1,'Schlepian & Bananasaurus Rex',4,'2011-10-01 00:00:00'), -(106,1,'Gig & takz',3,'2012-06-01 00:00:00'), -(106,1,'Imanex & Klooger',0,'2013-06-01 00:00:00'), -(107,1,'Chubfish & Exhale',2,'2011-10-01 00:00:00'), -(107,1,'DM_ & follon',0,'2015-04-01 00:00:00'), -(108,1,'DaFox & P',0,'2011-12-01 00:00:00'), -(109,1,'Schlepian & Tyronis',5,'2011-10-01 00:00:00'), -(109,1,'Chubfish & Exhale',0,'2011-12-01 00:00:00'), -(110,1,'Tyronis & mr.bob806',15,'2011-10-01 00:00:00'), -(110,1,'Schlepian & Chubfish',6,'2011-11-01 00:00:00'), -(110,1,'00svo & z1mb0bw4y',5,'2012-08-08 00:00:00'), -(110,1,'00svo & z1mb0bw4y',4,'2012-08-10 00:00:00'), -(110,1,'Klooger & z1mb0bw4y',2,'2014-02-01 00:00:00'), -(110,1,'DM_ & follon',0,'2015-04-01 00:00:00'); \ No newline at end of file +(102,1,'dawn & takz',3,'2011-11-18'), +(102,1,'Chubfish & Exhale',2,'2012-01-01'), +(102,1,'Imanex & Klooger',0,'2013-08-01'), +(103,1,'Schlepian & Lemonsunshine',0,'2011-10-01'), +(104,1,'Schlepian & Lemonsunshine',0,'2011-10-01'), +(105,1,'Blaizerazer & ?',8,'2011-10-01'), +(105,1,'Schlepian & Lemonsunshine',5,'2011-11-01'), +(105,1,'Imanex & Klooger',4,'2013-08-01'), +(105,1,'DM_ & wS',3,'2014-05-01'), +(105,1,'DM_ & follon',2,'2015-04-01'), +(106,1,'Schlepian & Bananasaurus Rex',4,'2011-10-01'), +(106,1,'Gig & takz',3,'2012-06-01'), +(106,1,'Imanex & Klooger',0,'2013-06-01'), +(107,1,'Chubfish & Exhale',2,'2011-10-01'), +(107,1,'DM_ & follon',0,'2015-04-01'), +(108,1,'DaFox & P',0,'2011-12-01'), +(109,1,'Schlepian & Tyronis',5,'2011-10-01'), +(109,1,'Chubfish & Exhale',0,'2011-12-01'), +(110,1,'Tyronis & mr.bob806',15,'2011-10-01'), +(110,1,'Schlepian & Chubfish',6,'2011-11-01'), +(110,1,'00svo & z1mb0bw4y',5,'2012-08-08'), +(110,1,'00svo & z1mb0bw4y',4,'2012-08-10'), +(110,1,'Klooger & z1mb0bw4y',2,'2014-02-01'), +(110,1,'DM_ & follon',0,'2015-04-01'); \ No newline at end of file diff --git a/backend/database/init.sql b/backend/database/init.sql index 50e7c15..11d4944 100644 --- a/backend/database/init.sql +++ b/backend/database/init.sql @@ -59,7 +59,7 @@ CREATE TABLE map_history ( category_id SMALLINT NOT NULL, user_name TEXT NOT NULL, score_count SMALLINT NOT NULL, - record_date TIMESTAMP NOT NULL, + record_date DATE NOT NULL, PRIMARY KEY (id), FOREIGN KEY (category_id) REFERENCES categories(id), FOREIGN KEY (map_id) REFERENCES maps(id), -- cgit v1.2.3 From ca7acc2fdc6e6c8371ca5bbeeaabb02d11bb1bee Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Sun, 20 Aug 2023 12:51:26 +0300 Subject: refactor: move structs around for better understanding Former-commit-id: 0030a6b0c7b228772d8e27f5722ee6de1718786b --- backend/controllers/homeController.go | 18 +- backend/controllers/loginController.go | 16 +- backend/controllers/mapController.go | 27 ++- backend/controllers/modController.go | 67 ++++--- backend/controllers/recordController.go | 20 ++- backend/controllers/userController.go | 43 +++-- backend/models/models.go | 14 ++ backend/models/requests.go | 39 ----- backend/models/responses.go | 64 ------- docs/docs.go | 300 ++++++++++++++++---------------- docs/swagger.json | 300 ++++++++++++++++---------------- docs/swagger.yaml | 216 +++++++++++------------ 12 files changed, 560 insertions(+), 564 deletions(-) delete mode 100644 backend/models/requests.go delete mode 100644 backend/models/responses.go diff --git a/backend/controllers/homeController.go b/backend/controllers/homeController.go index c94590a..d1b99cb 100644 --- a/backend/controllers/homeController.go +++ b/backend/controllers/homeController.go @@ -10,6 +10,16 @@ import ( "github.com/pektezol/leastportalshub/backend/models" ) +type SearchResponse struct { + Players []models.UserShort `json:"players"` + Maps []models.MapShort `json:"maps"` +} + +type RankingsResponse struct { + RankingsSP []models.UserRanking `json:"rankings_sp"` + RankingsMP []models.UserRanking `json:"rankings_mp"` +} + func Home(c *gin.Context) { user, exists := c.Get("user") if !exists { @@ -26,7 +36,7 @@ func Home(c *gin.Context) { // @Description Get rankings of every player. // @Tags rankings // @Produce json -// @Success 200 {object} models.Response{data=models.RankingsResponse} +// @Success 200 {object} models.Response{data=RankingsResponse} // @Failure 400 {object} models.Response // @Router /rankings [get] func Rankings(c *gin.Context) { @@ -116,7 +126,7 @@ func Rankings(c *gin.Context) { c.JSON(http.StatusOK, models.Response{ Success: true, Message: "Successfully retrieved rankings.", - Data: models.RankingsResponse{ + Data: RankingsResponse{ RankingsSP: spRankings, RankingsMP: mpRankings, }, @@ -129,14 +139,14 @@ func Rankings(c *gin.Context) { // @Tags search // @Produce json // @Param q query string false "Search user or map name." -// @Success 200 {object} models.Response{data=models.SearchResponse} +// @Success 200 {object} models.Response{data=SearchResponse} // @Failure 400 {object} models.Response // @Router /search [get] func SearchWithQuery(c *gin.Context) { query := c.Query("q") query = strings.ToLower(query) log.Println(query) - var response models.SearchResponse + var response SearchResponse // Cache all maps for faster response var maps = []models.MapShort{ {ID: 1, Name: "Container Ride"}, diff --git a/backend/controllers/loginController.go b/backend/controllers/loginController.go index e907b22..76bf51f 100644 --- a/backend/controllers/loginController.go +++ b/backend/controllers/loginController.go @@ -15,13 +15,17 @@ import ( "github.com/solovev/steam_go" ) +type LoginResponse struct { + Token string `json:"token"` +} + // Login // // @Description Get (redirect) login page for Steam auth. // @Tags login // @Accept json // @Produce json -// @Success 200 {object} models.Response{data=models.LoginResponse} +// @Success 200 {object} models.Response{data=LoginResponse} // @Failure 400 {object} models.Response // @Router /login [get] func Login(c *gin.Context) { @@ -85,7 +89,7 @@ func Login(c *gin.Context) { // c.JSON(http.StatusOK, models.Response{ // Success: true, // Message: "Successfully generated token.", - // Data: models.LoginResponse{ + // Data: LoginResponse{ // Token: tokenString, // }, // }) @@ -99,7 +103,7 @@ func Login(c *gin.Context) { // @Tags auth // @Produce json // -// @Success 200 {object} models.Response{data=models.LoginResponse} +// @Success 200 {object} models.Response{data=LoginResponse} // @Failure 404 {object} models.Response // @Router /token [get] func GetCookie(c *gin.Context) { @@ -111,7 +115,7 @@ func GetCookie(c *gin.Context) { c.JSON(http.StatusOK, models.Response{ Success: true, Message: "Token cookie successfully retrieved.", - Data: models.LoginResponse{ + Data: LoginResponse{ Token: cookie, }, }) @@ -123,7 +127,7 @@ func GetCookie(c *gin.Context) { // @Tags auth // @Produce json // -// @Success 200 {object} models.Response{data=models.LoginResponse} +// @Success 200 {object} models.Response{data=LoginResponse} // @Failure 404 {object} models.Response // @Router /token [delete] func DeleteCookie(c *gin.Context) { @@ -136,7 +140,7 @@ func DeleteCookie(c *gin.Context) { c.JSON(http.StatusOK, models.Response{ Success: true, Message: "Token cookie successfully deleted.", - Data: models.LoginResponse{ + Data: LoginResponse{ Token: cookie, }, }) diff --git a/backend/controllers/mapController.go b/backend/controllers/mapController.go index ebd65dd..0a324d6 100644 --- a/backend/controllers/mapController.go +++ b/backend/controllers/mapController.go @@ -9,18 +9,33 @@ import ( "github.com/pektezol/leastportalshub/backend/models" ) +type MapSummaryResponse struct { + Map models.Map `json:"map"` + Summary models.MapSummary `json:"summary"` +} + +type ChaptersResponse struct { + Game models.Game `json:"game"` + Chapters []models.Chapter `json:"chapters"` +} + +type ChapterMapsResponse struct { + Chapter models.Chapter `json:"chapter"` + Maps []models.MapShort `json:"maps"` +} + // GET Map Summary // // @Description Get map summary with specified id. // @Tags maps // @Produce json // @Param id path int true "Map ID" -// @Success 200 {object} models.Response{data=models.MapSummaryResponse} +// @Success 200 {object} models.Response{data=MapSummaryResponse} // @Failure 400 {object} models.Response // @Router /maps/{id}/summary [get] func FetchMapSummary(c *gin.Context) { id := c.Param("id") - response := models.MapSummaryResponse{Map: models.Map{}, Summary: models.MapSummary{Routes: []models.MapRoute{}}} + response := MapSummaryResponse{Map: models.Map{}, Summary: models.MapSummary{Routes: []models.MapRoute{}}} intID, err := strconv.Atoi(id) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) @@ -220,7 +235,7 @@ func FetchGames(c *gin.Context) { // @Tags games & chapters // @Produce json // @Param id path int true "Game ID" -// @Success 200 {object} models.Response{data=models.ChaptersResponse} +// @Success 200 {object} models.Response{data=ChaptersResponse} // @Failure 400 {object} models.Response // @Router /games/{id} [get] func FetchChapters(c *gin.Context) { @@ -230,7 +245,7 @@ func FetchChapters(c *gin.Context) { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } - var response models.ChaptersResponse + var response ChaptersResponse rows, err := database.DB.Query(`SELECT c.id, c.name, g.name FROM chapters c INNER JOIN games g ON c.game_id = g.id WHERE game_id = $1`, gameID) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) @@ -262,7 +277,7 @@ func FetchChapters(c *gin.Context) { // @Tags games & chapters // @Produce json // @Param id path int true "Chapter ID" -// @Success 200 {object} models.Response{data=models.ChapterMapsResponse} +// @Success 200 {object} models.Response{data=ChapterMapsResponse} // @Failure 400 {object} models.Response // @Router /chapters/{id} [get] func FetchChapterMaps(c *gin.Context) { @@ -272,7 +287,7 @@ func FetchChapterMaps(c *gin.Context) { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } - var response models.ChapterMapsResponse + var response ChapterMapsResponse rows, err := database.DB.Query(`SELECT m.id, m.name, c.name FROM maps m INNER JOIN chapters c ON m.chapter_id = c.id WHERE chapter_id = $1`, chapterID) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) diff --git a/backend/controllers/modController.go b/backend/controllers/modController.go index e2add1f..7ce5cb4 100644 --- a/backend/controllers/modController.go +++ b/backend/controllers/modController.go @@ -3,21 +3,48 @@ package controllers import ( "net/http" "strconv" + "time" "github.com/gin-gonic/gin" "github.com/pektezol/leastportalshub/backend/database" "github.com/pektezol/leastportalshub/backend/models" ) +type CreateMapSummaryRequest struct { + CategoryID int `json:"category_id" binding:"required"` + Description string `json:"description" binding:"required"` + Showcase string `json:"showcase"` + UserName string `json:"user_name" binding:"required"` + ScoreCount *int `json:"score_count" binding:"required"` + RecordDate time.Time `json:"record_date" binding:"required"` +} + +type EditMapSummaryRequest struct { + RouteID int `json:"route_id" binding:"required"` + Description string `json:"description" binding:"required"` + Showcase string `json:"showcase"` + UserName string `json:"user_name" binding:"required"` + ScoreCount *int `json:"score_count" binding:"required"` + RecordDate time.Time `json:"record_date" binding:"required"` +} + +type DeleteMapSummaryRequest struct { + RouteID int `json:"route_id" binding:"required"` +} + +type EditMapImageRequest struct { + Image string `json:"image" binding:"required"` +} + // POST Map Summary // // @Description Create map summary with specified map id. // @Tags maps // @Produce json -// @Param Authorization header string true "JWT Token" -// @Param id path int true "Map ID" -// @Param request body models.CreateMapSummaryRequest true "Body" -// @Success 200 {object} models.Response{data=models.CreateMapSummaryRequest} +// @Param Authorization header string true "JWT Token" +// @Param id path int true "Map ID" +// @Param request body CreateMapSummaryRequest true "Body" +// @Success 200 {object} models.Response{data=CreateMapSummaryRequest} // @Failure 400 {object} models.Response // @Router /maps/{id}/summary [post] func CreateMapSummary(c *gin.Context) { @@ -44,7 +71,7 @@ func CreateMapSummary(c *gin.Context) { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } - var request models.CreateMapSummaryRequest + var request CreateMapSummaryRequest if err := c.BindJSON(&request); err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return @@ -100,10 +127,10 @@ func CreateMapSummary(c *gin.Context) { // @Description Edit map summary with specified map id. // @Tags maps // @Produce json -// @Param Authorization header string true "JWT Token" -// @Param id path int true "Map ID" -// @Param request body models.EditMapSummaryRequest true "Body" -// @Success 200 {object} models.Response{data=models.EditMapSummaryRequest} +// @Param Authorization header string true "JWT Token" +// @Param id path int true "Map ID" +// @Param request body EditMapSummaryRequest true "Body" +// @Success 200 {object} models.Response{data=EditMapSummaryRequest} // @Failure 400 {object} models.Response // @Router /maps/{id}/summary [put] func EditMapSummary(c *gin.Context) { @@ -130,7 +157,7 @@ func EditMapSummary(c *gin.Context) { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } - var request models.EditMapSummaryRequest + var request EditMapSummaryRequest if err := c.BindJSON(&request); err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return @@ -186,10 +213,10 @@ func EditMapSummary(c *gin.Context) { // @Description Delete map summary with specified map id. // @Tags maps // @Produce json -// @Param Authorization header string true "JWT Token" -// @Param id path int true "Map ID" -// @Param request body models.DeleteMapSummaryRequest true "Body" -// @Success 200 {object} models.Response{data=models.DeleteMapSummaryRequest} +// @Param Authorization header string true "JWT Token" +// @Param id path int true "Map ID" +// @Param request body DeleteMapSummaryRequest true "Body" +// @Success 200 {object} models.Response{data=DeleteMapSummaryRequest} // @Failure 400 {object} models.Response // @Router /maps/{id}/summary [delete] func DeleteMapSummary(c *gin.Context) { @@ -216,7 +243,7 @@ func DeleteMapSummary(c *gin.Context) { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } - var request models.DeleteMapSummaryRequest + var request DeleteMapSummaryRequest if err := c.BindJSON(&request); err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return @@ -276,10 +303,10 @@ func DeleteMapSummary(c *gin.Context) { // @Description Edit map image with specified map id. // @Tags maps // @Produce json -// @Param Authorization header string true "JWT Token" -// @Param id path int true "Map ID" -// @Param request body models.EditMapImageRequest true "Body" -// @Success 200 {object} models.Response{data=models.EditMapImageRequest} +// @Param Authorization header string true "JWT Token" +// @Param id path int true "Map ID" +// @Param request body EditMapImageRequest true "Body" +// @Success 200 {object} models.Response{data=EditMapImageRequest} // @Failure 400 {object} models.Response // @Router /maps/{id}/image [put] func EditMapImage(c *gin.Context) { @@ -306,7 +333,7 @@ func EditMapImage(c *gin.Context) { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } - var request models.EditMapImageRequest + var request EditMapImageRequest if err := c.BindJSON(&request); err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return diff --git a/backend/controllers/recordController.go b/backend/controllers/recordController.go index 951be41..d141fc3 100644 --- a/backend/controllers/recordController.go +++ b/backend/controllers/recordController.go @@ -19,6 +19,18 @@ import ( "google.golang.org/api/drive/v3" ) +type RecordRequest struct { + HostDemo *multipart.FileHeader `json:"host_demo" form:"host_demo" binding:"required" swaggerignore:"true"` + PartnerDemo *multipart.FileHeader `json:"partner_demo" form:"partner_demo" swaggerignore:"true"` + IsPartnerOrange bool `json:"is_partner_orange" form:"is_partner_orange"` + PartnerID string `json:"partner_id" form:"partner_id"` +} + +type RecordResponse struct { + ScoreCount int `json:"score_count"` + ScoreTime int `json:"score_time"` +} + // POST Record // // @Description Post record with demo of a specific map. @@ -31,7 +43,7 @@ import ( // @Param partner_demo formData file false "Partner Demo" // @Param is_partner_orange formData boolean false "Is Partner Orange" // @Param partner_id formData string false "Partner ID" -// @Success 200 {object} models.Response{data=models.RecordResponse} +// @Success 200 {object} models.Response{data=RecordResponse} // @Failure 400 {object} models.Response // @Failure 401 {object} models.Response // @Router /maps/{id}/record [post] @@ -61,7 +73,7 @@ func CreateRecordWithDemo(c *gin.Context) { isCoop = true } // Get record request - var record models.RecordRequest + var record RecordRequest if err := c.ShouldBind(&record); err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return @@ -183,7 +195,7 @@ func CreateRecordWithDemo(c *gin.Context) { c.JSON(http.StatusOK, models.Response{ Success: true, Message: "Successfully created record.", - Data: models.RecordResponse{ScoreCount: hostDemoScoreCount, ScoreTime: hostDemoScoreTime}, + Data: RecordResponse{ScoreCount: hostDemoScoreCount, ScoreTime: hostDemoScoreTime}, }) } @@ -253,6 +265,7 @@ func serviceAccount() *http.Client { return client } +// Create Gdrive file func createFile(service *drive.Service, name string, mimeType string, content io.Reader, parentId string) (*drive.File, error) { f := &drive.File{ MimeType: mimeType, @@ -269,6 +282,7 @@ func createFile(service *drive.Service, name string, mimeType string, content io return file, nil } +// Delete Gdrive file func deleteFile(service *drive.Service, fileId string) { service.Files.Delete(fileId) } diff --git a/backend/controllers/userController.go b/backend/controllers/userController.go index 6aa77fc..0dae155 100644 --- a/backend/controllers/userController.go +++ b/backend/controllers/userController.go @@ -11,6 +11,21 @@ import ( "github.com/pektezol/leastportalshub/backend/models" ) +type ProfileResponse struct { + Profile bool `json:"profile"` + SteamID string `json:"steam_id"` + UserName string `json:"user_name"` + AvatarLink string `json:"avatar_link"` + CountryCode string `json:"country_code"` + ScoresSP []ScoreResponse `json:"scores_sp"` + ScoresMP []ScoreResponse `json:"scores_mp"` +} + +type ScoreResponse struct { + MapID int `json:"map_id"` + Records any `json:"records"` +} + // GET Profile // // @Description Get profile page of session user. @@ -18,7 +33,7 @@ import ( // @Accept json // @Produce json // @Param Authorization header string true "JWT Token" -// @Success 200 {object} models.Response{data=models.ProfileResponse} +// @Success 200 {object} models.Response{data=ProfileResponse} // @Failure 400 {object} models.Response // @Failure 401 {object} models.Response // @Router /profile [get] @@ -30,7 +45,7 @@ func Profile(c *gin.Context) { return } // Retrieve singleplayer records - var scoresSP []models.ScoreResponse + var scoresSP []ScoreResponse sql := `SELECT id, map_id, score_count, score_time, demo_id, record_date FROM records_sp WHERE user_id = $1 ORDER BY map_id` rows, err := database.DB.Query(sql, user.(models.User).SteamID) if err != nil { @@ -50,13 +65,13 @@ func Profile(c *gin.Context) { // New map recordsSP = []models.RecordSP{} recordsSP = append(recordsSP, record) - scoresSP = append(scoresSP, models.ScoreResponse{ + scoresSP = append(scoresSP, ScoreResponse{ MapID: mapID, Records: recordsSP, }) } // Retrieve multiplayer records - var scoresMP []models.ScoreResponse + var scoresMP []ScoreResponse sql = `SELECT id, map_id, host_id, partner_id, score_count, score_time, host_demo_id, partner_demo_id, record_date FROM records_mp WHERE host_id = $1 OR partner_id = $2 ORDER BY map_id` rows, err = database.DB.Query(sql, user.(models.User).SteamID, user.(models.User).SteamID) @@ -77,7 +92,7 @@ func Profile(c *gin.Context) { // New map recordsMP = []models.RecordMP{} recordsMP = append(recordsMP, record) - scoresMP = append(scoresMP, models.ScoreResponse{ + scoresMP = append(scoresMP, ScoreResponse{ MapID: mapID, Records: recordsMP, }) @@ -85,7 +100,7 @@ func Profile(c *gin.Context) { c.JSON(http.StatusOK, models.Response{ Success: true, Message: "Successfully retrieved user scores.", - Data: models.ProfileResponse{ + Data: ProfileResponse{ Profile: true, SteamID: user.(models.User).SteamID, UserName: user.(models.User).UserName, @@ -105,7 +120,7 @@ func Profile(c *gin.Context) { // @Accept json // @Produce json // @Param id path int true "User ID" -// @Success 200 {object} models.Response{data=models.ProfileResponse} +// @Success 200 {object} models.Response{data=ProfileResponse} // @Failure 400 {object} models.Response // @Failure 404 {object} models.Response // @Router /users/{id} [get] @@ -132,7 +147,7 @@ func FetchUser(c *gin.Context) { return } // Retrieve singleplayer records - var scoresSP []models.ScoreResponse + var scoresSP []ScoreResponse sql := `SELECT id, map_id, score_count, score_time, demo_id, record_date FROM records_sp WHERE user_id = $1 ORDER BY map_id` rows, err := database.DB.Query(sql, user.SteamID) if err != nil { @@ -152,13 +167,13 @@ func FetchUser(c *gin.Context) { // New map recordsSP = []models.RecordSP{} recordsSP = append(recordsSP, record) - scoresSP = append(scoresSP, models.ScoreResponse{ + scoresSP = append(scoresSP, ScoreResponse{ MapID: mapID, Records: recordsSP, }) } // Retrieve multiplayer records - var scoresMP []models.ScoreResponse + var scoresMP []ScoreResponse sql = `SELECT id, map_id, host_id, partner_id, score_count, score_time, host_demo_id, partner_demo_id, record_date FROM records_mp WHERE host_id = $1 OR partner_id = $2 ORDER BY map_id` rows, err = database.DB.Query(sql, user.SteamID, user.SteamID) @@ -179,7 +194,7 @@ func FetchUser(c *gin.Context) { // New map recordsMP = []models.RecordMP{} recordsMP = append(recordsMP, record) - scoresMP = append(scoresMP, models.ScoreResponse{ + scoresMP = append(scoresMP, ScoreResponse{ MapID: mapID, Records: recordsMP, }) @@ -187,7 +202,7 @@ func FetchUser(c *gin.Context) { c.JSON(http.StatusOK, models.Response{ Success: true, Message: "Successfully retrieved user scores.", - Data: models.ProfileResponse{ + Data: ProfileResponse{ Profile: true, SteamID: user.SteamID, UserName: user.UserName, @@ -207,7 +222,7 @@ func FetchUser(c *gin.Context) { // @Accept json // @Produce json // @Param Authorization header string true "JWT Token" -// @Success 200 {object} models.Response{data=models.ProfileResponse} +// @Success 200 {object} models.Response{data=ProfileResponse} // @Failure 400 {object} models.Response // @Failure 401 {object} models.Response // @Router /profile [post] @@ -233,7 +248,7 @@ func UpdateUser(c *gin.Context) { c.JSON(http.StatusOK, models.Response{ Success: true, Message: "Successfully updated user.", - Data: models.ProfileResponse{ + Data: ProfileResponse{ Profile: true, SteamID: user.(models.User).SteamID, UserName: profile.PersonaName, diff --git a/backend/models/models.go b/backend/models/models.go index 1231cb1..e21ba6a 100644 --- a/backend/models/models.go +++ b/backend/models/models.go @@ -4,6 +4,20 @@ import ( "time" ) +type Response struct { + Success bool `json:"success"` + Message string `json:"message"` + Data any `json:"data"` +} + +func ErrorResponse(message string) Response { + return Response{ + Success: false, + Message: message, + Data: nil, + } +} + type User struct { SteamID string `json:"steam_id"` UserName string `json:"user_name"` diff --git a/backend/models/requests.go b/backend/models/requests.go deleted file mode 100644 index 0113597..0000000 --- a/backend/models/requests.go +++ /dev/null @@ -1,39 +0,0 @@ -package models - -import ( - "mime/multipart" - "time" -) - -type CreateMapSummaryRequest struct { - CategoryID int `json:"category_id" binding:"required"` - Description string `json:"description" binding:"required"` - Showcase string `json:"showcase"` - UserName string `json:"user_name" binding:"required"` - ScoreCount *int `json:"score_count" binding:"required"` - RecordDate time.Time `json:"record_date" binding:"required"` -} - -type EditMapSummaryRequest struct { - RouteID int `json:"route_id" binding:"required"` - Description string `json:"description" binding:"required"` - Showcase string `json:"showcase"` - UserName string `json:"user_name" binding:"required"` - ScoreCount *int `json:"score_count" binding:"required"` - RecordDate time.Time `json:"record_date" binding:"required"` -} - -type DeleteMapSummaryRequest struct { - RouteID int `json:"route_id" binding:"required"` -} - -type EditMapImageRequest struct { - Image string `json:"image" binding:"required"` -} - -type RecordRequest struct { - HostDemo *multipart.FileHeader `json:"host_demo" form:"host_demo" binding:"required" swaggerignore:"true"` - PartnerDemo *multipart.FileHeader `json:"partner_demo" form:"partner_demo" swaggerignore:"true"` - IsPartnerOrange bool `json:"is_partner_orange" form:"is_partner_orange"` - PartnerID string `json:"partner_id" form:"partner_id"` -} diff --git a/backend/models/responses.go b/backend/models/responses.go deleted file mode 100644 index 459911c..0000000 --- a/backend/models/responses.go +++ /dev/null @@ -1,64 +0,0 @@ -package models - -type Response struct { - Success bool `json:"success"` - Message string `json:"message"` - Data any `json:"data"` -} - -type LoginResponse struct { - Token string `json:"token"` -} - -type RankingsResponse struct { - RankingsSP []UserRanking `json:"rankings_sp"` - RankingsMP []UserRanking `json:"rankings_mp"` -} - -type ProfileResponse struct { - Profile bool `json:"profile"` - SteamID string `json:"steam_id"` - UserName string `json:"user_name"` - AvatarLink string `json:"avatar_link"` - CountryCode string `json:"country_code"` - ScoresSP []ScoreResponse `json:"scores_sp"` - ScoresMP []ScoreResponse `json:"scores_mp"` -} - -type ScoreResponse struct { - MapID int `json:"map_id"` - Records any `json:"records"` -} - -type MapSummaryResponse struct { - Map Map `json:"map"` - Summary MapSummary `json:"summary"` -} - -type SearchResponse struct { - Players []UserShort `json:"players"` - Maps []MapShort `json:"maps"` -} - -type ChaptersResponse struct { - Game Game `json:"game"` - Chapters []Chapter `json:"chapters"` -} - -type ChapterMapsResponse struct { - Chapter Chapter `json:"chapter"` - Maps []MapShort `json:"maps"` -} - -type RecordResponse struct { - ScoreCount int `json:"score_count"` - ScoreTime int `json:"score_time"` -} - -func ErrorResponse(message string) Response { - return Response{ - Success: false, - Message: message, - Data: nil, - } -} diff --git a/docs/docs.go b/docs/docs.go index 423afad..df01379 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -50,7 +50,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.ChapterMapsResponse" + "$ref": "#/definitions/controllers.ChapterMapsResponse" } } } @@ -173,7 +173,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.ChaptersResponse" + "$ref": "#/definitions/controllers.ChaptersResponse" } } } @@ -213,7 +213,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.LoginResponse" + "$ref": "#/definitions/controllers.LoginResponse" } } } @@ -259,7 +259,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.EditMapImageRequest" + "$ref": "#/definitions/controllers.EditMapImageRequest" } } ], @@ -275,7 +275,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.EditMapImageRequest" + "$ref": "#/definitions/controllers.EditMapImageRequest" } } } @@ -414,7 +414,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.RecordResponse" + "$ref": "#/definitions/controllers.RecordResponse" } } } @@ -466,7 +466,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.MapSummaryResponse" + "$ref": "#/definitions/controllers.MapSummaryResponse" } } } @@ -510,7 +510,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.EditMapSummaryRequest" + "$ref": "#/definitions/controllers.EditMapSummaryRequest" } } ], @@ -526,7 +526,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.EditMapSummaryRequest" + "$ref": "#/definitions/controllers.EditMapSummaryRequest" } } } @@ -570,7 +570,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.CreateMapSummaryRequest" + "$ref": "#/definitions/controllers.CreateMapSummaryRequest" } } ], @@ -586,7 +586,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.CreateMapSummaryRequest" + "$ref": "#/definitions/controllers.CreateMapSummaryRequest" } } } @@ -630,7 +630,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.DeleteMapSummaryRequest" + "$ref": "#/definitions/controllers.DeleteMapSummaryRequest" } } ], @@ -646,7 +646,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.DeleteMapSummaryRequest" + "$ref": "#/definitions/controllers.DeleteMapSummaryRequest" } } } @@ -695,7 +695,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.ProfileResponse" + "$ref": "#/definitions/controllers.ProfileResponse" } } } @@ -796,7 +796,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.ProfileResponse" + "$ref": "#/definitions/controllers.ProfileResponse" } } } @@ -839,7 +839,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.RankingsResponse" + "$ref": "#/definitions/controllers.RankingsResponse" } } } @@ -884,7 +884,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.SearchResponse" + "$ref": "#/definitions/controllers.SearchResponse" } } } @@ -921,7 +921,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.LoginResponse" + "$ref": "#/definitions/controllers.LoginResponse" } } } @@ -956,7 +956,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.LoginResponse" + "$ref": "#/definitions/controllers.LoginResponse" } } } @@ -1005,7 +1005,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.ProfileResponse" + "$ref": "#/definitions/controllers.ProfileResponse" } } } @@ -1029,29 +1029,7 @@ const docTemplate = `{ } }, "definitions": { - "models.Category": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - } - } - }, - "models.Chapter": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - } - } - }, - "models.ChapterMapsResponse": { + "controllers.ChapterMapsResponse": { "type": "object", "properties": { "chapter": { @@ -1065,7 +1043,7 @@ const docTemplate = `{ } } }, - "models.ChaptersResponse": { + "controllers.ChaptersResponse": { "type": "object", "properties": { "chapters": { @@ -1079,7 +1057,7 @@ const docTemplate = `{ } } }, - "models.CreateMapSummaryRequest": { + "controllers.CreateMapSummaryRequest": { "type": "object", "required": [ "category_id", @@ -1109,7 +1087,7 @@ const docTemplate = `{ } } }, - "models.DeleteMapSummaryRequest": { + "controllers.DeleteMapSummaryRequest": { "type": "object", "required": [ "route_id" @@ -1120,7 +1098,7 @@ const docTemplate = `{ } } }, - "models.EditMapImageRequest": { + "controllers.EditMapImageRequest": { "type": "object", "required": [ "image" @@ -1131,7 +1109,7 @@ const docTemplate = `{ } } }, - "models.EditMapSummaryRequest": { + "controllers.EditMapSummaryRequest": { "type": "object", "required": [ "description", @@ -1161,24 +1139,143 @@ const docTemplate = `{ } } }, - "models.Game": { + "controllers.LoginResponse": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, + "controllers.MapSummaryResponse": { + "type": "object", + "properties": { + "map": { + "$ref": "#/definitions/models.Map" + }, + "summary": { + "$ref": "#/definitions/models.MapSummary" + } + } + }, + "controllers.ProfileResponse": { + "type": "object", + "properties": { + "avatar_link": { + "type": "string" + }, + "country_code": { + "type": "string" + }, + "profile": { + "type": "boolean" + }, + "scores_mp": { + "type": "array", + "items": { + "$ref": "#/definitions/controllers.ScoreResponse" + } + }, + "scores_sp": { + "type": "array", + "items": { + "$ref": "#/definitions/controllers.ScoreResponse" + } + }, + "steam_id": { + "type": "string" + }, + "user_name": { + "type": "string" + } + } + }, + "controllers.RankingsResponse": { + "type": "object", + "properties": { + "rankings_mp": { + "type": "array", + "items": { + "$ref": "#/definitions/models.UserRanking" + } + }, + "rankings_sp": { + "type": "array", + "items": { + "$ref": "#/definitions/models.UserRanking" + } + } + } + }, + "controllers.RecordResponse": { + "type": "object", + "properties": { + "score_count": { + "type": "integer" + }, + "score_time": { + "type": "integer" + } + } + }, + "controllers.ScoreResponse": { + "type": "object", + "properties": { + "map_id": { + "type": "integer" + }, + "records": {} + } + }, + "controllers.SearchResponse": { + "type": "object", + "properties": { + "maps": { + "type": "array", + "items": { + "$ref": "#/definitions/models.MapShort" + } + }, + "players": { + "type": "array", + "items": { + "$ref": "#/definitions/models.UserShort" + } + } + } + }, + "models.Category": { "type": "object", "properties": { "id": { "type": "integer" }, - "is_coop": { - "type": "boolean" + "name": { + "type": "string" + } + } + }, + "models.Chapter": { + "type": "object", + "properties": { + "id": { + "type": "integer" }, "name": { "type": "string" } } }, - "models.LoginResponse": { + "models.Game": { "type": "object", "properties": { - "token": { + "id": { + "type": "integer" + }, + "is_coop": { + "type": "boolean" + }, + "name": { "type": "string" } } @@ -1271,77 +1368,6 @@ const docTemplate = `{ } } }, - "models.MapSummaryResponse": { - "type": "object", - "properties": { - "map": { - "$ref": "#/definitions/models.Map" - }, - "summary": { - "$ref": "#/definitions/models.MapSummary" - } - } - }, - "models.ProfileResponse": { - "type": "object", - "properties": { - "avatar_link": { - "type": "string" - }, - "country_code": { - "type": "string" - }, - "profile": { - "type": "boolean" - }, - "scores_mp": { - "type": "array", - "items": { - "$ref": "#/definitions/models.ScoreResponse" - } - }, - "scores_sp": { - "type": "array", - "items": { - "$ref": "#/definitions/models.ScoreResponse" - } - }, - "steam_id": { - "type": "string" - }, - "user_name": { - "type": "string" - } - } - }, - "models.RankingsResponse": { - "type": "object", - "properties": { - "rankings_mp": { - "type": "array", - "items": { - "$ref": "#/definitions/models.UserRanking" - } - }, - "rankings_sp": { - "type": "array", - "items": { - "$ref": "#/definitions/models.UserRanking" - } - } - } - }, - "models.RecordResponse": { - "type": "object", - "properties": { - "score_count": { - "type": "integer" - }, - "score_time": { - "type": "integer" - } - } - }, "models.Response": { "type": "object", "properties": { @@ -1354,32 +1380,6 @@ const docTemplate = `{ } } }, - "models.ScoreResponse": { - "type": "object", - "properties": { - "map_id": { - "type": "integer" - }, - "records": {} - } - }, - "models.SearchResponse": { - "type": "object", - "properties": { - "maps": { - "type": "array", - "items": { - "$ref": "#/definitions/models.MapShort" - } - }, - "players": { - "type": "array", - "items": { - "$ref": "#/definitions/models.UserShort" - } - } - } - }, "models.UserRanking": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 2e1a789..0f3dade 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -43,7 +43,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.ChapterMapsResponse" + "$ref": "#/definitions/controllers.ChapterMapsResponse" } } } @@ -166,7 +166,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.ChaptersResponse" + "$ref": "#/definitions/controllers.ChaptersResponse" } } } @@ -206,7 +206,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.LoginResponse" + "$ref": "#/definitions/controllers.LoginResponse" } } } @@ -252,7 +252,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.EditMapImageRequest" + "$ref": "#/definitions/controllers.EditMapImageRequest" } } ], @@ -268,7 +268,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.EditMapImageRequest" + "$ref": "#/definitions/controllers.EditMapImageRequest" } } } @@ -407,7 +407,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.RecordResponse" + "$ref": "#/definitions/controllers.RecordResponse" } } } @@ -459,7 +459,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.MapSummaryResponse" + "$ref": "#/definitions/controllers.MapSummaryResponse" } } } @@ -503,7 +503,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.EditMapSummaryRequest" + "$ref": "#/definitions/controllers.EditMapSummaryRequest" } } ], @@ -519,7 +519,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.EditMapSummaryRequest" + "$ref": "#/definitions/controllers.EditMapSummaryRequest" } } } @@ -563,7 +563,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.CreateMapSummaryRequest" + "$ref": "#/definitions/controllers.CreateMapSummaryRequest" } } ], @@ -579,7 +579,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.CreateMapSummaryRequest" + "$ref": "#/definitions/controllers.CreateMapSummaryRequest" } } } @@ -623,7 +623,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/models.DeleteMapSummaryRequest" + "$ref": "#/definitions/controllers.DeleteMapSummaryRequest" } } ], @@ -639,7 +639,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.DeleteMapSummaryRequest" + "$ref": "#/definitions/controllers.DeleteMapSummaryRequest" } } } @@ -688,7 +688,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.ProfileResponse" + "$ref": "#/definitions/controllers.ProfileResponse" } } } @@ -789,7 +789,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.ProfileResponse" + "$ref": "#/definitions/controllers.ProfileResponse" } } } @@ -832,7 +832,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.RankingsResponse" + "$ref": "#/definitions/controllers.RankingsResponse" } } } @@ -877,7 +877,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.SearchResponse" + "$ref": "#/definitions/controllers.SearchResponse" } } } @@ -914,7 +914,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.LoginResponse" + "$ref": "#/definitions/controllers.LoginResponse" } } } @@ -949,7 +949,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.LoginResponse" + "$ref": "#/definitions/controllers.LoginResponse" } } } @@ -998,7 +998,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/models.ProfileResponse" + "$ref": "#/definitions/controllers.ProfileResponse" } } } @@ -1022,29 +1022,7 @@ } }, "definitions": { - "models.Category": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - } - } - }, - "models.Chapter": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "name": { - "type": "string" - } - } - }, - "models.ChapterMapsResponse": { + "controllers.ChapterMapsResponse": { "type": "object", "properties": { "chapter": { @@ -1058,7 +1036,7 @@ } } }, - "models.ChaptersResponse": { + "controllers.ChaptersResponse": { "type": "object", "properties": { "chapters": { @@ -1072,7 +1050,7 @@ } } }, - "models.CreateMapSummaryRequest": { + "controllers.CreateMapSummaryRequest": { "type": "object", "required": [ "category_id", @@ -1102,7 +1080,7 @@ } } }, - "models.DeleteMapSummaryRequest": { + "controllers.DeleteMapSummaryRequest": { "type": "object", "required": [ "route_id" @@ -1113,7 +1091,7 @@ } } }, - "models.EditMapImageRequest": { + "controllers.EditMapImageRequest": { "type": "object", "required": [ "image" @@ -1124,7 +1102,7 @@ } } }, - "models.EditMapSummaryRequest": { + "controllers.EditMapSummaryRequest": { "type": "object", "required": [ "description", @@ -1154,24 +1132,143 @@ } } }, - "models.Game": { + "controllers.LoginResponse": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, + "controllers.MapSummaryResponse": { + "type": "object", + "properties": { + "map": { + "$ref": "#/definitions/models.Map" + }, + "summary": { + "$ref": "#/definitions/models.MapSummary" + } + } + }, + "controllers.ProfileResponse": { + "type": "object", + "properties": { + "avatar_link": { + "type": "string" + }, + "country_code": { + "type": "string" + }, + "profile": { + "type": "boolean" + }, + "scores_mp": { + "type": "array", + "items": { + "$ref": "#/definitions/controllers.ScoreResponse" + } + }, + "scores_sp": { + "type": "array", + "items": { + "$ref": "#/definitions/controllers.ScoreResponse" + } + }, + "steam_id": { + "type": "string" + }, + "user_name": { + "type": "string" + } + } + }, + "controllers.RankingsResponse": { + "type": "object", + "properties": { + "rankings_mp": { + "type": "array", + "items": { + "$ref": "#/definitions/models.UserRanking" + } + }, + "rankings_sp": { + "type": "array", + "items": { + "$ref": "#/definitions/models.UserRanking" + } + } + } + }, + "controllers.RecordResponse": { + "type": "object", + "properties": { + "score_count": { + "type": "integer" + }, + "score_time": { + "type": "integer" + } + } + }, + "controllers.ScoreResponse": { + "type": "object", + "properties": { + "map_id": { + "type": "integer" + }, + "records": {} + } + }, + "controllers.SearchResponse": { + "type": "object", + "properties": { + "maps": { + "type": "array", + "items": { + "$ref": "#/definitions/models.MapShort" + } + }, + "players": { + "type": "array", + "items": { + "$ref": "#/definitions/models.UserShort" + } + } + } + }, + "models.Category": { "type": "object", "properties": { "id": { "type": "integer" }, - "is_coop": { - "type": "boolean" + "name": { + "type": "string" + } + } + }, + "models.Chapter": { + "type": "object", + "properties": { + "id": { + "type": "integer" }, "name": { "type": "string" } } }, - "models.LoginResponse": { + "models.Game": { "type": "object", "properties": { - "token": { + "id": { + "type": "integer" + }, + "is_coop": { + "type": "boolean" + }, + "name": { "type": "string" } } @@ -1264,77 +1361,6 @@ } } }, - "models.MapSummaryResponse": { - "type": "object", - "properties": { - "map": { - "$ref": "#/definitions/models.Map" - }, - "summary": { - "$ref": "#/definitions/models.MapSummary" - } - } - }, - "models.ProfileResponse": { - "type": "object", - "properties": { - "avatar_link": { - "type": "string" - }, - "country_code": { - "type": "string" - }, - "profile": { - "type": "boolean" - }, - "scores_mp": { - "type": "array", - "items": { - "$ref": "#/definitions/models.ScoreResponse" - } - }, - "scores_sp": { - "type": "array", - "items": { - "$ref": "#/definitions/models.ScoreResponse" - } - }, - "steam_id": { - "type": "string" - }, - "user_name": { - "type": "string" - } - } - }, - "models.RankingsResponse": { - "type": "object", - "properties": { - "rankings_mp": { - "type": "array", - "items": { - "$ref": "#/definitions/models.UserRanking" - } - }, - "rankings_sp": { - "type": "array", - "items": { - "$ref": "#/definitions/models.UserRanking" - } - } - } - }, - "models.RecordResponse": { - "type": "object", - "properties": { - "score_count": { - "type": "integer" - }, - "score_time": { - "type": "integer" - } - } - }, "models.Response": { "type": "object", "properties": { @@ -1347,32 +1373,6 @@ } } }, - "models.ScoreResponse": { - "type": "object", - "properties": { - "map_id": { - "type": "integer" - }, - "records": {} - } - }, - "models.SearchResponse": { - "type": "object", - "properties": { - "maps": { - "type": "array", - "items": { - "$ref": "#/definitions/models.MapShort" - } - }, - "players": { - "type": "array", - "items": { - "$ref": "#/definitions/models.UserShort" - } - } - } - }, "models.UserRanking": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 7571073..f50fecb 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,20 +1,6 @@ basePath: /v1 definitions: - models.Category: - properties: - id: - type: integer - name: - type: string - type: object - models.Chapter: - properties: - id: - type: integer - name: - type: string - type: object - models.ChapterMapsResponse: + controllers.ChapterMapsResponse: properties: chapter: $ref: '#/definitions/models.Chapter' @@ -23,7 +9,7 @@ definitions: $ref: '#/definitions/models.MapShort' type: array type: object - models.ChaptersResponse: + controllers.ChaptersResponse: properties: chapters: items: @@ -32,7 +18,7 @@ definitions: game: $ref: '#/definitions/models.Game' type: object - models.CreateMapSummaryRequest: + controllers.CreateMapSummaryRequest: properties: category_id: type: integer @@ -53,21 +39,21 @@ definitions: - score_count - user_name type: object - models.DeleteMapSummaryRequest: + controllers.DeleteMapSummaryRequest: properties: route_id: type: integer required: - route_id type: object - models.EditMapImageRequest: + controllers.EditMapImageRequest: properties: image: type: string required: - image type: object - models.EditMapSummaryRequest: + controllers.EditMapSummaryRequest: properties: description: type: string @@ -88,18 +74,95 @@ definitions: - score_count - user_name type: object - models.Game: + controllers.LoginResponse: + properties: + token: + type: string + type: object + controllers.MapSummaryResponse: + properties: + map: + $ref: '#/definitions/models.Map' + summary: + $ref: '#/definitions/models.MapSummary' + type: object + controllers.ProfileResponse: + properties: + avatar_link: + type: string + country_code: + type: string + profile: + type: boolean + scores_mp: + items: + $ref: '#/definitions/controllers.ScoreResponse' + type: array + scores_sp: + items: + $ref: '#/definitions/controllers.ScoreResponse' + type: array + steam_id: + type: string + user_name: + type: string + type: object + controllers.RankingsResponse: + properties: + rankings_mp: + items: + $ref: '#/definitions/models.UserRanking' + type: array + rankings_sp: + items: + $ref: '#/definitions/models.UserRanking' + type: array + type: object + controllers.RecordResponse: + properties: + score_count: + type: integer + score_time: + type: integer + type: object + controllers.ScoreResponse: + properties: + map_id: + type: integer + records: {} + type: object + controllers.SearchResponse: + properties: + maps: + items: + $ref: '#/definitions/models.MapShort' + type: array + players: + items: + $ref: '#/definitions/models.UserShort' + type: array + type: object + models.Category: properties: id: type: integer - is_coop: - type: boolean name: type: string type: object - models.LoginResponse: + models.Chapter: properties: - token: + id: + type: integer + name: + type: string + type: object + models.Game: + properties: + id: + type: integer + is_coop: + type: boolean + name: type: string type: object models.Map: @@ -159,52 +222,6 @@ definitions: $ref: '#/definitions/models.MapRoute' type: array type: object - models.MapSummaryResponse: - properties: - map: - $ref: '#/definitions/models.Map' - summary: - $ref: '#/definitions/models.MapSummary' - type: object - models.ProfileResponse: - properties: - avatar_link: - type: string - country_code: - type: string - profile: - type: boolean - scores_mp: - items: - $ref: '#/definitions/models.ScoreResponse' - type: array - scores_sp: - items: - $ref: '#/definitions/models.ScoreResponse' - type: array - steam_id: - type: string - user_name: - type: string - type: object - models.RankingsResponse: - properties: - rankings_mp: - items: - $ref: '#/definitions/models.UserRanking' - type: array - rankings_sp: - items: - $ref: '#/definitions/models.UserRanking' - type: array - type: object - models.RecordResponse: - properties: - score_count: - type: integer - score_time: - type: integer - type: object models.Response: properties: data: {} @@ -213,23 +230,6 @@ definitions: success: type: boolean type: object - models.ScoreResponse: - properties: - map_id: - type: integer - records: {} - type: object - models.SearchResponse: - properties: - maps: - items: - $ref: '#/definitions/models.MapShort' - type: array - players: - items: - $ref: '#/definitions/models.UserShort' - type: array - type: object models.UserRanking: properties: total_score: @@ -275,7 +275,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/models.ChapterMapsResponse' + $ref: '#/definitions/controllers.ChapterMapsResponse' type: object "400": description: Bad Request @@ -349,7 +349,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/models.ChaptersResponse' + $ref: '#/definitions/controllers.ChaptersResponse' type: object "400": description: Bad Request @@ -372,7 +372,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/models.LoginResponse' + $ref: '#/definitions/controllers.LoginResponse' type: object "400": description: Bad Request @@ -399,7 +399,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/models.EditMapImageRequest' + $ref: '#/definitions/controllers.EditMapImageRequest' produces: - application/json responses: @@ -410,7 +410,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/models.EditMapImageRequest' + $ref: '#/definitions/controllers.EditMapImageRequest' type: object "400": description: Bad Request @@ -493,7 +493,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/models.RecordResponse' + $ref: '#/definitions/controllers.RecordResponse' type: object "400": description: Bad Request @@ -524,7 +524,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/models.DeleteMapSummaryRequest' + $ref: '#/definitions/controllers.DeleteMapSummaryRequest' produces: - application/json responses: @@ -535,7 +535,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/models.DeleteMapSummaryRequest' + $ref: '#/definitions/controllers.DeleteMapSummaryRequest' type: object "400": description: Bad Request @@ -561,7 +561,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/models.MapSummaryResponse' + $ref: '#/definitions/controllers.MapSummaryResponse' type: object "400": description: Bad Request @@ -587,7 +587,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/models.CreateMapSummaryRequest' + $ref: '#/definitions/controllers.CreateMapSummaryRequest' produces: - application/json responses: @@ -598,7 +598,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/models.CreateMapSummaryRequest' + $ref: '#/definitions/controllers.CreateMapSummaryRequest' type: object "400": description: Bad Request @@ -624,7 +624,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/models.EditMapSummaryRequest' + $ref: '#/definitions/controllers.EditMapSummaryRequest' produces: - application/json responses: @@ -635,7 +635,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/models.EditMapSummaryRequest' + $ref: '#/definitions/controllers.EditMapSummaryRequest' type: object "400": description: Bad Request @@ -664,7 +664,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/models.ProfileResponse' + $ref: '#/definitions/controllers.ProfileResponse' type: object "400": description: Bad Request @@ -696,7 +696,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/models.ProfileResponse' + $ref: '#/definitions/controllers.ProfileResponse' type: object "400": description: Bad Request @@ -753,7 +753,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/models.RankingsResponse' + $ref: '#/definitions/controllers.RankingsResponse' type: object "400": description: Bad Request @@ -779,7 +779,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/models.SearchResponse' + $ref: '#/definitions/controllers.SearchResponse' type: object "400": description: Bad Request @@ -800,7 +800,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/models.LoginResponse' + $ref: '#/definitions/controllers.LoginResponse' type: object "404": description: Not Found @@ -820,7 +820,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/models.LoginResponse' + $ref: '#/definitions/controllers.LoginResponse' type: object "404": description: Not Found @@ -849,7 +849,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/models.ProfileResponse' + $ref: '#/definitions/controllers.ProfileResponse' type: object "400": description: Bad Request -- cgit v1.2.3 From db665af84a047d290ea204a4c7a55115dd1fa728 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Thu, 24 Aug 2023 09:30:58 +0000 Subject: fix: change title handling and login not working (#49) Former-commit-id: b18e6b1d7d5aa68d14d656167d0250a4881dc7a4 --- backend/controllers/loginController.go | 2 +- backend/middleware/auth.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/controllers/loginController.go b/backend/controllers/loginController.go index e907b22..cc7a803 100644 --- a/backend/controllers/loginController.go +++ b/backend/controllers/loginController.go @@ -60,7 +60,7 @@ func Login(c *gin.Context) { VALUES ($1, $2, $3, $4)`, steamID, user.PersonaName, user.AvatarFull, user.LocCountryCode) } moderator := false - rows, _ := database.DB.Query("SELECT title_name FROM titles WHERE user_id = $1", steamID) + rows, _ := database.DB.Query("SELECT title_name FROM titles t INNER JOIN user_titles ut ON t.id=ut.title_id WHERE ut.user_id = $1", steamID) for rows.Next() { var title string rows.Scan(&title) diff --git a/backend/middleware/auth.go b/backend/middleware/auth.go index 0744b3d..6a057da 100644 --- a/backend/middleware/auth.go +++ b/backend/middleware/auth.go @@ -45,7 +45,7 @@ func CheckAuth(c *gin.Context) { } // Get user titles from DB user.Titles = []string{} - rows, _ := database.DB.Query(`SELECT t.title_name FROM titles t WHERE t.user_id = $1`, user.SteamID) + rows, _ := database.DB.Query(`SELECT title_name FROM titles t INNER JOIN user_titles ut ON t.id=ut.title_id WHERE ut.user_id = $1`, user.SteamID) for rows.Next() { var title string rows.Scan(&title) -- cgit v1.2.3 From ca973edc28b5fe543c583217896590f4a2e98897 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Thu, 24 Aug 2023 22:34:05 +0300 Subject: fix: mod flag for easy check (#49) Former-commit-id: 06ee23ee9659834252d3cb5c3c255797e9f93b62 --- .gitignore | 3 +- backend/controllers/modController.go | 44 +++++----------- backend/controllers/userController.go | 95 +++++++++++++++++------------------ backend/database/init.sql | 15 +++++- backend/middleware/auth.go | 13 +++-- backend/models/models.go | 14 +++++- 6 files changed, 95 insertions(+), 89 deletions(-) diff --git a/.gitignore b/.gitignore index 1434a43..10d4fda 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .vscode *.sh *.txt -*.dem \ No newline at end of file +*.dem +*.json \ No newline at end of file diff --git a/backend/controllers/modController.go b/backend/controllers/modController.go index 7ce5cb4..7acdb5d 100644 --- a/backend/controllers/modController.go +++ b/backend/controllers/modController.go @@ -49,18 +49,13 @@ type EditMapImageRequest struct { // @Router /maps/{id}/summary [post] func CreateMapSummary(c *gin.Context) { // Check if user exists - user, exists := c.Get("user") + _, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) return } - var moderator bool - for _, title := range user.(models.User).Titles { - if title == "Moderator" { - moderator = true - } - } - if !moderator { + mod, exists := c.Get("mod") + if !exists || !mod.(bool) { c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) return } @@ -135,18 +130,13 @@ func CreateMapSummary(c *gin.Context) { // @Router /maps/{id}/summary [put] func EditMapSummary(c *gin.Context) { // Check if user exists - user, exists := c.Get("user") + _, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) return } - var moderator bool - for _, title := range user.(models.User).Titles { - if title == "Moderator" { - moderator = true - } - } - if !moderator { + mod, exists := c.Get("mod") + if !exists || !mod.(bool) { c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) return } @@ -221,18 +211,13 @@ func EditMapSummary(c *gin.Context) { // @Router /maps/{id}/summary [delete] func DeleteMapSummary(c *gin.Context) { // Check if user exists - user, exists := c.Get("user") + _, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) return } - var moderator bool - for _, title := range user.(models.User).Titles { - if title == "Moderator" { - moderator = true - } - } - if !moderator { + mod, exists := c.Get("mod") + if !exists || !mod.(bool) { c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) return } @@ -311,18 +296,13 @@ func DeleteMapSummary(c *gin.Context) { // @Router /maps/{id}/image [put] func EditMapImage(c *gin.Context) { // Check if user exists - user, exists := c.Get("user") + _, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) return } - var moderator bool - for _, title := range user.(models.User).Titles { - if title == "Moderator" { - moderator = true - } - } - if !moderator { + mod, exists := c.Get("mod") + if !exists || !mod.(bool) { c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) return } diff --git a/backend/controllers/userController.go b/backend/controllers/userController.go index 0dae155..64f144a 100644 --- a/backend/controllers/userController.go +++ b/backend/controllers/userController.go @@ -17,8 +17,39 @@ type ProfileResponse struct { UserName string `json:"user_name"` AvatarLink string `json:"avatar_link"` CountryCode string `json:"country_code"` - ScoresSP []ScoreResponse `json:"scores_sp"` - ScoresMP []ScoreResponse `json:"scores_mp"` + Titles []models.Title `json:"titles"` + Links models.Links `json:"links"` + Rankings ProfileRankings `json:"rankings"` + Records ProfileRecords `json:"records"` +} + +type ProfileRankings struct { + Overall ProfileRankingsDetails `json:"overall"` + Singleplayer ProfileRankingsDetails `json:"singleplayer"` + Cooperative ProfileRankingsDetails `json:"cooperative"` +} + +type ProfileRankingsDetails struct { + Rank int `json:"rank"` + CompletionCount int `json:"completion_count"` + CompletionTotal int `json:"completion_total"` +} + +type ProfileRecords struct { + P2Singleplayer ProfileRecordsDetails `json:"portal2_singleplayer"` + P2Cooperative ProfileRecordsDetails `json:"portal2_cooperative"` +} + +type ProfileRecordsDetails struct { + MapID int `json:"map_id"` + Scores []ProfileScores `json:"scores"` +} + +type ProfileScores struct { + DemoID string `json:"demo_id"` + ScoreCount int `json:"score_count"` + ScoreTime int `json:"score_time"` + Date time.Time `json:"date"` } type ScoreResponse struct { @@ -44,58 +75,22 @@ func Profile(c *gin.Context) { c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) return } - // Retrieve singleplayer records - var scoresSP []ScoreResponse - sql := `SELECT id, map_id, score_count, score_time, demo_id, record_date FROM records_sp WHERE user_id = $1 ORDER BY map_id` + // Get user titles + titles := []models.Title{} + sql := `SELECT t.title_name, t.title_color FROM titles t + INNER JOIN user_titles ut ON t.id=ut.title_id WHERE ut.user_id = $1` rows, err := database.DB.Query(sql, user.(models.User).SteamID) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } - var recordsSP []models.RecordSP - for rows.Next() { - var mapID int - var record models.RecordSP - rows.Scan(&record.RecordID, &mapID, &record.ScoreCount, &record.ScoreTime, &record.DemoID, &record.RecordDate) - // More than one record in one map - if len(scoresSP) != 0 && mapID == scoresSP[len(scoresSP)-1].MapID { - scoresSP[len(scoresSP)-1].Records = append(scoresSP[len(scoresSP)-1].Records.([]models.RecordSP), record) - continue - } - // New map - recordsSP = []models.RecordSP{} - recordsSP = append(recordsSP, record) - scoresSP = append(scoresSP, ScoreResponse{ - MapID: mapID, - Records: recordsSP, - }) - } - // Retrieve multiplayer records - var scoresMP []ScoreResponse - sql = `SELECT id, map_id, host_id, partner_id, score_count, score_time, host_demo_id, partner_demo_id, record_date FROM records_mp - WHERE host_id = $1 OR partner_id = $2 ORDER BY map_id` - rows, err = database.DB.Query(sql, user.(models.User).SteamID, user.(models.User).SteamID) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - var recordsMP []models.RecordMP for rows.Next() { - var mapID int - var record models.RecordMP - rows.Scan(&record.RecordID, &mapID, &record.HostID, &record.PartnerID, &record.ScoreCount, &record.ScoreTime, &record.HostDemoID, &record.PartnerDemoID, &record.RecordDate) - // More than one record in one map - if len(scoresMP) != 0 && mapID == scoresMP[len(scoresMP)-1].MapID { - scoresMP[len(scoresMP)-1].Records = append(scoresMP[len(scoresMP)-1].Records.([]models.RecordMP), record) - continue + var title models.Title + if err := rows.Scan(&title.Name, &title.Color); err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return } - // New map - recordsMP = []models.RecordMP{} - recordsMP = append(recordsMP, record) - scoresMP = append(scoresMP, ScoreResponse{ - MapID: mapID, - Records: recordsMP, - }) + titles = append(titles, title) } c.JSON(http.StatusOK, models.Response{ Success: true, @@ -106,8 +101,10 @@ func Profile(c *gin.Context) { UserName: user.(models.User).UserName, AvatarLink: user.(models.User).AvatarLink, CountryCode: user.(models.User).CountryCode, - ScoresSP: scoresSP, - ScoresMP: scoresMP, + Titles: user.(models.User).Titles, + Links: models.Links{}, + Rankings: ProfileRankings{}, + Records: ProfileRecords{}, }, }) return diff --git a/backend/database/init.sql b/backend/database/init.sql index 11d4944..25de872 100644 --- a/backend/database/init.sql +++ b/backend/database/init.sql @@ -3,6 +3,10 @@ CREATE TABLE users ( user_name TEXT NOT NULL, avatar_link TEXT NOT NULL, country_code CHAR(2) NOT NULL, + p2sr TEXT NOT NULL DEFAULT '-', + steam TEXT NOT NULL DEFAULT '-', + youtube TEXT NOT NULL DEFAULT '-', + twitch TEXT NOT NULL DEFAULT '-', created_at TIMESTAMP NOT NULL DEFAULT now(), updated_at TIMESTAMP NOT NULL DEFAULT now(), PRIMARY KEY (steam_id) @@ -117,9 +121,16 @@ CREATE TABLE records_mp ( ); CREATE TABLE titles ( - user_id TEXT, + id SERIAL, title_name TEXT NOT NULL, - PRIMARY KEY (user_id), + title_color CHAR(6) NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE user_titles ( + title_id INT NOT NULL, + user_id TEXT NOT NULL, + FOREIGN KEY (title_id) REFERENCES titles(id), FOREIGN KEY (user_id) REFERENCES users(steam_id) ); diff --git a/backend/middleware/auth.go b/backend/middleware/auth.go index 6a057da..e2c84fa 100644 --- a/backend/middleware/auth.go +++ b/backend/middleware/auth.go @@ -44,14 +44,19 @@ func CheckAuth(c *gin.Context) { return } // Get user titles from DB - user.Titles = []string{} - rows, _ := database.DB.Query(`SELECT title_name FROM titles t INNER JOIN user_titles ut ON t.id=ut.title_id WHERE ut.user_id = $1`, user.SteamID) + var moderator bool + user.Titles = []models.Title{} + rows, _ := database.DB.Query(`SELECT t.title_name, t.title_color FROM titles t INNER JOIN user_titles ut ON t.id=ut.title_id WHERE ut.user_id = $1`, user.SteamID) for rows.Next() { - var title string - rows.Scan(&title) + var title models.Title + rows.Scan(&title.Name, &title.Color) + if title.Name == "Moderator" { + moderator = true + } user.Titles = append(user.Titles, title) } c.Set("user", user) + c.Set("mod", moderator) c.Next() } else { c.Next() diff --git a/backend/models/models.go b/backend/models/models.go index e21ba6a..f124db5 100644 --- a/backend/models/models.go +++ b/backend/models/models.go @@ -25,7 +25,7 @@ type User struct { CountryCode string `json:"country_code"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - Titles []string `json:"titles"` + Titles []Title `json:"titles"` } type UserShort struct { @@ -92,6 +92,18 @@ type Category struct { Name string `json:"name"` } +type Title struct { + Name string `json:"name"` + Color string `json:"color"` +} + +type Links struct { + P2SR string `json:"p2sr"` + Steam string `json:"stream"` + YouTube string `json:"youtube"` + Twitch string `json:"twitch"` +} + type RecordSP struct { RecordID int `json:"record_id"` Placement int `json:"placement"` -- cgit v1.2.3 From 17fb28437214712d7b7a9db02a2c0b8d87f0559d Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Thu, 24 Aug 2023 22:42:22 +0300 Subject: feat: profile cleanup before rework (#51) Former-commit-id: 639d3d4a42b147cf0712775ce5218f613edf6de5 --- backend/controllers/userController.go | 59 +++++++---------------------------- 1 file changed, 12 insertions(+), 47 deletions(-) diff --git a/backend/controllers/userController.go b/backend/controllers/userController.go index 64f144a..960ecb4 100644 --- a/backend/controllers/userController.go +++ b/backend/controllers/userController.go @@ -143,58 +143,21 @@ func FetchUser(c *gin.Context) { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } - // Retrieve singleplayer records - var scoresSP []ScoreResponse - sql := `SELECT id, map_id, score_count, score_time, demo_id, record_date FROM records_sp WHERE user_id = $1 ORDER BY map_id` + // Get user titles + sql := `SELECT t.title_name, t.title_color FROM titles t + INNER JOIN user_titles ut ON t.id=ut.title_id WHERE ut.user_id = $1` rows, err := database.DB.Query(sql, user.SteamID) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } - var recordsSP []models.RecordSP for rows.Next() { - var mapID int - var record models.RecordSP - rows.Scan(&record.RecordID, &mapID, &record.ScoreCount, &record.ScoreTime, &record.DemoID, &record.RecordDate) - // More than one record in one map - if len(scoresSP) != 0 && mapID == scoresSP[len(scoresSP)-1].MapID { - scoresSP[len(scoresSP)-1].Records = append(scoresSP[len(scoresSP)-1].Records.([]models.RecordSP), record) - continue - } - // New map - recordsSP = []models.RecordSP{} - recordsSP = append(recordsSP, record) - scoresSP = append(scoresSP, ScoreResponse{ - MapID: mapID, - Records: recordsSP, - }) - } - // Retrieve multiplayer records - var scoresMP []ScoreResponse - sql = `SELECT id, map_id, host_id, partner_id, score_count, score_time, host_demo_id, partner_demo_id, record_date FROM records_mp - WHERE host_id = $1 OR partner_id = $2 ORDER BY map_id` - rows, err = database.DB.Query(sql, user.SteamID, user.SteamID) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - var recordsMP []models.RecordMP - for rows.Next() { - var mapID int - var record models.RecordMP - rows.Scan(&record.RecordID, &mapID, &record.HostID, &record.PartnerID, &record.ScoreCount, &record.ScoreTime, &record.HostDemoID, &record.PartnerDemoID, &record.RecordDate) - // More than one record in one map - if len(scoresMP) != 0 && mapID == scoresMP[len(scoresMP)-1].MapID { - scoresMP[len(scoresMP)-1].Records = append(scoresMP[len(scoresMP)-1].Records.([]models.RecordMP), record) - continue + var title models.Title + if err := rows.Scan(&title.Name, &title.Color); err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return } - // New map - recordsMP = []models.RecordMP{} - recordsMP = append(recordsMP, record) - scoresMP = append(scoresMP, ScoreResponse{ - MapID: mapID, - Records: recordsMP, - }) + user.Titles = append(user.Titles, title) } c.JSON(http.StatusOK, models.Response{ Success: true, @@ -205,8 +168,10 @@ func FetchUser(c *gin.Context) { UserName: user.UserName, AvatarLink: user.AvatarLink, CountryCode: user.CountryCode, - ScoresSP: scoresSP, - ScoresMP: scoresMP, + Titles: user.Titles, + Links: models.Links{}, + Rankings: ProfileRankings{}, + Records: ProfileRecords{}, }, }) return -- cgit v1.2.3 From aa93b382722cc34f63666dc56d68b53e86ce5394 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Fri, 25 Aug 2023 22:58:52 +0300 Subject: feat: better score handling in profiles (#51) Former-commit-id: 9d30f4ad6e9e077259868e7f05fbe7ac1f65229c --- backend/controllers/userController.go | 174 ++++++++++++++++++++++++++++------ 1 file changed, 147 insertions(+), 27 deletions(-) diff --git a/backend/controllers/userController.go b/backend/controllers/userController.go index 960ecb4..84d589a 100644 --- a/backend/controllers/userController.go +++ b/backend/controllers/userController.go @@ -36,13 +36,14 @@ type ProfileRankingsDetails struct { } type ProfileRecords struct { - P2Singleplayer ProfileRecordsDetails `json:"portal2_singleplayer"` - P2Cooperative ProfileRecordsDetails `json:"portal2_cooperative"` + P2Singleplayer []ProfileRecordsDetails `json:"portal2_singleplayer"` + P2Cooperative []ProfileRecordsDetails `json:"portal2_cooperative"` } type ProfileRecordsDetails struct { - MapID int `json:"map_id"` - Scores []ProfileScores `json:"scores"` + MapID int `json:"map_id"` + MapName string `json:"map_name"` + Scores []ProfileScores `json:"scores"` } type ProfileScores struct { @@ -75,22 +76,78 @@ func Profile(c *gin.Context) { c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) return } - // Get user titles - titles := []models.Title{} - sql := `SELECT t.title_name, t.title_color FROM titles t - INNER JOIN user_titles ut ON t.id=ut.title_id WHERE ut.user_id = $1` + // Get user links + links := models.Links{} + sql := `SELECT u.p2sr, u.steam, u.youtube, u.twitch FROM users u WHERE u.steam_id = $1` + err := database.DB.QueryRow(sql, user.(models.User).SteamID).Scan(&links.P2SR, &links.Steam, &links.YouTube, &links.Twitch) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + // TODO: Get rankings (all maps done in one game) + records := ProfileRecords{ + P2Singleplayer: []ProfileRecordsDetails{}, + P2Cooperative: []ProfileRecordsDetails{}, + } + // Get singleplayer records + sql = `SELECT m.game_id, sp.map_id, m."name", sp.score_count, sp.score_time, sp.demo_id, sp.record_date + FROM records_sp sp INNER JOIN maps m ON sp.map_id = m.id WHERE sp.user_id = $1 ORDER BY sp.map_id, sp.score_count, sp.score_time;` rows, err := database.DB.Query(sql, user.(models.User).SteamID) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } for rows.Next() { - var title models.Title - if err := rows.Scan(&title.Name, &title.Color); err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return + var mapID int + var mapName string + var gameID int + score := ProfileScores{} + rows.Scan(&gameID, &mapID, &mapName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) + if gameID != 1 { + continue + } + // More than one record in one map + if len(records.P2Singleplayer) != 0 && mapID == records.P2Singleplayer[len(records.P2Singleplayer)-1].MapID { + records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores = append(records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores, score) + continue } - titles = append(titles, title) + // New map + records.P2Singleplayer = append(records.P2Singleplayer, ProfileRecordsDetails{ + MapID: mapID, + MapName: mapName, + Scores: []ProfileScores{}, + }) + records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores = append(records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores, score) + } + // Get multiplayer records + sql = `SELECT m.game_id, mp.map_id, m."name", mp.score_count, mp.score_time, CASE WHEN host_id = $1 THEN mp.host_demo_id WHEN partner_id = $1 THEN mp.partner_demo_id END demo_id, mp.record_date + FROM records_mp mp INNER JOIN maps m ON mp.map_id = m.id WHERE mp.host_id = $1 OR mp.partner_id = $1 ORDER BY mp.map_id, mp.score_count, mp.score_time;` + rows, err = database.DB.Query(sql, user.(models.User).SteamID) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + for rows.Next() { + var mapID int + var mapName string + var gameID int + score := ProfileScores{} + rows.Scan(&gameID, &mapID, &mapName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) + if gameID != 1 { + continue + } + // More than one record in one map + if len(records.P2Cooperative) != 0 && mapID == records.P2Cooperative[len(records.P2Cooperative)-1].MapID { + records.P2Cooperative[len(records.P2Cooperative)-1].Scores = append(records.P2Cooperative[len(records.P2Cooperative)-1].Scores, score) + continue + } + // New map + records.P2Cooperative = append(records.P2Cooperative, ProfileRecordsDetails{ + MapID: mapID, + MapName: mapName, + Scores: []ProfileScores{}, + }) + records.P2Cooperative[len(records.P2Cooperative)-1].Scores = append(records.P2Cooperative[len(records.P2Cooperative)-1].Scores, score) } c.JSON(http.StatusOK, models.Response{ Success: true, @@ -102,12 +159,11 @@ func Profile(c *gin.Context) { AvatarLink: user.(models.User).AvatarLink, CountryCode: user.(models.User).CountryCode, Titles: user.(models.User).Titles, - Links: models.Links{}, + Links: links, Rankings: ProfileRankings{}, - Records: ProfileRecords{}, + Records: records, }, }) - return } // GET User @@ -131,20 +187,20 @@ func FetchUser(c *gin.Context) { } // Check if user exists var user models.User - err := database.DB.QueryRow(`SELECT * FROM users WHERE steam_id = $1`, id).Scan( - &user.SteamID, &user.UserName, &user.AvatarLink, &user.CountryCode, - &user.CreatedAt, &user.UpdatedAt) + links := models.Links{} + sql := `SELECT u.steam_id, u.user_name, u.avatar_link, u.country_code, u.created_at, u.updated_at, u.p2sr, u.steam, u.youtube, u.twitch FROM users u WHERE u.steam_id = $1` + err := database.DB.QueryRow(sql, id).Scan(&user.SteamID, &user.UserName, &user.AvatarLink, &user.CountryCode, &user.CreatedAt, &user.UpdatedAt, &links.P2SR, &links.Steam, &links.YouTube, &links.Twitch) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } if user.SteamID == "" { // User does not exist c.JSON(http.StatusNotFound, models.ErrorResponse("User not found.")) return } - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } // Get user titles - sql := `SELECT t.title_name, t.title_color FROM titles t + sql = `SELECT t.title_name, t.title_color FROM titles t INNER JOIN user_titles ut ON t.id=ut.title_id WHERE ut.user_id = $1` rows, err := database.DB.Query(sql, user.SteamID) if err != nil { @@ -159,6 +215,71 @@ func FetchUser(c *gin.Context) { } user.Titles = append(user.Titles, title) } + // TODO: Get rankings (all maps done in one game) + records := ProfileRecords{ + P2Singleplayer: []ProfileRecordsDetails{}, + P2Cooperative: []ProfileRecordsDetails{}, + } + // Get singleplayer records + sql = `SELECT m.game_id, sp.map_id, m."name", sp.score_count, sp.score_time, sp.demo_id, sp.record_date + FROM records_sp sp INNER JOIN maps m ON sp.map_id = m.id WHERE sp.user_id = $1 ORDER BY sp.map_id, sp.score_count, sp.score_time;` + rows, err = database.DB.Query(sql, user.SteamID) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + for rows.Next() { + var mapID int + var mapName string + var gameID int + score := ProfileScores{} + rows.Scan(&gameID, &mapID, &mapName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) + if gameID != 1 { + continue + } + // More than one record in one map + if len(records.P2Singleplayer) != 0 && mapID == records.P2Singleplayer[len(records.P2Singleplayer)-1].MapID { + records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores = append(records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores, score) + continue + } + // New map + records.P2Singleplayer = append(records.P2Singleplayer, ProfileRecordsDetails{ + MapID: mapID, + MapName: mapName, + Scores: []ProfileScores{}, + }) + records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores = append(records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores, score) + } + // Get multiplayer records + sql = `SELECT m.game_id, mp.map_id, m."name", mp.score_count, mp.score_time, CASE WHEN host_id = $1 THEN mp.host_demo_id WHEN partner_id = $1 THEN mp.partner_demo_id END demo_id, mp.record_date + FROM records_mp mp INNER JOIN maps m ON mp.map_id = m.id WHERE mp.host_id = $1 OR mp.partner_id = $1 ORDER BY mp.map_id, mp.score_count, mp.score_time;` + rows, err = database.DB.Query(sql, user.SteamID) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + for rows.Next() { + var mapID int + var mapName string + var gameID int + score := ProfileScores{} + rows.Scan(&gameID, &mapID, &mapName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) + if gameID != 1 { + continue + } + // More than one record in one map + if len(records.P2Cooperative) != 0 && mapID == records.P2Cooperative[len(records.P2Cooperative)-1].MapID { + records.P2Cooperative[len(records.P2Cooperative)-1].Scores = append(records.P2Cooperative[len(records.P2Cooperative)-1].Scores, score) + continue + } + // New map + records.P2Cooperative = append(records.P2Cooperative, ProfileRecordsDetails{ + MapID: mapID, + MapName: mapName, + Scores: []ProfileScores{}, + }) + records.P2Cooperative[len(records.P2Cooperative)-1].Scores = append(records.P2Cooperative[len(records.P2Cooperative)-1].Scores, score) + } c.JSON(http.StatusOK, models.Response{ Success: true, Message: "Successfully retrieved user scores.", @@ -169,12 +290,11 @@ func FetchUser(c *gin.Context) { AvatarLink: user.AvatarLink, CountryCode: user.CountryCode, Titles: user.Titles, - Links: models.Links{}, + Links: links, Rankings: ProfileRankings{}, - Records: ProfileRecords{}, + Records: records, }, }) - return } // PUT Profile -- cgit v1.2.3 From ba09abbe6a8d0c1f3a54c8c7426eda7b930e1447 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Fri, 25 Aug 2023 23:41:13 +0300 Subject: docs: profile improvement swagger (#51) Former-commit-id: ff35be478c4f29c611c40fa34ca5d134e970cc16 --- docs/docs.go | 137 +++++++++++++++++++++++++++++++++++++++++++++++------- docs/swagger.json | 137 +++++++++++++++++++++++++++++++++++++++++++++++------- docs/swagger.yaml | 93 ++++++++++++++++++++++++++++++------ main.go | 4 +- 4 files changed, 319 insertions(+), 52 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index df01379..d0c78ac 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1158,6 +1158,68 @@ const docTemplate = `{ } } }, + "controllers.ProfileRankings": { + "type": "object", + "properties": { + "cooperative": { + "$ref": "#/definitions/controllers.ProfileRankingsDetails" + }, + "overall": { + "$ref": "#/definitions/controllers.ProfileRankingsDetails" + }, + "singleplayer": { + "$ref": "#/definitions/controllers.ProfileRankingsDetails" + } + } + }, + "controllers.ProfileRankingsDetails": { + "type": "object", + "properties": { + "completion_count": { + "type": "integer" + }, + "completion_total": { + "type": "integer" + }, + "rank": { + "type": "integer" + } + } + }, + "controllers.ProfileRecords": { + "type": "object", + "properties": { + "portal2_cooperative": { + "type": "array", + "items": { + "$ref": "#/definitions/controllers.ProfileRecordsDetails" + } + }, + "portal2_singleplayer": { + "type": "array", + "items": { + "$ref": "#/definitions/controllers.ProfileRecordsDetails" + } + } + } + }, + "controllers.ProfileRecordsDetails": { + "type": "object", + "properties": { + "map_id": { + "type": "integer" + }, + "map_name": { + "type": "string" + }, + "scores": { + "type": "array", + "items": { + "$ref": "#/definitions/controllers.ProfileScores" + } + } + } + }, "controllers.ProfileResponse": { "type": "object", "properties": { @@ -1167,26 +1229,46 @@ const docTemplate = `{ "country_code": { "type": "string" }, + "links": { + "$ref": "#/definitions/models.Links" + }, "profile": { "type": "boolean" }, - "scores_mp": { - "type": "array", - "items": { - "$ref": "#/definitions/controllers.ScoreResponse" - } + "rankings": { + "$ref": "#/definitions/controllers.ProfileRankings" + }, + "records": { + "$ref": "#/definitions/controllers.ProfileRecords" }, - "scores_sp": { + "steam_id": { + "type": "string" + }, + "titles": { "type": "array", "items": { - "$ref": "#/definitions/controllers.ScoreResponse" + "$ref": "#/definitions/models.Title" } }, - "steam_id": { + "user_name": { + "type": "string" + } + } + }, + "controllers.ProfileScores": { + "type": "object", + "properties": { + "date": { "type": "string" }, - "user_name": { + "demo_id": { "type": "string" + }, + "score_count": { + "type": "integer" + }, + "score_time": { + "type": "integer" } } }, @@ -1218,15 +1300,6 @@ const docTemplate = `{ } } }, - "controllers.ScoreResponse": { - "type": "object", - "properties": { - "map_id": { - "type": "integer" - }, - "records": {} - } - }, "controllers.SearchResponse": { "type": "object", "properties": { @@ -1280,6 +1353,23 @@ const docTemplate = `{ } } }, + "models.Links": { + "type": "object", + "properties": { + "p2sr": { + "type": "string" + }, + "stream": { + "type": "string" + }, + "twitch": { + "type": "string" + }, + "youtube": { + "type": "string" + } + } + }, "models.Map": { "type": "object", "properties": { @@ -1380,6 +1470,17 @@ const docTemplate = `{ } } }, + "models.Title": { + "type": "object", + "properties": { + "color": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "models.UserRanking": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 0f3dade..cf37a5a 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1151,6 +1151,68 @@ } } }, + "controllers.ProfileRankings": { + "type": "object", + "properties": { + "cooperative": { + "$ref": "#/definitions/controllers.ProfileRankingsDetails" + }, + "overall": { + "$ref": "#/definitions/controllers.ProfileRankingsDetails" + }, + "singleplayer": { + "$ref": "#/definitions/controllers.ProfileRankingsDetails" + } + } + }, + "controllers.ProfileRankingsDetails": { + "type": "object", + "properties": { + "completion_count": { + "type": "integer" + }, + "completion_total": { + "type": "integer" + }, + "rank": { + "type": "integer" + } + } + }, + "controllers.ProfileRecords": { + "type": "object", + "properties": { + "portal2_cooperative": { + "type": "array", + "items": { + "$ref": "#/definitions/controllers.ProfileRecordsDetails" + } + }, + "portal2_singleplayer": { + "type": "array", + "items": { + "$ref": "#/definitions/controllers.ProfileRecordsDetails" + } + } + } + }, + "controllers.ProfileRecordsDetails": { + "type": "object", + "properties": { + "map_id": { + "type": "integer" + }, + "map_name": { + "type": "string" + }, + "scores": { + "type": "array", + "items": { + "$ref": "#/definitions/controllers.ProfileScores" + } + } + } + }, "controllers.ProfileResponse": { "type": "object", "properties": { @@ -1160,26 +1222,46 @@ "country_code": { "type": "string" }, + "links": { + "$ref": "#/definitions/models.Links" + }, "profile": { "type": "boolean" }, - "scores_mp": { - "type": "array", - "items": { - "$ref": "#/definitions/controllers.ScoreResponse" - } + "rankings": { + "$ref": "#/definitions/controllers.ProfileRankings" + }, + "records": { + "$ref": "#/definitions/controllers.ProfileRecords" }, - "scores_sp": { + "steam_id": { + "type": "string" + }, + "titles": { "type": "array", "items": { - "$ref": "#/definitions/controllers.ScoreResponse" + "$ref": "#/definitions/models.Title" } }, - "steam_id": { + "user_name": { + "type": "string" + } + } + }, + "controllers.ProfileScores": { + "type": "object", + "properties": { + "date": { "type": "string" }, - "user_name": { + "demo_id": { "type": "string" + }, + "score_count": { + "type": "integer" + }, + "score_time": { + "type": "integer" } } }, @@ -1211,15 +1293,6 @@ } } }, - "controllers.ScoreResponse": { - "type": "object", - "properties": { - "map_id": { - "type": "integer" - }, - "records": {} - } - }, "controllers.SearchResponse": { "type": "object", "properties": { @@ -1273,6 +1346,23 @@ } } }, + "models.Links": { + "type": "object", + "properties": { + "p2sr": { + "type": "string" + }, + "stream": { + "type": "string" + }, + "twitch": { + "type": "string" + }, + "youtube": { + "type": "string" + } + } + }, "models.Map": { "type": "object", "properties": { @@ -1373,6 +1463,17 @@ } } }, + "models.Title": { + "type": "object", + "properties": { + "color": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "models.UserRanking": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index f50fecb..0dd514d 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -86,27 +86,80 @@ definitions: summary: $ref: '#/definitions/models.MapSummary' type: object + controllers.ProfileRankings: + properties: + cooperative: + $ref: '#/definitions/controllers.ProfileRankingsDetails' + overall: + $ref: '#/definitions/controllers.ProfileRankingsDetails' + singleplayer: + $ref: '#/definitions/controllers.ProfileRankingsDetails' + type: object + controllers.ProfileRankingsDetails: + properties: + completion_count: + type: integer + completion_total: + type: integer + rank: + type: integer + type: object + controllers.ProfileRecords: + properties: + portal2_cooperative: + items: + $ref: '#/definitions/controllers.ProfileRecordsDetails' + type: array + portal2_singleplayer: + items: + $ref: '#/definitions/controllers.ProfileRecordsDetails' + type: array + type: object + controllers.ProfileRecordsDetails: + properties: + map_id: + type: integer + map_name: + type: string + scores: + items: + $ref: '#/definitions/controllers.ProfileScores' + type: array + type: object controllers.ProfileResponse: properties: avatar_link: type: string country_code: type: string + links: + $ref: '#/definitions/models.Links' profile: type: boolean - scores_mp: - items: - $ref: '#/definitions/controllers.ScoreResponse' - type: array - scores_sp: - items: - $ref: '#/definitions/controllers.ScoreResponse' - type: array + rankings: + $ref: '#/definitions/controllers.ProfileRankings' + records: + $ref: '#/definitions/controllers.ProfileRecords' steam_id: type: string + titles: + items: + $ref: '#/definitions/models.Title' + type: array user_name: type: string type: object + controllers.ProfileScores: + properties: + date: + type: string + demo_id: + type: string + score_count: + type: integer + score_time: + type: integer + type: object controllers.RankingsResponse: properties: rankings_mp: @@ -125,12 +178,6 @@ definitions: score_time: type: integer type: object - controllers.ScoreResponse: - properties: - map_id: - type: integer - records: {} - type: object controllers.SearchResponse: properties: maps: @@ -165,6 +212,17 @@ definitions: name: type: string type: object + models.Links: + properties: + p2sr: + type: string + stream: + type: string + twitch: + type: string + youtube: + type: string + type: object models.Map: properties: chapter_name: @@ -230,6 +288,13 @@ definitions: success: type: boolean type: object + models.Title: + properties: + color: + type: string + name: + type: string + type: object models.UserRanking: properties: total_score: diff --git a/main.go b/main.go index 9b531fe..69f7c86 100644 --- a/main.go +++ b/main.go @@ -19,8 +19,8 @@ import ( // @license.name GNU General Public License, Version 2 // @license.url https://www.gnu.org/licenses/old-licenses/gpl-2.0.html -// @host lp.ardapektezol.com/api -// @BasePath /v1 +// @host lp.ardapektezol.com/api +// @BasePath /v1 func main() { if os.Getenv("ENV") == "PROD" { gin.SetMode(gin.ReleaseMode) -- cgit v1.2.3 From f1b7589b2936335957a6a1da1eea3d66233ad0ce Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Sat, 26 Aug 2023 08:53:24 +0300 Subject: refactor: reorganizing packages Former-commit-id: 99410223654c2a5ffc15fdab6ec3e921b5410cba --- backend/api/auth.go | 65 ++++++ backend/api/routes.go | 40 ++++ backend/controllers/homeController.go | 294 ------------------------ backend/controllers/loginController.go | 170 -------------- backend/controllers/mapController.go | 314 -------------------------- backend/controllers/modController.go | 334 ---------------------------- backend/controllers/recordController.go | 288 ------------------------ backend/controllers/userController.go | 383 -------------------------------- backend/handlers/home.go | 294 ++++++++++++++++++++++++ backend/handlers/login.go | 166 ++++++++++++++ backend/handlers/map.go | 314 ++++++++++++++++++++++++++ backend/handlers/mod.go | 334 ++++++++++++++++++++++++++++ backend/handlers/record.go | 292 ++++++++++++++++++++++++ backend/handlers/user.go | 383 ++++++++++++++++++++++++++++++++ backend/middleware/auth.go | 65 ------ backend/routes/routes.go | 41 ---- main.go | 8 +- 17 files changed, 1892 insertions(+), 1893 deletions(-) create mode 100644 backend/api/auth.go create mode 100644 backend/api/routes.go delete mode 100644 backend/controllers/homeController.go delete mode 100644 backend/controllers/loginController.go delete mode 100644 backend/controllers/mapController.go delete mode 100644 backend/controllers/modController.go delete mode 100644 backend/controllers/recordController.go delete mode 100644 backend/controllers/userController.go create mode 100644 backend/handlers/home.go create mode 100644 backend/handlers/login.go create mode 100644 backend/handlers/map.go create mode 100644 backend/handlers/mod.go create mode 100644 backend/handlers/record.go create mode 100644 backend/handlers/user.go delete mode 100644 backend/middleware/auth.go delete mode 100644 backend/routes/routes.go diff --git a/backend/api/auth.go b/backend/api/auth.go new file mode 100644 index 0000000..91ef80c --- /dev/null +++ b/backend/api/auth.go @@ -0,0 +1,65 @@ +package api + +import ( + "fmt" + "os" + "time" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v4" + "github.com/pektezol/leastportalshub/backend/database" + "github.com/pektezol/leastportalshub/backend/models" +) + +func CheckAuth(c *gin.Context) { + tokenString := c.GetHeader("Authorization") + // Validate token + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(os.Getenv("SECRET_KEY")), nil + }) + if token == nil { + c.Next() + return + } + if err != nil { + c.Next() + return + } + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + // Check exp + if float64(time.Now().Unix()) > claims["exp"].(float64) { + c.Next() + return + } + // Get user from DB + var user models.User + 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( + &user.SteamID, &user.UserName, &user.AvatarLink, + &user.CountryCode, &user.CreatedAt, &user.UpdatedAt) + if user.SteamID == "" { + c.Next() + return + } + // Get user titles from DB + var moderator bool + user.Titles = []models.Title{} + rows, _ := database.DB.Query(`SELECT t.title_name, t.title_color FROM titles t INNER JOIN user_titles ut ON t.id=ut.title_id WHERE ut.user_id = $1`, user.SteamID) + for rows.Next() { + var title models.Title + rows.Scan(&title.Name, &title.Color) + if title.Name == "Moderator" { + moderator = true + } + user.Titles = append(user.Titles, title) + } + c.Set("user", user) + c.Set("mod", moderator) + c.Next() + } else { + c.Next() + return + } +} diff --git a/backend/api/routes.go b/backend/api/routes.go new file mode 100644 index 0000000..4dd8660 --- /dev/null +++ b/backend/api/routes.go @@ -0,0 +1,40 @@ +package api + +import ( + "github.com/gin-gonic/gin" + "github.com/pektezol/leastportalshub/backend/handlers" + swaggerfiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" +) + +func InitRoutes(router *gin.Engine) { + api := router.Group("/api") + { + v1 := api.Group("/v1") + v1.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) + v1.GET("/", func(c *gin.Context) { + c.File("docs/index.html") + }) + v1.GET("/token", handlers.GetCookie) + v1.DELETE("/token", handlers.DeleteCookie) + v1.GET("/home", CheckAuth, handlers.Home) + v1.GET("/login", handlers.Login) + v1.GET("/profile", CheckAuth, handlers.Profile) + v1.PUT("/profile", CheckAuth, handlers.UpdateCountryCode) + v1.POST("/profile", CheckAuth, handlers.UpdateUser) + v1.GET("/users/:id", CheckAuth, handlers.FetchUser) + v1.GET("/demos", handlers.DownloadDemoWithID) + v1.GET("/maps/:id/summary", handlers.FetchMapSummary) + v1.POST("/maps/:id/summary", CheckAuth, handlers.CreateMapSummary) + v1.PUT("/maps/:id/summary", CheckAuth, handlers.EditMapSummary) + v1.DELETE("/maps/:id/summary", CheckAuth, handlers.DeleteMapSummary) + v1.PUT("/maps/:id/image", CheckAuth, handlers.EditMapImage) + v1.GET("/maps/:id/leaderboards", handlers.FetchMapLeaderboards) + v1.POST("/maps/:id/record", CheckAuth, handlers.CreateRecordWithDemo) + v1.GET("/rankings", handlers.Rankings) + v1.GET("/search", handlers.SearchWithQuery) + v1.GET("/games", handlers.FetchGames) + v1.GET("/games/:id", handlers.FetchChapters) + v1.GET("/chapters/:id", handlers.FetchChapterMaps) + } +} diff --git a/backend/controllers/homeController.go b/backend/controllers/homeController.go deleted file mode 100644 index d1b99cb..0000000 --- a/backend/controllers/homeController.go +++ /dev/null @@ -1,294 +0,0 @@ -package controllers - -import ( - "log" - "net/http" - "strings" - - "github.com/gin-gonic/gin" - "github.com/pektezol/leastportalshub/backend/database" - "github.com/pektezol/leastportalshub/backend/models" -) - -type SearchResponse struct { - Players []models.UserShort `json:"players"` - Maps []models.MapShort `json:"maps"` -} - -type RankingsResponse struct { - RankingsSP []models.UserRanking `json:"rankings_sp"` - RankingsMP []models.UserRanking `json:"rankings_mp"` -} - -func Home(c *gin.Context) { - user, exists := c.Get("user") - if !exists { - c.JSON(200, "no id, not auth") - } else { - c.JSON(200, gin.H{ - "output": user, - }) - } -} - -// GET Rankings -// -// @Description Get rankings of every player. -// @Tags rankings -// @Produce json -// @Success 200 {object} models.Response{data=RankingsResponse} -// @Failure 400 {object} models.Response -// @Router /rankings [get] -func Rankings(c *gin.Context) { - rows, err := database.DB.Query(`SELECT steam_id, user_name FROM users`) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - var spRankings []models.UserRanking - var mpRankings []models.UserRanking - for rows.Next() { - var userID, username string - err := rows.Scan(&userID, &username) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - // Getting all sp records for each user - var uniqueSingleUserRecords, totalSingleMaps int - sql := `SELECT COUNT(DISTINCT map_id), (SELECT COUNT(map_name) FROM maps - WHERE is_coop = FALSE AND is_disabled = false) FROM records_sp WHERE user_id = $1` - err = database.DB.QueryRow(sql, userID).Scan(&uniqueSingleUserRecords, &totalSingleMaps) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - // Has all singleplayer records - if uniqueSingleUserRecords == totalSingleMaps { - var ranking models.UserRanking - ranking.UserID = userID - ranking.UserName = username - sql := `SELECT DISTINCT map_id, score_count FROM records_sp WHERE user_id = $1 ORDER BY map_id, score_count` - rows, err := database.DB.Query(sql, userID) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - totalScore := 0 - var maps []int - for rows.Next() { - var mapID, scoreCount int - rows.Scan(&mapID, &scoreCount) - if len(maps) != 0 && maps[len(maps)-1] == mapID { - continue - } - totalScore += scoreCount - maps = append(maps, mapID) - } - ranking.TotalScore = totalScore - spRankings = append(spRankings, ranking) - } - // Getting all mp records for each user - var uniqueMultiUserRecords, totalMultiMaps int - sql = `SELECT COUNT(DISTINCT map_id), (SELECT COUNT(map_name) FROM maps - WHERE is_coop = TRUE AND is_disabled = false) FROM records_mp WHERE host_id = $1 OR partner_id = $2` - err = database.DB.QueryRow(sql, userID, userID).Scan(&uniqueMultiUserRecords, &totalMultiMaps) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - // Has all singleplayer records - if uniqueMultiUserRecords == totalMultiMaps { - var ranking models.UserRanking - ranking.UserID = userID - ranking.UserName = username - sql := `SELECT DISTINCT map_id, score_count FROM records_mp WHERE host_id = $1 OR partner_id = $2 ORDER BY map_id, score_count` - rows, err := database.DB.Query(sql, userID, userID) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - totalScore := 0 - var maps []int - for rows.Next() { - var mapID, scoreCount int - rows.Scan(&mapID, &scoreCount) - if len(maps) != 0 && maps[len(maps)-1] == mapID { - continue - } - totalScore += scoreCount - maps = append(maps, mapID) - } - ranking.TotalScore = totalScore - mpRankings = append(mpRankings, ranking) - } - } - c.JSON(http.StatusOK, models.Response{ - Success: true, - Message: "Successfully retrieved rankings.", - Data: RankingsResponse{ - RankingsSP: spRankings, - RankingsMP: mpRankings, - }, - }) -} - -// GET Search With Query -// -// @Description Get all user and map data matching to the query. -// @Tags search -// @Produce json -// @Param q query string false "Search user or map name." -// @Success 200 {object} models.Response{data=SearchResponse} -// @Failure 400 {object} models.Response -// @Router /search [get] -func SearchWithQuery(c *gin.Context) { - query := c.Query("q") - query = strings.ToLower(query) - log.Println(query) - var response SearchResponse - // Cache all maps for faster response - var maps = []models.MapShort{ - {ID: 1, Name: "Container Ride"}, - {ID: 2, Name: "Portal Carousel"}, - {ID: 3, Name: "Portal Gun"}, - {ID: 4, Name: "Smooth Jazz"}, - {ID: 5, Name: "Cube Momentum"}, - {ID: 6, Name: "Future Starter"}, - {ID: 7, Name: "Secret Panel"}, - {ID: 8, Name: "Wakeup"}, - {ID: 9, Name: "Incinerator"}, - {ID: 10, Name: "Laser Intro"}, - {ID: 11, Name: "Laser Stairs"}, - {ID: 12, Name: "Dual Lasers"}, - {ID: 13, Name: "Laser Over Goo"}, - {ID: 14, Name: "Catapult Intro"}, - {ID: 15, Name: "Trust Fling"}, - {ID: 16, Name: "Pit Flings"}, - {ID: 17, Name: "Fizzler Intro"}, - {ID: 18, Name: "Ceiling Catapult"}, - {ID: 19, Name: "Ricochet"}, - {ID: 20, Name: "Bridge Intro"}, - {ID: 21, Name: "Bridge The Gap"}, - {ID: 22, Name: "Turret Intro"}, - {ID: 23, Name: "Laser Relays"}, - {ID: 24, Name: "Turret Blocker"}, - {ID: 25, Name: "Laser vs Turret"}, - {ID: 26, Name: "Pull The Rug"}, - {ID: 27, Name: "Column Blocker"}, - {ID: 28, Name: "Laser Chaining"}, - {ID: 29, Name: "Triple Laser"}, - {ID: 30, Name: "Jail Break"}, - {ID: 31, Name: "Escape"}, - {ID: 32, Name: "Turret Factory"}, - {ID: 33, Name: "Turret Sabotage"}, - {ID: 34, Name: "Neurotoxin Sabotage"}, - {ID: 35, Name: "Core"}, - {ID: 36, Name: "Underground"}, - {ID: 37, Name: "Cave Johnson"}, - {ID: 38, Name: "Repulsion Intro"}, - {ID: 39, Name: "Bomb Flings"}, - {ID: 40, Name: "Crazy Box"}, - {ID: 41, Name: "PotatOS"}, - {ID: 42, Name: "Propulsion Intro"}, - {ID: 43, Name: "Propulsion Flings"}, - {ID: 44, Name: "Conversion Intro"}, - {ID: 45, Name: "Three Gels"}, - {ID: 46, Name: "Test"}, - {ID: 47, Name: "Funnel Intro"}, - {ID: 48, Name: "Ceiling Button"}, - {ID: 49, Name: "Wall Button"}, - {ID: 50, Name: "Polarity"}, - {ID: 51, Name: "Funnel Catch"}, - {ID: 52, Name: "Stop The Box"}, - {ID: 53, Name: "Laser Catapult"}, - {ID: 54, Name: "Laser Platform"}, - {ID: 55, Name: "Propulsion Catch"}, - {ID: 56, Name: "Repulsion Polarity"}, - {ID: 57, Name: "Finale 1"}, - {ID: 58, Name: "Finale 2"}, - {ID: 59, Name: "Finale 3"}, - {ID: 60, Name: "Finale 4"}, - {ID: 61, Name: "Calibration"}, - {ID: 62, Name: "Hub"}, - {ID: 63, Name: "Doors"}, - {ID: 64, Name: "Buttons"}, - {ID: 65, Name: "Lasers"}, - {ID: 66, Name: "Rat Maze"}, - {ID: 67, Name: "Laser Crusher"}, - {ID: 68, Name: "Behind The Scenes"}, - {ID: 69, Name: "Flings"}, - {ID: 70, Name: "Infinifling"}, - {ID: 71, Name: "Team Retrieval"}, - {ID: 72, Name: "Vertical Flings"}, - {ID: 73, Name: "Catapults"}, - {ID: 74, Name: "Multifling"}, - {ID: 75, Name: "Fling Crushers"}, - {ID: 76, Name: "Industrial Fan"}, - {ID: 77, Name: "Cooperative Bridges"}, - {ID: 78, Name: "Bridge Swap"}, - {ID: 79, Name: "Fling Block"}, - {ID: 80, Name: "Catapult Block"}, - {ID: 81, Name: "Bridge Fling"}, - {ID: 82, Name: "Turret Walls"}, - {ID: 83, Name: "Turret Assasin"}, - {ID: 84, Name: "Bridge Testing"}, - {ID: 85, Name: "Cooperative Funnels"}, - {ID: 86, Name: "Funnel Drill"}, - {ID: 87, Name: "Funnel Catch"}, - {ID: 88, Name: "Funnel Laser"}, - {ID: 89, Name: "Cooperative Polarity"}, - {ID: 90, Name: "Funnel Hop"}, - {ID: 91, Name: "Advanced Polarity"}, - {ID: 92, Name: "Funnel Maze"}, - {ID: 93, Name: "Turret Warehouse"}, - {ID: 94, Name: "Repulsion Jumps"}, - {ID: 95, Name: "Double Bounce"}, - {ID: 96, Name: "Bridge Repulsion"}, - {ID: 97, Name: "Wall Repulsion"}, - {ID: 98, Name: "Propulsion Crushers"}, - {ID: 99, Name: "Turret Ninja"}, - {ID: 100, Name: "Propulsion Retrieval"}, - {ID: 101, Name: "Vault Entrance"}, - {ID: 102, Name: "Seperation"}, - {ID: 103, Name: "Triple Axis"}, - {ID: 104, Name: "Catapult Catch"}, - {ID: 105, Name: "Bridge Gels"}, - {ID: 106, Name: "Maintenance"}, - {ID: 107, Name: "Bridge Catch"}, - {ID: 108, Name: "Double Lift"}, - {ID: 109, Name: "Gel Maze"}, - {ID: 110, Name: "Crazier Box"}, - } - var filteredMaps []models.MapShort - for _, m := range maps { - if strings.Contains(strings.ToLower(m.Name), strings.ToLower(query)) { - filteredMaps = append(filteredMaps, m) - } - } - response.Maps = filteredMaps - if len(response.Maps) == 0 { - response.Maps = []models.MapShort{} - } - rows, err := database.DB.Query("SELECT steam_id, user_name FROM users WHERE lower(user_name) LIKE $1", "%"+query+"%") - if err != nil { - log.Fatal(err) - } - defer rows.Close() - for rows.Next() { - var user models.UserShort - if err := rows.Scan(&user.SteamID, &user.UserName); err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - response.Players = append(response.Players, user) - } - if len(response.Players) == 0 { - response.Players = []models.UserShort{} - } - c.JSON(http.StatusOK, models.Response{ - Success: true, - Message: "Search successfully retrieved.", - Data: response, - }) -} diff --git a/backend/controllers/loginController.go b/backend/controllers/loginController.go deleted file mode 100644 index 9d772a5..0000000 --- a/backend/controllers/loginController.go +++ /dev/null @@ -1,170 +0,0 @@ -package controllers - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "os" - "time" - - "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v4" - "github.com/pektezol/leastportalshub/backend/database" - "github.com/pektezol/leastportalshub/backend/models" - "github.com/solovev/steam_go" -) - -type LoginResponse struct { - Token string `json:"token"` -} - -// Login -// -// @Description Get (redirect) login page for Steam auth. -// @Tags login -// @Accept json -// @Produce json -// @Success 200 {object} models.Response{data=LoginResponse} -// @Failure 400 {object} models.Response -// @Router /login [get] -func Login(c *gin.Context) { - openID := steam_go.NewOpenId(c.Request) - switch openID.Mode() { - case "": - c.Redirect(http.StatusMovedPermanently, openID.AuthUrl()) - case "cancel": - c.Redirect(http.StatusMovedPermanently, "/") - default: - steamID, err := openID.ValidateAndGetId() - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - // Create user if new - var checkSteamID int64 - err = database.DB.QueryRow("SELECT steam_id FROM users WHERE steam_id = $1", steamID).Scan(&checkSteamID) - // if err != nil { - // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - // return - // } - // User does not exist - if checkSteamID == 0 { - user, err := GetPlayerSummaries(steamID, os.Getenv("API_KEY")) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - // Empty country code check - if user.LocCountryCode == "" { - user.LocCountryCode = "XX" - } - // Insert new user to database - database.DB.Exec(`INSERT INTO users (steam_id, user_name, avatar_link, country_code) - VALUES ($1, $2, $3, $4)`, steamID, user.PersonaName, user.AvatarFull, user.LocCountryCode) - } - moderator := false - rows, _ := database.DB.Query("SELECT title_name FROM titles t INNER JOIN user_titles ut ON t.id=ut.title_id WHERE ut.user_id = $1", steamID) - for rows.Next() { - var title string - rows.Scan(&title) - if title == "Moderator" { - moderator = true - } - } - // Generate JWT token - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "sub": steamID, - "exp": time.Now().Add(time.Hour * 24 * 30).Unix(), - "mod": moderator, - }) - // Sign and get the complete encoded token as a string using the secret - tokenString, err := token.SignedString([]byte(os.Getenv("SECRET_KEY"))) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse("Failed to generate token.")) - return - } - c.SetCookie("token", tokenString, 3600*24*30, "/", "", true, true) - c.Redirect(http.StatusTemporaryRedirect, "/") - // c.JSON(http.StatusOK, models.Response{ - // Success: true, - // Message: "Successfully generated token.", - // Data: LoginResponse{ - // Token: tokenString, - // }, - // }) - return - } -} - -// GET Token -// -// @Description Gets the token cookie value from the user. -// @Tags auth -// @Produce json -// -// @Success 200 {object} models.Response{data=LoginResponse} -// @Failure 404 {object} models.Response -// @Router /token [get] -func GetCookie(c *gin.Context) { - cookie, err := c.Cookie("token") - if err != nil { - c.JSON(http.StatusNotFound, models.ErrorResponse("No token cookie found.")) - return - } - c.JSON(http.StatusOK, models.Response{ - Success: true, - Message: "Token cookie successfully retrieved.", - Data: LoginResponse{ - Token: cookie, - }, - }) -} - -// DELETE Token -// -// @Description Deletes the token cookie from the user. -// @Tags auth -// @Produce json -// -// @Success 200 {object} models.Response{data=LoginResponse} -// @Failure 404 {object} models.Response -// @Router /token [delete] -func DeleteCookie(c *gin.Context) { - cookie, err := c.Cookie("token") - if err != nil { - c.JSON(http.StatusNotFound, models.ErrorResponse("No token cookie found.")) - return - } - c.SetCookie("token", "", -1, "/", "", true, true) - c.JSON(http.StatusOK, models.Response{ - Success: true, - Message: "Token cookie successfully deleted.", - Data: LoginResponse{ - Token: cookie, - }, - }) -} - -func GetPlayerSummaries(steamId, apiKey string) (*models.PlayerSummaries, error) { - url := fmt.Sprintf("http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/?key=%s&steamids=%s", apiKey, steamId) - resp, err := http.Get(url) - if err != nil { - return nil, err - } - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - type Result struct { - Response struct { - Players []models.PlayerSummaries `json:"players"` - } `json:"response"` - } - var data Result - if err := json.Unmarshal(body, &data); err != nil { - return nil, err - } - return &data.Response.Players[0], err -} diff --git a/backend/controllers/mapController.go b/backend/controllers/mapController.go deleted file mode 100644 index 0a324d6..0000000 --- a/backend/controllers/mapController.go +++ /dev/null @@ -1,314 +0,0 @@ -package controllers - -import ( - "net/http" - "strconv" - - "github.com/gin-gonic/gin" - "github.com/pektezol/leastportalshub/backend/database" - "github.com/pektezol/leastportalshub/backend/models" -) - -type MapSummaryResponse struct { - Map models.Map `json:"map"` - Summary models.MapSummary `json:"summary"` -} - -type ChaptersResponse struct { - Game models.Game `json:"game"` - Chapters []models.Chapter `json:"chapters"` -} - -type ChapterMapsResponse struct { - Chapter models.Chapter `json:"chapter"` - Maps []models.MapShort `json:"maps"` -} - -// GET Map Summary -// -// @Description Get map summary with specified id. -// @Tags maps -// @Produce json -// @Param id path int true "Map ID" -// @Success 200 {object} models.Response{data=MapSummaryResponse} -// @Failure 400 {object} models.Response -// @Router /maps/{id}/summary [get] -func FetchMapSummary(c *gin.Context) { - id := c.Param("id") - response := MapSummaryResponse{Map: models.Map{}, Summary: models.MapSummary{Routes: []models.MapRoute{}}} - intID, err := strconv.Atoi(id) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - // Get map data - response.Map.ID = intID - sql := `SELECT m.id, g.name, c.name, m.name, m.image, g.is_coop - FROM maps m - INNER JOIN games g ON m.game_id = g.id - INNER JOIN chapters c ON m.chapter_id = c.id - WHERE m.id = $1` - 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) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - // Get map routes and histories - 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 - INNER JOIN categories c ON r.category_id = c.id - INNER JOIN map_history h ON r.map_id = h.map_id AND r.category_id = h.category_id - LEFT JOIN map_ratings rt ON r.map_id = rt.map_id AND r.category_id = rt.category_id - 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 - ORDER BY h.record_date ASC;` - rows, err := database.DB.Query(sql, id) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - for rows.Next() { - route := models.MapRoute{Category: models.Category{}, History: models.MapHistory{}} - 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) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - response.Summary.Routes = append(response.Summary.Routes, route) - } - // Return response - c.JSON(http.StatusOK, models.Response{ - Success: true, - Message: "Successfully retrieved map summary.", - Data: response, - }) -} - -// GET Map Leaderboards -// -// @Description Get map leaderboards with specified id. -// @Tags maps -// @Produce json -// @Param id path int true "Map ID" -// @Success 200 {object} models.Response{data=models.Map{data=models.MapRecords}} -// @Failure 400 {object} models.Response -// @Router /maps/{id}/leaderboards [get] -func FetchMapLeaderboards(c *gin.Context) { - // TODO: make new response type - id := c.Param("id") - // Get map data - var mapData models.Map - var mapRecordsData models.MapRecords - var isDisabled bool - intID, err := strconv.Atoi(id) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - mapData.ID = intID - sql := `SELECT g.name, c.name, m.name, is_disabled, m.image - FROM maps m - INNER JOIN games g ON m.game_id = g.id - INNER JOIN chapters c ON m.chapter_id = c.id - WHERE m.id = $1` - err = database.DB.QueryRow(sql, id).Scan(&mapData.GameName, &mapData.ChapterName, &mapData.MapName, &isDisabled, &mapData.Image) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - if isDisabled { - c.JSON(http.StatusBadRequest, models.ErrorResponse("Map is not available for competitive boards.")) - return - } - // TODO: avatar and names for host & partner - // Get records from the map - if mapData.GameName == "Portal 2 - Cooperative" { - var records []models.RecordMP - sql = `SELECT id, host_id, partner_id, score_count, score_time, host_demo_id, partner_demo_id, record_date - FROM ( - SELECT id, host_id, partner_id, score_count, score_time, host_demo_id, partner_demo_id, record_date, - ROW_NUMBER() OVER (PARTITION BY host_id, partner_id ORDER BY score_count, score_time) AS rn - FROM records_mp - WHERE map_id = $1 - ) sub - WHERE rn = 1` - rows, err := database.DB.Query(sql, id) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - placement := 1 - ties := 0 - for rows.Next() { - var record models.RecordMP - err := rows.Scan(&record.RecordID, &record.HostID, &record.PartnerID, &record.ScoreCount, &record.ScoreTime, &record.HostDemoID, &record.PartnerDemoID, &record.RecordDate) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - if len(records) != 0 && records[len(records)-1].ScoreTime == record.ScoreTime { - ties++ - record.Placement = placement - ties - } else { - record.Placement = placement - } - records = append(records, record) - placement++ - } - mapRecordsData.Records = records - } else { - var records []models.RecordSP - sql = `SELECT id, user_id, users.user_name, users.avatar_link, score_count, score_time, demo_id, record_date - FROM ( - SELECT id, user_id, score_count, score_time, demo_id, record_date, - ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY score_count, score_time) AS rn - FROM records_sp - WHERE map_id = $1 - ) sub - INNER JOIN users ON user_id = users.steam_id - WHERE rn = 1` - rows, err := database.DB.Query(sql, id) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - placement := 1 - ties := 0 - for rows.Next() { - var record models.RecordSP - err := rows.Scan(&record.RecordID, &record.UserID, &record.UserName, &record.UserAvatar, &record.ScoreCount, &record.ScoreTime, &record.DemoID, &record.RecordDate) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - if len(records) != 0 && records[len(records)-1].ScoreTime == record.ScoreTime { - ties++ - record.Placement = placement - ties - } else { - record.Placement = placement - } - records = append(records, record) - placement++ - } - mapRecordsData.Records = records - } - // mapData.Data = mapRecordsData - // Return response - c.JSON(http.StatusOK, models.Response{ - Success: true, - Message: "Successfully retrieved map leaderboards.", - Data: mapData, - }) -} - -// GET Games -// -// @Description Get games from the leaderboards. -// @Tags games & chapters -// @Produce json -// @Success 200 {object} models.Response{data=[]models.Game} -// @Failure 400 {object} models.Response -// @Router /games [get] -func FetchGames(c *gin.Context) { - rows, err := database.DB.Query(`SELECT id, name, is_coop FROM games`) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - var games []models.Game - for rows.Next() { - var game models.Game - if err := rows.Scan(&game.ID, &game.Name, &game.IsCoop); err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - games = append(games, game) - } - c.JSON(http.StatusOK, models.Response{ - Success: true, - Message: "Successfully retrieved games.", - Data: games, - }) -} - -// GET Chapters of a Game -// -// @Description Get chapters from the specified game id. -// @Tags games & chapters -// @Produce json -// @Param id path int true "Game ID" -// @Success 200 {object} models.Response{data=ChaptersResponse} -// @Failure 400 {object} models.Response -// @Router /games/{id} [get] -func FetchChapters(c *gin.Context) { - gameID := c.Param("id") - intID, err := strconv.Atoi(gameID) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - var response ChaptersResponse - rows, err := database.DB.Query(`SELECT c.id, c.name, g.name FROM chapters c INNER JOIN games g ON c.game_id = g.id WHERE game_id = $1`, gameID) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - var chapters []models.Chapter - var gameName string - for rows.Next() { - var chapter models.Chapter - if err := rows.Scan(&chapter.ID, &chapter.Name, &gameName); err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - chapters = append(chapters, chapter) - } - response.Game.ID = intID - response.Game.Name = gameName - response.Chapters = chapters - c.JSON(http.StatusOK, models.Response{ - Success: true, - Message: "Successfully retrieved chapters.", - Data: response, - }) -} - -// GET Maps of a Chapter -// -// @Description Get maps from the specified chapter id. -// @Tags games & chapters -// @Produce json -// @Param id path int true "Chapter ID" -// @Success 200 {object} models.Response{data=ChapterMapsResponse} -// @Failure 400 {object} models.Response -// @Router /chapters/{id} [get] -func FetchChapterMaps(c *gin.Context) { - chapterID := c.Param("id") - intID, err := strconv.Atoi(chapterID) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - var response ChapterMapsResponse - rows, err := database.DB.Query(`SELECT m.id, m.name, c.name FROM maps m INNER JOIN chapters c ON m.chapter_id = c.id WHERE chapter_id = $1`, chapterID) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - var maps []models.MapShort - var chapterName string - for rows.Next() { - var mapShort models.MapShort - if err := rows.Scan(&mapShort.ID, &mapShort.Name, &chapterName); err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - maps = append(maps, mapShort) - } - response.Chapter.ID = intID - response.Chapter.Name = chapterName - response.Maps = maps - c.JSON(http.StatusOK, models.Response{ - Success: true, - Message: "Successfully retrieved maps.", - Data: response, - }) -} diff --git a/backend/controllers/modController.go b/backend/controllers/modController.go deleted file mode 100644 index 7acdb5d..0000000 --- a/backend/controllers/modController.go +++ /dev/null @@ -1,334 +0,0 @@ -package controllers - -import ( - "net/http" - "strconv" - "time" - - "github.com/gin-gonic/gin" - "github.com/pektezol/leastportalshub/backend/database" - "github.com/pektezol/leastportalshub/backend/models" -) - -type CreateMapSummaryRequest struct { - CategoryID int `json:"category_id" binding:"required"` - Description string `json:"description" binding:"required"` - Showcase string `json:"showcase"` - UserName string `json:"user_name" binding:"required"` - ScoreCount *int `json:"score_count" binding:"required"` - RecordDate time.Time `json:"record_date" binding:"required"` -} - -type EditMapSummaryRequest struct { - RouteID int `json:"route_id" binding:"required"` - Description string `json:"description" binding:"required"` - Showcase string `json:"showcase"` - UserName string `json:"user_name" binding:"required"` - ScoreCount *int `json:"score_count" binding:"required"` - RecordDate time.Time `json:"record_date" binding:"required"` -} - -type DeleteMapSummaryRequest struct { - RouteID int `json:"route_id" binding:"required"` -} - -type EditMapImageRequest struct { - Image string `json:"image" binding:"required"` -} - -// POST Map Summary -// -// @Description Create map summary with specified map id. -// @Tags maps -// @Produce json -// @Param Authorization header string true "JWT Token" -// @Param id path int true "Map ID" -// @Param request body CreateMapSummaryRequest true "Body" -// @Success 200 {object} models.Response{data=CreateMapSummaryRequest} -// @Failure 400 {object} models.Response -// @Router /maps/{id}/summary [post] -func CreateMapSummary(c *gin.Context) { - // Check if user exists - _, exists := c.Get("user") - if !exists { - c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) - return - } - mod, exists := c.Get("mod") - if !exists || !mod.(bool) { - c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) - return - } - // Bind parameter and body - id := c.Param("id") - mapID, err := strconv.Atoi(id) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - var request CreateMapSummaryRequest - if err := c.BindJSON(&request); err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - // Start database transaction - tx, err := database.DB.Begin() - if err != nil { - c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) - return - } - defer tx.Rollback() - // Fetch route category and score count - var checkMapID int - sql := `SELECT m.id FROM maps m WHERE m.id = $1` - err = database.DB.QueryRow(sql, mapID).Scan(&checkMapID) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - if mapID != checkMapID { - c.JSON(http.StatusBadRequest, models.ErrorResponse("Map ID does not exist.")) - return - } - // Update database with new data - sql = `INSERT INTO map_routes (map_id,category_id,score_count,description,showcase) - VALUES ($1,$2,$3,$4,$5)` - _, err = tx.Exec(sql, mapID, request.CategoryID, request.ScoreCount, request.Description, request.Showcase) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - sql = `INSERT INTO map_history (map_id,category_id,user_name,score_count,record_date) - VALUES ($1,$2,$3,$4,$5)` - _, err = tx.Exec(sql, mapID, request.CategoryID, request.UserName, request.ScoreCount, request.RecordDate) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - if err = tx.Commit(); err != nil { - c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) - return - } - // Return response - c.JSON(http.StatusOK, models.Response{ - Success: true, - Message: "Successfully created map summary.", - Data: request, - }) -} - -// PUT Map Summary -// -// @Description Edit map summary with specified map id. -// @Tags maps -// @Produce json -// @Param Authorization header string true "JWT Token" -// @Param id path int true "Map ID" -// @Param request body EditMapSummaryRequest true "Body" -// @Success 200 {object} models.Response{data=EditMapSummaryRequest} -// @Failure 400 {object} models.Response -// @Router /maps/{id}/summary [put] -func EditMapSummary(c *gin.Context) { - // Check if user exists - _, exists := c.Get("user") - if !exists { - c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) - return - } - mod, exists := c.Get("mod") - if !exists || !mod.(bool) { - c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) - return - } - // Bind parameter and body - id := c.Param("id") - mapID, err := strconv.Atoi(id) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - var request EditMapSummaryRequest - if err := c.BindJSON(&request); err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - // Start database transaction - tx, err := database.DB.Begin() - if err != nil { - c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) - return - } - defer tx.Rollback() - // Fetch route category and score count - var categoryID, scoreCount, historyID int - 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` - err = database.DB.QueryRow(sql, mapID, request.RouteID).Scan(&categoryID, &scoreCount) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - sql = `SELECT mh.id FROM map_history mh WHERE mh.score_count = $1 AND mh.category_id = $2 AND mh.map_id = $3` - err = database.DB.QueryRow(sql, scoreCount, categoryID, mapID).Scan(&historyID) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - // Update database with new data - sql = `UPDATE map_routes SET score_count = $2, description = $3, showcase = $4 WHERE id = $1` - _, err = tx.Exec(sql, request.RouteID, request.ScoreCount, request.Description, request.Showcase) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - sql = `UPDATE map_history SET user_name = $2, score_count = $3, record_date = $4 WHERE id = $1` - _, err = tx.Exec(sql, historyID, request.UserName, request.ScoreCount, request.RecordDate) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - if err = tx.Commit(); err != nil { - c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) - return - } - // Return response - c.JSON(http.StatusOK, models.Response{ - Success: true, - Message: "Successfully updated map summary.", - Data: request, - }) -} - -// DELETE Map Summary -// -// @Description Delete map summary with specified map id. -// @Tags maps -// @Produce json -// @Param Authorization header string true "JWT Token" -// @Param id path int true "Map ID" -// @Param request body DeleteMapSummaryRequest true "Body" -// @Success 200 {object} models.Response{data=DeleteMapSummaryRequest} -// @Failure 400 {object} models.Response -// @Router /maps/{id}/summary [delete] -func DeleteMapSummary(c *gin.Context) { - // Check if user exists - _, exists := c.Get("user") - if !exists { - c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) - return - } - mod, exists := c.Get("mod") - if !exists || !mod.(bool) { - c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) - return - } - // Bind parameter and body - id := c.Param("id") - mapID, err := strconv.Atoi(id) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - var request DeleteMapSummaryRequest - if err := c.BindJSON(&request); err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - // Start database transaction - tx, err := database.DB.Begin() - if err != nil { - c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) - return - } - defer tx.Rollback() - // Fetch route category and score count - var checkMapID, scoreCount, mapHistoryID int - 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` - err = database.DB.QueryRow(sql, mapID, request.RouteID).Scan(&checkMapID, &scoreCount) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - if mapID != checkMapID { - c.JSON(http.StatusBadRequest, models.ErrorResponse("Map ID does not exist.")) - return - } - 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` - err = database.DB.QueryRow(sql, mapID, request.RouteID, scoreCount).Scan(&mapHistoryID) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - // Update database with new data - sql = `DELETE FROM map_routes mr WHERE mr.id = $1 ` - _, err = tx.Exec(sql, request.RouteID) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - sql = `DELETE FROM map_history mh WHERE mh.id = $1` - _, err = tx.Exec(sql, mapHistoryID) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - if err = tx.Commit(); err != nil { - c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) - return - } - // Return response - c.JSON(http.StatusOK, models.Response{ - Success: true, - Message: "Successfully delete map summary.", - Data: request, - }) -} - -// PUT Map Image -// -// @Description Edit map image with specified map id. -// @Tags maps -// @Produce json -// @Param Authorization header string true "JWT Token" -// @Param id path int true "Map ID" -// @Param request body EditMapImageRequest true "Body" -// @Success 200 {object} models.Response{data=EditMapImageRequest} -// @Failure 400 {object} models.Response -// @Router /maps/{id}/image [put] -func EditMapImage(c *gin.Context) { - // Check if user exists - _, exists := c.Get("user") - if !exists { - c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) - return - } - mod, exists := c.Get("mod") - if !exists || !mod.(bool) { - c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) - return - } - // Bind parameter and body - id := c.Param("id") - mapID, err := strconv.Atoi(id) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - var request EditMapImageRequest - if err := c.BindJSON(&request); err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - // Update database with new data - sql := `UPDATE maps SET image = $2 WHERE id = $1` - _, err = database.DB.Exec(sql, mapID, request.Image) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - // Return response - c.JSON(http.StatusOK, models.Response{ - Success: true, - Message: "Successfully updated map image.", - Data: request, - }) -} diff --git a/backend/controllers/recordController.go b/backend/controllers/recordController.go deleted file mode 100644 index d141fc3..0000000 --- a/backend/controllers/recordController.go +++ /dev/null @@ -1,288 +0,0 @@ -package controllers - -import ( - "context" - "encoding/base64" - "io" - "log" - "mime/multipart" - "net/http" - "os" - - "github.com/gin-gonic/gin" - "github.com/google/uuid" - "github.com/pektezol/leastportalshub/backend/database" - "github.com/pektezol/leastportalshub/backend/models" - "github.com/pektezol/leastportalshub/backend/parser" - "golang.org/x/oauth2/google" - "golang.org/x/oauth2/jwt" - "google.golang.org/api/drive/v3" -) - -type RecordRequest struct { - HostDemo *multipart.FileHeader `json:"host_demo" form:"host_demo" binding:"required" swaggerignore:"true"` - PartnerDemo *multipart.FileHeader `json:"partner_demo" form:"partner_demo" swaggerignore:"true"` - IsPartnerOrange bool `json:"is_partner_orange" form:"is_partner_orange"` - PartnerID string `json:"partner_id" form:"partner_id"` -} - -type RecordResponse struct { - ScoreCount int `json:"score_count"` - ScoreTime int `json:"score_time"` -} - -// POST Record -// -// @Description Post record with demo of a specific map. -// @Tags maps -// @Accept mpfd -// @Produce json -// @Param id path int true "Map ID" -// @Param Authorization header string true "JWT Token" -// @Param host_demo formData file true "Host Demo" -// @Param partner_demo formData file false "Partner Demo" -// @Param is_partner_orange formData boolean false "Is Partner Orange" -// @Param partner_id formData string false "Partner ID" -// @Success 200 {object} models.Response{data=RecordResponse} -// @Failure 400 {object} models.Response -// @Failure 401 {object} models.Response -// @Router /maps/{id}/record [post] -func CreateRecordWithDemo(c *gin.Context) { - mapId := c.Param("id") - // Check if user exists - user, exists := c.Get("user") - if !exists { - c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) - return - } - // Check if map is sp or mp - var gameName string - var isCoop bool - var isDisabled bool - sql := `SELECT g.name, m.is_disabled FROM maps m INNER JOIN games g ON m.game_id=g.id WHERE m.id = $1` - err := database.DB.QueryRow(sql, mapId).Scan(&gameName, &isDisabled) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - if isDisabled { - c.JSON(http.StatusBadRequest, models.ErrorResponse("Map is not available for competitive boards.")) - return - } - if gameName == "Portal 2 - Cooperative" { - isCoop = true - } - // Get record request - var record RecordRequest - if err := c.ShouldBind(&record); err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - if isCoop && (record.PartnerDemo == nil || record.PartnerID == "") { - c.JSON(http.StatusBadRequest, models.ErrorResponse("Invalid entry for coop record submission.")) - return - } - // Demo files - demoFiles := []*multipart.FileHeader{record.HostDemo} - if isCoop { - demoFiles = append(demoFiles, record.PartnerDemo) - } - var hostDemoUUID, hostDemoFileID, partnerDemoUUID, partnerDemoFileID string - var hostDemoScoreCount, hostDemoScoreTime int - client := serviceAccount() - srv, err := drive.New(client) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - // Create database transaction for inserts - tx, err := database.DB.Begin() - if err != nil { - c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) - return - } - // Defer to a rollback in case anything fails - defer tx.Rollback() - for i, header := range demoFiles { - uuid := uuid.New().String() - // Upload & insert into demos - err = c.SaveUploadedFile(header, "backend/parser/"+uuid+".dem") - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - defer os.Remove("backend/parser/" + uuid + ".dem") - f, err := os.Open("backend/parser/" + uuid + ".dem") - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - defer f.Close() - file, err := createFile(srv, uuid+".dem", "application/octet-stream", f, os.Getenv("GOOGLE_FOLDER_ID")) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - hostDemoScoreCount, hostDemoScoreTime, err = parser.ProcessDemo("backend/parser/" + uuid + ".dem") - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - if i == 0 { - hostDemoFileID = file.Id - hostDemoUUID = uuid - } else if i == 1 { - partnerDemoFileID = file.Id - partnerDemoUUID = uuid - } - _, err = tx.Exec(`INSERT INTO demos (id,location_id) VALUES ($1,$2)`, uuid, file.Id) - if err != nil { - deleteFile(srv, file.Id) - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - } - // Insert into records - if isCoop { - sql := `INSERT INTO records_mp(map_id,score_count,score_time,host_id,partner_id,host_demo_id,partner_demo_id) - VALUES($1, $2, $3, $4, $5, $6, $7)` - var hostID string - var partnerID string - if record.IsPartnerOrange { - hostID = user.(models.User).SteamID - partnerID = record.PartnerID - } else { - partnerID = user.(models.User).SteamID - hostID = record.PartnerID - } - _, err := tx.Exec(sql, mapId, hostDemoScoreCount, hostDemoScoreTime, hostID, partnerID, hostDemoUUID, partnerDemoUUID) - if err != nil { - deleteFile(srv, hostDemoFileID) - deleteFile(srv, partnerDemoFileID) - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - // If a new world record based on portal count - // if record.ScoreCount < wrScore { - // _, err := tx.Exec(`UPDATE maps SET wr_score = $1, wr_time = $2 WHERE id = $3`, record.ScoreCount, record.ScoreTime, mapId) - // if err != nil { - // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - // return - // } - // } - } else { - sql := `INSERT INTO records_sp(map_id,score_count,score_time,user_id,demo_id) - VALUES($1, $2, $3, $4, $5)` - _, err := tx.Exec(sql, mapId, hostDemoScoreCount, hostDemoScoreTime, user.(models.User).SteamID, hostDemoUUID) - if err != nil { - deleteFile(srv, hostDemoFileID) - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - // If a new world record based on portal count - // if record.ScoreCount < wrScore { - // _, err := tx.Exec(`UPDATE maps SET wr_score = $1, wr_time = $2 WHERE id = $3`, record.ScoreCount, record.ScoreTime, mapId) - // if err != nil { - // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - // return - // } - // } - } - if err = tx.Commit(); err != nil { - c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) - return - } - c.JSON(http.StatusOK, models.Response{ - Success: true, - Message: "Successfully created record.", - Data: RecordResponse{ScoreCount: hostDemoScoreCount, ScoreTime: hostDemoScoreTime}, - }) -} - -// GET Demo -// -// @Description Get demo with specified demo uuid. -// @Tags demo -// @Accept json -// @Produce octet-stream -// @Param uuid query string true "Demo UUID" -// @Success 200 {file} binary "Demo File" -// @Failure 400 {object} models.Response -// @Router /demos [get] -func DownloadDemoWithID(c *gin.Context) { - uuid := c.Query("uuid") - var locationID string - if uuid == "" { - c.JSON(http.StatusBadRequest, models.ErrorResponse("Invalid id given.")) - return - } - err := database.DB.QueryRow(`SELECT location_id FROM demos WHERE id = $1`, uuid).Scan(&locationID) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - if locationID == "" { - c.JSON(http.StatusBadRequest, models.ErrorResponse("Invalid id given.")) - return - } - url := "https://drive.google.com/uc?export=download&id=" + locationID - fileName := uuid + ".dem" - output, err := os.Create(fileName) - defer os.Remove(fileName) - defer output.Close() - response, err := http.Get(url) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - defer response.Body.Close() - _, err = io.Copy(output, response.Body) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - // Downloaded file - c.Header("Content-Description", "File Transfer") - c.Header("Content-Transfer-Encoding", "binary") - c.Header("Content-Disposition", "attachment; filename="+fileName) - c.Header("Content-Type", "application/octet-stream") - c.File(fileName) - // c.FileAttachment() -} - -// Use Service account -func serviceAccount() *http.Client { - privateKey, _ := base64.StdEncoding.DecodeString(os.Getenv("GOOGLE_PRIVATE_KEY_BASE64")) - config := &jwt.Config{ - Email: os.Getenv("GOOGLE_CLIENT_EMAIL"), - PrivateKey: []byte(privateKey), - Scopes: []string{ - drive.DriveScope, - }, - TokenURL: google.JWTTokenURL, - } - client := config.Client(context.Background()) - return client -} - -// Create Gdrive file -func createFile(service *drive.Service, name string, mimeType string, content io.Reader, parentId string) (*drive.File, error) { - f := &drive.File{ - MimeType: mimeType, - Name: name, - Parents: []string{parentId}, - } - file, err := service.Files.Create(f).Media(content).Do() - - if err != nil { - log.Println("Could not create file: " + err.Error()) - return nil, err - } - - return file, nil -} - -// Delete Gdrive file -func deleteFile(service *drive.Service, fileId string) { - service.Files.Delete(fileId) -} diff --git a/backend/controllers/userController.go b/backend/controllers/userController.go deleted file mode 100644 index 84d589a..0000000 --- a/backend/controllers/userController.go +++ /dev/null @@ -1,383 +0,0 @@ -package controllers - -import ( - "net/http" - "os" - "regexp" - "time" - - "github.com/gin-gonic/gin" - "github.com/pektezol/leastportalshub/backend/database" - "github.com/pektezol/leastportalshub/backend/models" -) - -type ProfileResponse struct { - Profile bool `json:"profile"` - SteamID string `json:"steam_id"` - UserName string `json:"user_name"` - AvatarLink string `json:"avatar_link"` - CountryCode string `json:"country_code"` - Titles []models.Title `json:"titles"` - Links models.Links `json:"links"` - Rankings ProfileRankings `json:"rankings"` - Records ProfileRecords `json:"records"` -} - -type ProfileRankings struct { - Overall ProfileRankingsDetails `json:"overall"` - Singleplayer ProfileRankingsDetails `json:"singleplayer"` - Cooperative ProfileRankingsDetails `json:"cooperative"` -} - -type ProfileRankingsDetails struct { - Rank int `json:"rank"` - CompletionCount int `json:"completion_count"` - CompletionTotal int `json:"completion_total"` -} - -type ProfileRecords struct { - P2Singleplayer []ProfileRecordsDetails `json:"portal2_singleplayer"` - P2Cooperative []ProfileRecordsDetails `json:"portal2_cooperative"` -} - -type ProfileRecordsDetails struct { - MapID int `json:"map_id"` - MapName string `json:"map_name"` - Scores []ProfileScores `json:"scores"` -} - -type ProfileScores struct { - DemoID string `json:"demo_id"` - ScoreCount int `json:"score_count"` - ScoreTime int `json:"score_time"` - Date time.Time `json:"date"` -} - -type ScoreResponse struct { - MapID int `json:"map_id"` - Records any `json:"records"` -} - -// GET Profile -// -// @Description Get profile page of session user. -// @Tags users -// @Accept json -// @Produce json -// @Param Authorization header string true "JWT Token" -// @Success 200 {object} models.Response{data=ProfileResponse} -// @Failure 400 {object} models.Response -// @Failure 401 {object} models.Response -// @Router /profile [get] -func Profile(c *gin.Context) { - // Check if user exists - user, exists := c.Get("user") - if !exists { - c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) - return - } - // Get user links - links := models.Links{} - sql := `SELECT u.p2sr, u.steam, u.youtube, u.twitch FROM users u WHERE u.steam_id = $1` - err := database.DB.QueryRow(sql, user.(models.User).SteamID).Scan(&links.P2SR, &links.Steam, &links.YouTube, &links.Twitch) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - // TODO: Get rankings (all maps done in one game) - records := ProfileRecords{ - P2Singleplayer: []ProfileRecordsDetails{}, - P2Cooperative: []ProfileRecordsDetails{}, - } - // Get singleplayer records - sql = `SELECT m.game_id, sp.map_id, m."name", sp.score_count, sp.score_time, sp.demo_id, sp.record_date - FROM records_sp sp INNER JOIN maps m ON sp.map_id = m.id WHERE sp.user_id = $1 ORDER BY sp.map_id, sp.score_count, sp.score_time;` - rows, err := database.DB.Query(sql, user.(models.User).SteamID) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - for rows.Next() { - var mapID int - var mapName string - var gameID int - score := ProfileScores{} - rows.Scan(&gameID, &mapID, &mapName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) - if gameID != 1 { - continue - } - // More than one record in one map - if len(records.P2Singleplayer) != 0 && mapID == records.P2Singleplayer[len(records.P2Singleplayer)-1].MapID { - records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores = append(records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores, score) - continue - } - // New map - records.P2Singleplayer = append(records.P2Singleplayer, ProfileRecordsDetails{ - MapID: mapID, - MapName: mapName, - Scores: []ProfileScores{}, - }) - records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores = append(records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores, score) - } - // Get multiplayer records - sql = `SELECT m.game_id, mp.map_id, m."name", mp.score_count, mp.score_time, CASE WHEN host_id = $1 THEN mp.host_demo_id WHEN partner_id = $1 THEN mp.partner_demo_id END demo_id, mp.record_date - FROM records_mp mp INNER JOIN maps m ON mp.map_id = m.id WHERE mp.host_id = $1 OR mp.partner_id = $1 ORDER BY mp.map_id, mp.score_count, mp.score_time;` - rows, err = database.DB.Query(sql, user.(models.User).SteamID) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - for rows.Next() { - var mapID int - var mapName string - var gameID int - score := ProfileScores{} - rows.Scan(&gameID, &mapID, &mapName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) - if gameID != 1 { - continue - } - // More than one record in one map - if len(records.P2Cooperative) != 0 && mapID == records.P2Cooperative[len(records.P2Cooperative)-1].MapID { - records.P2Cooperative[len(records.P2Cooperative)-1].Scores = append(records.P2Cooperative[len(records.P2Cooperative)-1].Scores, score) - continue - } - // New map - records.P2Cooperative = append(records.P2Cooperative, ProfileRecordsDetails{ - MapID: mapID, - MapName: mapName, - Scores: []ProfileScores{}, - }) - records.P2Cooperative[len(records.P2Cooperative)-1].Scores = append(records.P2Cooperative[len(records.P2Cooperative)-1].Scores, score) - } - c.JSON(http.StatusOK, models.Response{ - Success: true, - Message: "Successfully retrieved user scores.", - Data: ProfileResponse{ - Profile: true, - SteamID: user.(models.User).SteamID, - UserName: user.(models.User).UserName, - AvatarLink: user.(models.User).AvatarLink, - CountryCode: user.(models.User).CountryCode, - Titles: user.(models.User).Titles, - Links: links, - Rankings: ProfileRankings{}, - Records: records, - }, - }) -} - -// GET User -// -// @Description Get profile page of another user. -// @Tags users -// @Accept json -// @Produce json -// @Param id path int true "User ID" -// @Success 200 {object} models.Response{data=ProfileResponse} -// @Failure 400 {object} models.Response -// @Failure 404 {object} models.Response -// @Router /users/{id} [get] -func FetchUser(c *gin.Context) { - id := c.Param("id") - // Check if id is all numbers and 17 length - match, _ := regexp.MatchString("^[0-9]{17}$", id) - if !match { - c.JSON(http.StatusNotFound, models.ErrorResponse("User not found.")) - return - } - // Check if user exists - var user models.User - links := models.Links{} - sql := `SELECT u.steam_id, u.user_name, u.avatar_link, u.country_code, u.created_at, u.updated_at, u.p2sr, u.steam, u.youtube, u.twitch FROM users u WHERE u.steam_id = $1` - err := database.DB.QueryRow(sql, id).Scan(&user.SteamID, &user.UserName, &user.AvatarLink, &user.CountryCode, &user.CreatedAt, &user.UpdatedAt, &links.P2SR, &links.Steam, &links.YouTube, &links.Twitch) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - if user.SteamID == "" { - // User does not exist - c.JSON(http.StatusNotFound, models.ErrorResponse("User not found.")) - return - } - // Get user titles - sql = `SELECT t.title_name, t.title_color FROM titles t - INNER JOIN user_titles ut ON t.id=ut.title_id WHERE ut.user_id = $1` - rows, err := database.DB.Query(sql, user.SteamID) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - for rows.Next() { - var title models.Title - if err := rows.Scan(&title.Name, &title.Color); err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - user.Titles = append(user.Titles, title) - } - // TODO: Get rankings (all maps done in one game) - records := ProfileRecords{ - P2Singleplayer: []ProfileRecordsDetails{}, - P2Cooperative: []ProfileRecordsDetails{}, - } - // Get singleplayer records - sql = `SELECT m.game_id, sp.map_id, m."name", sp.score_count, sp.score_time, sp.demo_id, sp.record_date - FROM records_sp sp INNER JOIN maps m ON sp.map_id = m.id WHERE sp.user_id = $1 ORDER BY sp.map_id, sp.score_count, sp.score_time;` - rows, err = database.DB.Query(sql, user.SteamID) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - for rows.Next() { - var mapID int - var mapName string - var gameID int - score := ProfileScores{} - rows.Scan(&gameID, &mapID, &mapName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) - if gameID != 1 { - continue - } - // More than one record in one map - if len(records.P2Singleplayer) != 0 && mapID == records.P2Singleplayer[len(records.P2Singleplayer)-1].MapID { - records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores = append(records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores, score) - continue - } - // New map - records.P2Singleplayer = append(records.P2Singleplayer, ProfileRecordsDetails{ - MapID: mapID, - MapName: mapName, - Scores: []ProfileScores{}, - }) - records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores = append(records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores, score) - } - // Get multiplayer records - sql = `SELECT m.game_id, mp.map_id, m."name", mp.score_count, mp.score_time, CASE WHEN host_id = $1 THEN mp.host_demo_id WHEN partner_id = $1 THEN mp.partner_demo_id END demo_id, mp.record_date - FROM records_mp mp INNER JOIN maps m ON mp.map_id = m.id WHERE mp.host_id = $1 OR mp.partner_id = $1 ORDER BY mp.map_id, mp.score_count, mp.score_time;` - rows, err = database.DB.Query(sql, user.SteamID) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - for rows.Next() { - var mapID int - var mapName string - var gameID int - score := ProfileScores{} - rows.Scan(&gameID, &mapID, &mapName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) - if gameID != 1 { - continue - } - // More than one record in one map - if len(records.P2Cooperative) != 0 && mapID == records.P2Cooperative[len(records.P2Cooperative)-1].MapID { - records.P2Cooperative[len(records.P2Cooperative)-1].Scores = append(records.P2Cooperative[len(records.P2Cooperative)-1].Scores, score) - continue - } - // New map - records.P2Cooperative = append(records.P2Cooperative, ProfileRecordsDetails{ - MapID: mapID, - MapName: mapName, - Scores: []ProfileScores{}, - }) - records.P2Cooperative[len(records.P2Cooperative)-1].Scores = append(records.P2Cooperative[len(records.P2Cooperative)-1].Scores, score) - } - c.JSON(http.StatusOK, models.Response{ - Success: true, - Message: "Successfully retrieved user scores.", - Data: ProfileResponse{ - Profile: true, - SteamID: user.SteamID, - UserName: user.UserName, - AvatarLink: user.AvatarLink, - CountryCode: user.CountryCode, - Titles: user.Titles, - Links: links, - Rankings: ProfileRankings{}, - Records: records, - }, - }) -} - -// PUT Profile -// -// @Description Update profile page of session user. -// @Tags users -// @Accept json -// @Produce json -// @Param Authorization header string true "JWT Token" -// @Success 200 {object} models.Response{data=ProfileResponse} -// @Failure 400 {object} models.Response -// @Failure 401 {object} models.Response -// @Router /profile [post] -func UpdateUser(c *gin.Context) { - // Check if user exists - user, exists := c.Get("user") - if !exists { - c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) - return - } - profile, err := GetPlayerSummaries(user.(models.User).SteamID, os.Getenv("API_KEY")) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - // Update profile - _, err = database.DB.Exec(`UPDATE users SET username = $1, avatar_link = $2, country_code = $3, updated_at = $4 - WHERE steam_id = $5`, profile.PersonaName, profile.AvatarFull, profile.LocCountryCode, time.Now().UTC(), user.(models.User).SteamID) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - c.JSON(http.StatusOK, models.Response{ - Success: true, - Message: "Successfully updated user.", - Data: ProfileResponse{ - Profile: true, - SteamID: user.(models.User).SteamID, - UserName: profile.PersonaName, - AvatarLink: profile.AvatarFull, - CountryCode: profile.LocCountryCode, - }, - }) -} - -// PUT Profile/CountryCode -// -// @Description Update country code of session user. -// @Tags users -// @Accept json -// @Produce json -// @Param Authorization header string true "JWT Token" -// @Param country_code query string true "Country Code [XX]" -// @Success 200 {object} models.Response -// @Failure 400 {object} models.Response -// @Failure 401 {object} models.Response -// @Router /profile [put] -func UpdateCountryCode(c *gin.Context) { - // Check if user exists - user, exists := c.Get("user") - if !exists { - c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) - return - } - code := c.Query("country_code") - if code == "" { - c.JSON(http.StatusNotFound, models.ErrorResponse("Enter a valid country code.")) - return - } - var validCode string - err := database.DB.QueryRow(`SELECT country_code FROM countries WHERE country_code = $1`, code).Scan(&validCode) - if err != nil { - c.JSON(http.StatusNotFound, models.ErrorResponse(err.Error())) - return - } - // Valid code, update profile - _, err = database.DB.Exec(`UPDATE users SET country_code = $1 WHERE steam_id = $2`, validCode, user.(models.User).SteamID) - if err != nil { - c.JSON(http.StatusNotFound, models.ErrorResponse(err.Error())) - return - } - c.JSON(http.StatusOK, models.Response{ - Success: true, - Message: "Successfully updated country code.", - }) -} diff --git a/backend/handlers/home.go b/backend/handlers/home.go new file mode 100644 index 0000000..6e9a0df --- /dev/null +++ b/backend/handlers/home.go @@ -0,0 +1,294 @@ +package handlers + +import ( + "log" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/pektezol/leastportalshub/backend/database" + "github.com/pektezol/leastportalshub/backend/models" +) + +type SearchResponse struct { + Players []models.UserShort `json:"players"` + Maps []models.MapShort `json:"maps"` +} + +type RankingsResponse struct { + RankingsSP []models.UserRanking `json:"rankings_sp"` + RankingsMP []models.UserRanking `json:"rankings_mp"` +} + +func Home(c *gin.Context) { + user, exists := c.Get("user") + if !exists { + c.JSON(200, "no id, not auth") + } else { + c.JSON(200, gin.H{ + "output": user, + }) + } +} + +// GET Rankings +// +// @Description Get rankings of every player. +// @Tags rankings +// @Produce json +// @Success 200 {object} models.Response{data=RankingsResponse} +// @Failure 400 {object} models.Response +// @Router /rankings [get] +func Rankings(c *gin.Context) { + rows, err := database.DB.Query(`SELECT steam_id, user_name FROM users`) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + var spRankings []models.UserRanking + var mpRankings []models.UserRanking + for rows.Next() { + var userID, username string + err := rows.Scan(&userID, &username) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + // Getting all sp records for each user + var uniqueSingleUserRecords, totalSingleMaps int + sql := `SELECT COUNT(DISTINCT map_id), (SELECT COUNT(map_name) FROM maps + WHERE is_coop = FALSE AND is_disabled = false) FROM records_sp WHERE user_id = $1` + err = database.DB.QueryRow(sql, userID).Scan(&uniqueSingleUserRecords, &totalSingleMaps) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + // Has all singleplayer records + if uniqueSingleUserRecords == totalSingleMaps { + var ranking models.UserRanking + ranking.UserID = userID + ranking.UserName = username + sql := `SELECT DISTINCT map_id, score_count FROM records_sp WHERE user_id = $1 ORDER BY map_id, score_count` + rows, err := database.DB.Query(sql, userID) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + totalScore := 0 + var maps []int + for rows.Next() { + var mapID, scoreCount int + rows.Scan(&mapID, &scoreCount) + if len(maps) != 0 && maps[len(maps)-1] == mapID { + continue + } + totalScore += scoreCount + maps = append(maps, mapID) + } + ranking.TotalScore = totalScore + spRankings = append(spRankings, ranking) + } + // Getting all mp records for each user + var uniqueMultiUserRecords, totalMultiMaps int + sql = `SELECT COUNT(DISTINCT map_id), (SELECT COUNT(map_name) FROM maps + WHERE is_coop = TRUE AND is_disabled = false) FROM records_mp WHERE host_id = $1 OR partner_id = $2` + err = database.DB.QueryRow(sql, userID, userID).Scan(&uniqueMultiUserRecords, &totalMultiMaps) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + // Has all singleplayer records + if uniqueMultiUserRecords == totalMultiMaps { + var ranking models.UserRanking + ranking.UserID = userID + ranking.UserName = username + sql := `SELECT DISTINCT map_id, score_count FROM records_mp WHERE host_id = $1 OR partner_id = $2 ORDER BY map_id, score_count` + rows, err := database.DB.Query(sql, userID, userID) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + totalScore := 0 + var maps []int + for rows.Next() { + var mapID, scoreCount int + rows.Scan(&mapID, &scoreCount) + if len(maps) != 0 && maps[len(maps)-1] == mapID { + continue + } + totalScore += scoreCount + maps = append(maps, mapID) + } + ranking.TotalScore = totalScore + mpRankings = append(mpRankings, ranking) + } + } + c.JSON(http.StatusOK, models.Response{ + Success: true, + Message: "Successfully retrieved rankings.", + Data: RankingsResponse{ + RankingsSP: spRankings, + RankingsMP: mpRankings, + }, + }) +} + +// GET Search With Query +// +// @Description Get all user and map data matching to the query. +// @Tags search +// @Produce json +// @Param q query string false "Search user or map name." +// @Success 200 {object} models.Response{data=SearchResponse} +// @Failure 400 {object} models.Response +// @Router /search [get] +func SearchWithQuery(c *gin.Context) { + query := c.Query("q") + query = strings.ToLower(query) + log.Println(query) + var response SearchResponse + // Cache all maps for faster response + var maps = []models.MapShort{ + {ID: 1, Name: "Container Ride"}, + {ID: 2, Name: "Portal Carousel"}, + {ID: 3, Name: "Portal Gun"}, + {ID: 4, Name: "Smooth Jazz"}, + {ID: 5, Name: "Cube Momentum"}, + {ID: 6, Name: "Future Starter"}, + {ID: 7, Name: "Secret Panel"}, + {ID: 8, Name: "Wakeup"}, + {ID: 9, Name: "Incinerator"}, + {ID: 10, Name: "Laser Intro"}, + {ID: 11, Name: "Laser Stairs"}, + {ID: 12, Name: "Dual Lasers"}, + {ID: 13, Name: "Laser Over Goo"}, + {ID: 14, Name: "Catapult Intro"}, + {ID: 15, Name: "Trust Fling"}, + {ID: 16, Name: "Pit Flings"}, + {ID: 17, Name: "Fizzler Intro"}, + {ID: 18, Name: "Ceiling Catapult"}, + {ID: 19, Name: "Ricochet"}, + {ID: 20, Name: "Bridge Intro"}, + {ID: 21, Name: "Bridge The Gap"}, + {ID: 22, Name: "Turret Intro"}, + {ID: 23, Name: "Laser Relays"}, + {ID: 24, Name: "Turret Blocker"}, + {ID: 25, Name: "Laser vs Turret"}, + {ID: 26, Name: "Pull The Rug"}, + {ID: 27, Name: "Column Blocker"}, + {ID: 28, Name: "Laser Chaining"}, + {ID: 29, Name: "Triple Laser"}, + {ID: 30, Name: "Jail Break"}, + {ID: 31, Name: "Escape"}, + {ID: 32, Name: "Turret Factory"}, + {ID: 33, Name: "Turret Sabotage"}, + {ID: 34, Name: "Neurotoxin Sabotage"}, + {ID: 35, Name: "Core"}, + {ID: 36, Name: "Underground"}, + {ID: 37, Name: "Cave Johnson"}, + {ID: 38, Name: "Repulsion Intro"}, + {ID: 39, Name: "Bomb Flings"}, + {ID: 40, Name: "Crazy Box"}, + {ID: 41, Name: "PotatOS"}, + {ID: 42, Name: "Propulsion Intro"}, + {ID: 43, Name: "Propulsion Flings"}, + {ID: 44, Name: "Conversion Intro"}, + {ID: 45, Name: "Three Gels"}, + {ID: 46, Name: "Test"}, + {ID: 47, Name: "Funnel Intro"}, + {ID: 48, Name: "Ceiling Button"}, + {ID: 49, Name: "Wall Button"}, + {ID: 50, Name: "Polarity"}, + {ID: 51, Name: "Funnel Catch"}, + {ID: 52, Name: "Stop The Box"}, + {ID: 53, Name: "Laser Catapult"}, + {ID: 54, Name: "Laser Platform"}, + {ID: 55, Name: "Propulsion Catch"}, + {ID: 56, Name: "Repulsion Polarity"}, + {ID: 57, Name: "Finale 1"}, + {ID: 58, Name: "Finale 2"}, + {ID: 59, Name: "Finale 3"}, + {ID: 60, Name: "Finale 4"}, + {ID: 61, Name: "Calibration"}, + {ID: 62, Name: "Hub"}, + {ID: 63, Name: "Doors"}, + {ID: 64, Name: "Buttons"}, + {ID: 65, Name: "Lasers"}, + {ID: 66, Name: "Rat Maze"}, + {ID: 67, Name: "Laser Crusher"}, + {ID: 68, Name: "Behind The Scenes"}, + {ID: 69, Name: "Flings"}, + {ID: 70, Name: "Infinifling"}, + {ID: 71, Name: "Team Retrieval"}, + {ID: 72, Name: "Vertical Flings"}, + {ID: 73, Name: "Catapults"}, + {ID: 74, Name: "Multifling"}, + {ID: 75, Name: "Fling Crushers"}, + {ID: 76, Name: "Industrial Fan"}, + {ID: 77, Name: "Cooperative Bridges"}, + {ID: 78, Name: "Bridge Swap"}, + {ID: 79, Name: "Fling Block"}, + {ID: 80, Name: "Catapult Block"}, + {ID: 81, Name: "Bridge Fling"}, + {ID: 82, Name: "Turret Walls"}, + {ID: 83, Name: "Turret Assasin"}, + {ID: 84, Name: "Bridge Testing"}, + {ID: 85, Name: "Cooperative Funnels"}, + {ID: 86, Name: "Funnel Drill"}, + {ID: 87, Name: "Funnel Catch"}, + {ID: 88, Name: "Funnel Laser"}, + {ID: 89, Name: "Cooperative Polarity"}, + {ID: 90, Name: "Funnel Hop"}, + {ID: 91, Name: "Advanced Polarity"}, + {ID: 92, Name: "Funnel Maze"}, + {ID: 93, Name: "Turret Warehouse"}, + {ID: 94, Name: "Repulsion Jumps"}, + {ID: 95, Name: "Double Bounce"}, + {ID: 96, Name: "Bridge Repulsion"}, + {ID: 97, Name: "Wall Repulsion"}, + {ID: 98, Name: "Propulsion Crushers"}, + {ID: 99, Name: "Turret Ninja"}, + {ID: 100, Name: "Propulsion Retrieval"}, + {ID: 101, Name: "Vault Entrance"}, + {ID: 102, Name: "Seperation"}, + {ID: 103, Name: "Triple Axis"}, + {ID: 104, Name: "Catapult Catch"}, + {ID: 105, Name: "Bridge Gels"}, + {ID: 106, Name: "Maintenance"}, + {ID: 107, Name: "Bridge Catch"}, + {ID: 108, Name: "Double Lift"}, + {ID: 109, Name: "Gel Maze"}, + {ID: 110, Name: "Crazier Box"}, + } + var filteredMaps []models.MapShort + for _, m := range maps { + if strings.Contains(strings.ToLower(m.Name), strings.ToLower(query)) { + filteredMaps = append(filteredMaps, m) + } + } + response.Maps = filteredMaps + if len(response.Maps) == 0 { + response.Maps = []models.MapShort{} + } + rows, err := database.DB.Query("SELECT steam_id, user_name FROM users WHERE lower(user_name) LIKE $1", "%"+query+"%") + if err != nil { + log.Fatal(err) + } + defer rows.Close() + for rows.Next() { + var user models.UserShort + if err := rows.Scan(&user.SteamID, &user.UserName); err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + response.Players = append(response.Players, user) + } + if len(response.Players) == 0 { + response.Players = []models.UserShort{} + } + c.JSON(http.StatusOK, models.Response{ + Success: true, + Message: "Search successfully retrieved.", + Data: response, + }) +} diff --git a/backend/handlers/login.go b/backend/handlers/login.go new file mode 100644 index 0000000..4b151c2 --- /dev/null +++ b/backend/handlers/login.go @@ -0,0 +1,166 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "time" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v4" + "github.com/pektezol/leastportalshub/backend/database" + "github.com/pektezol/leastportalshub/backend/models" + "github.com/solovev/steam_go" +) + +type LoginResponse struct { + Token string `json:"token"` +} + +// Login +// +// @Description Get (redirect) login page for Steam auth. +// @Tags login +// @Accept json +// @Produce json +// @Success 200 {object} models.Response{data=LoginResponse} +// @Failure 400 {object} models.Response +// @Router /login [get] +func Login(c *gin.Context) { + openID := steam_go.NewOpenId(c.Request) + switch openID.Mode() { + case "": + c.Redirect(http.StatusMovedPermanently, openID.AuthUrl()) + case "cancel": + c.Redirect(http.StatusMovedPermanently, "/") + default: + steamID, err := openID.ValidateAndGetId() + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + // Create user if new + var checkSteamID int64 + database.DB.QueryRow("SELECT steam_id FROM users WHERE steam_id = $1", steamID).Scan(&checkSteamID) + // User does not exist + if checkSteamID == 0 { + user, err := GetPlayerSummaries(steamID, os.Getenv("API_KEY")) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + // Empty country code check + if user.LocCountryCode == "" { + user.LocCountryCode = "XX" + } + // Insert new user to database + database.DB.Exec(`INSERT INTO users (steam_id, user_name, avatar_link, country_code) + VALUES ($1, $2, $3, $4)`, steamID, user.PersonaName, user.AvatarFull, user.LocCountryCode) + } + moderator := false + rows, _ := database.DB.Query("SELECT title_name FROM titles t INNER JOIN user_titles ut ON t.id=ut.title_id WHERE ut.user_id = $1", steamID) + for rows.Next() { + var title string + rows.Scan(&title) + if title == "Moderator" { + moderator = true + } + } + // Generate JWT token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "sub": steamID, + "exp": time.Now().Add(time.Hour * 24 * 30).Unix(), + "mod": moderator, + }) + // Sign and get the complete encoded token as a string using the secret + tokenString, err := token.SignedString([]byte(os.Getenv("SECRET_KEY"))) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse("Failed to generate token.")) + return + } + c.SetCookie("token", tokenString, 3600*24*30, "/", "", true, true) + c.Redirect(http.StatusTemporaryRedirect, "/") + // c.JSON(http.StatusOK, models.Response{ + // Success: true, + // Message: "Successfully generated token.", + // Data: LoginResponse{ + // Token: tokenString, + // }, + // }) + return + } +} + +// GET Token +// +// @Description Gets the token cookie value from the user. +// @Tags auth +// @Produce json +// +// @Success 200 {object} models.Response{data=LoginResponse} +// @Failure 404 {object} models.Response +// @Router /token [get] +func GetCookie(c *gin.Context) { + cookie, err := c.Cookie("token") + if err != nil { + c.JSON(http.StatusNotFound, models.ErrorResponse("No token cookie found.")) + return + } + c.JSON(http.StatusOK, models.Response{ + Success: true, + Message: "Token cookie successfully retrieved.", + Data: LoginResponse{ + Token: cookie, + }, + }) +} + +// DELETE Token +// +// @Description Deletes the token cookie from the user. +// @Tags auth +// @Produce json +// +// @Success 200 {object} models.Response{data=LoginResponse} +// @Failure 404 {object} models.Response +// @Router /token [delete] +func DeleteCookie(c *gin.Context) { + cookie, err := c.Cookie("token") + if err != nil { + c.JSON(http.StatusNotFound, models.ErrorResponse("No token cookie found.")) + return + } + c.SetCookie("token", "", -1, "/", "", true, true) + c.JSON(http.StatusOK, models.Response{ + Success: true, + Message: "Token cookie successfully deleted.", + Data: LoginResponse{ + Token: cookie, + }, + }) +} + +func GetPlayerSummaries(steamId, apiKey string) (*models.PlayerSummaries, error) { + url := fmt.Sprintf("http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/?key=%s&steamids=%s", apiKey, steamId) + resp, err := http.Get(url) + if err != nil { + return nil, err + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + type Result struct { + Response struct { + Players []models.PlayerSummaries `json:"players"` + } `json:"response"` + } + var data Result + if err := json.Unmarshal(body, &data); err != nil { + return nil, err + } + return &data.Response.Players[0], err +} diff --git a/backend/handlers/map.go b/backend/handlers/map.go new file mode 100644 index 0000000..b47e793 --- /dev/null +++ b/backend/handlers/map.go @@ -0,0 +1,314 @@ +package handlers + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/pektezol/leastportalshub/backend/database" + "github.com/pektezol/leastportalshub/backend/models" +) + +type MapSummaryResponse struct { + Map models.Map `json:"map"` + Summary models.MapSummary `json:"summary"` +} + +type ChaptersResponse struct { + Game models.Game `json:"game"` + Chapters []models.Chapter `json:"chapters"` +} + +type ChapterMapsResponse struct { + Chapter models.Chapter `json:"chapter"` + Maps []models.MapShort `json:"maps"` +} + +// GET Map Summary +// +// @Description Get map summary with specified id. +// @Tags maps +// @Produce json +// @Param id path int true "Map ID" +// @Success 200 {object} models.Response{data=MapSummaryResponse} +// @Failure 400 {object} models.Response +// @Router /maps/{id}/summary [get] +func FetchMapSummary(c *gin.Context) { + id := c.Param("id") + response := MapSummaryResponse{Map: models.Map{}, Summary: models.MapSummary{Routes: []models.MapRoute{}}} + intID, err := strconv.Atoi(id) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + // Get map data + response.Map.ID = intID + sql := `SELECT m.id, g.name, c.name, m.name, m.image, g.is_coop + FROM maps m + INNER JOIN games g ON m.game_id = g.id + INNER JOIN chapters c ON m.chapter_id = c.id + WHERE m.id = $1` + 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) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + // Get map routes and histories + 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 + INNER JOIN categories c ON r.category_id = c.id + INNER JOIN map_history h ON r.map_id = h.map_id AND r.category_id = h.category_id + LEFT JOIN map_ratings rt ON r.map_id = rt.map_id AND r.category_id = rt.category_id + 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 + ORDER BY h.record_date ASC;` + rows, err := database.DB.Query(sql, id) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + for rows.Next() { + route := models.MapRoute{Category: models.Category{}, History: models.MapHistory{}} + 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) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + response.Summary.Routes = append(response.Summary.Routes, route) + } + // Return response + c.JSON(http.StatusOK, models.Response{ + Success: true, + Message: "Successfully retrieved map summary.", + Data: response, + }) +} + +// GET Map Leaderboards +// +// @Description Get map leaderboards with specified id. +// @Tags maps +// @Produce json +// @Param id path int true "Map ID" +// @Success 200 {object} models.Response{data=models.Map{data=models.MapRecords}} +// @Failure 400 {object} models.Response +// @Router /maps/{id}/leaderboards [get] +func FetchMapLeaderboards(c *gin.Context) { + // TODO: make new response type + id := c.Param("id") + // Get map data + var mapData models.Map + var mapRecordsData models.MapRecords + var isDisabled bool + intID, err := strconv.Atoi(id) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + mapData.ID = intID + sql := `SELECT g.name, c.name, m.name, is_disabled, m.image + FROM maps m + INNER JOIN games g ON m.game_id = g.id + INNER JOIN chapters c ON m.chapter_id = c.id + WHERE m.id = $1` + err = database.DB.QueryRow(sql, id).Scan(&mapData.GameName, &mapData.ChapterName, &mapData.MapName, &isDisabled, &mapData.Image) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + if isDisabled { + c.JSON(http.StatusBadRequest, models.ErrorResponse("Map is not available for competitive boards.")) + return + } + // TODO: avatar and names for host & partner + // Get records from the map + if mapData.GameName == "Portal 2 - Cooperative" { + var records []models.RecordMP + sql = `SELECT id, host_id, partner_id, score_count, score_time, host_demo_id, partner_demo_id, record_date + FROM ( + SELECT id, host_id, partner_id, score_count, score_time, host_demo_id, partner_demo_id, record_date, + ROW_NUMBER() OVER (PARTITION BY host_id, partner_id ORDER BY score_count, score_time) AS rn + FROM records_mp + WHERE map_id = $1 + ) sub + WHERE rn = 1` + rows, err := database.DB.Query(sql, id) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + placement := 1 + ties := 0 + for rows.Next() { + var record models.RecordMP + err := rows.Scan(&record.RecordID, &record.HostID, &record.PartnerID, &record.ScoreCount, &record.ScoreTime, &record.HostDemoID, &record.PartnerDemoID, &record.RecordDate) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + if len(records) != 0 && records[len(records)-1].ScoreTime == record.ScoreTime { + ties++ + record.Placement = placement - ties + } else { + record.Placement = placement + } + records = append(records, record) + placement++ + } + mapRecordsData.Records = records + } else { + var records []models.RecordSP + sql = `SELECT id, user_id, users.user_name, users.avatar_link, score_count, score_time, demo_id, record_date + FROM ( + SELECT id, user_id, score_count, score_time, demo_id, record_date, + ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY score_count, score_time) AS rn + FROM records_sp + WHERE map_id = $1 + ) sub + INNER JOIN users ON user_id = users.steam_id + WHERE rn = 1` + rows, err := database.DB.Query(sql, id) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + placement := 1 + ties := 0 + for rows.Next() { + var record models.RecordSP + err := rows.Scan(&record.RecordID, &record.UserID, &record.UserName, &record.UserAvatar, &record.ScoreCount, &record.ScoreTime, &record.DemoID, &record.RecordDate) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + if len(records) != 0 && records[len(records)-1].ScoreTime == record.ScoreTime { + ties++ + record.Placement = placement - ties + } else { + record.Placement = placement + } + records = append(records, record) + placement++ + } + mapRecordsData.Records = records + } + // mapData.Data = mapRecordsData + // Return response + c.JSON(http.StatusOK, models.Response{ + Success: true, + Message: "Successfully retrieved map leaderboards.", + Data: mapData, + }) +} + +// GET Games +// +// @Description Get games from the leaderboards. +// @Tags games & chapters +// @Produce json +// @Success 200 {object} models.Response{data=[]models.Game} +// @Failure 400 {object} models.Response +// @Router /games [get] +func FetchGames(c *gin.Context) { + rows, err := database.DB.Query(`SELECT id, name, is_coop FROM games`) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + var games []models.Game + for rows.Next() { + var game models.Game + if err := rows.Scan(&game.ID, &game.Name, &game.IsCoop); err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + games = append(games, game) + } + c.JSON(http.StatusOK, models.Response{ + Success: true, + Message: "Successfully retrieved games.", + Data: games, + }) +} + +// GET Chapters of a Game +// +// @Description Get chapters from the specified game id. +// @Tags games & chapters +// @Produce json +// @Param id path int true "Game ID" +// @Success 200 {object} models.Response{data=ChaptersResponse} +// @Failure 400 {object} models.Response +// @Router /games/{id} [get] +func FetchChapters(c *gin.Context) { + gameID := c.Param("id") + intID, err := strconv.Atoi(gameID) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + var response ChaptersResponse + rows, err := database.DB.Query(`SELECT c.id, c.name, g.name FROM chapters c INNER JOIN games g ON c.game_id = g.id WHERE game_id = $1`, gameID) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + var chapters []models.Chapter + var gameName string + for rows.Next() { + var chapter models.Chapter + if err := rows.Scan(&chapter.ID, &chapter.Name, &gameName); err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + chapters = append(chapters, chapter) + } + response.Game.ID = intID + response.Game.Name = gameName + response.Chapters = chapters + c.JSON(http.StatusOK, models.Response{ + Success: true, + Message: "Successfully retrieved chapters.", + Data: response, + }) +} + +// GET Maps of a Chapter +// +// @Description Get maps from the specified chapter id. +// @Tags games & chapters +// @Produce json +// @Param id path int true "Chapter ID" +// @Success 200 {object} models.Response{data=ChapterMapsResponse} +// @Failure 400 {object} models.Response +// @Router /chapters/{id} [get] +func FetchChapterMaps(c *gin.Context) { + chapterID := c.Param("id") + intID, err := strconv.Atoi(chapterID) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + var response ChapterMapsResponse + rows, err := database.DB.Query(`SELECT m.id, m.name, c.name FROM maps m INNER JOIN chapters c ON m.chapter_id = c.id WHERE chapter_id = $1`, chapterID) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + var maps []models.MapShort + var chapterName string + for rows.Next() { + var mapShort models.MapShort + if err := rows.Scan(&mapShort.ID, &mapShort.Name, &chapterName); err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + maps = append(maps, mapShort) + } + response.Chapter.ID = intID + response.Chapter.Name = chapterName + response.Maps = maps + c.JSON(http.StatusOK, models.Response{ + Success: true, + Message: "Successfully retrieved maps.", + Data: response, + }) +} diff --git a/backend/handlers/mod.go b/backend/handlers/mod.go new file mode 100644 index 0000000..e47cb3f --- /dev/null +++ b/backend/handlers/mod.go @@ -0,0 +1,334 @@ +package handlers + +import ( + "net/http" + "strconv" + "time" + + "github.com/gin-gonic/gin" + "github.com/pektezol/leastportalshub/backend/database" + "github.com/pektezol/leastportalshub/backend/models" +) + +type CreateMapSummaryRequest struct { + CategoryID int `json:"category_id" binding:"required"` + Description string `json:"description" binding:"required"` + Showcase string `json:"showcase"` + UserName string `json:"user_name" binding:"required"` + ScoreCount *int `json:"score_count" binding:"required"` + RecordDate time.Time `json:"record_date" binding:"required"` +} + +type EditMapSummaryRequest struct { + RouteID int `json:"route_id" binding:"required"` + Description string `json:"description" binding:"required"` + Showcase string `json:"showcase"` + UserName string `json:"user_name" binding:"required"` + ScoreCount *int `json:"score_count" binding:"required"` + RecordDate time.Time `json:"record_date" binding:"required"` +} + +type DeleteMapSummaryRequest struct { + RouteID int `json:"route_id" binding:"required"` +} + +type EditMapImageRequest struct { + Image string `json:"image" binding:"required"` +} + +// POST Map Summary +// +// @Description Create map summary with specified map id. +// @Tags maps +// @Produce json +// @Param Authorization header string true "JWT Token" +// @Param id path int true "Map ID" +// @Param request body CreateMapSummaryRequest true "Body" +// @Success 200 {object} models.Response{data=CreateMapSummaryRequest} +// @Failure 400 {object} models.Response +// @Router /maps/{id}/summary [post] +func CreateMapSummary(c *gin.Context) { + // Check if user exists + _, exists := c.Get("user") + if !exists { + c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) + return + } + mod, exists := c.Get("mod") + if !exists || !mod.(bool) { + c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) + return + } + // Bind parameter and body + id := c.Param("id") + mapID, err := strconv.Atoi(id) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + var request CreateMapSummaryRequest + if err := c.BindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + // Start database transaction + tx, err := database.DB.Begin() + if err != nil { + c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) + return + } + defer tx.Rollback() + // Fetch route category and score count + var checkMapID int + sql := `SELECT m.id FROM maps m WHERE m.id = $1` + err = database.DB.QueryRow(sql, mapID).Scan(&checkMapID) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + if mapID != checkMapID { + c.JSON(http.StatusBadRequest, models.ErrorResponse("Map ID does not exist.")) + return + } + // Update database with new data + sql = `INSERT INTO map_routes (map_id,category_id,score_count,description,showcase) + VALUES ($1,$2,$3,$4,$5)` + _, err = tx.Exec(sql, mapID, request.CategoryID, request.ScoreCount, request.Description, request.Showcase) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + sql = `INSERT INTO map_history (map_id,category_id,user_name,score_count,record_date) + VALUES ($1,$2,$3,$4,$5)` + _, err = tx.Exec(sql, mapID, request.CategoryID, request.UserName, request.ScoreCount, request.RecordDate) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + if err = tx.Commit(); err != nil { + c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) + return + } + // Return response + c.JSON(http.StatusOK, models.Response{ + Success: true, + Message: "Successfully created map summary.", + Data: request, + }) +} + +// PUT Map Summary +// +// @Description Edit map summary with specified map id. +// @Tags maps +// @Produce json +// @Param Authorization header string true "JWT Token" +// @Param id path int true "Map ID" +// @Param request body EditMapSummaryRequest true "Body" +// @Success 200 {object} models.Response{data=EditMapSummaryRequest} +// @Failure 400 {object} models.Response +// @Router /maps/{id}/summary [put] +func EditMapSummary(c *gin.Context) { + // Check if user exists + _, exists := c.Get("user") + if !exists { + c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) + return + } + mod, exists := c.Get("mod") + if !exists || !mod.(bool) { + c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) + return + } + // Bind parameter and body + id := c.Param("id") + mapID, err := strconv.Atoi(id) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + var request EditMapSummaryRequest + if err := c.BindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + // Start database transaction + tx, err := database.DB.Begin() + if err != nil { + c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) + return + } + defer tx.Rollback() + // Fetch route category and score count + var categoryID, scoreCount, historyID int + 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` + err = database.DB.QueryRow(sql, mapID, request.RouteID).Scan(&categoryID, &scoreCount) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + sql = `SELECT mh.id FROM map_history mh WHERE mh.score_count = $1 AND mh.category_id = $2 AND mh.map_id = $3` + err = database.DB.QueryRow(sql, scoreCount, categoryID, mapID).Scan(&historyID) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + // Update database with new data + sql = `UPDATE map_routes SET score_count = $2, description = $3, showcase = $4 WHERE id = $1` + _, err = tx.Exec(sql, request.RouteID, request.ScoreCount, request.Description, request.Showcase) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + sql = `UPDATE map_history SET user_name = $2, score_count = $3, record_date = $4 WHERE id = $1` + _, err = tx.Exec(sql, historyID, request.UserName, request.ScoreCount, request.RecordDate) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + if err = tx.Commit(); err != nil { + c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) + return + } + // Return response + c.JSON(http.StatusOK, models.Response{ + Success: true, + Message: "Successfully updated map summary.", + Data: request, + }) +} + +// DELETE Map Summary +// +// @Description Delete map summary with specified map id. +// @Tags maps +// @Produce json +// @Param Authorization header string true "JWT Token" +// @Param id path int true "Map ID" +// @Param request body DeleteMapSummaryRequest true "Body" +// @Success 200 {object} models.Response{data=DeleteMapSummaryRequest} +// @Failure 400 {object} models.Response +// @Router /maps/{id}/summary [delete] +func DeleteMapSummary(c *gin.Context) { + // Check if user exists + _, exists := c.Get("user") + if !exists { + c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) + return + } + mod, exists := c.Get("mod") + if !exists || !mod.(bool) { + c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) + return + } + // Bind parameter and body + id := c.Param("id") + mapID, err := strconv.Atoi(id) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + var request DeleteMapSummaryRequest + if err := c.BindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + // Start database transaction + tx, err := database.DB.Begin() + if err != nil { + c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) + return + } + defer tx.Rollback() + // Fetch route category and score count + var checkMapID, scoreCount, mapHistoryID int + 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` + err = database.DB.QueryRow(sql, mapID, request.RouteID).Scan(&checkMapID, &scoreCount) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + if mapID != checkMapID { + c.JSON(http.StatusBadRequest, models.ErrorResponse("Map ID does not exist.")) + return + } + 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` + err = database.DB.QueryRow(sql, mapID, request.RouteID, scoreCount).Scan(&mapHistoryID) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + // Update database with new data + sql = `DELETE FROM map_routes mr WHERE mr.id = $1 ` + _, err = tx.Exec(sql, request.RouteID) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + sql = `DELETE FROM map_history mh WHERE mh.id = $1` + _, err = tx.Exec(sql, mapHistoryID) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + if err = tx.Commit(); err != nil { + c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) + return + } + // Return response + c.JSON(http.StatusOK, models.Response{ + Success: true, + Message: "Successfully delete map summary.", + Data: request, + }) +} + +// PUT Map Image +// +// @Description Edit map image with specified map id. +// @Tags maps +// @Produce json +// @Param Authorization header string true "JWT Token" +// @Param id path int true "Map ID" +// @Param request body EditMapImageRequest true "Body" +// @Success 200 {object} models.Response{data=EditMapImageRequest} +// @Failure 400 {object} models.Response +// @Router /maps/{id}/image [put] +func EditMapImage(c *gin.Context) { + // Check if user exists + _, exists := c.Get("user") + if !exists { + c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) + return + } + mod, exists := c.Get("mod") + if !exists || !mod.(bool) { + c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) + return + } + // Bind parameter and body + id := c.Param("id") + mapID, err := strconv.Atoi(id) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + var request EditMapImageRequest + if err := c.BindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + // Update database with new data + sql := `UPDATE maps SET image = $2 WHERE id = $1` + _, err = database.DB.Exec(sql, mapID, request.Image) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + // Return response + c.JSON(http.StatusOK, models.Response{ + Success: true, + Message: "Successfully updated map image.", + Data: request, + }) +} diff --git a/backend/handlers/record.go b/backend/handlers/record.go new file mode 100644 index 0000000..00c9b7d --- /dev/null +++ b/backend/handlers/record.go @@ -0,0 +1,292 @@ +package handlers + +import ( + "context" + "encoding/base64" + "io" + "log" + "mime/multipart" + "net/http" + "os" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/pektezol/leastportalshub/backend/database" + "github.com/pektezol/leastportalshub/backend/models" + "github.com/pektezol/leastportalshub/backend/parser" + "golang.org/x/oauth2/google" + "golang.org/x/oauth2/jwt" + "google.golang.org/api/drive/v3" +) + +type RecordRequest struct { + HostDemo *multipart.FileHeader `json:"host_demo" form:"host_demo" binding:"required" swaggerignore:"true"` + PartnerDemo *multipart.FileHeader `json:"partner_demo" form:"partner_demo" swaggerignore:"true"` + IsPartnerOrange bool `json:"is_partner_orange" form:"is_partner_orange"` + PartnerID string `json:"partner_id" form:"partner_id"` +} + +type RecordResponse struct { + ScoreCount int `json:"score_count"` + ScoreTime int `json:"score_time"` +} + +// POST Record +// +// @Description Post record with demo of a specific map. +// @Tags maps +// @Accept mpfd +// @Produce json +// @Param id path int true "Map ID" +// @Param Authorization header string true "JWT Token" +// @Param host_demo formData file true "Host Demo" +// @Param partner_demo formData file false "Partner Demo" +// @Param is_partner_orange formData boolean false "Is Partner Orange" +// @Param partner_id formData string false "Partner ID" +// @Success 200 {object} models.Response{data=RecordResponse} +// @Failure 400 {object} models.Response +// @Failure 401 {object} models.Response +// @Router /maps/{id}/record [post] +func CreateRecordWithDemo(c *gin.Context) { + mapId := c.Param("id") + // Check if user exists + user, exists := c.Get("user") + if !exists { + c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) + return + } + // Check if map is sp or mp + var gameName string + var isCoop bool + var isDisabled bool + sql := `SELECT g.name, m.is_disabled FROM maps m INNER JOIN games g ON m.game_id=g.id WHERE m.id = $1` + err := database.DB.QueryRow(sql, mapId).Scan(&gameName, &isDisabled) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + if isDisabled { + c.JSON(http.StatusBadRequest, models.ErrorResponse("Map is not available for competitive boards.")) + return + } + if gameName == "Portal 2 - Cooperative" { + isCoop = true + } + // Get record request + var record RecordRequest + if err := c.ShouldBind(&record); err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + if isCoop && (record.PartnerDemo == nil || record.PartnerID == "") { + c.JSON(http.StatusBadRequest, models.ErrorResponse("Invalid entry for coop record submission.")) + return + } + // Demo files + demoFiles := []*multipart.FileHeader{record.HostDemo} + if isCoop { + demoFiles = append(demoFiles, record.PartnerDemo) + } + var hostDemoUUID, hostDemoFileID, partnerDemoUUID, partnerDemoFileID string + var hostDemoScoreCount, hostDemoScoreTime int + client := serviceAccount() + srv, err := drive.New(client) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + // Create database transaction for inserts + tx, err := database.DB.Begin() + if err != nil { + c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) + return + } + // Defer to a rollback in case anything fails + defer tx.Rollback() + for i, header := range demoFiles { + uuid := uuid.New().String() + // Upload & insert into demos + err = c.SaveUploadedFile(header, "backend/parser/"+uuid+".dem") + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + defer os.Remove("backend/parser/" + uuid + ".dem") + f, err := os.Open("backend/parser/" + uuid + ".dem") + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + defer f.Close() + file, err := createFile(srv, uuid+".dem", "application/octet-stream", f, os.Getenv("GOOGLE_FOLDER_ID")) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + hostDemoScoreCount, hostDemoScoreTime, err = parser.ProcessDemo("backend/parser/" + uuid + ".dem") + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + if i == 0 { + hostDemoFileID = file.Id + hostDemoUUID = uuid + } else if i == 1 { + partnerDemoFileID = file.Id + partnerDemoUUID = uuid + } + _, err = tx.Exec(`INSERT INTO demos (id,location_id) VALUES ($1,$2)`, uuid, file.Id) + if err != nil { + deleteFile(srv, file.Id) + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + } + // Insert into records + if isCoop { + sql := `INSERT INTO records_mp(map_id,score_count,score_time,host_id,partner_id,host_demo_id,partner_demo_id) + VALUES($1, $2, $3, $4, $5, $6, $7)` + var hostID string + var partnerID string + if record.IsPartnerOrange { + hostID = user.(models.User).SteamID + partnerID = record.PartnerID + } else { + partnerID = user.(models.User).SteamID + hostID = record.PartnerID + } + _, err := tx.Exec(sql, mapId, hostDemoScoreCount, hostDemoScoreTime, hostID, partnerID, hostDemoUUID, partnerDemoUUID) + if err != nil { + deleteFile(srv, hostDemoFileID) + deleteFile(srv, partnerDemoFileID) + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + // If a new world record based on portal count + // if record.ScoreCount < wrScore { + // _, err := tx.Exec(`UPDATE maps SET wr_score = $1, wr_time = $2 WHERE id = $3`, record.ScoreCount, record.ScoreTime, mapId) + // if err != nil { + // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + // return + // } + // } + } else { + sql := `INSERT INTO records_sp(map_id,score_count,score_time,user_id,demo_id) + VALUES($1, $2, $3, $4, $5)` + _, err := tx.Exec(sql, mapId, hostDemoScoreCount, hostDemoScoreTime, user.(models.User).SteamID, hostDemoUUID) + if err != nil { + deleteFile(srv, hostDemoFileID) + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + // If a new world record based on portal count + // if record.ScoreCount < wrScore { + // _, err := tx.Exec(`UPDATE maps SET wr_score = $1, wr_time = $2 WHERE id = $3`, record.ScoreCount, record.ScoreTime, mapId) + // if err != nil { + // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + // return + // } + // } + } + if err = tx.Commit(); err != nil { + c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) + return + } + c.JSON(http.StatusOK, models.Response{ + Success: true, + Message: "Successfully created record.", + Data: RecordResponse{ScoreCount: hostDemoScoreCount, ScoreTime: hostDemoScoreTime}, + }) +} + +// GET Demo +// +// @Description Get demo with specified demo uuid. +// @Tags demo +// @Accept json +// @Produce octet-stream +// @Param uuid query string true "Demo UUID" +// @Success 200 {file} binary "Demo File" +// @Failure 400 {object} models.Response +// @Router /demos [get] +func DownloadDemoWithID(c *gin.Context) { + uuid := c.Query("uuid") + var locationID string + if uuid == "" { + c.JSON(http.StatusBadRequest, models.ErrorResponse("Invalid id given.")) + return + } + err := database.DB.QueryRow(`SELECT location_id FROM demos WHERE id = $1`, uuid).Scan(&locationID) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + if locationID == "" { + c.JSON(http.StatusBadRequest, models.ErrorResponse("Invalid id given.")) + return + } + url := "https://drive.google.com/uc?export=download&id=" + locationID + fileName := uuid + ".dem" + output, err := os.Create(fileName) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + defer os.Remove(fileName) + defer output.Close() + response, err := http.Get(url) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + defer response.Body.Close() + _, err = io.Copy(output, response.Body) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + // Downloaded file + c.Header("Content-Description", "File Transfer") + c.Header("Content-Transfer-Encoding", "binary") + c.Header("Content-Disposition", "attachment; filename="+fileName) + c.Header("Content-Type", "application/octet-stream") + c.File(fileName) + // c.FileAttachment() +} + +// Use Service account +func serviceAccount() *http.Client { + privateKey, _ := base64.StdEncoding.DecodeString(os.Getenv("GOOGLE_PRIVATE_KEY_BASE64")) + config := &jwt.Config{ + Email: os.Getenv("GOOGLE_CLIENT_EMAIL"), + PrivateKey: []byte(privateKey), + Scopes: []string{ + drive.DriveScope, + }, + TokenURL: google.JWTTokenURL, + } + client := config.Client(context.Background()) + return client +} + +// Create Gdrive file +func createFile(service *drive.Service, name string, mimeType string, content io.Reader, parentId string) (*drive.File, error) { + f := &drive.File{ + MimeType: mimeType, + Name: name, + Parents: []string{parentId}, + } + file, err := service.Files.Create(f).Media(content).Do() + + if err != nil { + log.Println("Could not create file: " + err.Error()) + return nil, err + } + + return file, nil +} + +// Delete Gdrive file +func deleteFile(service *drive.Service, fileId string) { + service.Files.Delete(fileId) +} diff --git a/backend/handlers/user.go b/backend/handlers/user.go new file mode 100644 index 0000000..51eadb4 --- /dev/null +++ b/backend/handlers/user.go @@ -0,0 +1,383 @@ +package handlers + +import ( + "net/http" + "os" + "regexp" + "time" + + "github.com/gin-gonic/gin" + "github.com/pektezol/leastportalshub/backend/database" + "github.com/pektezol/leastportalshub/backend/models" +) + +type ProfileResponse struct { + Profile bool `json:"profile"` + SteamID string `json:"steam_id"` + UserName string `json:"user_name"` + AvatarLink string `json:"avatar_link"` + CountryCode string `json:"country_code"` + Titles []models.Title `json:"titles"` + Links models.Links `json:"links"` + Rankings ProfileRankings `json:"rankings"` + Records ProfileRecords `json:"records"` +} + +type ProfileRankings struct { + Overall ProfileRankingsDetails `json:"overall"` + Singleplayer ProfileRankingsDetails `json:"singleplayer"` + Cooperative ProfileRankingsDetails `json:"cooperative"` +} + +type ProfileRankingsDetails struct { + Rank int `json:"rank"` + CompletionCount int `json:"completion_count"` + CompletionTotal int `json:"completion_total"` +} + +type ProfileRecords struct { + P2Singleplayer []ProfileRecordsDetails `json:"portal2_singleplayer"` + P2Cooperative []ProfileRecordsDetails `json:"portal2_cooperative"` +} + +type ProfileRecordsDetails struct { + MapID int `json:"map_id"` + MapName string `json:"map_name"` + Scores []ProfileScores `json:"scores"` +} + +type ProfileScores struct { + DemoID string `json:"demo_id"` + ScoreCount int `json:"score_count"` + ScoreTime int `json:"score_time"` + Date time.Time `json:"date"` +} + +type ScoreResponse struct { + MapID int `json:"map_id"` + Records any `json:"records"` +} + +// GET Profile +// +// @Description Get profile page of session user. +// @Tags users +// @Accept json +// @Produce json +// @Param Authorization header string true "JWT Token" +// @Success 200 {object} models.Response{data=ProfileResponse} +// @Failure 400 {object} models.Response +// @Failure 401 {object} models.Response +// @Router /profile [get] +func Profile(c *gin.Context) { + // Check if user exists + user, exists := c.Get("user") + if !exists { + c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) + return + } + // Get user links + links := models.Links{} + sql := `SELECT u.p2sr, u.steam, u.youtube, u.twitch FROM users u WHERE u.steam_id = $1` + err := database.DB.QueryRow(sql, user.(models.User).SteamID).Scan(&links.P2SR, &links.Steam, &links.YouTube, &links.Twitch) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + // TODO: Get rankings (all maps done in one game) + records := ProfileRecords{ + P2Singleplayer: []ProfileRecordsDetails{}, + P2Cooperative: []ProfileRecordsDetails{}, + } + // Get singleplayer records + sql = `SELECT m.game_id, sp.map_id, m."name", sp.score_count, sp.score_time, sp.demo_id, sp.record_date + FROM records_sp sp INNER JOIN maps m ON sp.map_id = m.id WHERE sp.user_id = $1 ORDER BY sp.map_id, sp.score_count, sp.score_time;` + rows, err := database.DB.Query(sql, user.(models.User).SteamID) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + for rows.Next() { + var mapID int + var mapName string + var gameID int + score := ProfileScores{} + rows.Scan(&gameID, &mapID, &mapName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) + if gameID != 1 { + continue + } + // More than one record in one map + if len(records.P2Singleplayer) != 0 && mapID == records.P2Singleplayer[len(records.P2Singleplayer)-1].MapID { + records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores = append(records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores, score) + continue + } + // New map + records.P2Singleplayer = append(records.P2Singleplayer, ProfileRecordsDetails{ + MapID: mapID, + MapName: mapName, + Scores: []ProfileScores{}, + }) + records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores = append(records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores, score) + } + // Get multiplayer records + sql = `SELECT m.game_id, mp.map_id, m."name", mp.score_count, mp.score_time, CASE WHEN host_id = $1 THEN mp.host_demo_id WHEN partner_id = $1 THEN mp.partner_demo_id END demo_id, mp.record_date + FROM records_mp mp INNER JOIN maps m ON mp.map_id = m.id WHERE mp.host_id = $1 OR mp.partner_id = $1 ORDER BY mp.map_id, mp.score_count, mp.score_time;` + rows, err = database.DB.Query(sql, user.(models.User).SteamID) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + for rows.Next() { + var mapID int + var mapName string + var gameID int + score := ProfileScores{} + rows.Scan(&gameID, &mapID, &mapName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) + if gameID != 1 { + continue + } + // More than one record in one map + if len(records.P2Cooperative) != 0 && mapID == records.P2Cooperative[len(records.P2Cooperative)-1].MapID { + records.P2Cooperative[len(records.P2Cooperative)-1].Scores = append(records.P2Cooperative[len(records.P2Cooperative)-1].Scores, score) + continue + } + // New map + records.P2Cooperative = append(records.P2Cooperative, ProfileRecordsDetails{ + MapID: mapID, + MapName: mapName, + Scores: []ProfileScores{}, + }) + records.P2Cooperative[len(records.P2Cooperative)-1].Scores = append(records.P2Cooperative[len(records.P2Cooperative)-1].Scores, score) + } + c.JSON(http.StatusOK, models.Response{ + Success: true, + Message: "Successfully retrieved user scores.", + Data: ProfileResponse{ + Profile: true, + SteamID: user.(models.User).SteamID, + UserName: user.(models.User).UserName, + AvatarLink: user.(models.User).AvatarLink, + CountryCode: user.(models.User).CountryCode, + Titles: user.(models.User).Titles, + Links: links, + Rankings: ProfileRankings{}, + Records: records, + }, + }) +} + +// GET User +// +// @Description Get profile page of another user. +// @Tags users +// @Accept json +// @Produce json +// @Param id path int true "User ID" +// @Success 200 {object} models.Response{data=ProfileResponse} +// @Failure 400 {object} models.Response +// @Failure 404 {object} models.Response +// @Router /users/{id} [get] +func FetchUser(c *gin.Context) { + id := c.Param("id") + // Check if id is all numbers and 17 length + match, _ := regexp.MatchString("^[0-9]{17}$", id) + if !match { + c.JSON(http.StatusNotFound, models.ErrorResponse("User not found.")) + return + } + // Check if user exists + var user models.User + links := models.Links{} + sql := `SELECT u.steam_id, u.user_name, u.avatar_link, u.country_code, u.created_at, u.updated_at, u.p2sr, u.steam, u.youtube, u.twitch FROM users u WHERE u.steam_id = $1` + err := database.DB.QueryRow(sql, id).Scan(&user.SteamID, &user.UserName, &user.AvatarLink, &user.CountryCode, &user.CreatedAt, &user.UpdatedAt, &links.P2SR, &links.Steam, &links.YouTube, &links.Twitch) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + if user.SteamID == "" { + // User does not exist + c.JSON(http.StatusNotFound, models.ErrorResponse("User not found.")) + return + } + // Get user titles + sql = `SELECT t.title_name, t.title_color FROM titles t + INNER JOIN user_titles ut ON t.id=ut.title_id WHERE ut.user_id = $1` + rows, err := database.DB.Query(sql, user.SteamID) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + for rows.Next() { + var title models.Title + if err := rows.Scan(&title.Name, &title.Color); err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + user.Titles = append(user.Titles, title) + } + // TODO: Get rankings (all maps done in one game) + records := ProfileRecords{ + P2Singleplayer: []ProfileRecordsDetails{}, + P2Cooperative: []ProfileRecordsDetails{}, + } + // Get singleplayer records + sql = `SELECT m.game_id, sp.map_id, m."name", sp.score_count, sp.score_time, sp.demo_id, sp.record_date + FROM records_sp sp INNER JOIN maps m ON sp.map_id = m.id WHERE sp.user_id = $1 ORDER BY sp.map_id, sp.score_count, sp.score_time;` + rows, err = database.DB.Query(sql, user.SteamID) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + for rows.Next() { + var mapID int + var mapName string + var gameID int + score := ProfileScores{} + rows.Scan(&gameID, &mapID, &mapName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) + if gameID != 1 { + continue + } + // More than one record in one map + if len(records.P2Singleplayer) != 0 && mapID == records.P2Singleplayer[len(records.P2Singleplayer)-1].MapID { + records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores = append(records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores, score) + continue + } + // New map + records.P2Singleplayer = append(records.P2Singleplayer, ProfileRecordsDetails{ + MapID: mapID, + MapName: mapName, + Scores: []ProfileScores{}, + }) + records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores = append(records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores, score) + } + // Get multiplayer records + sql = `SELECT m.game_id, mp.map_id, m."name", mp.score_count, mp.score_time, CASE WHEN host_id = $1 THEN mp.host_demo_id WHEN partner_id = $1 THEN mp.partner_demo_id END demo_id, mp.record_date + FROM records_mp mp INNER JOIN maps m ON mp.map_id = m.id WHERE mp.host_id = $1 OR mp.partner_id = $1 ORDER BY mp.map_id, mp.score_count, mp.score_time;` + rows, err = database.DB.Query(sql, user.SteamID) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + for rows.Next() { + var mapID int + var mapName string + var gameID int + score := ProfileScores{} + rows.Scan(&gameID, &mapID, &mapName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) + if gameID != 1 { + continue + } + // More than one record in one map + if len(records.P2Cooperative) != 0 && mapID == records.P2Cooperative[len(records.P2Cooperative)-1].MapID { + records.P2Cooperative[len(records.P2Cooperative)-1].Scores = append(records.P2Cooperative[len(records.P2Cooperative)-1].Scores, score) + continue + } + // New map + records.P2Cooperative = append(records.P2Cooperative, ProfileRecordsDetails{ + MapID: mapID, + MapName: mapName, + Scores: []ProfileScores{}, + }) + records.P2Cooperative[len(records.P2Cooperative)-1].Scores = append(records.P2Cooperative[len(records.P2Cooperative)-1].Scores, score) + } + c.JSON(http.StatusOK, models.Response{ + Success: true, + Message: "Successfully retrieved user scores.", + Data: ProfileResponse{ + Profile: true, + SteamID: user.SteamID, + UserName: user.UserName, + AvatarLink: user.AvatarLink, + CountryCode: user.CountryCode, + Titles: user.Titles, + Links: links, + Rankings: ProfileRankings{}, + Records: records, + }, + }) +} + +// PUT Profile +// +// @Description Update profile page of session user. +// @Tags users +// @Accept json +// @Produce json +// @Param Authorization header string true "JWT Token" +// @Success 200 {object} models.Response{data=ProfileResponse} +// @Failure 400 {object} models.Response +// @Failure 401 {object} models.Response +// @Router /profile [post] +func UpdateUser(c *gin.Context) { + // Check if user exists + user, exists := c.Get("user") + if !exists { + c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) + return + } + profile, err := GetPlayerSummaries(user.(models.User).SteamID, os.Getenv("API_KEY")) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + // Update profile + _, err = database.DB.Exec(`UPDATE users SET username = $1, avatar_link = $2, country_code = $3, updated_at = $4 + WHERE steam_id = $5`, profile.PersonaName, profile.AvatarFull, profile.LocCountryCode, time.Now().UTC(), user.(models.User).SteamID) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + c.JSON(http.StatusOK, models.Response{ + Success: true, + Message: "Successfully updated user.", + Data: ProfileResponse{ + Profile: true, + SteamID: user.(models.User).SteamID, + UserName: profile.PersonaName, + AvatarLink: profile.AvatarFull, + CountryCode: profile.LocCountryCode, + }, + }) +} + +// PUT Profile/CountryCode +// +// @Description Update country code of session user. +// @Tags users +// @Accept json +// @Produce json +// @Param Authorization header string true "JWT Token" +// @Param country_code query string true "Country Code [XX]" +// @Success 200 {object} models.Response +// @Failure 400 {object} models.Response +// @Failure 401 {object} models.Response +// @Router /profile [put] +func UpdateCountryCode(c *gin.Context) { + // Check if user exists + user, exists := c.Get("user") + if !exists { + c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) + return + } + code := c.Query("country_code") + if code == "" { + c.JSON(http.StatusNotFound, models.ErrorResponse("Enter a valid country code.")) + return + } + var validCode string + err := database.DB.QueryRow(`SELECT country_code FROM countries WHERE country_code = $1`, code).Scan(&validCode) + if err != nil { + c.JSON(http.StatusNotFound, models.ErrorResponse(err.Error())) + return + } + // Valid code, update profile + _, err = database.DB.Exec(`UPDATE users SET country_code = $1 WHERE steam_id = $2`, validCode, user.(models.User).SteamID) + if err != nil { + c.JSON(http.StatusNotFound, models.ErrorResponse(err.Error())) + return + } + c.JSON(http.StatusOK, models.Response{ + Success: true, + Message: "Successfully updated country code.", + }) +} diff --git a/backend/middleware/auth.go b/backend/middleware/auth.go deleted file mode 100644 index e2c84fa..0000000 --- a/backend/middleware/auth.go +++ /dev/null @@ -1,65 +0,0 @@ -package middleware - -import ( - "fmt" - "os" - "time" - - "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v4" - "github.com/pektezol/leastportalshub/backend/database" - "github.com/pektezol/leastportalshub/backend/models" -) - -func CheckAuth(c *gin.Context) { - tokenString := c.GetHeader("Authorization") - // Validate token - token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) - } - return []byte(os.Getenv("SECRET_KEY")), nil - }) - if token == nil { - c.Next() - return - } - if err != nil { - c.Next() - return - } - if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { - // Check exp - if float64(time.Now().Unix()) > claims["exp"].(float64) { - c.Next() - return - } - // Get user from DB - var user models.User - 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( - &user.SteamID, &user.UserName, &user.AvatarLink, - &user.CountryCode, &user.CreatedAt, &user.UpdatedAt) - if user.SteamID == "" { - c.Next() - return - } - // Get user titles from DB - var moderator bool - user.Titles = []models.Title{} - rows, _ := database.DB.Query(`SELECT t.title_name, t.title_color FROM titles t INNER JOIN user_titles ut ON t.id=ut.title_id WHERE ut.user_id = $1`, user.SteamID) - for rows.Next() { - var title models.Title - rows.Scan(&title.Name, &title.Color) - if title.Name == "Moderator" { - moderator = true - } - user.Titles = append(user.Titles, title) - } - c.Set("user", user) - c.Set("mod", moderator) - c.Next() - } else { - c.Next() - return - } -} diff --git a/backend/routes/routes.go b/backend/routes/routes.go deleted file mode 100644 index 0b80678..0000000 --- a/backend/routes/routes.go +++ /dev/null @@ -1,41 +0,0 @@ -package routes - -import ( - "github.com/gin-gonic/gin" - "github.com/pektezol/leastportalshub/backend/controllers" - "github.com/pektezol/leastportalshub/backend/middleware" - swaggerfiles "github.com/swaggo/files" - ginSwagger "github.com/swaggo/gin-swagger" -) - -func InitRoutes(router *gin.Engine) { - api := router.Group("/api") - { - v1 := api.Group("/v1") - v1.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) - v1.GET("/", func(c *gin.Context) { - c.File("docs/index.html") - }) - v1.GET("/token", controllers.GetCookie) - v1.DELETE("/token", controllers.DeleteCookie) - v1.GET("/home", middleware.CheckAuth, controllers.Home) - v1.GET("/login", controllers.Login) - v1.GET("/profile", middleware.CheckAuth, controllers.Profile) - v1.PUT("/profile", middleware.CheckAuth, controllers.UpdateCountryCode) - v1.POST("/profile", middleware.CheckAuth, controllers.UpdateUser) - v1.GET("/users/:id", middleware.CheckAuth, controllers.FetchUser) - v1.GET("/demos", controllers.DownloadDemoWithID) - v1.GET("/maps/:id/summary", controllers.FetchMapSummary) - v1.POST("/maps/:id/summary", middleware.CheckAuth, controllers.CreateMapSummary) - v1.PUT("/maps/:id/summary", middleware.CheckAuth, controllers.EditMapSummary) - v1.DELETE("/maps/:id/summary", middleware.CheckAuth, controllers.DeleteMapSummary) - v1.PUT("/maps/:id/image", middleware.CheckAuth, controllers.EditMapImage) - v1.GET("/maps/:id/leaderboards", controllers.FetchMapLeaderboards) - v1.POST("/maps/:id/record", middleware.CheckAuth, controllers.CreateRecordWithDemo) - v1.GET("/rankings", controllers.Rankings) - v1.GET("/search", controllers.SearchWithQuery) - v1.GET("/games", controllers.FetchGames) - v1.GET("/games/:id", controllers.FetchChapters) - v1.GET("/chapters/:id", controllers.FetchChapterMaps) - } -} diff --git a/main.go b/main.go index 69f7c86..868db1e 100644 --- a/main.go +++ b/main.go @@ -7,8 +7,8 @@ import ( "github.com/gin-gonic/gin" "github.com/joho/godotenv" + "github.com/pektezol/leastportalshub/backend/api" "github.com/pektezol/leastportalshub/backend/database" - "github.com/pektezol/leastportalshub/backend/routes" _ "github.com/pektezol/leastportalshub/docs" ) @@ -19,8 +19,8 @@ import ( // @license.name GNU General Public License, Version 2 // @license.url https://www.gnu.org/licenses/old-licenses/gpl-2.0.html -// @host lp.ardapektezol.com/api -// @BasePath /v1 +// @host lp.ardapektezol.com/api +// @BasePath /v1 func main() { if os.Getenv("ENV") == "PROD" { gin.SetMode(gin.ReleaseMode) @@ -31,6 +31,6 @@ func main() { } router := gin.Default() database.ConnectDB() - routes.InitRoutes(router) + api.InitRoutes(router) router.Run(fmt.Sprintf(":%s", os.Getenv("PORT"))) } -- cgit v1.2.3 From ee533d9405e3ffe1fd1a073e29f839568c465ba7 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Wed, 30 Aug 2023 16:31:29 +0300 Subject: feat: better leaderboards response, coop additions (#45) Former-commit-id: 3a4a8af14d054512204b5ca4c25a6603ec94773e --- backend/handlers/map.go | 104 +++++++++++++++++++++++++++++++++++------------ backend/models/models.go | 28 ------------- 2 files changed, 79 insertions(+), 53 deletions(-) diff --git a/backend/handlers/map.go b/backend/handlers/map.go index b47e793..9b0caef 100644 --- a/backend/handlers/map.go +++ b/backend/handlers/map.go @@ -3,6 +3,7 @@ package handlers import ( "net/http" "strconv" + "time" "github.com/gin-gonic/gin" "github.com/pektezol/leastportalshub/backend/database" @@ -14,6 +15,11 @@ type MapSummaryResponse struct { Summary models.MapSummary `json:"summary"` } +type MapLeaderboardsResponse struct { + Map models.Map `json:"map"` + Records any `json:"records"` +} + type ChaptersResponse struct { Game models.Game `json:"game"` Chapters []models.Chapter `json:"chapters"` @@ -24,6 +30,34 @@ type ChapterMapsResponse struct { Maps []models.MapShort `json:"maps"` } +type RecordSingleplayer struct { + Placement int `json:"placement"` + RecordID int `json:"record_id"` + ScoreCount int `json:"score_count"` + ScoreTime int `json:"score_time"` + UserID string `json:"user_id"` + UserName string `json:"user_name"` + UserAvatar string `json:"user_avatar"` + DemoID string `json:"demo_id"` + RecordDate time.Time `json:"record_date"` +} + +type RecordMultiplayer struct { + Placement int `json:"placement"` + RecordID int `json:"record_id"` + ScoreCount int `json:"score_count"` + ScoreTime int `json:"score_time"` + HostID string `json:"host_id"` + HostName string `json:"host_name"` + HostAvatar string `json:"host_avatar"` + PartnerID string `json:"partner_id"` + PartnerName string `json:"partner_name"` + PartnerAvatar string `json:"partner_avatar"` + HostDemoID string `json:"host_demo_id"` + PartnerDemoID string `json:"partner_demo_id"` + RecordDate time.Time `json:"record_date"` +} + // GET Map Summary // // @Description Get map summary with specified id. @@ -88,28 +122,29 @@ func FetchMapSummary(c *gin.Context) { // @Tags maps // @Produce json // @Param id path int true "Map ID" -// @Success 200 {object} models.Response{data=models.Map{data=models.MapRecords}} +// @Success 200 {object} models.Response{data=MapLeaderboardsResponse} // @Failure 400 {object} models.Response // @Router /maps/{id}/leaderboards [get] func FetchMapLeaderboards(c *gin.Context) { // TODO: make new response type id := c.Param("id") // Get map data - var mapData models.Map - var mapRecordsData models.MapRecords + response := MapLeaderboardsResponse{Map: models.Map{}, Records: nil} + // var mapData models.Map + // var mapRecordsData models.MapRecords var isDisabled bool intID, err := strconv.Atoi(id) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } - mapData.ID = intID + response.Map.ID = intID sql := `SELECT g.name, c.name, m.name, is_disabled, m.image FROM maps m INNER JOIN games g ON m.game_id = g.id INNER JOIN chapters c ON m.chapter_id = c.id WHERE m.id = $1` - err = database.DB.QueryRow(sql, id).Scan(&mapData.GameName, &mapData.ChapterName, &mapData.MapName, &isDisabled, &mapData.Image) + err = database.DB.QueryRow(sql, id).Scan(&response.Map.GameName, &response.Map.ChapterName, &response.Map.MapName, &isDisabled, &response.Map.Image) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return @@ -119,17 +154,38 @@ func FetchMapLeaderboards(c *gin.Context) { return } // TODO: avatar and names for host & partner - // Get records from the map - if mapData.GameName == "Portal 2 - Cooperative" { - var records []models.RecordMP - sql = `SELECT id, host_id, partner_id, score_count, score_time, host_demo_id, partner_demo_id, record_date - FROM ( - SELECT id, host_id, partner_id, score_count, score_time, host_demo_id, partner_demo_id, record_date, - ROW_NUMBER() OVER (PARTITION BY host_id, partner_id ORDER BY score_count, score_time) AS rn - FROM records_mp - WHERE map_id = $1 - ) sub - WHERE rn = 1` + if response.Map.GameName == "Portal 2 - Cooperative" { + records := []RecordMultiplayer{} + sql = `SELECT + sub.id, + sub.host_id, + host.user_name AS host_user_name, + host.avatar_link AS host_avatar_link, + sub.partner_id, + partner.user_name AS partner_user_name, + partner.avatar_link AS partner_avatar_link, + sub.score_count, + sub.score_time, + sub.host_demo_id, + sub.partner_demo_id, + sub.record_date + FROM ( + SELECT + id, + host_id, + partner_id, + score_count, + score_time, + host_demo_id, + partner_demo_id, + record_date, + ROW_NUMBER() OVER (PARTITION BY host_id, partner_id ORDER BY score_count, score_time) AS rn + FROM records_mp + WHERE map_id = $1 + ) sub + JOIN users AS host ON sub.host_id = host.steam_id + JOIN users AS partner ON sub.partner_id = partner.steam_id + WHERE sub.rn = 1;` rows, err := database.DB.Query(sql, id) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) @@ -138,8 +194,8 @@ func FetchMapLeaderboards(c *gin.Context) { placement := 1 ties := 0 for rows.Next() { - var record models.RecordMP - err := rows.Scan(&record.RecordID, &record.HostID, &record.PartnerID, &record.ScoreCount, &record.ScoreTime, &record.HostDemoID, &record.PartnerDemoID, &record.RecordDate) + var record RecordMultiplayer + err := rows.Scan(&record.RecordID, &record.HostID, &record.HostName, &record.HostAvatar, &record.PartnerID, &record.PartnerName, &record.PartnerAvatar, &record.ScoreCount, &record.ScoreTime, &record.HostDemoID, &record.PartnerDemoID, &record.RecordDate) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return @@ -153,9 +209,9 @@ func FetchMapLeaderboards(c *gin.Context) { records = append(records, record) placement++ } - mapRecordsData.Records = records + response.Records = records } else { - var records []models.RecordSP + records := []RecordSingleplayer{} sql = `SELECT id, user_id, users.user_name, users.avatar_link, score_count, score_time, demo_id, record_date FROM ( SELECT id, user_id, score_count, score_time, demo_id, record_date, @@ -173,7 +229,7 @@ func FetchMapLeaderboards(c *gin.Context) { placement := 1 ties := 0 for rows.Next() { - var record models.RecordSP + var record RecordSingleplayer err := rows.Scan(&record.RecordID, &record.UserID, &record.UserName, &record.UserAvatar, &record.ScoreCount, &record.ScoreTime, &record.DemoID, &record.RecordDate) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) @@ -188,14 +244,12 @@ func FetchMapLeaderboards(c *gin.Context) { records = append(records, record) placement++ } - mapRecordsData.Records = records + response.Records = records } - // mapData.Data = mapRecordsData - // Return response c.JSON(http.StatusOK, models.Response{ Success: true, Message: "Successfully retrieved map leaderboards.", - Data: mapData, + Data: response, }) } diff --git a/backend/models/models.go b/backend/models/models.go index f124db5..b706d25 100644 --- a/backend/models/models.go +++ b/backend/models/models.go @@ -104,34 +104,6 @@ type Links struct { Twitch string `json:"twitch"` } -type RecordSP struct { - RecordID int `json:"record_id"` - Placement int `json:"placement"` - UserID string `json:"user_id"` - UserName string `json:"user_name"` - UserAvatar string `json:"user_avatar"` - ScoreCount int `json:"score_count"` - ScoreTime int `json:"score_time"` - DemoID string `json:"demo_id"` - RecordDate time.Time `json:"record_date"` -} - -type RecordMP struct { - RecordID int `json:"record_id"` - Placement int `json:"placement"` - HostID string `json:"host_id"` - HostName string `json:"host_name"` - HostAvatar string `json:"host_avatar"` - PartnerID string `json:"partner_id"` - PartnerName string `json:"partner_name"` - PartnerAvatar string `json:"partner_avatar"` - ScoreCount int `json:"score_count"` - ScoreTime int `json:"score_time"` - HostDemoID string `json:"host_demo_id"` - PartnerDemoID string `json:"partner_demo_id"` - RecordDate time.Time `json:"record_date"` -} - type PlayerSummaries struct { SteamId string `json:"steamid"` CommunityVisibilityState int `json:"communityvisibilitystate"` -- cgit v1.2.3 From c709ff5c9f275ff096153c3405b820baa5175231 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Wed, 30 Aug 2023 16:33:25 +0300 Subject: docs: map leaderboards response Former-commit-id: a2c27052fbdacaa0e329270e1dd961602623d5d1 --- docs/docs.go | 119 +++++++++++++++++++++++++----------------------------- docs/swagger.json | 119 +++++++++++++++++++++++++----------------------------- docs/swagger.yaml | 107 ++++++++++++++++++++++++------------------------ 3 files changed, 162 insertions(+), 183 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index d0c78ac..6357150 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -50,7 +50,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.ChapterMapsResponse" + "$ref": "#/definitions/handlers.ChapterMapsResponse" } } } @@ -173,7 +173,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.ChaptersResponse" + "$ref": "#/definitions/handlers.ChaptersResponse" } } } @@ -213,7 +213,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.LoginResponse" + "$ref": "#/definitions/handlers.LoginResponse" } } } @@ -259,7 +259,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/controllers.EditMapImageRequest" + "$ref": "#/definitions/handlers.EditMapImageRequest" } } ], @@ -275,7 +275,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.EditMapImageRequest" + "$ref": "#/definitions/handlers.EditMapImageRequest" } } } @@ -321,19 +321,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "allOf": [ - { - "$ref": "#/definitions/models.Map" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/models.MapRecords" - } - } - } - ] + "$ref": "#/definitions/handlers.MapLeaderboardsResponse" } } } @@ -414,7 +402,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.RecordResponse" + "$ref": "#/definitions/handlers.RecordResponse" } } } @@ -466,7 +454,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.MapSummaryResponse" + "$ref": "#/definitions/handlers.MapSummaryResponse" } } } @@ -510,7 +498,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/controllers.EditMapSummaryRequest" + "$ref": "#/definitions/handlers.EditMapSummaryRequest" } } ], @@ -526,7 +514,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.EditMapSummaryRequest" + "$ref": "#/definitions/handlers.EditMapSummaryRequest" } } } @@ -570,7 +558,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/controllers.CreateMapSummaryRequest" + "$ref": "#/definitions/handlers.CreateMapSummaryRequest" } } ], @@ -586,7 +574,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.CreateMapSummaryRequest" + "$ref": "#/definitions/handlers.CreateMapSummaryRequest" } } } @@ -630,7 +618,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/controllers.DeleteMapSummaryRequest" + "$ref": "#/definitions/handlers.DeleteMapSummaryRequest" } } ], @@ -646,7 +634,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.DeleteMapSummaryRequest" + "$ref": "#/definitions/handlers.DeleteMapSummaryRequest" } } } @@ -695,7 +683,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.ProfileResponse" + "$ref": "#/definitions/handlers.ProfileResponse" } } } @@ -796,7 +784,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.ProfileResponse" + "$ref": "#/definitions/handlers.ProfileResponse" } } } @@ -839,7 +827,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.RankingsResponse" + "$ref": "#/definitions/handlers.RankingsResponse" } } } @@ -884,7 +872,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.SearchResponse" + "$ref": "#/definitions/handlers.SearchResponse" } } } @@ -921,7 +909,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.LoginResponse" + "$ref": "#/definitions/handlers.LoginResponse" } } } @@ -956,7 +944,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.LoginResponse" + "$ref": "#/definitions/handlers.LoginResponse" } } } @@ -1005,7 +993,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.ProfileResponse" + "$ref": "#/definitions/handlers.ProfileResponse" } } } @@ -1029,7 +1017,7 @@ const docTemplate = `{ } }, "definitions": { - "controllers.ChapterMapsResponse": { + "handlers.ChapterMapsResponse": { "type": "object", "properties": { "chapter": { @@ -1043,7 +1031,7 @@ const docTemplate = `{ } } }, - "controllers.ChaptersResponse": { + "handlers.ChaptersResponse": { "type": "object", "properties": { "chapters": { @@ -1057,7 +1045,7 @@ const docTemplate = `{ } } }, - "controllers.CreateMapSummaryRequest": { + "handlers.CreateMapSummaryRequest": { "type": "object", "required": [ "category_id", @@ -1087,7 +1075,7 @@ const docTemplate = `{ } } }, - "controllers.DeleteMapSummaryRequest": { + "handlers.DeleteMapSummaryRequest": { "type": "object", "required": [ "route_id" @@ -1098,7 +1086,7 @@ const docTemplate = `{ } } }, - "controllers.EditMapImageRequest": { + "handlers.EditMapImageRequest": { "type": "object", "required": [ "image" @@ -1109,7 +1097,7 @@ const docTemplate = `{ } } }, - "controllers.EditMapSummaryRequest": { + "handlers.EditMapSummaryRequest": { "type": "object", "required": [ "description", @@ -1139,7 +1127,7 @@ const docTemplate = `{ } } }, - "controllers.LoginResponse": { + "handlers.LoginResponse": { "type": "object", "properties": { "token": { @@ -1147,7 +1135,16 @@ const docTemplate = `{ } } }, - "controllers.MapSummaryResponse": { + "handlers.MapLeaderboardsResponse": { + "type": "object", + "properties": { + "map": { + "$ref": "#/definitions/models.Map" + }, + "records": {} + } + }, + "handlers.MapSummaryResponse": { "type": "object", "properties": { "map": { @@ -1158,21 +1155,21 @@ const docTemplate = `{ } } }, - "controllers.ProfileRankings": { + "handlers.ProfileRankings": { "type": "object", "properties": { "cooperative": { - "$ref": "#/definitions/controllers.ProfileRankingsDetails" + "$ref": "#/definitions/handlers.ProfileRankingsDetails" }, "overall": { - "$ref": "#/definitions/controllers.ProfileRankingsDetails" + "$ref": "#/definitions/handlers.ProfileRankingsDetails" }, "singleplayer": { - "$ref": "#/definitions/controllers.ProfileRankingsDetails" + "$ref": "#/definitions/handlers.ProfileRankingsDetails" } } }, - "controllers.ProfileRankingsDetails": { + "handlers.ProfileRankingsDetails": { "type": "object", "properties": { "completion_count": { @@ -1186,24 +1183,24 @@ const docTemplate = `{ } } }, - "controllers.ProfileRecords": { + "handlers.ProfileRecords": { "type": "object", "properties": { "portal2_cooperative": { "type": "array", "items": { - "$ref": "#/definitions/controllers.ProfileRecordsDetails" + "$ref": "#/definitions/handlers.ProfileRecordsDetails" } }, "portal2_singleplayer": { "type": "array", "items": { - "$ref": "#/definitions/controllers.ProfileRecordsDetails" + "$ref": "#/definitions/handlers.ProfileRecordsDetails" } } } }, - "controllers.ProfileRecordsDetails": { + "handlers.ProfileRecordsDetails": { "type": "object", "properties": { "map_id": { @@ -1215,12 +1212,12 @@ const docTemplate = `{ "scores": { "type": "array", "items": { - "$ref": "#/definitions/controllers.ProfileScores" + "$ref": "#/definitions/handlers.ProfileScores" } } } }, - "controllers.ProfileResponse": { + "handlers.ProfileResponse": { "type": "object", "properties": { "avatar_link": { @@ -1236,10 +1233,10 @@ const docTemplate = `{ "type": "boolean" }, "rankings": { - "$ref": "#/definitions/controllers.ProfileRankings" + "$ref": "#/definitions/handlers.ProfileRankings" }, "records": { - "$ref": "#/definitions/controllers.ProfileRecords" + "$ref": "#/definitions/handlers.ProfileRecords" }, "steam_id": { "type": "string" @@ -1255,7 +1252,7 @@ const docTemplate = `{ } } }, - "controllers.ProfileScores": { + "handlers.ProfileScores": { "type": "object", "properties": { "date": { @@ -1272,7 +1269,7 @@ const docTemplate = `{ } } }, - "controllers.RankingsResponse": { + "handlers.RankingsResponse": { "type": "object", "properties": { "rankings_mp": { @@ -1289,7 +1286,7 @@ const docTemplate = `{ } } }, - "controllers.RecordResponse": { + "handlers.RecordResponse": { "type": "object", "properties": { "score_count": { @@ -1300,7 +1297,7 @@ const docTemplate = `{ } } }, - "controllers.SearchResponse": { + "handlers.SearchResponse": { "type": "object", "properties": { "maps": { @@ -1407,12 +1404,6 @@ const docTemplate = `{ } } }, - "models.MapRecords": { - "type": "object", - "properties": { - "records": {} - } - }, "models.MapRoute": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index cf37a5a..22e30d2 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -43,7 +43,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.ChapterMapsResponse" + "$ref": "#/definitions/handlers.ChapterMapsResponse" } } } @@ -166,7 +166,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.ChaptersResponse" + "$ref": "#/definitions/handlers.ChaptersResponse" } } } @@ -206,7 +206,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.LoginResponse" + "$ref": "#/definitions/handlers.LoginResponse" } } } @@ -252,7 +252,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/controllers.EditMapImageRequest" + "$ref": "#/definitions/handlers.EditMapImageRequest" } } ], @@ -268,7 +268,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.EditMapImageRequest" + "$ref": "#/definitions/handlers.EditMapImageRequest" } } } @@ -314,19 +314,7 @@ "type": "object", "properties": { "data": { - "allOf": [ - { - "$ref": "#/definitions/models.Map" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/models.MapRecords" - } - } - } - ] + "$ref": "#/definitions/handlers.MapLeaderboardsResponse" } } } @@ -407,7 +395,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.RecordResponse" + "$ref": "#/definitions/handlers.RecordResponse" } } } @@ -459,7 +447,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.MapSummaryResponse" + "$ref": "#/definitions/handlers.MapSummaryResponse" } } } @@ -503,7 +491,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/controllers.EditMapSummaryRequest" + "$ref": "#/definitions/handlers.EditMapSummaryRequest" } } ], @@ -519,7 +507,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.EditMapSummaryRequest" + "$ref": "#/definitions/handlers.EditMapSummaryRequest" } } } @@ -563,7 +551,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/controllers.CreateMapSummaryRequest" + "$ref": "#/definitions/handlers.CreateMapSummaryRequest" } } ], @@ -579,7 +567,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.CreateMapSummaryRequest" + "$ref": "#/definitions/handlers.CreateMapSummaryRequest" } } } @@ -623,7 +611,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/controllers.DeleteMapSummaryRequest" + "$ref": "#/definitions/handlers.DeleteMapSummaryRequest" } } ], @@ -639,7 +627,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.DeleteMapSummaryRequest" + "$ref": "#/definitions/handlers.DeleteMapSummaryRequest" } } } @@ -688,7 +676,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.ProfileResponse" + "$ref": "#/definitions/handlers.ProfileResponse" } } } @@ -789,7 +777,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.ProfileResponse" + "$ref": "#/definitions/handlers.ProfileResponse" } } } @@ -832,7 +820,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.RankingsResponse" + "$ref": "#/definitions/handlers.RankingsResponse" } } } @@ -877,7 +865,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.SearchResponse" + "$ref": "#/definitions/handlers.SearchResponse" } } } @@ -914,7 +902,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.LoginResponse" + "$ref": "#/definitions/handlers.LoginResponse" } } } @@ -949,7 +937,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.LoginResponse" + "$ref": "#/definitions/handlers.LoginResponse" } } } @@ -998,7 +986,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/controllers.ProfileResponse" + "$ref": "#/definitions/handlers.ProfileResponse" } } } @@ -1022,7 +1010,7 @@ } }, "definitions": { - "controllers.ChapterMapsResponse": { + "handlers.ChapterMapsResponse": { "type": "object", "properties": { "chapter": { @@ -1036,7 +1024,7 @@ } } }, - "controllers.ChaptersResponse": { + "handlers.ChaptersResponse": { "type": "object", "properties": { "chapters": { @@ -1050,7 +1038,7 @@ } } }, - "controllers.CreateMapSummaryRequest": { + "handlers.CreateMapSummaryRequest": { "type": "object", "required": [ "category_id", @@ -1080,7 +1068,7 @@ } } }, - "controllers.DeleteMapSummaryRequest": { + "handlers.DeleteMapSummaryRequest": { "type": "object", "required": [ "route_id" @@ -1091,7 +1079,7 @@ } } }, - "controllers.EditMapImageRequest": { + "handlers.EditMapImageRequest": { "type": "object", "required": [ "image" @@ -1102,7 +1090,7 @@ } } }, - "controllers.EditMapSummaryRequest": { + "handlers.EditMapSummaryRequest": { "type": "object", "required": [ "description", @@ -1132,7 +1120,7 @@ } } }, - "controllers.LoginResponse": { + "handlers.LoginResponse": { "type": "object", "properties": { "token": { @@ -1140,7 +1128,16 @@ } } }, - "controllers.MapSummaryResponse": { + "handlers.MapLeaderboardsResponse": { + "type": "object", + "properties": { + "map": { + "$ref": "#/definitions/models.Map" + }, + "records": {} + } + }, + "handlers.MapSummaryResponse": { "type": "object", "properties": { "map": { @@ -1151,21 +1148,21 @@ } } }, - "controllers.ProfileRankings": { + "handlers.ProfileRankings": { "type": "object", "properties": { "cooperative": { - "$ref": "#/definitions/controllers.ProfileRankingsDetails" + "$ref": "#/definitions/handlers.ProfileRankingsDetails" }, "overall": { - "$ref": "#/definitions/controllers.ProfileRankingsDetails" + "$ref": "#/definitions/handlers.ProfileRankingsDetails" }, "singleplayer": { - "$ref": "#/definitions/controllers.ProfileRankingsDetails" + "$ref": "#/definitions/handlers.ProfileRankingsDetails" } } }, - "controllers.ProfileRankingsDetails": { + "handlers.ProfileRankingsDetails": { "type": "object", "properties": { "completion_count": { @@ -1179,24 +1176,24 @@ } } }, - "controllers.ProfileRecords": { + "handlers.ProfileRecords": { "type": "object", "properties": { "portal2_cooperative": { "type": "array", "items": { - "$ref": "#/definitions/controllers.ProfileRecordsDetails" + "$ref": "#/definitions/handlers.ProfileRecordsDetails" } }, "portal2_singleplayer": { "type": "array", "items": { - "$ref": "#/definitions/controllers.ProfileRecordsDetails" + "$ref": "#/definitions/handlers.ProfileRecordsDetails" } } } }, - "controllers.ProfileRecordsDetails": { + "handlers.ProfileRecordsDetails": { "type": "object", "properties": { "map_id": { @@ -1208,12 +1205,12 @@ "scores": { "type": "array", "items": { - "$ref": "#/definitions/controllers.ProfileScores" + "$ref": "#/definitions/handlers.ProfileScores" } } } }, - "controllers.ProfileResponse": { + "handlers.ProfileResponse": { "type": "object", "properties": { "avatar_link": { @@ -1229,10 +1226,10 @@ "type": "boolean" }, "rankings": { - "$ref": "#/definitions/controllers.ProfileRankings" + "$ref": "#/definitions/handlers.ProfileRankings" }, "records": { - "$ref": "#/definitions/controllers.ProfileRecords" + "$ref": "#/definitions/handlers.ProfileRecords" }, "steam_id": { "type": "string" @@ -1248,7 +1245,7 @@ } } }, - "controllers.ProfileScores": { + "handlers.ProfileScores": { "type": "object", "properties": { "date": { @@ -1265,7 +1262,7 @@ } } }, - "controllers.RankingsResponse": { + "handlers.RankingsResponse": { "type": "object", "properties": { "rankings_mp": { @@ -1282,7 +1279,7 @@ } } }, - "controllers.RecordResponse": { + "handlers.RecordResponse": { "type": "object", "properties": { "score_count": { @@ -1293,7 +1290,7 @@ } } }, - "controllers.SearchResponse": { + "handlers.SearchResponse": { "type": "object", "properties": { "maps": { @@ -1400,12 +1397,6 @@ } } }, - "models.MapRecords": { - "type": "object", - "properties": { - "records": {} - } - }, "models.MapRoute": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 0dd514d..462679e 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,6 +1,6 @@ basePath: /v1 definitions: - controllers.ChapterMapsResponse: + handlers.ChapterMapsResponse: properties: chapter: $ref: '#/definitions/models.Chapter' @@ -9,7 +9,7 @@ definitions: $ref: '#/definitions/models.MapShort' type: array type: object - controllers.ChaptersResponse: + handlers.ChaptersResponse: properties: chapters: items: @@ -18,7 +18,7 @@ definitions: game: $ref: '#/definitions/models.Game' type: object - controllers.CreateMapSummaryRequest: + handlers.CreateMapSummaryRequest: properties: category_id: type: integer @@ -39,21 +39,21 @@ definitions: - score_count - user_name type: object - controllers.DeleteMapSummaryRequest: + handlers.DeleteMapSummaryRequest: properties: route_id: type: integer required: - route_id type: object - controllers.EditMapImageRequest: + handlers.EditMapImageRequest: properties: image: type: string required: - image type: object - controllers.EditMapSummaryRequest: + handlers.EditMapSummaryRequest: properties: description: type: string @@ -74,28 +74,34 @@ definitions: - score_count - user_name type: object - controllers.LoginResponse: + handlers.LoginResponse: properties: token: type: string type: object - controllers.MapSummaryResponse: + handlers.MapLeaderboardsResponse: + properties: + map: + $ref: '#/definitions/models.Map' + records: {} + type: object + handlers.MapSummaryResponse: properties: map: $ref: '#/definitions/models.Map' summary: $ref: '#/definitions/models.MapSummary' type: object - controllers.ProfileRankings: + handlers.ProfileRankings: properties: cooperative: - $ref: '#/definitions/controllers.ProfileRankingsDetails' + $ref: '#/definitions/handlers.ProfileRankingsDetails' overall: - $ref: '#/definitions/controllers.ProfileRankingsDetails' + $ref: '#/definitions/handlers.ProfileRankingsDetails' singleplayer: - $ref: '#/definitions/controllers.ProfileRankingsDetails' + $ref: '#/definitions/handlers.ProfileRankingsDetails' type: object - controllers.ProfileRankingsDetails: + handlers.ProfileRankingsDetails: properties: completion_count: type: integer @@ -104,18 +110,18 @@ definitions: rank: type: integer type: object - controllers.ProfileRecords: + handlers.ProfileRecords: properties: portal2_cooperative: items: - $ref: '#/definitions/controllers.ProfileRecordsDetails' + $ref: '#/definitions/handlers.ProfileRecordsDetails' type: array portal2_singleplayer: items: - $ref: '#/definitions/controllers.ProfileRecordsDetails' + $ref: '#/definitions/handlers.ProfileRecordsDetails' type: array type: object - controllers.ProfileRecordsDetails: + handlers.ProfileRecordsDetails: properties: map_id: type: integer @@ -123,10 +129,10 @@ definitions: type: string scores: items: - $ref: '#/definitions/controllers.ProfileScores' + $ref: '#/definitions/handlers.ProfileScores' type: array type: object - controllers.ProfileResponse: + handlers.ProfileResponse: properties: avatar_link: type: string @@ -137,9 +143,9 @@ definitions: profile: type: boolean rankings: - $ref: '#/definitions/controllers.ProfileRankings' + $ref: '#/definitions/handlers.ProfileRankings' records: - $ref: '#/definitions/controllers.ProfileRecords' + $ref: '#/definitions/handlers.ProfileRecords' steam_id: type: string titles: @@ -149,7 +155,7 @@ definitions: user_name: type: string type: object - controllers.ProfileScores: + handlers.ProfileScores: properties: date: type: string @@ -160,7 +166,7 @@ definitions: score_time: type: integer type: object - controllers.RankingsResponse: + handlers.RankingsResponse: properties: rankings_mp: items: @@ -171,14 +177,14 @@ definitions: $ref: '#/definitions/models.UserRanking' type: array type: object - controllers.RecordResponse: + handlers.RecordResponse: properties: score_count: type: integer score_time: type: integer type: object - controllers.SearchResponse: + handlers.SearchResponse: properties: maps: items: @@ -247,10 +253,6 @@ definitions: score_count: type: integer type: object - models.MapRecords: - properties: - records: {} - type: object models.MapRoute: properties: category: @@ -340,7 +342,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/controllers.ChapterMapsResponse' + $ref: '#/definitions/handlers.ChapterMapsResponse' type: object "400": description: Bad Request @@ -414,7 +416,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/controllers.ChaptersResponse' + $ref: '#/definitions/handlers.ChaptersResponse' type: object "400": description: Bad Request @@ -437,7 +439,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/controllers.LoginResponse' + $ref: '#/definitions/handlers.LoginResponse' type: object "400": description: Bad Request @@ -464,7 +466,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/controllers.EditMapImageRequest' + $ref: '#/definitions/handlers.EditMapImageRequest' produces: - application/json responses: @@ -475,7 +477,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/controllers.EditMapImageRequest' + $ref: '#/definitions/handlers.EditMapImageRequest' type: object "400": description: Bad Request @@ -502,12 +504,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - allOf: - - $ref: '#/definitions/models.Map' - - properties: - data: - $ref: '#/definitions/models.MapRecords' - type: object + $ref: '#/definitions/handlers.MapLeaderboardsResponse' type: object "400": description: Bad Request @@ -558,7 +555,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/controllers.RecordResponse' + $ref: '#/definitions/handlers.RecordResponse' type: object "400": description: Bad Request @@ -589,7 +586,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/controllers.DeleteMapSummaryRequest' + $ref: '#/definitions/handlers.DeleteMapSummaryRequest' produces: - application/json responses: @@ -600,7 +597,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/controllers.DeleteMapSummaryRequest' + $ref: '#/definitions/handlers.DeleteMapSummaryRequest' type: object "400": description: Bad Request @@ -626,7 +623,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/controllers.MapSummaryResponse' + $ref: '#/definitions/handlers.MapSummaryResponse' type: object "400": description: Bad Request @@ -652,7 +649,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/controllers.CreateMapSummaryRequest' + $ref: '#/definitions/handlers.CreateMapSummaryRequest' produces: - application/json responses: @@ -663,7 +660,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/controllers.CreateMapSummaryRequest' + $ref: '#/definitions/handlers.CreateMapSummaryRequest' type: object "400": description: Bad Request @@ -689,7 +686,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/controllers.EditMapSummaryRequest' + $ref: '#/definitions/handlers.EditMapSummaryRequest' produces: - application/json responses: @@ -700,7 +697,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/controllers.EditMapSummaryRequest' + $ref: '#/definitions/handlers.EditMapSummaryRequest' type: object "400": description: Bad Request @@ -729,7 +726,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/controllers.ProfileResponse' + $ref: '#/definitions/handlers.ProfileResponse' type: object "400": description: Bad Request @@ -761,7 +758,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/controllers.ProfileResponse' + $ref: '#/definitions/handlers.ProfileResponse' type: object "400": description: Bad Request @@ -818,7 +815,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/controllers.RankingsResponse' + $ref: '#/definitions/handlers.RankingsResponse' type: object "400": description: Bad Request @@ -844,7 +841,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/controllers.SearchResponse' + $ref: '#/definitions/handlers.SearchResponse' type: object "400": description: Bad Request @@ -865,7 +862,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/controllers.LoginResponse' + $ref: '#/definitions/handlers.LoginResponse' type: object "404": description: Not Found @@ -885,7 +882,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/controllers.LoginResponse' + $ref: '#/definitions/handlers.LoginResponse' type: object "404": description: Not Found @@ -914,7 +911,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/controllers.ProfileResponse' + $ref: '#/definitions/handlers.ProfileResponse' type: object "400": description: Bad Request -- cgit v1.2.3 From 5a285a5d811a955a8eb380a598595e159e9935e5 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Wed, 30 Aug 2023 16:40:27 +0300 Subject: fix: actually get is_coop data for the given map (#45) Former-commit-id: f29b6ca5d0e23ae622451b011e7d64758a8f598c --- backend/handlers/map.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/handlers/map.go b/backend/handlers/map.go index 9b0caef..0a0206e 100644 --- a/backend/handlers/map.go +++ b/backend/handlers/map.go @@ -139,12 +139,12 @@ func FetchMapLeaderboards(c *gin.Context) { return } response.Map.ID = intID - sql := `SELECT g.name, c.name, m.name, is_disabled, m.image + sql := `SELECT g.name, c.name, m.name, is_disabled, m.image, g.is_coop FROM maps m INNER JOIN games g ON m.game_id = g.id INNER JOIN chapters c ON m.chapter_id = c.id WHERE m.id = $1` - err = database.DB.QueryRow(sql, id).Scan(&response.Map.GameName, &response.Map.ChapterName, &response.Map.MapName, &isDisabled, &response.Map.Image) + err = database.DB.QueryRow(sql, id).Scan(&response.Map.GameName, &response.Map.ChapterName, &response.Map.MapName, &isDisabled, &response.Map.Image, &response.Map.IsCoop) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return -- cgit v1.2.3 From 12ce00fdcc24b9c4e710156e56469acf10a30c27 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Wed, 30 Aug 2023 17:14:41 +0300 Subject: fix: change disabled maps Former-commit-id: ed6aca7192bd4d142107983cf0cda5efc14e0609 --- backend/database/maps.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/database/maps.sql b/backend/database/maps.sql index 50689e2..637d2c2 100644 --- a/backend/database/maps.sql +++ b/backend/database/maps.sql @@ -15,7 +15,7 @@ INSERT INTO maps(game_id, chapter_id, name, description, showcase, is_disabled) (1,2,'Laser Stairs','','',false), (1,2,'Dual Lasers','','',false), (1,2,'Laser Over Goo','','',false), -(1,2,'Catapult Intro','','',true), +(1,2,'Catapult Intro','','',false), (1,2,'Trust Fling','','',false), (1,2,'Pit Flings','','',false), (1,2,'Fizzler Intro','','',false), @@ -71,7 +71,7 @@ INSERT INTO maps(game_id, chapter_id, name, description, showcase, is_disabled) (1,9,'Finale 4','','',false), -- Portal 2 Cooperative -- 0 -(2,10,'Calibration','','',false), +(2,10,'Calibration','','',true), (2,10,'Hub','','',true), -- 1 (2,11,'Doors','','',false), -- cgit v1.2.3 From c40fe29289dbe2a8ea8847d79183fa256268075c Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Wed, 30 Aug 2023 19:14:36 +0300 Subject: feat: finalized profile scores in one array (#51) Former-commit-id: 2a8e03ef339fb40d10b4770b004e35809ba05d7a --- backend/handlers/user.go | 158 ++++++++++++++++++++++++----------------------- 1 file changed, 80 insertions(+), 78 deletions(-) diff --git a/backend/handlers/user.go b/backend/handlers/user.go index 51eadb4..affebb0 100644 --- a/backend/handlers/user.go +++ b/backend/handlers/user.go @@ -1,6 +1,7 @@ package handlers import ( + "log" "net/http" "os" "regexp" @@ -12,15 +13,15 @@ import ( ) type ProfileResponse struct { - Profile bool `json:"profile"` - SteamID string `json:"steam_id"` - UserName string `json:"user_name"` - AvatarLink string `json:"avatar_link"` - CountryCode string `json:"country_code"` - Titles []models.Title `json:"titles"` - Links models.Links `json:"links"` - Rankings ProfileRankings `json:"rankings"` - Records ProfileRecords `json:"records"` + Profile bool `json:"profile"` + SteamID string `json:"steam_id"` + UserName string `json:"user_name"` + AvatarLink string `json:"avatar_link"` + CountryCode string `json:"country_code"` + Titles []models.Title `json:"titles"` + Links models.Links `json:"links"` + Rankings ProfileRankings `json:"rankings"` + Records []ProfileRecords `json:"records"` } type ProfileRankings struct { @@ -34,16 +35,13 @@ type ProfileRankingsDetails struct { CompletionCount int `json:"completion_count"` CompletionTotal int `json:"completion_total"` } - type ProfileRecords struct { - P2Singleplayer []ProfileRecordsDetails `json:"portal2_singleplayer"` - P2Cooperative []ProfileRecordsDetails `json:"portal2_cooperative"` -} - -type ProfileRecordsDetails struct { - MapID int `json:"map_id"` - MapName string `json:"map_name"` - Scores []ProfileScores `json:"scores"` + GameID int `json:"game_id"` + CategoryID int `json:"category_id"` + MapID int `json:"map_id"` + MapName string `json:"map_name"` + MapWRCount int `json:"map_wr_count"` + Scores []ProfileScores `json:"scores"` } type ProfileScores struct { @@ -85,42 +83,42 @@ func Profile(c *gin.Context) { return } // TODO: Get rankings (all maps done in one game) - records := ProfileRecords{ - P2Singleplayer: []ProfileRecordsDetails{}, - P2Cooperative: []ProfileRecordsDetails{}, - } + records := []ProfileRecords{} // Get singleplayer records - sql = `SELECT m.game_id, sp.map_id, m."name", sp.score_count, sp.score_time, sp.demo_id, sp.record_date + sql = `SELECT m.game_id, m.chapter_id, sp.map_id, m."name", (SELECT mr.score_count FROM map_routes mr WHERE mr.map_id = sp.map_id ORDER BY mr.score_count ASC LIMIT 1) AS wr_count, sp.score_count, sp.score_time, sp.demo_id, sp.record_date FROM records_sp sp INNER JOIN maps m ON sp.map_id = m.id WHERE sp.user_id = $1 ORDER BY sp.map_id, sp.score_count, sp.score_time;` rows, err := database.DB.Query(sql, user.(models.User).SteamID) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } + log.Println("rows:", rows) for rows.Next() { + var gameID int + var categoryID int var mapID int var mapName string - var gameID int + var mapWR int score := ProfileScores{} - rows.Scan(&gameID, &mapID, &mapName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) - if gameID != 1 { - continue - } + rows.Scan(&gameID, &categoryID, &mapID, &mapName, &mapWR, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) // More than one record in one map - if len(records.P2Singleplayer) != 0 && mapID == records.P2Singleplayer[len(records.P2Singleplayer)-1].MapID { - records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores = append(records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores, score) + if len(records) != 0 && mapID == records[len(records)-1].MapID { + records[len(records)-1].Scores = append(records[len(records)-1].Scores, score) continue } // New map - records.P2Singleplayer = append(records.P2Singleplayer, ProfileRecordsDetails{ - MapID: mapID, - MapName: mapName, - Scores: []ProfileScores{}, + records = append(records, ProfileRecords{ + GameID: gameID, + CategoryID: categoryID, + MapID: mapID, + MapName: mapName, + MapWRCount: mapWR, + Scores: []ProfileScores{}, }) - records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores = append(records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores, score) + records[len(records)-1].Scores = append(records[len(records)-1].Scores, score) } // Get multiplayer records - sql = `SELECT m.game_id, mp.map_id, m."name", mp.score_count, mp.score_time, CASE WHEN host_id = $1 THEN mp.host_demo_id WHEN partner_id = $1 THEN mp.partner_demo_id END demo_id, mp.record_date + sql = `SELECT m.game_id, m.chapter_id, mp.map_id, m."name", (SELECT mr.score_count FROM map_routes mr WHERE mr.map_id = mp.map_id ORDER BY mr.score_count ASC LIMIT 1) AS wr_count, mp.score_count, mp.score_time, CASE WHEN host_id = $1 THEN mp.host_demo_id WHEN partner_id = $1 THEN mp.partner_demo_id END demo_id, mp.record_date FROM records_mp mp INNER JOIN maps m ON mp.map_id = m.id WHERE mp.host_id = $1 OR mp.partner_id = $1 ORDER BY mp.map_id, mp.score_count, mp.score_time;` rows, err = database.DB.Query(sql, user.(models.User).SteamID) if err != nil { @@ -128,26 +126,28 @@ func Profile(c *gin.Context) { return } for rows.Next() { + var gameID int + var categoryID int var mapID int var mapName string - var gameID int + var mapWR int score := ProfileScores{} - rows.Scan(&gameID, &mapID, &mapName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) - if gameID != 1 { - continue - } + rows.Scan(&gameID, &categoryID, &mapID, &mapName, &mapWR, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) // More than one record in one map - if len(records.P2Cooperative) != 0 && mapID == records.P2Cooperative[len(records.P2Cooperative)-1].MapID { - records.P2Cooperative[len(records.P2Cooperative)-1].Scores = append(records.P2Cooperative[len(records.P2Cooperative)-1].Scores, score) + if len(records) != 0 && mapID == records[len(records)-1].MapID { + records[len(records)-1].Scores = append(records[len(records)-1].Scores, score) continue } // New map - records.P2Cooperative = append(records.P2Cooperative, ProfileRecordsDetails{ - MapID: mapID, - MapName: mapName, - Scores: []ProfileScores{}, + records = append(records, ProfileRecords{ + GameID: gameID, + CategoryID: categoryID, + MapID: mapID, + MapName: mapName, + MapWRCount: mapWR, + Scores: []ProfileScores{}, }) - records.P2Cooperative[len(records.P2Cooperative)-1].Scores = append(records.P2Cooperative[len(records.P2Cooperative)-1].Scores, score) + records[len(records)-1].Scores = append(records[len(records)-1].Scores, score) } c.JSON(http.StatusOK, models.Response{ Success: true, @@ -216,42 +216,42 @@ func FetchUser(c *gin.Context) { user.Titles = append(user.Titles, title) } // TODO: Get rankings (all maps done in one game) - records := ProfileRecords{ - P2Singleplayer: []ProfileRecordsDetails{}, - P2Cooperative: []ProfileRecordsDetails{}, - } + records := []ProfileRecords{} // Get singleplayer records - sql = `SELECT m.game_id, sp.map_id, m."name", sp.score_count, sp.score_time, sp.demo_id, sp.record_date + sql = `SELECT m.game_id, m.chapter_id, sp.map_id, m."name", (SELECT mr.score_count FROM map_routes mr WHERE mr.map_id = sp.map_id ORDER BY mr.score_count ASC LIMIT 1) AS wr_count, sp.score_count, sp.score_time, sp.demo_id, sp.record_date FROM records_sp sp INNER JOIN maps m ON sp.map_id = m.id WHERE sp.user_id = $1 ORDER BY sp.map_id, sp.score_count, sp.score_time;` rows, err = database.DB.Query(sql, user.SteamID) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } + log.Println("rows:", rows) for rows.Next() { + var gameID int + var categoryID int var mapID int var mapName string - var gameID int + var mapWR int score := ProfileScores{} - rows.Scan(&gameID, &mapID, &mapName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) - if gameID != 1 { - continue - } + rows.Scan(&gameID, &categoryID, &mapID, &mapName, &mapWR, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) // More than one record in one map - if len(records.P2Singleplayer) != 0 && mapID == records.P2Singleplayer[len(records.P2Singleplayer)-1].MapID { - records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores = append(records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores, score) + if len(records) != 0 && mapID == records[len(records)-1].MapID { + records[len(records)-1].Scores = append(records[len(records)-1].Scores, score) continue } // New map - records.P2Singleplayer = append(records.P2Singleplayer, ProfileRecordsDetails{ - MapID: mapID, - MapName: mapName, - Scores: []ProfileScores{}, + records = append(records, ProfileRecords{ + GameID: gameID, + CategoryID: categoryID, + MapID: mapID, + MapName: mapName, + MapWRCount: mapWR, + Scores: []ProfileScores{}, }) - records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores = append(records.P2Singleplayer[len(records.P2Singleplayer)-1].Scores, score) + records[len(records)-1].Scores = append(records[len(records)-1].Scores, score) } // Get multiplayer records - sql = `SELECT m.game_id, mp.map_id, m."name", mp.score_count, mp.score_time, CASE WHEN host_id = $1 THEN mp.host_demo_id WHEN partner_id = $1 THEN mp.partner_demo_id END demo_id, mp.record_date + sql = `SELECT m.game_id, m.chapter_id, mp.map_id, m."name", (SELECT mr.score_count FROM map_routes mr WHERE mr.map_id = mp.map_id ORDER BY mr.score_count ASC LIMIT 1) AS wr_count, mp.score_count, mp.score_time, CASE WHEN host_id = $1 THEN mp.host_demo_id WHEN partner_id = $1 THEN mp.partner_demo_id END demo_id, mp.record_date FROM records_mp mp INNER JOIN maps m ON mp.map_id = m.id WHERE mp.host_id = $1 OR mp.partner_id = $1 ORDER BY mp.map_id, mp.score_count, mp.score_time;` rows, err = database.DB.Query(sql, user.SteamID) if err != nil { @@ -259,26 +259,28 @@ func FetchUser(c *gin.Context) { return } for rows.Next() { + var gameID int + var categoryID int var mapID int var mapName string - var gameID int + var mapWR int score := ProfileScores{} - rows.Scan(&gameID, &mapID, &mapName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) - if gameID != 1 { - continue - } + rows.Scan(&gameID, &categoryID, &mapID, &mapName, &mapWR, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) // More than one record in one map - if len(records.P2Cooperative) != 0 && mapID == records.P2Cooperative[len(records.P2Cooperative)-1].MapID { - records.P2Cooperative[len(records.P2Cooperative)-1].Scores = append(records.P2Cooperative[len(records.P2Cooperative)-1].Scores, score) + if len(records) != 0 && mapID == records[len(records)-1].MapID { + records[len(records)-1].Scores = append(records[len(records)-1].Scores, score) continue } // New map - records.P2Cooperative = append(records.P2Cooperative, ProfileRecordsDetails{ - MapID: mapID, - MapName: mapName, - Scores: []ProfileScores{}, + records = append(records, ProfileRecords{ + GameID: gameID, + CategoryID: categoryID, + MapID: mapID, + MapName: mapName, + MapWRCount: mapWR, + Scores: []ProfileScores{}, }) - records.P2Cooperative[len(records.P2Cooperative)-1].Scores = append(records.P2Cooperative[len(records.P2Cooperative)-1].Scores, score) + records[len(records)-1].Scores = append(records[len(records)-1].Scores, score) } c.JSON(http.StatusOK, models.Response{ Success: true, -- cgit v1.2.3 From 1e41c244446b81f3fdb7ebb3028cd68eaa919982 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Wed, 30 Aug 2023 19:14:46 +0300 Subject: docs: finalized profile scores in one array (#51) Former-commit-id: 93b44c71389b5ff98d9a610611be21fd12c88664 --- docs/docs.go | 29 ++++++++++++----------------- docs/swagger.json | 29 ++++++++++++----------------- docs/swagger.yaml | 21 +++++++++------------ 3 files changed, 33 insertions(+), 46 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 6357150..72a1434 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1186,29 +1186,21 @@ const docTemplate = `{ "handlers.ProfileRecords": { "type": "object", "properties": { - "portal2_cooperative": { - "type": "array", - "items": { - "$ref": "#/definitions/handlers.ProfileRecordsDetails" - } + "category_id": { + "type": "integer" + }, + "game_id": { + "type": "integer" }, - "portal2_singleplayer": { - "type": "array", - "items": { - "$ref": "#/definitions/handlers.ProfileRecordsDetails" - } - } - } - }, - "handlers.ProfileRecordsDetails": { - "type": "object", - "properties": { "map_id": { "type": "integer" }, "map_name": { "type": "string" }, + "map_wr_count": { + "type": "integer" + }, "scores": { "type": "array", "items": { @@ -1236,7 +1228,10 @@ const docTemplate = `{ "$ref": "#/definitions/handlers.ProfileRankings" }, "records": { - "$ref": "#/definitions/handlers.ProfileRecords" + "type": "array", + "items": { + "$ref": "#/definitions/handlers.ProfileRecords" + } }, "steam_id": { "type": "string" diff --git a/docs/swagger.json b/docs/swagger.json index 22e30d2..6fbe02e 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1179,29 +1179,21 @@ "handlers.ProfileRecords": { "type": "object", "properties": { - "portal2_cooperative": { - "type": "array", - "items": { - "$ref": "#/definitions/handlers.ProfileRecordsDetails" - } + "category_id": { + "type": "integer" + }, + "game_id": { + "type": "integer" }, - "portal2_singleplayer": { - "type": "array", - "items": { - "$ref": "#/definitions/handlers.ProfileRecordsDetails" - } - } - } - }, - "handlers.ProfileRecordsDetails": { - "type": "object", - "properties": { "map_id": { "type": "integer" }, "map_name": { "type": "string" }, + "map_wr_count": { + "type": "integer" + }, "scores": { "type": "array", "items": { @@ -1229,7 +1221,10 @@ "$ref": "#/definitions/handlers.ProfileRankings" }, "records": { - "$ref": "#/definitions/handlers.ProfileRecords" + "type": "array", + "items": { + "$ref": "#/definitions/handlers.ProfileRecords" + } }, "steam_id": { "type": "string" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 462679e..4b59ee9 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -112,21 +112,16 @@ definitions: type: object handlers.ProfileRecords: properties: - portal2_cooperative: - items: - $ref: '#/definitions/handlers.ProfileRecordsDetails' - type: array - portal2_singleplayer: - items: - $ref: '#/definitions/handlers.ProfileRecordsDetails' - type: array - type: object - handlers.ProfileRecordsDetails: - properties: + category_id: + type: integer + game_id: + type: integer map_id: type: integer map_name: type: string + map_wr_count: + type: integer scores: items: $ref: '#/definitions/handlers.ProfileScores' @@ -145,7 +140,9 @@ definitions: rankings: $ref: '#/definitions/handlers.ProfileRankings' records: - $ref: '#/definitions/handlers.ProfileRecords' + items: + $ref: '#/definitions/handlers.ProfileRecords' + type: array steam_id: type: string titles: -- cgit v1.2.3 From e2739241c4a1b4d12f93c967a69779a50aece5aa Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Wed, 30 Aug 2023 20:14:27 +0300 Subject: docs: added privacy policy Former-commit-id: 891383147a4ec44c3647f71752a7928dcc2bcd3c --- README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e1d8c80..e1bc0b7 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,15 @@ If you have any questions or feedback, please feel free to contact us at our [Di If you want to support the creator, you can do it via using GitHub sponsorships by clicking [here](https://github.com/sponsors/pektezol). +## Privacy Policy + +* We store a JWT (JSON Web Token) on your device as a cookie to facilitate authentication to LPHUB. +* We collect and store your publicly available id, username, avatar link, and country code from Steam during your first login to LPHUB. The exact date and time of your LPHUB account creation is also stored. +* In a case of profile update, newly changed values replaces the old data, and the update time is also stored. +* All of the demo proof submitted by users are stored in a publicly accessible Google Drive folder. By submitting demo as a proof, you agree that your demo proof becomes available to the public. +* Any information, ideas, or solutions shared on LPHUB are intended to be openly accessible and available for collaborative purposes. Users should understand that the content they contribute might be viewed and used by others for the purpose of generating insights and solutions. Sharing content on this platform does not transfer ownership of intellectual property rights to the platform owner or other users. Contributors retain their rights to their own content while granting others the opportunity to engage and collaborate. We do not place emphasis on determining ownership of individual contributions. +* At any time, Privacy Policy may get updated to reflect changes in LPHUB. The effective date at the bottom of the README indicates when the most recent changes were made. + ## Disclaimer This project, "Portal 2 Least Portals Hub" (hereafter referred to as "LPHUB"), is an unofficial community-driven resource providing strategies, routes, leaderboards, and other information related to the "Least Portals" category of the game "Portal 2". LPHUB is not affiliated with or endorsed by the creators, developers, or publishers of "Portal 2", including but not limited to Valve Corporation. @@ -48,4 +57,8 @@ The source code for LPHUB is licensed under the GNU Affero General Public Licens Your use of LPHUB is at your own risk. LPHUB and its administrators and moderators disclaim all liability for any damages, whether direct, indirect, incidental, or consequential, that may result from your use of LPHUB or the strategies, routes, or other information provided therein. -By using LPHUB, you acknowledge that you have read and understood this disclaimer and agree to its terms. \ No newline at end of file +By using LPHUB, you acknowledge that you have read and understood this disclaimer and agree to its terms. + +## Last Update + +2023-08-30 \ No newline at end of file -- cgit v1.2.3 From 69c5423f7954b641109166e03ad0ab174b3d55c6 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Sat, 2 Sep 2023 11:09:42 +0300 Subject: feat: testing logging system (#55) Former-commit-id: d8c5fda30ab08b42218aead1febdf83200948763 --- backend/api/routes.go | 2 + backend/database/init.sql | 10 ++++ backend/handlers/login.go | 4 ++ backend/handlers/logs.go | 114 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+) create mode 100644 backend/handlers/logs.go diff --git a/backend/api/routes.go b/backend/api/routes.go index 4dd8660..ac622d4 100644 --- a/backend/api/routes.go +++ b/backend/api/routes.go @@ -36,5 +36,7 @@ func InitRoutes(router *gin.Engine) { v1.GET("/games", handlers.FetchGames) v1.GET("/games/:id", handlers.FetchChapters) v1.GET("/chapters/:id", handlers.FetchChapterMaps) + v1.GET("/logs/score", handlers.ScoreLogs) + v1.GET("/logs/mod", CheckAuth, handlers.ModLogs) } } diff --git a/backend/database/init.sql b/backend/database/init.sql index 25de872..abace5c 100644 --- a/backend/database/init.sql +++ b/backend/database/init.sql @@ -139,3 +139,13 @@ CREATE TABLE countries ( country_name TEXT NOT NULL, PRIMARY KEY (country_code) ); + +CREATE TABLE logs ( + id SERIAL, + user_id TEXT NOT NULL, + type TEXT NOT NULL, + description TEXT NOT NULL, + date TIMESTAMP NOT NULL DEFAULT now(), + PRIMARY KEY (id), + FOREIGN KEY (user_id) REFERENCES users(steam_id) +); \ No newline at end of file diff --git a/backend/handlers/login.go b/backend/handlers/login.go index 4b151c2..80f697e 100644 --- a/backend/handlers/login.go +++ b/backend/handlers/login.go @@ -38,6 +38,7 @@ func Login(c *gin.Context) { default: steamID, err := openID.ValidateAndGetId() if err != nil { + CreateLog(steamID, LogTypeLogin, LogDescriptionLoginFailValidate) c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } @@ -48,6 +49,7 @@ func Login(c *gin.Context) { if checkSteamID == 0 { user, err := GetPlayerSummaries(steamID, os.Getenv("API_KEY")) if err != nil { + CreateLog(steamID, LogTypeLogin, LogDescriptionLoginFailSummary) c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } @@ -77,10 +79,12 @@ func Login(c *gin.Context) { // Sign and get the complete encoded token as a string using the secret tokenString, err := token.SignedString([]byte(os.Getenv("SECRET_KEY"))) if err != nil { + CreateLog(steamID, LogTypeLogin, LogDescriptionLoginFailToken) c.JSON(http.StatusBadRequest, models.ErrorResponse("Failed to generate token.")) return } c.SetCookie("token", tokenString, 3600*24*30, "/", "", true, true) + CreateLog(steamID, LogTypeLogin, LogDescriptionLoginSuccess) c.Redirect(http.StatusTemporaryRedirect, "/") // c.JSON(http.StatusOK, models.Response{ // Success: true, diff --git a/backend/handlers/logs.go b/backend/handlers/logs.go new file mode 100644 index 0000000..2b5713d --- /dev/null +++ b/backend/handlers/logs.go @@ -0,0 +1,114 @@ +package handlers + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/pektezol/leastportalshub/backend/database" + "github.com/pektezol/leastportalshub/backend/models" +) + +const ( + LogTypeMod string = "Mod" + LogTypeScore string = "Score" + LogTypeLogin string = "Login" + + LogDescriptionLoginSuccess string = "Success" + LogDescriptionLoginFailToken string = "TokenFail" + LogDescriptionLoginFailValidate string = "ValidateFail" + LogDescriptionLoginFailSummary string = "SummaryFail" +) + +type Log struct { + User models.UserShort `json:"user"` + Type string `json:"type"` + Description string `json:"description"` +} + +type LogsResponse struct { + Logs []LogsResponseDetails `json:"logs"` +} + +type LogsResponseDetails struct { + User models.UserShort `json:"user"` + Log string `json:"detail"` +} + +func ModLogs(c *gin.Context) { + mod, exists := c.Get("mod") + if !exists || !mod.(bool) { + c.JSON(http.StatusUnauthorized, models.ErrorResponse("Insufficient permissions.")) + return + } + response := LogsResponse{Logs: []LogsResponseDetails{}} + sql := `SELECT u.user_name, l.user_id, l.type, l.description + FROM logs l INNER JOIN users u ON l.user_id = u.steam_id WHERE type != 'Score'` + rows, err := database.DB.Query(sql) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + for rows.Next() { + log := Log{} + err = rows.Scan(log.User.UserName, log.User.SteamID, log.Type, log.Description) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + detail := fmt.Sprintf("%s.%s", log.Type, log.Description) + response.Logs = append(response.Logs, LogsResponseDetails{ + User: models.UserShort{ + SteamID: log.User.SteamID, + UserName: log.User.UserName, + }, + Log: detail, + }) + } + c.JSON(http.StatusOK, models.Response{ + Success: true, + Message: "Successfully retrieved logs.", + Data: response, + }) +} + +func ScoreLogs(c *gin.Context) { + response := LogsResponse{Logs: []LogsResponseDetails{}} + sql := `SELECT u.user_name, l.user_id, l.type, l.description + FROM logs l INNER JOIN users u ON l.user_id = u.steam_id WHERE type = 'Score'` + rows, err := database.DB.Query(sql) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + for rows.Next() { + log := Log{} + err = rows.Scan(log.User.UserName, log.User.SteamID, log.Type, log.Description) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + detail := fmt.Sprintf("%s.%s", log.Type, log.Description) + response.Logs = append(response.Logs, LogsResponseDetails{ + User: models.UserShort{ + SteamID: log.User.SteamID, + UserName: log.User.UserName, + }, + Log: detail, + }) + } + c.JSON(http.StatusOK, models.Response{ + Success: true, + Message: "Successfully retrieved score logs.", + Data: response, + }) +} + +func CreateLog(user_id string, log_type string, log_description string) (err error) { + sql := `INSERT INTO logs (user_id, "type", description) VALUES($1, $2, $3)` + _, err = database.DB.Exec(sql) + if err != nil { + return err + } + return nil +} -- cgit v1.2.3 From 62de256539511038bb899eb85d3b931dfe1f4c4d Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Sat, 2 Sep 2023 11:16:37 +0300 Subject: fix: actually pass the parameters for insert (#55) Former-commit-id: 5b532bdc1797151e3d5fbfa365c5126d1f8bab71 --- backend/handlers/logs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/handlers/logs.go b/backend/handlers/logs.go index 2b5713d..3eadbb0 100644 --- a/backend/handlers/logs.go +++ b/backend/handlers/logs.go @@ -106,7 +106,7 @@ func ScoreLogs(c *gin.Context) { func CreateLog(user_id string, log_type string, log_description string) (err error) { sql := `INSERT INTO logs (user_id, "type", description) VALUES($1, $2, $3)` - _, err = database.DB.Exec(sql) + _, err = database.DB.Exec(sql, user_id, log_type, log_description) if err != nil { return err } -- cgit v1.2.3 From b4a7ac2f7914064b6c9a55fcea6d0105bf941c00 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Sat, 2 Sep 2023 11:18:00 +0300 Subject: fix: row scan pointers (#55) Former-commit-id: ffcbfb6b07e1d7e41b19aba6dc8e3b2d261bf910 --- backend/handlers/logs.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/handlers/logs.go b/backend/handlers/logs.go index 3eadbb0..31fd6fd 100644 --- a/backend/handlers/logs.go +++ b/backend/handlers/logs.go @@ -51,7 +51,7 @@ func ModLogs(c *gin.Context) { } for rows.Next() { log := Log{} - err = rows.Scan(log.User.UserName, log.User.SteamID, log.Type, log.Description) + err = rows.Scan(&log.User.UserName, &log.User.SteamID, &log.Type, &log.Description) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return @@ -83,7 +83,7 @@ func ScoreLogs(c *gin.Context) { } for rows.Next() { log := Log{} - err = rows.Scan(log.User.UserName, log.User.SteamID, log.Type, log.Description) + err = rows.Scan(&log.User.UserName, &log.User.SteamID, &log.Type, &log.Description) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return -- cgit v1.2.3 From 5ee6c4abb03fdb583e94496306627d9d3db62629 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Sat, 2 Sep 2023 13:40:58 +0300 Subject: feat: finalize score logs (#55) Former-commit-id: 5151b31745579814336797091d52c6252b14a4c2 --- backend/handlers/logs.go | 57 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/backend/handlers/logs.go b/backend/handlers/logs.go index 31fd6fd..c77d643 100644 --- a/backend/handlers/logs.go +++ b/backend/handlers/logs.go @@ -3,6 +3,7 @@ package handlers import ( "fmt" "net/http" + "time" "github.com/gin-gonic/gin" "github.com/pektezol/leastportalshub/backend/database" @@ -11,7 +12,6 @@ import ( const ( LogTypeMod string = "Mod" - LogTypeScore string = "Score" LogTypeLogin string = "Login" LogDescriptionLoginSuccess string = "Success" @@ -35,6 +35,19 @@ type LogsResponseDetails struct { Log string `json:"detail"` } +type ScoreLogsResponse struct { + Logs []ScoreLogsResponseDetails `json:"scores"` +} + +type ScoreLogsResponseDetails struct { + User models.UserShort `json:"user"` + Map models.MapShort `json:"map"` + ScoreCount int `json:"score_count"` + ScoreTime int `json:"score_time"` + DemoID string `json:"demo_id"` + Date time.Time `json:"date"` +} + func ModLogs(c *gin.Context) { mod, exists := c.Get("mod") if !exists || !mod.(bool) { @@ -73,29 +86,45 @@ func ModLogs(c *gin.Context) { } func ScoreLogs(c *gin.Context) { - response := LogsResponse{Logs: []LogsResponseDetails{}} - sql := `SELECT u.user_name, l.user_id, l.type, l.description - FROM logs l INNER JOIN users u ON l.user_id = u.steam_id WHERE type = 'Score'` + response := ScoreLogsResponse{Logs: []ScoreLogsResponseDetails{}} + sql := `SELECT rs.map_id, + m.name AS map_name, + u.steam_id, + u.user_name, + rs.score_count, + rs.score_time, + rs.demo_id, + rs.record_date + FROM ( + SELECT id, map_id, user_id, score_count, score_time, demo_id, record_date + FROM public.records_sp + + UNION ALL + + SELECT id, map_id, host_id AS user_id, score_count, score_time, host_demo_id AS demo_id, record_date + FROM public.records_mp + + UNION ALL + + SELECT id, map_id, partner_id AS user_id, score_count, score_time, partner_demo_id AS demo_id, record_date + FROM public.records_mp + ) AS rs + JOIN public.users AS u ON rs.user_id = u.steam_id + JOIN public.maps AS m ON rs.map_id = m.id + ORDER BY rs.record_date DESC LIMIT 100;` rows, err := database.DB.Query(sql) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } for rows.Next() { - log := Log{} - err = rows.Scan(&log.User.UserName, &log.User.SteamID, &log.Type, &log.Description) + score := ScoreLogsResponseDetails{} + err = rows.Scan(&score.Map.ID, &score.Map.Name, &score.User.SteamID, &score.User.UserName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } - detail := fmt.Sprintf("%s.%s", log.Type, log.Description) - response.Logs = append(response.Logs, LogsResponseDetails{ - User: models.UserShort{ - SteamID: log.User.SteamID, - UserName: log.User.UserName, - }, - Log: detail, - }) + response.Logs = append(response.Logs, score) } c.JSON(http.StatusOK, models.Response{ Success: true, -- cgit v1.2.3 From bef18a1641c8c9f0a8faf7d613729f439dc80a82 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Sat, 2 Sep 2023 13:48:55 +0300 Subject: feat: sike, add game to score logs (#55) Former-commit-id: 4ffa3b1269c0859b6cc85defc3fa6e095102252a --- backend/handlers/logs.go | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/backend/handlers/logs.go b/backend/handlers/logs.go index c77d643..0ebba47 100644 --- a/backend/handlers/logs.go +++ b/backend/handlers/logs.go @@ -40,6 +40,7 @@ type ScoreLogsResponse struct { } type ScoreLogsResponseDetails struct { + Game models.Game `json:"game"` User models.UserShort `json:"user"` Map models.MapShort `json:"map"` ScoreCount int `json:"score_count"` @@ -87,7 +88,10 @@ func ModLogs(c *gin.Context) { func ScoreLogs(c *gin.Context) { response := ScoreLogsResponse{Logs: []ScoreLogsResponseDetails{}} - sql := `SELECT rs.map_id, + sql := `SELECT g.id, + g."name", + g.is_coop, + rs.map_id, m.name AS map_name, u.steam_id, u.user_name, @@ -97,20 +101,21 @@ func ScoreLogs(c *gin.Context) { rs.record_date FROM ( SELECT id, map_id, user_id, score_count, score_time, demo_id, record_date - FROM public.records_sp + FROM records_sp UNION ALL SELECT id, map_id, host_id AS user_id, score_count, score_time, host_demo_id AS demo_id, record_date - FROM public.records_mp + FROM records_mp UNION ALL SELECT id, map_id, partner_id AS user_id, score_count, score_time, partner_demo_id AS demo_id, record_date - FROM public.records_mp + FROM records_mp ) AS rs - JOIN public.users AS u ON rs.user_id = u.steam_id - JOIN public.maps AS m ON rs.map_id = m.id + JOIN users u ON rs.user_id = u.steam_id + JOIN maps m ON rs.map_id = m.id + JOIN games g ON m.game_id = g.id ORDER BY rs.record_date DESC LIMIT 100;` rows, err := database.DB.Query(sql) if err != nil { @@ -119,7 +124,7 @@ func ScoreLogs(c *gin.Context) { } for rows.Next() { score := ScoreLogsResponseDetails{} - err = rows.Scan(&score.Map.ID, &score.Map.Name, &score.User.SteamID, &score.User.UserName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) + err = rows.Scan(&score.Game.ID, &score.Game.Name, &score.Game.IsCoop, &score.Map.ID, &score.Map.Name, &score.User.SteamID, &score.User.UserName, &score.ScoreCount, &score.ScoreTime, &score.DemoID, &score.Date) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return -- cgit v1.2.3 From 60fcf785ced334998e8dd19aea3b71be6a2db14f Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Sat, 2 Sep 2023 13:52:45 +0300 Subject: docs: update docs for logs (#55) Former-commit-id: 0478aa459bfe9a7c81073d4ff8d8264aa1258bc1 --- backend/handlers/logs.go | 17 +++++++ docs/docs.go | 130 ++++++++++++++++++++++++++++++++++++++++++++++- docs/swagger.json | 130 ++++++++++++++++++++++++++++++++++++++++++++++- docs/swagger.yaml | 80 ++++++++++++++++++++++++++++- 4 files changed, 351 insertions(+), 6 deletions(-) diff --git a/backend/handlers/logs.go b/backend/handlers/logs.go index 0ebba47..5233738 100644 --- a/backend/handlers/logs.go +++ b/backend/handlers/logs.go @@ -49,6 +49,15 @@ type ScoreLogsResponseDetails struct { Date time.Time `json:"date"` } +// GET Mod Logs +// +// @Description Get mod logs. +// @Tags rankings +// @Produce json +// @Param Authorization header string true "JWT Token" +// @Success 200 {object} models.Response{data=ScoreLogsResponse} +// @Failure 400 {object} models.Response +// @Router /logs/mod [get] func ModLogs(c *gin.Context) { mod, exists := c.Get("mod") if !exists || !mod.(bool) { @@ -86,6 +95,14 @@ func ModLogs(c *gin.Context) { }) } +// GET Score Logs +// +// @Description Get score logs of every player. +// @Tags rankings +// @Produce json +// @Success 200 {object} models.Response{data=ScoreLogsResponse} +// @Failure 400 {object} models.Response +// @Router /logs/score [get] func ScoreLogs(c *gin.Context) { response := ScoreLogsResponse{Logs: []ScoreLogsResponseDetails{}} sql := `SELECT g.id, diff --git a/docs/docs.go b/docs/docs.go index 72a1434..3d140cc 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -229,6 +229,89 @@ const docTemplate = `{ } } }, + "/logs/mod": { + "get": { + "description": "Get mod logs.", + "produces": [ + "application/json" + ], + "tags": [ + "rankings" + ], + "parameters": [ + { + "type": "string", + "description": "JWT Token", + "name": "Authorization", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/models.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.ScoreLogsResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.Response" + } + } + } + } + }, + "/logs/score": { + "get": { + "description": "Get score logs of every player.", + "produces": [ + "application/json" + ], + "tags": [ + "rankings" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/models.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.ScoreLogsResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.Response" + } + } + } + } + }, "/maps/{id}/image": { "put": { "description": "Edit map image with specified map id.", @@ -1267,13 +1350,19 @@ const docTemplate = `{ "handlers.RankingsResponse": { "type": "object", "properties": { - "rankings_mp": { + "rankings_multiplayer": { + "type": "array", + "items": { + "$ref": "#/definitions/models.UserRanking" + } + }, + "rankings_overall": { "type": "array", "items": { "$ref": "#/definitions/models.UserRanking" } }, - "rankings_sp": { + "rankings_singleplayer": { "type": "array", "items": { "$ref": "#/definitions/models.UserRanking" @@ -1292,6 +1381,43 @@ const docTemplate = `{ } } }, + "handlers.ScoreLogsResponse": { + "type": "object", + "properties": { + "scores": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.ScoreLogsResponseDetails" + } + } + } + }, + "handlers.ScoreLogsResponseDetails": { + "type": "object", + "properties": { + "date": { + "type": "string" + }, + "demo_id": { + "type": "string" + }, + "game": { + "$ref": "#/definitions/models.Game" + }, + "map": { + "$ref": "#/definitions/models.MapShort" + }, + "score_count": { + "type": "integer" + }, + "score_time": { + "type": "integer" + }, + "user": { + "$ref": "#/definitions/models.UserShort" + } + } + }, "handlers.SearchResponse": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 6fbe02e..f55190e 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -222,6 +222,89 @@ } } }, + "/logs/mod": { + "get": { + "description": "Get mod logs.", + "produces": [ + "application/json" + ], + "tags": [ + "rankings" + ], + "parameters": [ + { + "type": "string", + "description": "JWT Token", + "name": "Authorization", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/models.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.ScoreLogsResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.Response" + } + } + } + } + }, + "/logs/score": { + "get": { + "description": "Get score logs of every player.", + "produces": [ + "application/json" + ], + "tags": [ + "rankings" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/models.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handlers.ScoreLogsResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/models.Response" + } + } + } + } + }, "/maps/{id}/image": { "put": { "description": "Edit map image with specified map id.", @@ -1260,13 +1343,19 @@ "handlers.RankingsResponse": { "type": "object", "properties": { - "rankings_mp": { + "rankings_multiplayer": { + "type": "array", + "items": { + "$ref": "#/definitions/models.UserRanking" + } + }, + "rankings_overall": { "type": "array", "items": { "$ref": "#/definitions/models.UserRanking" } }, - "rankings_sp": { + "rankings_singleplayer": { "type": "array", "items": { "$ref": "#/definitions/models.UserRanking" @@ -1285,6 +1374,43 @@ } } }, + "handlers.ScoreLogsResponse": { + "type": "object", + "properties": { + "scores": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.ScoreLogsResponseDetails" + } + } + } + }, + "handlers.ScoreLogsResponseDetails": { + "type": "object", + "properties": { + "date": { + "type": "string" + }, + "demo_id": { + "type": "string" + }, + "game": { + "$ref": "#/definitions/models.Game" + }, + "map": { + "$ref": "#/definitions/models.MapShort" + }, + "score_count": { + "type": "integer" + }, + "score_time": { + "type": "integer" + }, + "user": { + "$ref": "#/definitions/models.UserShort" + } + } + }, "handlers.SearchResponse": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 4b59ee9..4d5661e 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -165,11 +165,15 @@ definitions: type: object handlers.RankingsResponse: properties: - rankings_mp: + rankings_multiplayer: items: $ref: '#/definitions/models.UserRanking' type: array - rankings_sp: + rankings_overall: + items: + $ref: '#/definitions/models.UserRanking' + type: array + rankings_singleplayer: items: $ref: '#/definitions/models.UserRanking' type: array @@ -181,6 +185,30 @@ definitions: score_time: type: integer type: object + handlers.ScoreLogsResponse: + properties: + scores: + items: + $ref: '#/definitions/handlers.ScoreLogsResponseDetails' + type: array + type: object + handlers.ScoreLogsResponseDetails: + properties: + date: + type: string + demo_id: + type: string + game: + $ref: '#/definitions/models.Game' + map: + $ref: '#/definitions/models.MapShort' + score_count: + type: integer + score_time: + type: integer + user: + $ref: '#/definitions/models.UserShort' + type: object handlers.SearchResponse: properties: maps: @@ -444,6 +472,54 @@ paths: $ref: '#/definitions/models.Response' tags: - login + /logs/mod: + get: + description: Get mod logs. + parameters: + - description: JWT Token + in: header + name: Authorization + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/models.Response' + - properties: + data: + $ref: '#/definitions/handlers.ScoreLogsResponse' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.Response' + tags: + - rankings + /logs/score: + get: + description: Get score logs of every player. + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/models.Response' + - properties: + data: + $ref: '#/definitions/handlers.ScoreLogsResponse' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/models.Response' + tags: + - rankings /maps/{id}/image: put: description: Edit map image with specified map id. -- cgit v1.2.3 From 0f493cef0fd6442bddeb3f7fa1f9ca4bd3c47917 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Sat, 2 Sep 2023 13:54:11 +0300 Subject: docs: change tags for logs (#55) Former-commit-id: c3f9968ecb1a8734bbc87ad4fe28073b14b9920c --- backend/handlers/logs.go | 4 ++-- docs/docs.go | 4 ++-- docs/swagger.json | 4 ++-- docs/swagger.yaml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/backend/handlers/logs.go b/backend/handlers/logs.go index 5233738..882632d 100644 --- a/backend/handlers/logs.go +++ b/backend/handlers/logs.go @@ -52,7 +52,7 @@ type ScoreLogsResponseDetails struct { // GET Mod Logs // // @Description Get mod logs. -// @Tags rankings +// @Tags logs // @Produce json // @Param Authorization header string true "JWT Token" // @Success 200 {object} models.Response{data=ScoreLogsResponse} @@ -98,7 +98,7 @@ func ModLogs(c *gin.Context) { // GET Score Logs // // @Description Get score logs of every player. -// @Tags rankings +// @Tags logs // @Produce json // @Success 200 {object} models.Response{data=ScoreLogsResponse} // @Failure 400 {object} models.Response diff --git a/docs/docs.go b/docs/docs.go index 3d140cc..0523288 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -236,7 +236,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "rankings" + "logs" ], "parameters": [ { @@ -282,7 +282,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "rankings" + "logs" ], "responses": { "200": { diff --git a/docs/swagger.json b/docs/swagger.json index f55190e..6837d8a 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -229,7 +229,7 @@ "application/json" ], "tags": [ - "rankings" + "logs" ], "parameters": [ { @@ -275,7 +275,7 @@ "application/json" ], "tags": [ - "rankings" + "logs" ], "responses": { "200": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 4d5661e..48e84b2 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -498,7 +498,7 @@ paths: schema: $ref: '#/definitions/models.Response' tags: - - rankings + - logs /logs/score: get: description: Get score logs of every player. @@ -519,7 +519,7 @@ paths: schema: $ref: '#/definitions/models.Response' tags: - - rankings + - logs /maps/{id}/image: put: description: Edit map image with specified map id. -- cgit v1.2.3 From cfd4fdadb0e4625b0921f76cef457bea4dfa2b74 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Sat, 2 Sep 2023 16:12:24 +0300 Subject: fix: remove redundant log Former-commit-id: 802218f21552ee2ea453013974d69464bbe25bc3 --- backend/handlers/user.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/backend/handlers/user.go b/backend/handlers/user.go index affebb0..e5b824b 100644 --- a/backend/handlers/user.go +++ b/backend/handlers/user.go @@ -1,7 +1,6 @@ package handlers import ( - "log" "net/http" "os" "regexp" @@ -92,7 +91,6 @@ func Profile(c *gin.Context) { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } - log.Println("rows:", rows) for rows.Next() { var gameID int var categoryID int @@ -225,7 +223,6 @@ func FetchUser(c *gin.Context) { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } - log.Println("rows:", rows) for rows.Next() { var gameID int var categoryID int -- cgit v1.2.3 From f2bfade4812a9f7b1f97593754c95359ce99e2f1 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Sat, 2 Sep 2023 16:12:46 +0300 Subject: fix: optimize rankings Former-commit-id: 16b247861acea7b5c70a872db7b0397f9dc5c879 --- backend/handlers/home.go | 149 +++++++++++++++++++++++++---------------------- 1 file changed, 78 insertions(+), 71 deletions(-) diff --git a/backend/handlers/home.go b/backend/handlers/home.go index 6e9a0df..53be1de 100644 --- a/backend/handlers/home.go +++ b/backend/handlers/home.go @@ -3,6 +3,7 @@ package handlers import ( "log" "net/http" + "sort" "strings" "github.com/gin-gonic/gin" @@ -16,8 +17,9 @@ type SearchResponse struct { } type RankingsResponse struct { - RankingsSP []models.UserRanking `json:"rankings_sp"` - RankingsMP []models.UserRanking `json:"rankings_mp"` + Overall []models.UserRanking `json:"rankings_overall"` + Singleplayer []models.UserRanking `json:"rankings_singleplayer"` + Multiplayer []models.UserRanking `json:"rankings_multiplayer"` } func Home(c *gin.Context) { @@ -40,96 +42,101 @@ func Home(c *gin.Context) { // @Failure 400 {object} models.Response // @Router /rankings [get] func Rankings(c *gin.Context) { - rows, err := database.DB.Query(`SELECT steam_id, user_name FROM users`) + response := RankingsResponse{ + Overall: []models.UserRanking{}, + Singleplayer: []models.UserRanking{}, + Multiplayer: []models.UserRanking{}, + } + // Singleplayer rankings + sql := `SELECT u.steam_id, u.user_name, COUNT(DISTINCT map_id), + (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false), + (SELECT SUM(min_score_count) AS total_min_score_count FROM ( + SELECT + user_id, + MIN(score_count) AS min_score_count + FROM records_sp + GROUP BY user_id, map_id + ) AS subquery + WHERE user_id = u.steam_id) + FROM records_sp sp JOIN users u ON u.steam_id = sp.user_id GROUP BY u.steam_id, u.user_name` + rows, err := database.DB.Query(sql) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } - var spRankings []models.UserRanking - var mpRankings []models.UserRanking for rows.Next() { - var userID, username string - err := rows.Scan(&userID, &username) + ranking := models.UserRanking{} + var currentCount int + var totalCount int + err = rows.Scan(&ranking.UserID, &ranking.UserName, ¤tCount, &totalCount, &ranking.TotalScore) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } - // Getting all sp records for each user - var uniqueSingleUserRecords, totalSingleMaps int - sql := `SELECT COUNT(DISTINCT map_id), (SELECT COUNT(map_name) FROM maps - WHERE is_coop = FALSE AND is_disabled = false) FROM records_sp WHERE user_id = $1` - err = database.DB.QueryRow(sql, userID).Scan(&uniqueSingleUserRecords, &totalSingleMaps) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return + if currentCount != totalCount { + continue } - // Has all singleplayer records - if uniqueSingleUserRecords == totalSingleMaps { - var ranking models.UserRanking - ranking.UserID = userID - ranking.UserName = username - sql := `SELECT DISTINCT map_id, score_count FROM records_sp WHERE user_id = $1 ORDER BY map_id, score_count` - rows, err := database.DB.Query(sql, userID) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - totalScore := 0 - var maps []int - for rows.Next() { - var mapID, scoreCount int - rows.Scan(&mapID, &scoreCount) - if len(maps) != 0 && maps[len(maps)-1] == mapID { - continue - } - totalScore += scoreCount - maps = append(maps, mapID) - } - ranking.TotalScore = totalScore - spRankings = append(spRankings, ranking) - } - // Getting all mp records for each user - var uniqueMultiUserRecords, totalMultiMaps int - sql = `SELECT COUNT(DISTINCT map_id), (SELECT COUNT(map_name) FROM maps - WHERE is_coop = TRUE AND is_disabled = false) FROM records_mp WHERE host_id = $1 OR partner_id = $2` - err = database.DB.QueryRow(sql, userID, userID).Scan(&uniqueMultiUserRecords, &totalMultiMaps) + response.Singleplayer = append(response.Singleplayer, ranking) + } + // Multiplayer rankings + sql = `SELECT u.steam_id, u.user_name, COUNT(DISTINCT map_id), + (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false), + (SELECT SUM(min_score_count) AS total_min_score_count FROM ( + SELECT + host_id, + partner_id, + MIN(score_count) AS min_score_count + FROM records_mp + GROUP BY host_id, partner_id, map_id + ) AS subquery + WHERE host_id = u.steam_id OR partner_id = u.steam_id) + FROM records_mp mp JOIN users u ON u.steam_id = mp.host_id OR u.steam_id = mp.partner_id GROUP BY u.steam_id, u.user_name` + rows, err = database.DB.Query(sql) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + for rows.Next() { + ranking := models.UserRanking{} + var currentCount int + var totalCount int + err = rows.Scan(&ranking.UserID, &ranking.UserName, ¤tCount, &totalCount, &ranking.TotalScore) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } - // Has all singleplayer records - if uniqueMultiUserRecords == totalMultiMaps { - var ranking models.UserRanking - ranking.UserID = userID - ranking.UserName = username - sql := `SELECT DISTINCT map_id, score_count FROM records_mp WHERE host_id = $1 OR partner_id = $2 ORDER BY map_id, score_count` - rows, err := database.DB.Query(sql, userID, userID) - if err != nil { - c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) - return - } - totalScore := 0 - var maps []int - for rows.Next() { - var mapID, scoreCount int - rows.Scan(&mapID, &scoreCount) - if len(maps) != 0 && maps[len(maps)-1] == mapID { - continue + if currentCount != totalCount { + continue + } + response.Multiplayer = append(response.Multiplayer, ranking) + } + // Has both so they are qualified for overall ranking + for _, spRanking := range response.Singleplayer { + for _, mpRanking := range response.Multiplayer { + if spRanking.UserID == mpRanking.UserID { + totalScore := spRanking.TotalScore + mpRanking.TotalScore + overallRanking := models.UserRanking{ + UserID: spRanking.UserID, + UserName: spRanking.UserName, + TotalScore: totalScore, } - totalScore += scoreCount - maps = append(maps, mapID) + response.Overall = append(response.Overall, overallRanking) } - ranking.TotalScore = totalScore - mpRankings = append(mpRankings, ranking) } } + sort.Slice(response.Singleplayer, func(i, j int) bool { + return response.Singleplayer[i].TotalScore < response.Singleplayer[j].TotalScore + }) + sort.Slice(response.Multiplayer, func(i, j int) bool { + return response.Multiplayer[i].TotalScore < response.Multiplayer[j].TotalScore + }) + sort.Slice(response.Overall, func(i, j int) bool { + return response.Overall[i].TotalScore < response.Overall[j].TotalScore + }) c.JSON(http.StatusOK, models.Response{ Success: true, Message: "Successfully retrieved rankings.", - Data: RankingsResponse{ - RankingsSP: spRankings, - RankingsMP: mpRankings, - }, + Data: response, }) } -- cgit v1.2.3 From dd9a047dc6512b3b6419ac4bd05a77bc22be95b0 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Sat, 2 Sep 2023 16:26:48 +0300 Subject: fix: imrpoved user models on responses Former-commit-id: 3aeb7e76928081664f33a4012bd1d84c81eef77c --- backend/handlers/home.go | 9 ++++----- backend/handlers/map.go | 42 ++++++++++++++++++------------------------ backend/models/models.go | 11 ++++++++--- 3 files changed, 30 insertions(+), 32 deletions(-) diff --git a/backend/handlers/home.go b/backend/handlers/home.go index 53be1de..eb3912c 100644 --- a/backend/handlers/home.go +++ b/backend/handlers/home.go @@ -68,7 +68,7 @@ func Rankings(c *gin.Context) { ranking := models.UserRanking{} var currentCount int var totalCount int - err = rows.Scan(&ranking.UserID, &ranking.UserName, ¤tCount, &totalCount, &ranking.TotalScore) + err = rows.Scan(&ranking.User.SteamID, &ranking.User.UserName, ¤tCount, &totalCount, &ranking.TotalScore) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return @@ -100,7 +100,7 @@ func Rankings(c *gin.Context) { ranking := models.UserRanking{} var currentCount int var totalCount int - err = rows.Scan(&ranking.UserID, &ranking.UserName, ¤tCount, &totalCount, &ranking.TotalScore) + err = rows.Scan(&ranking.User.SteamID, &ranking.User.UserName, ¤tCount, &totalCount, &ranking.TotalScore) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return @@ -113,11 +113,10 @@ func Rankings(c *gin.Context) { // Has both so they are qualified for overall ranking for _, spRanking := range response.Singleplayer { for _, mpRanking := range response.Multiplayer { - if spRanking.UserID == mpRanking.UserID { + if spRanking.User.SteamID == mpRanking.User.SteamID { totalScore := spRanking.TotalScore + mpRanking.TotalScore overallRanking := models.UserRanking{ - UserID: spRanking.UserID, - UserName: spRanking.UserName, + User: spRanking.User, TotalScore: totalScore, } response.Overall = append(response.Overall, overallRanking) diff --git a/backend/handlers/map.go b/backend/handlers/map.go index 0a0206e..1d9cee8 100644 --- a/backend/handlers/map.go +++ b/backend/handlers/map.go @@ -31,31 +31,25 @@ type ChapterMapsResponse struct { } type RecordSingleplayer struct { - Placement int `json:"placement"` - RecordID int `json:"record_id"` - ScoreCount int `json:"score_count"` - ScoreTime int `json:"score_time"` - UserID string `json:"user_id"` - UserName string `json:"user_name"` - UserAvatar string `json:"user_avatar"` - DemoID string `json:"demo_id"` - RecordDate time.Time `json:"record_date"` + Placement int `json:"placement"` + RecordID int `json:"record_id"` + ScoreCount int `json:"score_count"` + ScoreTime int `json:"score_time"` + User models.UserShortWithAvatar `json:"user"` + DemoID string `json:"demo_id"` + RecordDate time.Time `json:"record_date"` } type RecordMultiplayer struct { - Placement int `json:"placement"` - RecordID int `json:"record_id"` - ScoreCount int `json:"score_count"` - ScoreTime int `json:"score_time"` - HostID string `json:"host_id"` - HostName string `json:"host_name"` - HostAvatar string `json:"host_avatar"` - PartnerID string `json:"partner_id"` - PartnerName string `json:"partner_name"` - PartnerAvatar string `json:"partner_avatar"` - HostDemoID string `json:"host_demo_id"` - PartnerDemoID string `json:"partner_demo_id"` - RecordDate time.Time `json:"record_date"` + Placement int `json:"placement"` + RecordID int `json:"record_id"` + ScoreCount int `json:"score_count"` + ScoreTime int `json:"score_time"` + Host models.UserShortWithAvatar `json:"host"` + Partner models.UserShortWithAvatar `json:"partner"` + HostDemoID string `json:"host_demo_id"` + PartnerDemoID string `json:"partner_demo_id"` + RecordDate time.Time `json:"record_date"` } // GET Map Summary @@ -195,7 +189,7 @@ func FetchMapLeaderboards(c *gin.Context) { ties := 0 for rows.Next() { var record RecordMultiplayer - err := rows.Scan(&record.RecordID, &record.HostID, &record.HostName, &record.HostAvatar, &record.PartnerID, &record.PartnerName, &record.PartnerAvatar, &record.ScoreCount, &record.ScoreTime, &record.HostDemoID, &record.PartnerDemoID, &record.RecordDate) + err := rows.Scan(&record.RecordID, &record.Host.SteamID, &record.Host.UserName, &record.Host.AvatarLink, &record.Partner.SteamID, &record.Partner.UserName, &record.Partner.AvatarLink, &record.ScoreCount, &record.ScoreTime, &record.HostDemoID, &record.PartnerDemoID, &record.RecordDate) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return @@ -230,7 +224,7 @@ func FetchMapLeaderboards(c *gin.Context) { ties := 0 for rows.Next() { var record RecordSingleplayer - err := rows.Scan(&record.RecordID, &record.UserID, &record.UserName, &record.UserAvatar, &record.ScoreCount, &record.ScoreTime, &record.DemoID, &record.RecordDate) + err := rows.Scan(&record.RecordID, &record.User.SteamID, &record.User.UserName, &record.User.AvatarLink, &record.ScoreCount, &record.ScoreTime, &record.DemoID, &record.RecordDate) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return diff --git a/backend/models/models.go b/backend/models/models.go index b706d25..2d54295 100644 --- a/backend/models/models.go +++ b/backend/models/models.go @@ -33,6 +33,12 @@ type UserShort struct { UserName string `json:"user_name"` } +type UserShortWithAvatar struct { + SteamID string `json:"steam_id"` + UserName string `json:"user_name"` + AvatarLink string `json:"avatar_link"` +} + type Map struct { ID int `json:"id"` GameName string `json:"game_name"` @@ -71,9 +77,8 @@ type MapRecords struct { } type UserRanking struct { - UserID string `json:"user_id"` - UserName string `json:"user_name"` - TotalScore int `json:"total_score"` + User UserShort `json:"user"` + TotalScore int `json:"total_score"` } type Game struct { -- cgit v1.2.3 From bb5ec62a0da2cd689bc5c2d65f639ae90f4cc3b4 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Sat, 2 Sep 2023 21:40:39 +0300 Subject: fix: remove redundant home route Former-commit-id: fbc6789b2ca54aeb13d8bc6e8ec406e524e00606 --- backend/api/routes.go | 1 - backend/handlers/home.go | 11 ----------- 2 files changed, 12 deletions(-) diff --git a/backend/api/routes.go b/backend/api/routes.go index ac622d4..fd3b8cc 100644 --- a/backend/api/routes.go +++ b/backend/api/routes.go @@ -17,7 +17,6 @@ func InitRoutes(router *gin.Engine) { }) v1.GET("/token", handlers.GetCookie) v1.DELETE("/token", handlers.DeleteCookie) - v1.GET("/home", CheckAuth, handlers.Home) v1.GET("/login", handlers.Login) v1.GET("/profile", CheckAuth, handlers.Profile) v1.PUT("/profile", CheckAuth, handlers.UpdateCountryCode) diff --git a/backend/handlers/home.go b/backend/handlers/home.go index eb3912c..2095a74 100644 --- a/backend/handlers/home.go +++ b/backend/handlers/home.go @@ -22,17 +22,6 @@ type RankingsResponse struct { Multiplayer []models.UserRanking `json:"rankings_multiplayer"` } -func Home(c *gin.Context) { - user, exists := c.Get("user") - if !exists { - c.JSON(200, "no id, not auth") - } else { - c.JSON(200, gin.H{ - "output": user, - }) - } -} - // GET Rankings // // @Description Get rankings of every player. -- cgit v1.2.3 From f56488d3cf25a1141ce73378241ee0048f440842 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Sat, 2 Sep 2023 21:41:02 +0300 Subject: feat: map summary logs (#55) Former-commit-id: 4ef8800f1270e3b3aa61913bef8cc07ce4a55656 --- backend/handlers/logs.go | 5 +++++ backend/handlers/mod.go | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/backend/handlers/logs.go b/backend/handlers/logs.go index 882632d..0fef6a1 100644 --- a/backend/handlers/logs.go +++ b/backend/handlers/logs.go @@ -18,6 +18,11 @@ const ( LogDescriptionLoginFailToken string = "TokenFail" LogDescriptionLoginFailValidate string = "ValidateFail" LogDescriptionLoginFailSummary string = "SummaryFail" + + LogDescriptionMapSummaryCreate string = "MapSummaryCreate" + LogDescriptionMapSummaryEdit string = "MapSummaryEdit" + LogDescriptionMapSummaryEditImage string = "MapSummaryEditImage" + LogDescriptionMapSummaryDelete string = "MapSummaryDelete" ) type Log struct { diff --git a/backend/handlers/mod.go b/backend/handlers/mod.go index e47cb3f..a9b7a20 100644 --- a/backend/handlers/mod.go +++ b/backend/handlers/mod.go @@ -15,7 +15,7 @@ type CreateMapSummaryRequest struct { Description string `json:"description" binding:"required"` Showcase string `json:"showcase"` UserName string `json:"user_name" binding:"required"` - ScoreCount *int `json:"score_count" binding:"required"` + ScoreCount int `json:"score_count" binding:"required"` RecordDate time.Time `json:"record_date" binding:"required"` } @@ -24,7 +24,7 @@ type EditMapSummaryRequest struct { Description string `json:"description" binding:"required"` Showcase string `json:"showcase"` UserName string `json:"user_name" binding:"required"` - ScoreCount *int `json:"score_count" binding:"required"` + ScoreCount int `json:"score_count" binding:"required"` RecordDate time.Time `json:"record_date" binding:"required"` } @@ -49,7 +49,7 @@ type EditMapImageRequest struct { // @Router /maps/{id}/summary [post] func CreateMapSummary(c *gin.Context) { // Check if user exists - _, exists := c.Get("user") + user, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) return @@ -109,7 +109,7 @@ func CreateMapSummary(c *gin.Context) { c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) return } - // Return response + CreateLog(user.(models.User).SteamID, LogTypeMod, LogDescriptionMapSummaryCreate) c.JSON(http.StatusOK, models.Response{ Success: true, Message: "Successfully created map summary.", @@ -130,7 +130,7 @@ func CreateMapSummary(c *gin.Context) { // @Router /maps/{id}/summary [put] func EditMapSummary(c *gin.Context) { // Check if user exists - _, exists := c.Get("user") + user, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) return @@ -190,7 +190,7 @@ func EditMapSummary(c *gin.Context) { c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) return } - // Return response + CreateLog(user.(models.User).SteamID, LogTypeMod, LogDescriptionMapSummaryEdit) c.JSON(http.StatusOK, models.Response{ Success: true, Message: "Successfully updated map summary.", @@ -211,7 +211,7 @@ func EditMapSummary(c *gin.Context) { // @Router /maps/{id}/summary [delete] func DeleteMapSummary(c *gin.Context) { // Check if user exists - _, exists := c.Get("user") + user, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) return @@ -275,7 +275,7 @@ func DeleteMapSummary(c *gin.Context) { c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) return } - // Return response + CreateLog(user.(models.User).SteamID, LogTypeMod, LogDescriptionMapSummaryDelete) c.JSON(http.StatusOK, models.Response{ Success: true, Message: "Successfully delete map summary.", @@ -296,7 +296,7 @@ func DeleteMapSummary(c *gin.Context) { // @Router /maps/{id}/image [put] func EditMapImage(c *gin.Context) { // Check if user exists - _, exists := c.Get("user") + user, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, models.ErrorResponse("User not logged in.")) return @@ -325,7 +325,7 @@ func EditMapImage(c *gin.Context) { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } - // Return response + CreateLog(user.(models.User).SteamID, LogTypeMod, LogDescriptionMapSummaryEditImage) c.JSON(http.StatusOK, models.Response{ Success: true, Message: "Successfully updated map image.", -- cgit v1.2.3 From 266698e46744063752f7cc893c539f132e3cdc37 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Sat, 2 Sep 2023 22:02:11 +0300 Subject: feat: detailed logs for records (#55) Former-commit-id: af37633ce05f08fc517cb3e8fec3e84a0bd5ba49 --- backend/handlers/logs.go | 14 ++++++++++++-- backend/handlers/record.go | 11 +++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/backend/handlers/logs.go b/backend/handlers/logs.go index 0fef6a1..c971f79 100644 --- a/backend/handlers/logs.go +++ b/backend/handlers/logs.go @@ -11,8 +11,9 @@ import ( ) const ( - LogTypeMod string = "Mod" - LogTypeLogin string = "Login" + LogTypeMod string = "Mod" + LogTypeLogin string = "Login" + LogTypeRecord string = "Record" LogDescriptionLoginSuccess string = "Success" LogDescriptionLoginFailToken string = "TokenFail" @@ -23,6 +24,15 @@ const ( LogDescriptionMapSummaryEdit string = "MapSummaryEdit" LogDescriptionMapSummaryEditImage string = "MapSummaryEditImage" LogDescriptionMapSummaryDelete string = "MapSummaryDelete" + + LogDescriptionRecordSuccess string = "Success" + LogDescriptionRecordFailInsertRecord string = "InsertRecordFail" + LogDescriptionRecordFailInsertDemo string = "InsertDemoFail" + LogDescriptionRecordFailProcessDemo string = "ProcessDemoFail" + LogDescriptionRecordFailCreateDemo string = "CreateDemoFail" + LogDescriptionRecordFailOpenDemo string = "OpenDemoFail" + LogDescriptionRecordFailSaveDemo string = "SaveDemoFail" + LogDescriptionRecordFailInvalidRequest string = "InvalidRequestFail" ) type Log struct { diff --git a/backend/handlers/record.go b/backend/handlers/record.go index 00c9b7d..3d29eb8 100644 --- a/backend/handlers/record.go +++ b/backend/handlers/record.go @@ -66,6 +66,7 @@ func CreateRecordWithDemo(c *gin.Context) { return } if isDisabled { + CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInvalidRequest) c.JSON(http.StatusBadRequest, models.ErrorResponse("Map is not available for competitive boards.")) return } @@ -75,10 +76,12 @@ func CreateRecordWithDemo(c *gin.Context) { // Get record request var record RecordRequest if err := c.ShouldBind(&record); err != nil { + CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInvalidRequest) c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } if isCoop && (record.PartnerDemo == nil || record.PartnerID == "") { + CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInvalidRequest) c.JSON(http.StatusBadRequest, models.ErrorResponse("Invalid entry for coop record submission.")) return } @@ -108,23 +111,27 @@ func CreateRecordWithDemo(c *gin.Context) { // Upload & insert into demos err = c.SaveUploadedFile(header, "backend/parser/"+uuid+".dem") if err != nil { + CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailSaveDemo) c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } defer os.Remove("backend/parser/" + uuid + ".dem") f, err := os.Open("backend/parser/" + uuid + ".dem") if err != nil { + CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailOpenDemo) c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } defer f.Close() file, err := createFile(srv, uuid+".dem", "application/octet-stream", f, os.Getenv("GOOGLE_FOLDER_ID")) if err != nil { + CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailCreateDemo) c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } hostDemoScoreCount, hostDemoScoreTime, err = parser.ProcessDemo("backend/parser/" + uuid + ".dem") if err != nil { + CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailProcessDemo) c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } @@ -138,6 +145,7 @@ func CreateRecordWithDemo(c *gin.Context) { _, err = tx.Exec(`INSERT INTO demos (id,location_id) VALUES ($1,$2)`, uuid, file.Id) if err != nil { deleteFile(srv, file.Id) + CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInsertDemo) c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } @@ -159,6 +167,7 @@ func CreateRecordWithDemo(c *gin.Context) { if err != nil { deleteFile(srv, hostDemoFileID) deleteFile(srv, partnerDemoFileID) + CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInsertRecord) c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } @@ -176,6 +185,7 @@ func CreateRecordWithDemo(c *gin.Context) { _, err := tx.Exec(sql, mapId, hostDemoScoreCount, hostDemoScoreTime, user.(models.User).SteamID, hostDemoUUID) if err != nil { deleteFile(srv, hostDemoFileID) + CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordFailInsertRecord) c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } @@ -192,6 +202,7 @@ func CreateRecordWithDemo(c *gin.Context) { c.JSON(http.StatusInternalServerError, models.ErrorResponse(err.Error())) return } + CreateLog(user.(models.User).SteamID, LogTypeRecord, LogDescriptionRecordSuccess) c.JSON(http.StatusOK, models.Response{ Success: true, Message: "Successfully created record.", -- cgit v1.2.3 From 323311123f35cbe6dc0924ae7bb4109b4ba55e9c Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Sat, 2 Sep 2023 22:02:32 +0300 Subject: docs: update swagger for changed models Former-commit-id: 45b281132d5d5cf05297230ac9b17f2790a4c75f --- docs/docs.go | 7 ++----- docs/swagger.json | 7 ++----- docs/swagger.yaml | 6 ++---- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 0523288..60a39bf 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1599,11 +1599,8 @@ const docTemplate = `{ "total_score": { "type": "integer" }, - "user_id": { - "type": "string" - }, - "user_name": { - "type": "string" + "user": { + "$ref": "#/definitions/models.UserShort" } } }, diff --git a/docs/swagger.json b/docs/swagger.json index 6837d8a..bba8488 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1592,11 +1592,8 @@ "total_score": { "type": "integer" }, - "user_id": { - "type": "string" - }, - "user_name": { - "type": "string" + "user": { + "$ref": "#/definitions/models.UserShort" } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 48e84b2..166f960 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -326,10 +326,8 @@ definitions: properties: total_score: type: integer - user_id: - type: string - user_name: - type: string + user: + $ref: '#/definitions/models.UserShort' type: object models.UserShort: properties: -- cgit v1.2.3 From e84def27c803fc8982bdd21077a43967ab6031df Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Sun, 3 Sep 2023 13:31:45 +0300 Subject: docs: update log response type Former-commit-id: 8e2be25b6ce30d3f42b388b7c67ee14e230cce80 --- docs/docs.go | 27 ++++++++++++++++++++++++++- docs/swagger.json | 27 ++++++++++++++++++++++++++- docs/swagger.yaml | 18 +++++++++++++++++- 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 60a39bf..4129343 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -259,7 +259,7 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/handlers.ScoreLogsResponse" + "$ref": "#/definitions/handlers.LogsResponse" } } } @@ -1218,6 +1218,31 @@ const docTemplate = `{ } } }, + "handlers.LogsResponse": { + "type": "object", + "properties": { + "logs": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.LogsResponseDetails" + } + } + } + }, + "handlers.LogsResponseDetails": { + "type": "object", + "properties": { + "date": { + "type": "string" + }, + "detail": { + "type": "string" + }, + "user": { + "$ref": "#/definitions/models.UserShort" + } + } + }, "handlers.MapLeaderboardsResponse": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index bba8488..646da0f 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -252,7 +252,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/handlers.ScoreLogsResponse" + "$ref": "#/definitions/handlers.LogsResponse" } } } @@ -1211,6 +1211,31 @@ } } }, + "handlers.LogsResponse": { + "type": "object", + "properties": { + "logs": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.LogsResponseDetails" + } + } + } + }, + "handlers.LogsResponseDetails": { + "type": "object", + "properties": { + "date": { + "type": "string" + }, + "detail": { + "type": "string" + }, + "user": { + "$ref": "#/definitions/models.UserShort" + } + } + }, "handlers.MapLeaderboardsResponse": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 166f960..6b1e6ea 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -79,6 +79,22 @@ definitions: token: type: string type: object + handlers.LogsResponse: + properties: + logs: + items: + $ref: '#/definitions/handlers.LogsResponseDetails' + type: array + type: object + handlers.LogsResponseDetails: + properties: + date: + type: string + detail: + type: string + user: + $ref: '#/definitions/models.UserShort' + type: object handlers.MapLeaderboardsResponse: properties: map: @@ -489,7 +505,7 @@ paths: - $ref: '#/definitions/models.Response' - properties: data: - $ref: '#/definitions/handlers.ScoreLogsResponse' + $ref: '#/definitions/handlers.LogsResponse' type: object "400": description: Bad Request -- cgit v1.2.3 From 3eb88ce01d1e723dc93971dc25da728437912e8f Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Sun, 3 Sep 2023 13:32:25 +0300 Subject: feat: update user logs (#55) Former-commit-id: 9041bf919cbd96eb135abc14318910b81340f95d --- backend/handlers/login.go | 8 ++++---- backend/handlers/logs.go | 21 ++++++++++++--------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/backend/handlers/login.go b/backend/handlers/login.go index 80f697e..5949fdd 100644 --- a/backend/handlers/login.go +++ b/backend/handlers/login.go @@ -38,7 +38,7 @@ func Login(c *gin.Context) { default: steamID, err := openID.ValidateAndGetId() if err != nil { - CreateLog(steamID, LogTypeLogin, LogDescriptionLoginFailValidate) + CreateLog(steamID, LogTypeLogin, LogDescriptionUserLoginFailValidate) c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } @@ -49,7 +49,7 @@ func Login(c *gin.Context) { if checkSteamID == 0 { user, err := GetPlayerSummaries(steamID, os.Getenv("API_KEY")) if err != nil { - CreateLog(steamID, LogTypeLogin, LogDescriptionLoginFailSummary) + CreateLog(steamID, LogTypeLogin, LogDescriptionUserLoginFailSummary) c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } @@ -79,12 +79,12 @@ func Login(c *gin.Context) { // Sign and get the complete encoded token as a string using the secret tokenString, err := token.SignedString([]byte(os.Getenv("SECRET_KEY"))) if err != nil { - CreateLog(steamID, LogTypeLogin, LogDescriptionLoginFailToken) + CreateLog(steamID, LogTypeLogin, LogDescriptionUserLoginFailToken) c.JSON(http.StatusBadRequest, models.ErrorResponse("Failed to generate token.")) return } c.SetCookie("token", tokenString, 3600*24*30, "/", "", true, true) - CreateLog(steamID, LogTypeLogin, LogDescriptionLoginSuccess) + CreateLog(steamID, LogTypeLogin, LogDescriptionUserLoginSuccess) c.Redirect(http.StatusTemporaryRedirect, "/") // c.JSON(http.StatusOK, models.Response{ // Success: true, diff --git a/backend/handlers/logs.go b/backend/handlers/logs.go index c971f79..b5704d6 100644 --- a/backend/handlers/logs.go +++ b/backend/handlers/logs.go @@ -12,13 +12,13 @@ import ( const ( LogTypeMod string = "Mod" - LogTypeLogin string = "Login" + LogTypeLogin string = "User" LogTypeRecord string = "Record" - LogDescriptionLoginSuccess string = "Success" - LogDescriptionLoginFailToken string = "TokenFail" - LogDescriptionLoginFailValidate string = "ValidateFail" - LogDescriptionLoginFailSummary string = "SummaryFail" + LogDescriptionUserLoginSuccess string = "LoginSuccess" + LogDescriptionUserLoginFailToken string = "LoginTokenFail" + LogDescriptionUserLoginFailValidate string = "LoginValidateFail" + LogDescriptionUserLoginFailSummary string = "LoginSummaryFail" LogDescriptionMapSummaryCreate string = "MapSummaryCreate" LogDescriptionMapSummaryEdit string = "MapSummaryEdit" @@ -39,6 +39,7 @@ type Log struct { User models.UserShort `json:"user"` Type string `json:"type"` Description string `json:"description"` + Date time.Time `json:"date"` } type LogsResponse struct { @@ -48,6 +49,7 @@ type LogsResponse struct { type LogsResponseDetails struct { User models.UserShort `json:"user"` Log string `json:"detail"` + Date time.Time `json:"date"` } type ScoreLogsResponse struct { @@ -70,7 +72,7 @@ type ScoreLogsResponseDetails struct { // @Tags logs // @Produce json // @Param Authorization header string true "JWT Token" -// @Success 200 {object} models.Response{data=ScoreLogsResponse} +// @Success 200 {object} models.Response{data=LogsResponse} // @Failure 400 {object} models.Response // @Router /logs/mod [get] func ModLogs(c *gin.Context) { @@ -80,7 +82,7 @@ func ModLogs(c *gin.Context) { return } response := LogsResponse{Logs: []LogsResponseDetails{}} - sql := `SELECT u.user_name, l.user_id, l.type, l.description + sql := `SELECT u.user_name, l.user_id, l.type, l.description, l.date FROM logs l INNER JOIN users u ON l.user_id = u.steam_id WHERE type != 'Score'` rows, err := database.DB.Query(sql) if err != nil { @@ -89,7 +91,7 @@ func ModLogs(c *gin.Context) { } for rows.Next() { log := Log{} - err = rows.Scan(&log.User.UserName, &log.User.SteamID, &log.Type, &log.Description) + err = rows.Scan(&log.User.UserName, &log.User.SteamID, &log.Type, &log.Description, &log.Date) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return @@ -100,7 +102,8 @@ func ModLogs(c *gin.Context) { SteamID: log.User.SteamID, UserName: log.User.UserName, }, - Log: detail, + Log: detail, + Date: log.Date, }) } c.JSON(http.StatusOK, models.Response{ -- cgit v1.2.3 From 0183c98bd8b21e15ada176431cdda32581cd119a Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Sun, 3 Sep 2023 13:35:46 +0300 Subject: fix: order mod logs by most recent (#55) Former-commit-id: 661ee1f70cbc8e8705e9145de9cfdc48c97daf02 --- backend/handlers/logs.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/handlers/logs.go b/backend/handlers/logs.go index b5704d6..8880607 100644 --- a/backend/handlers/logs.go +++ b/backend/handlers/logs.go @@ -83,7 +83,8 @@ func ModLogs(c *gin.Context) { } response := LogsResponse{Logs: []LogsResponseDetails{}} sql := `SELECT u.user_name, l.user_id, l.type, l.description, l.date - FROM logs l INNER JOIN users u ON l.user_id = u.steam_id WHERE type != 'Score'` + FROM logs l INNER JOIN users u ON l.user_id = u.steam_id WHERE type != 'Score' + ORDER BY l.date DESC LIMIT 100;` rows, err := database.DB.Query(sql) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) -- cgit v1.2.3 From 48f60f8a5baeaa0814c890c7d3f6be56e144d334 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Sun, 3 Sep 2023 16:15:51 +0300 Subject: feat: working on rankings, overall rank left (#51) Former-commit-id: 3e2a57f4592259c94abef84f408405869c6df922 --- backend/handlers/user.go | 352 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 341 insertions(+), 11 deletions(-) diff --git a/backend/handlers/user.go b/backend/handlers/user.go index e5b824b..e0f1dff 100644 --- a/backend/handlers/user.go +++ b/backend/handlers/user.go @@ -81,12 +81,185 @@ func Profile(c *gin.Context) { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } - // TODO: Get rankings (all maps done in one game) + // Get rankings (all maps done in one game) + rankings := ProfileRankings{ + Overall: ProfileRankingsDetails{}, + Singleplayer: ProfileRankingsDetails{}, + Cooperative: ProfileRankingsDetails{}, + } + // Get total map count + sql = `SELECT count(id), (SELECT count(id) FROM maps m WHERE m.game_id = 2 AND m.is_disabled = false) FROM maps m WHERE m.game_id = 1 AND m.is_disabled = false;` + err = database.DB.QueryRow(sql).Scan(&rankings.Singleplayer.CompletionTotal, &rankings.Cooperative.CompletionTotal) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + rankings.Overall.CompletionTotal = rankings.Singleplayer.CompletionTotal + rankings.Cooperative.CompletionTotal + // Get user completion count + sql = `SELECT 'records_sp' AS table_name, COUNT(rs.id) AS total_user_scores + FROM public.records_sp rs JOIN ( + SELECT mr.map_id, MIN(mr.score_count) AS min_score_count + FROM public.map_routes mr WHERE mr.category_id = 1 GROUP BY mr.map_id + ) AS subquery_sp ON rs.map_id = subquery_sp.map_id AND rs.score_count = subquery_sp.min_score_count + WHERE rs.user_id = $1 + UNION ALL + SELECT 'records_mp' AS table_name, COUNT(rm.id) AS total_user_scores + FROM public.records_mp rm JOIN ( + SELECT mr.map_id, MIN(mr.score_count) AS min_score_count + FROM public.map_routes mr WHERE mr.category_id = 1 GROUP BY mr.map_id + ) AS subquery_mp ON rm.map_id = subquery_mp.map_id AND rm.score_count = subquery_mp.min_score_count + WHERE rm.host_id = $1 OR rm.partner_id = $1;` + rows, err := database.DB.Query(sql, user.(models.User).SteamID) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + for rows.Next() { + var tableName string + var completionCount int + err = rows.Scan(&tableName, &completionCount) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + if tableName == "records_sp" { + rankings.Singleplayer.CompletionCount = completionCount + continue + } + if tableName == "records_mp" { + rankings.Cooperative.CompletionCount = completionCount + continue + } + } + rankings.Overall.CompletionCount = rankings.Singleplayer.CompletionCount + rankings.Cooperative.CompletionCount + // Get user ranking placement for singleplayer + sql = `SELECT u.steam_id, COUNT(DISTINCT map_id), + (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false), + (SELECT SUM(min_score_count) AS total_min_score_count FROM ( + SELECT user_id, MIN(score_count) AS min_score_count FROM records_sp GROUP BY user_id, map_id) AS subquery WHERE user_id = u.steam_id) + FROM records_sp sp JOIN users u ON u.steam_id = sp.user_id GROUP BY u.steam_id, u.user_name + HAVING COUNT(DISTINCT map_id) = (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false) + ORDER BY total_min_score_count ASC;` + rows, err = database.DB.Query(sql) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + placement := 1 + for rows.Next() { + var steamID string + var completionCount int + var totalCount int + var userPortalCount int + err = rows.Scan(&steamID, &completionCount, &totalCount, &userPortalCount) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + if completionCount != totalCount { + placement++ + continue + } + if steamID != user.(models.User).SteamID { + placement++ + continue + } + rankings.Singleplayer.Rank = placement + } + // Get user ranking placement for multiplayer + sql = `SELECT u.steam_id, COUNT(DISTINCT map_id), + (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false), + (SELECT SUM(min_score_count) AS total_min_score_count FROM ( + SELECT host_id, partner_id, MIN(score_count) AS min_score_count FROM records_mp GROUP BY host_id, partner_id, map_id) AS subquery WHERE host_id = u.steam_id OR partner_id = u.steam_id) + FROM records_mp mp JOIN users u ON u.steam_id = mp.host_id OR u.steam_id = mp.partner_id GROUP BY u.steam_id, u.user_name + HAVING COUNT(DISTINCT map_id) = (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false) + ORDER BY total_min_score_count ASC;` + rows, err = database.DB.Query(sql) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + placement = 1 + for rows.Next() { + var steamID string + var completionCount int + var totalCount int + var userPortalCount int + err = rows.Scan(&steamID, &completionCount, &totalCount, &userPortalCount) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + if completionCount != totalCount { + placement++ + continue + } + if steamID != user.(models.User).SteamID { + placement++ + continue + } + rankings.Cooperative.Rank = placement + } + // TODO: Get user ranking placement for overall if they qualify + // if (rankings.Singleplayer.Rank != 0) && (rankings.Cooperative.Rank != 0) { + // sql = `SELECT steam_id, SUM(total_min_score_count) AS total_score + // FROM ( + // SELECT u.steam_id, + // (SELECT SUM(min_score_count) AS total_min_score_count FROM ( + // SELECT + // user_id, + // MIN(score_count) AS min_score_count + // FROM records_sp + // GROUP BY user_id, map_id + // ) AS subquery + // WHERE user_id = u.steam_id) AS total_min_score_count + // FROM records_sp sp + // JOIN users u ON u.steam_id = sp.user_id + // UNION ALL + // SELECT u.steam_id, + // (SELECT SUM(min_score_count) AS total_min_score_count FROM ( + // SELECT + // host_id, + // partner_id, + // MIN(score_count) AS min_score_count + // FROM records_mp + // GROUP BY host_id, partner_id, map_id + // ) AS subquery + // WHERE host_id = u.steam_id OR partner_id = u.steam_id) AS total_min_score_count + // FROM records_mp mp + // JOIN users u ON u.steam_id = mp.host_id OR u.steam_id = mp.partner_id + // ) AS combined_scores + // GROUP BY steam_id ORDER BY total_score ASC;` + // rows, err = database.DB.Query(sql) + // if err != nil { + // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + // return + // } + // placement = 1 + // for rows.Next() { + // var steamID string + // var userPortalCount int + // err = rows.Scan(&steamID, &userPortalCount) + // if err != nil { + // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + // return + // } + // if completionCount != totalCount { + // placement++ + // continue + // } + // if steamID != user.(models.User).SteamID { + // placement++ + // continue + // } + // rankings.Cooperative.Rank = placement + // } + // } records := []ProfileRecords{} // Get singleplayer records sql = `SELECT m.game_id, m.chapter_id, sp.map_id, m."name", (SELECT mr.score_count FROM map_routes mr WHERE mr.map_id = sp.map_id ORDER BY mr.score_count ASC LIMIT 1) AS wr_count, sp.score_count, sp.score_time, sp.demo_id, sp.record_date FROM records_sp sp INNER JOIN maps m ON sp.map_id = m.id WHERE sp.user_id = $1 ORDER BY sp.map_id, sp.score_count, sp.score_time;` - rows, err := database.DB.Query(sql, user.(models.User).SteamID) + rows, err = database.DB.Query(sql, user.(models.User).SteamID) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return @@ -158,7 +331,7 @@ func Profile(c *gin.Context) { CountryCode: user.(models.User).CountryCode, Titles: user.(models.User).Titles, Links: links, - Rankings: ProfileRankings{}, + Rankings: rankings, Records: records, }, }) @@ -197,23 +370,180 @@ func FetchUser(c *gin.Context) { c.JSON(http.StatusNotFound, models.ErrorResponse("User not found.")) return } - // Get user titles - sql = `SELECT t.title_name, t.title_color FROM titles t - INNER JOIN user_titles ut ON t.id=ut.title_id WHERE ut.user_id = $1` + // Get rankings (all maps done in one game) + rankings := ProfileRankings{ + Overall: ProfileRankingsDetails{}, + Singleplayer: ProfileRankingsDetails{}, + Cooperative: ProfileRankingsDetails{}, + } + // Get total map count + sql = `SELECT count(id), (SELECT count(id) FROM maps m WHERE m.game_id = 2 AND m.is_disabled = false) FROM maps m WHERE m.game_id = 1 AND m.is_disabled = false;` + err = database.DB.QueryRow(sql).Scan(&rankings.Singleplayer.CompletionTotal, &rankings.Cooperative.CompletionTotal) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + rankings.Overall.CompletionTotal = rankings.Singleplayer.CompletionTotal + rankings.Cooperative.CompletionTotal + // Get user completion count + sql = `SELECT 'records_sp' AS table_name, COUNT(rs.id) AS total_user_scores + FROM public.records_sp rs JOIN ( + SELECT mr.map_id, MIN(mr.score_count) AS min_score_count + FROM public.map_routes mr WHERE mr.category_id = 1 GROUP BY mr.map_id + ) AS subquery_sp ON rs.map_id = subquery_sp.map_id AND rs.score_count = subquery_sp.min_score_count + WHERE rs.user_id = $1 + UNION ALL + SELECT 'records_mp' AS table_name, COUNT(rm.id) AS total_user_scores + FROM public.records_mp rm JOIN ( + SELECT mr.map_id, MIN(mr.score_count) AS min_score_count + FROM public.map_routes mr WHERE mr.category_id = 1 GROUP BY mr.map_id + ) AS subquery_mp ON rm.map_id = subquery_mp.map_id AND rm.score_count = subquery_mp.min_score_count + WHERE rm.host_id = $1 OR rm.partner_id = $1;` rows, err := database.DB.Query(sql, user.SteamID) if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } for rows.Next() { - var title models.Title - if err := rows.Scan(&title.Name, &title.Color); err != nil { + var tableName string + var completionCount int + err = rows.Scan(&tableName, &completionCount) + if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } - user.Titles = append(user.Titles, title) + if tableName == "records_sp" { + rankings.Singleplayer.CompletionCount = completionCount + continue + } + if tableName == "records_mp" { + rankings.Cooperative.CompletionCount = completionCount + continue + } + } + rankings.Overall.CompletionCount = rankings.Singleplayer.CompletionCount + rankings.Cooperative.CompletionCount + // Get user ranking placement for singleplayer + sql = `SELECT u.steam_id, COUNT(DISTINCT map_id), + (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false), + (SELECT SUM(min_score_count) AS total_min_score_count FROM ( + SELECT user_id, MIN(score_count) AS min_score_count FROM records_sp GROUP BY user_id, map_id) AS subquery WHERE user_id = u.steam_id) + FROM records_sp sp JOIN users u ON u.steam_id = sp.user_id GROUP BY u.steam_id, u.user_name + HAVING COUNT(DISTINCT map_id) = (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false) + ORDER BY total_min_score_count ASC;` + rows, err = database.DB.Query(sql) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + placement := 1 + for rows.Next() { + var steamID string + var completionCount int + var totalCount int + var userPortalCount int + err = rows.Scan(&steamID, &completionCount, &totalCount, &userPortalCount) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + if completionCount != totalCount { + placement++ + continue + } + if steamID != user.SteamID { + placement++ + continue + } + rankings.Singleplayer.Rank = placement + } + // Get user ranking placement for multiplayer + sql = `SELECT u.steam_id, COUNT(DISTINCT map_id), + (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false), + (SELECT SUM(min_score_count) AS total_min_score_count FROM ( + SELECT host_id, partner_id, MIN(score_count) AS min_score_count FROM records_mp GROUP BY host_id, partner_id, map_id) AS subquery WHERE host_id = u.steam_id OR partner_id = u.steam_id) + FROM records_mp mp JOIN users u ON u.steam_id = mp.host_id OR u.steam_id = mp.partner_id GROUP BY u.steam_id, u.user_name + HAVING COUNT(DISTINCT map_id) = (SELECT COUNT(maps.name) FROM maps INNER JOIN games g ON maps.game_id = g.id WHERE g.is_coop = FALSE AND is_disabled = false) + ORDER BY total_min_score_count ASC;` + rows, err = database.DB.Query(sql) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + placement = 1 + for rows.Next() { + var steamID string + var completionCount int + var totalCount int + var userPortalCount int + err = rows.Scan(&steamID, &completionCount, &totalCount, &userPortalCount) + if err != nil { + c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + return + } + if completionCount != totalCount { + placement++ + continue + } + if steamID != user.SteamID { + placement++ + continue + } + rankings.Cooperative.Rank = placement } - // TODO: Get rankings (all maps done in one game) + // TODO: Get user ranking placement for overall if they qualify + // if (rankings.Singleplayer.Rank != 0) && (rankings.Cooperative.Rank != 0) { + // sql = `SELECT steam_id, SUM(total_min_score_count) AS total_score + // FROM ( + // SELECT u.steam_id, + // (SELECT SUM(min_score_count) AS total_min_score_count FROM ( + // SELECT + // user_id, + // MIN(score_count) AS min_score_count + // FROM records_sp + // GROUP BY user_id, map_id + // ) AS subquery + // WHERE user_id = u.steam_id) AS total_min_score_count + // FROM records_sp sp + // JOIN users u ON u.steam_id = sp.user_id + // UNION ALL + // SELECT u.steam_id, + // (SELECT SUM(min_score_count) AS total_min_score_count FROM ( + // SELECT + // host_id, + // partner_id, + // MIN(score_count) AS min_score_count + // FROM records_mp + // GROUP BY host_id, partner_id, map_id + // ) AS subquery + // WHERE host_id = u.steam_id OR partner_id = u.steam_id) AS total_min_score_count + // FROM records_mp mp + // JOIN users u ON u.steam_id = mp.host_id OR u.steam_id = mp.partner_id + // ) AS combined_scores + // GROUP BY steam_id ORDER BY total_score ASC;` + // rows, err = database.DB.Query(sql) + // if err != nil { + // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + // return + // } + // placement = 1 + // for rows.Next() { + // var steamID string + // var userPortalCount int + // err = rows.Scan(&steamID, &userPortalCount) + // if err != nil { + // c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) + // return + // } + // if completionCount != totalCount { + // placement++ + // continue + // } + // if steamID != user.SteamID { + // placement++ + // continue + // } + // rankings.Cooperative.Rank = placement + // } + // } records := []ProfileRecords{} // Get singleplayer records sql = `SELECT m.game_id, m.chapter_id, sp.map_id, m."name", (SELECT mr.score_count FROM map_routes mr WHERE mr.map_id = sp.map_id ORDER BY mr.score_count ASC LIMIT 1) AS wr_count, sp.score_count, sp.score_time, sp.demo_id, sp.record_date @@ -290,7 +620,7 @@ func FetchUser(c *gin.Context) { CountryCode: user.CountryCode, Titles: user.Titles, Links: links, - Rankings: ProfileRankings{}, + Rankings: rankings, Records: records, }, }) -- cgit v1.2.3 From 3279b30b3de8e4e9aaa6558ceaa9048b5b9314bd Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Sun, 3 Sep 2023 16:20:16 +0300 Subject: feat: added logs for user update (#55) Former-commit-id: b1cfb15cd595e93494b370dda6572e48a175d46e --- backend/handlers/login.go | 8 ++++---- backend/handlers/logs.go | 15 ++++++++++----- backend/handlers/user.go | 7 +++++++ 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/backend/handlers/login.go b/backend/handlers/login.go index 5949fdd..85ffd63 100644 --- a/backend/handlers/login.go +++ b/backend/handlers/login.go @@ -38,7 +38,7 @@ func Login(c *gin.Context) { default: steamID, err := openID.ValidateAndGetId() if err != nil { - CreateLog(steamID, LogTypeLogin, LogDescriptionUserLoginFailValidate) + CreateLog(steamID, LogTypeUser, LogDescriptionUserLoginFailValidate) c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } @@ -49,7 +49,7 @@ func Login(c *gin.Context) { if checkSteamID == 0 { user, err := GetPlayerSummaries(steamID, os.Getenv("API_KEY")) if err != nil { - CreateLog(steamID, LogTypeLogin, LogDescriptionUserLoginFailSummary) + CreateLog(steamID, LogTypeUser, LogDescriptionUserLoginFailSummary) c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } @@ -79,12 +79,12 @@ func Login(c *gin.Context) { // Sign and get the complete encoded token as a string using the secret tokenString, err := token.SignedString([]byte(os.Getenv("SECRET_KEY"))) if err != nil { - CreateLog(steamID, LogTypeLogin, LogDescriptionUserLoginFailToken) + CreateLog(steamID, LogTypeUser, LogDescriptionUserLoginFailToken) c.JSON(http.StatusBadRequest, models.ErrorResponse("Failed to generate token.")) return } c.SetCookie("token", tokenString, 3600*24*30, "/", "", true, true) - CreateLog(steamID, LogTypeLogin, LogDescriptionUserLoginSuccess) + CreateLog(steamID, LogTypeUser, LogDescriptionUserLoginSuccess) c.Redirect(http.StatusTemporaryRedirect, "/") // c.JSON(http.StatusOK, models.Response{ // Success: true, diff --git a/backend/handlers/logs.go b/backend/handlers/logs.go index 8880607..2b8223a 100644 --- a/backend/handlers/logs.go +++ b/backend/handlers/logs.go @@ -12,13 +12,18 @@ import ( const ( LogTypeMod string = "Mod" - LogTypeLogin string = "User" + LogTypeUser string = "User" LogTypeRecord string = "Record" - LogDescriptionUserLoginSuccess string = "LoginSuccess" - LogDescriptionUserLoginFailToken string = "LoginTokenFail" - LogDescriptionUserLoginFailValidate string = "LoginValidateFail" - LogDescriptionUserLoginFailSummary string = "LoginSummaryFail" + LogDescriptionUserLoginSuccess string = "LoginSuccess" + LogDescriptionUserLoginFailToken string = "LoginTokenFail" + LogDescriptionUserLoginFailValidate string = "LoginValidateFail" + LogDescriptionUserLoginFailSummary string = "LoginSummaryFail" + LogDescriptionUserUpdateSuccess string = "UpdateSuccess" + LogDescriptionUserUpdateFail string = "UpdateFail" + LogDescriptionUserUpdateSummaryFail string = "UpdateSummaryFail" + LogDescriptionUserUpdateCountrySuccess string = "UpdateCountrySuccess" + LogDescriptionUserUpdateCountryFail string = "UpdateCountryFail" LogDescriptionMapSummaryCreate string = "MapSummaryCreate" LogDescriptionMapSummaryEdit string = "MapSummaryEdit" diff --git a/backend/handlers/user.go b/backend/handlers/user.go index e0f1dff..742a57c 100644 --- a/backend/handlers/user.go +++ b/backend/handlers/user.go @@ -646,6 +646,7 @@ func UpdateUser(c *gin.Context) { } profile, err := GetPlayerSummaries(user.(models.User).SteamID, os.Getenv("API_KEY")) if err != nil { + CreateLog(user.(models.User).SteamID, LogTypeUser, LogDescriptionUserUpdateSummaryFail) c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } @@ -653,9 +654,11 @@ func UpdateUser(c *gin.Context) { _, err = database.DB.Exec(`UPDATE users SET username = $1, avatar_link = $2, country_code = $3, updated_at = $4 WHERE steam_id = $5`, profile.PersonaName, profile.AvatarFull, profile.LocCountryCode, time.Now().UTC(), user.(models.User).SteamID) if err != nil { + CreateLog(user.(models.User).SteamID, LogTypeUser, LogDescriptionUserUpdateFail) c.JSON(http.StatusBadRequest, models.ErrorResponse(err.Error())) return } + CreateLog(user.(models.User).SteamID, LogTypeUser, LogDescriptionUserUpdateSuccess) c.JSON(http.StatusOK, models.Response{ Success: true, Message: "Successfully updated user.", @@ -690,21 +693,25 @@ func UpdateCountryCode(c *gin.Context) { } code := c.Query("country_code") if code == "" { + CreateLog(user.(models.User).SteamID, LogTypeUser, LogDescriptionUserUpdateCountryFail) c.JSON(http.StatusNotFound, models.ErrorResponse("Enter a valid country code.")) return } var validCode string err := database.DB.QueryRow(`SELECT country_code FROM countries WHERE country_code = $1`, code).Scan(&validCode) if err != nil { + CreateLog(user.(models.User).SteamID, LogTypeUser, LogDescriptionUserUpdateCountryFail) c.JSON(http.StatusNotFound, models.ErrorResponse(err.Error())) return } // Valid code, update profile _, err = database.DB.Exec(`UPDATE users SET country_code = $1 WHERE steam_id = $2`, validCode, user.(models.User).SteamID) if err != nil { + CreateLog(user.(models.User).SteamID, LogTypeUser, LogDescriptionUserUpdateCountryFail) c.JSON(http.StatusNotFound, models.ErrorResponse(err.Error())) return } + CreateLog(user.(models.User).SteamID, LogTypeUser, LogDescriptionUserUpdateCountrySuccess) c.JSON(http.StatusOK, models.Response{ Success: true, Message: "Successfully updated country code.", -- cgit v1.2.3 From 7a9fb3cfda18aece4f003185b93e826d5619ecd8 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Sun, 3 Sep 2023 16:41:50 +0300 Subject: fix: create map summary, why the fuck does this have to be a pointer integer?? Former-commit-id: d44c3830e00975a1c4d41da83e762f452b2caaf4 --- backend/handlers/mod.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/handlers/mod.go b/backend/handlers/mod.go index a9b7a20..9e93395 100644 --- a/backend/handlers/mod.go +++ b/backend/handlers/mod.go @@ -15,7 +15,7 @@ type CreateMapSummaryRequest struct { Description string `json:"description" binding:"required"` Showcase string `json:"showcase"` UserName string `json:"user_name" binding:"required"` - ScoreCount int `json:"score_count" binding:"required"` + ScoreCount *int `json:"score_count" binding:"required"` RecordDate time.Time `json:"record_date" binding:"required"` } -- cgit v1.2.3