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(-) (limited to 'backend') 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 (limited to 'backend') 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(-) (limited to 'backend') 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(-) (limited to 'backend') 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(-) (limited to 'backend') 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(-) (limited to 'backend') 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 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 (limited to 'backend') 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(-) (limited to 'backend') 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 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(-) (limited to 'backend') 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(-) (limited to 'backend') 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(-) (limited to 'backend') 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 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 (limited to 'backend') 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(-) (limited to 'backend') 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(-) (limited to 'backend') 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(-) (limited to 'backend') 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(-) (limited to 'backend') 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(-) (limited to 'backend') 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(-) (limited to 'backend') 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(-) (limited to 'backend') 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(-) (limited to 'backend') 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(-) (limited to 'backend') 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(-) (limited to 'backend') 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(-) (limited to 'backend') 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(-) (limited to 'backend') 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 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(-) (limited to 'backend') 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(-) (limited to 'backend') 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(-) (limited to 'backend') 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(-) (limited to 'backend') 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(-) (limited to 'backend') 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