From 0d7fdb93dadadfe7d6a58f43c69fa356c1673cb4 Mon Sep 17 00:00:00 2001 From: Ann Riabykina Date: Tue, 3 Feb 2026 23:33:17 +0200 Subject: [PATCH 1/4] Solution --- src/database/seed_data/test_data.csv | 50 +-- src/routes/accounts.py | 443 +++++++-------------------- src/routes/profiles.py | 151 ++++++++- src/schemas/accounts.py | 80 +++-- src/schemas/profiles.py | 61 +++- src/validation/profile.py | 5 +- 6 files changed, 400 insertions(+), 390 deletions(-) diff --git a/src/database/seed_data/test_data.csv b/src/database/seed_data/test_data.csv index 6e1d9645..4e470447 100644 --- a/src/database/seed_data/test_data.csv +++ b/src/database/seed_data/test_data.csv @@ -1,25 +1,25 @@ -names,date_x,score,genre,overview,crew,orig_title,status,orig_lang,budget_x,revenue,country -Creed III,2023-03-02,73.0,"Drama,Action","After dominating the boxing world, Adonis Creed has been thriving in both his career and family life. When a childhood friend and former boxing prodigy, Damien Anderson, resurfaces after serving a long sentence in prison, he is eager to prove that he deserves his shot in the ring. The face-off between former friends is more than just a fight. To settle the score, Adonis must put his future on the line to battle Damien — a fighter who has nothing to lose.","AdonisCreed,AmaraCreed,BiancaTaylor,DamienAnderson,FelixChavez,FlorianMunteanu,JonathanMajors,JoséBenavidezJr.,LauraChavez,MaryAnneCreed,MichaelB.Jordan,MilaDavis-Kent,PhyliciaRashād,SelenisLeyva,TessaThompson,Tony'LittleDuke'Evers,ViktorDrago,WoodHarris",Creed III,Released,English,75000000.0,271616668.0,AU -Avatar: The Way of Water,2022-12-15,78.0,"Science Fiction,Adventure,Action","Set more than a decade after the events of the first film, learn the story of the Sully family (Jake, Neytiri, and their kids), the trouble that follows them, the lengths they go to keep each other safe, the battles they fight to stay alive, and the tragedies they endure.","CCHPounder,CliffCurtis,ColonelMilesQuaritch,EdieFalco,GeneralFrancesArdmore,JakeSully,JoelDavidMoore,KateWinslet,Kiri/Dr.GraceAugustine,Mo'at,Neytiri,NormSpellman,Ronal,SamWorthington,SigourneyWeaver,StephenLang,Tonowari,ZoeSaldaña",Avatar: The Way of Water,Released,English,460000000.0,2316794914.0,AU -The Super Mario Bros. Movie,2023-04-05,76.0,"Animation,Adventure,Family,Fantasy,Comedy","While working underground to fix a water main, Brooklyn plumbers—and brothers—Mario and Luigi are transported down a mysterious pipe and wander into a magical new world. But when the brothers are separated, Mario embarks on an epic quest to find Luigi.","AnyaTaylor-Joy,Bowser(voice),CharlieDay,ChrisPratt,CrankyKong(voice),DonkeyKong(voice),FredArmisen,JackBlack,Kamek(voice),Keegan-MichaelKey,KevinMichaelRichardson,Luigi(voice),Mario(voice),PrincessPeach(voice),SebastianManiscalco,SethRogen,Spike(voice),Toad(voice)",The Super Mario Bros. Movie,Released,English,100000000.0,724459031.0,AU -Mummies,2023-01-05,70.0,"Animation,Comedy,Family,Adventure,Fantasy","Through a series of unfortunate events, three mummies end up in present-day London and embark on a wacky and hilarious journey in search of an old ring belonging to the Royal Family, stolen by ambitious archaeologist Lord Carnaby.","AleixEstadella,AnaEstherAlborg,Carnaby(voice),Danny(voice),Dennis(voice),Ed(voice),JaumeSolà,JoséJavierSerranoRodríguez,JoséLuisMediavilla,LuisPérezReina,Madre(voice),MaríaLuisaSolá,MaríaMoscardó,Nefer(voice),Sekhem(voice),Thut(voice),Usi(voice),ÓscarBarberán", Momias,Released,"Spanish,Castilian",12300000.0,34200000.0,AU -Supercell,2023-03-17,61.0,Action,"Good-hearted teenager William always lived in hope of following in his late father’s footsteps and becoming a storm chaser. His father’s legacy has now been turned into a storm-chasing tourist business, managed by the greedy and reckless Zane Rogers, who is now using William as the main attraction to lead a group of unsuspecting adventurers deep into the eye of the most dangerous supercell ever seen.","AlecBaldwin,Amy,AnjulNigam,AnneHeche,BillBrody,DanielDiemer,DrQuinnBrody,HarperHunter,JohnnyWactor,JordanKristineSeamón,Martin,PrayaLundberg,Ramesh,RichardGunn,RoyCameron,SkeetUlrich,WilliamBrody,ZaneRogers",Supercell,Released,English,77000000.0,340941958.6,US -Cocaine Bear,2023-02-23,66.0,"Thriller,Comedy,Crime","Inspired by a true story, an oddball group of cops, criminals, tourists and teens converge in a Georgia forest where a 500-pound black bear goes on a murderous rampage after unintentionally ingesting cocaine.","AldenEhrenreich,Bob,ChristianConvery,Daveed,Eddie,Henry,IsiahWhitlockJr.,JesseTylerFerguson,KeriRussell,KristoferHivju,MargoMartindale,O'SheaJacksonJr.,Olaf(Kristoffer),Peter,RangerLiz,RayLiotta,Sari,Syd",Cocaine Bear,Released,English,35000000.0,80000000.0,AU -John Wick: Chapter 4,2023-03-23,80.0,"Action,Thriller,Crime","With the price on his head ever increasing, John Wick uncovers a path to defeating The High Table. But before he can earn his freedom, Wick must face off against a new enemy with powerful alliances across the globe and forces that turn old friends into foes.","BillSkarsgård,BoweryKing,Caine,Charon,ClancyBrown,DonnieYen,HiroyukiSanada,IanMcShane,JohnWick,KeanuReeves,LanceReddick,LaurenceFishburne,MarquisdeGramont,MrNobody,ShamierAnderson,Shimazu,TheHarbinger,Winston",John Wick: Chapter 4,Released,English,100000000.0,351349364.0,AU -Puss in Boots: The Last Wish,2022-12-26,83.0,"Animation,Family,Fantasy,Adventure,Comedy","Puss in Boots discovers that his passion for adventure has taken its toll: He has burned through eight of his nine lives, leaving him with only one life left. Puss sets out on an epic journey to find the mythical Last Wish and restore his nine lives.","AntonioBanderas,BabyBear(voice),FlorencePugh,Goldilocks(voice),HarveyGuillén,JackHorner(voice),JohnMulaney,KittySoftpaws(voice),MamaBear(voice),OliviaColman,PapaBear(voice),Perrito(voice),PussinBoots(voice),RayWinstone,SalmaHayek,SamsonKayo,WagnerMoura,Wolf(voice)",Puss in Boots: The Last Wish,Released,English,90000000.0,483480577.0,AU -Attack on Titan,2022-09-30,59.0,"Action,Science Fiction","As viable water is depleted on Earth, a mission is sent to Saturn's moon Titan to retrieve sustainable H2O reserves from its alien inhabitants. But just as the humans acquire the precious resource, they are attacked by Titan rebels, who don't trust that the Earthlings will leave in peace.","AdrianNaidu,AllisonQuince,AnthonyJensen,Computer(voice),ErinCoker,HeidiQuince,JackPearson,JennyTran,Jowers,JustinTanks,KaranSagoo,KimCosta,MarkMorales,MaxReece,NatalieStorrs,NeliSabour,PaulBianchi,SaoirseParker",Attack on Titan,Released,English,71000000.0,254946484.2,US -The Park,2023-03-02,58.0,"Action,Drama,Horror,Science Fiction,Thriller","A dystopian coming-of-age movie focused on three kids who find themselves in an abandoned amusement park, aiming to unite whoever remains. With dangers lurking around every corner, they will do whatever it takes to survive their hellish Neverland.","Bennett,BillySlaughter,Bui,CarliMcIntyre,CarminaGaray,ChloeGuidry,Ines,Jack,Kuan,LauraCoover,LegendJayJones,MartinParker,NhedrickJabier,PresleyRichardson,Reporter,Rue,SeanPapajohn,SlingshotGang",The Park,Released,English,119200000.0,488962491.0,US -Winnie the Pooh: Blood and Honey,2023-02-14,58.0,"Horror,Thriller","Christopher Robin is headed off to college and he has abandoned his old friends, Pooh and Piglet, which then leads to the duo embracing their inner monsters.","Alice,AmberDoig-Thorne,Charlene,ChrisCordell,ChristopherRobin,CraigDavidDowsett,DanielleRonald,DanielleScott,Lara,Maria,MariaTaylor,MayKelly,NatashaTosini,NikolaiLeon,Piglet,PoohBear,Tina,Zoe",Winnie the Pooh: Blood and Honey,Released,English,100000.0,3200000.0,AU -The Exorcist,2022-11-02,55.0,Horror,"Ophelia, a young nun recently arriving in the town of San Ramon, is forced to perform an exorcism on a pregnant woman in danger of dying. Just when she thinks her possession has ended, she discovers that the evil presence hasn't disappeared yet. The director of the award-winning Here Comes the Devil and Late Phases adds a new twist to possession movies in one of this year's Latin American horror surprises.",",DianaBracho,Fabian,Julia,JulioBracho,MaríaEvoli,NormaLazareno,Ofelia,PadreVictor,PilarSantacruz,RamónMedína,SalvadorSánchez,Sandra,SeñorRamirez,SeñoraRamirez,TinaRomero", La Exorcista,Released,"Spanish,Castilian",12000000.0,428214478.0,MX -Murder Mystery 2,2023-03-31,65.0,"Comedy,Mystery,Action","After starting their own detective agency, Nick and Audrey Spitz land a career-making case when their billionaire pal is kidnapped from his wedding.","AdamSandler,AdeelAkhtar,Audrey,Claudette,ColonelUlenga,CountessSekou,DanyBoon,InspectorDelacroix,JenniferAniston,JodieTurner-Smith,JohnKani,KuhooVerma,MarkStrong,Miller,MélanieLaurent,Nick,Saira,TheMaharajah",Murder Mystery 2,Released,English,43800000.0,489543087.4,AU -Black Panther: Wakanda Forever,2022-11-10,73.0,"Action,Adventure,Science Fiction","Queen Ramonda, Shuri, M’Baku, Okoye and the Dora Milaje fight to protect their nation from intervening world powers in the wake of King T’Challa’s death. As the Wakandans strive to embrace their next chapter, the heroes must band together with the help of War Dog Nakia and Everett Ross and forge a new path for the kingdom of Wakanda.","Aneka,AngelaBassett,Ayo,DanaiGurira,DominiqueThorne,FlorenceKasumba,LetitiaWright,LupitaNyong'o,M'Baku,MichaelaCoel,Nakia,Namor,Okoye,Ramonda,RiriWilliams/Ironheart,Shuri/BlackPanther,TenochHuertaMejía,WinstonDuke",Black Panther: Wakanda Forever,Released,English,250000000.0,854041058.0,AU -The Pope's Exorcist,2023-04-06,72.0,"Horror,Mystery","Father Gabriele Amorth, Chief Exorcist of the Vatican, investigates a young boy's terrifying possession and ends up uncovering a centuries-old conspiracy the Vatican has desperately tried to keep hidden.","AlexEssoe,Amy,BishopBarbuto,BishopLumumba,CornellJohn,DanielZovatto,Demon(voice),Enzo,FatherEsquibel,FatherGabrieleAmorth,FrancoNero,Julia,LaurelMarsden,PabloRaybould,RalphIneson,RiverHawkins,RussellCrowe,ThePope",The Pope's Exorcist,Released,English,92000000.0,462216471.8,AU -Prizefighter: The Life of Jem Belcher,2022-07-22,62.0,"Drama,History","At the turn of the 19th century, Pugilism was the sport of kings and a gifted young boxer fought his way to becoming champion of England.","BillWarr,JackSlack,JemBelcher,JodhiMay,JulianGlover,LordAshford,LordRushworth,MartonCsokas,MaryBelcher,MattHookings,RayWinstone,RussellCrowe,StevenBerkoff,Walter",Prizefighter: The Life of Jem Belcher,Released,English,77600000.0,562112551.0,GB -Knock at the Cabin,2023-02-02,64.0,"Thriller,Mystery,Horror,Fantasy","While vacationing at a remote cabin, a young girl and her two fathers are taken hostage by four armed strangers who demand that the family make an unthinkable choice to avert the apocalypse. With limited access to the outside world, the family must decide what they believe before all is lost.","AbbyQuinn,Adriane,Andrew,Andrew'sMom,BenAldridge,ClareLouiseFrost,DaveBautista,Eric,InfomercialCo-Host,JonathanGroff,KristenCui,Leonard,McKennaKerrigan,NikkiAmuka-Bird,Redmond,RupertGrint,Sabrina,Wen",Knock at the Cabin,Released,English,20000000.0,54629497.0,AU -The Devil Conspiracy,2023-01-13,65.0,"Horror,Thriller","The hottest biotech company in the world has discovered they can clone history’s most influential people from the dead. Now, they are auctioning clones of Michelangelo, Galileo, Vivaldi, and others for tens of millions of dollars to the world’s ultra-rich. But when they steal the Shroud of Turin and clone the DNA of Jesus Christ, all hell breaks loose.","AliceOrr-Ewing,ArchangelMichael,BeastoftheGround,Brenda,BrianCaspe,CardinalVincini,Dr.Laurent,EvelineHall,FatherMarconi/Michael,JamesFaulkner,JoeAnderson,JoeDoyle,Laura,Liz,Lucifer,PeterMensah,SpencerWilding,VictoriaChilap",The Devil Conspiracy,Released,English,54060000.0,492646395.4,US -Cazadora,2023-01-19,57.0,Thriller,"In a dystopian future, a mother and her teenage son go hunting in the mountains and encounter a stranger who threatens to upend their relationship.","AlexandraVonHummel,Emilia,FelipeValenzuela,Mateo,NataliaReddersen,Rena,Salvador,WillySemler",Cazadora,Released,"Spanish,Castilian",110400000.0,318117812.6,CL -Gold Run,2022-12-15,65.0,"War,Action,Adventure,Thriller","Fredrik isn’t the bravest of men, but now he is faced with a great responsibility and an enormous task - to get the entire Norwegian gold reserve away from the Nazis during the invasion of Norway.","AnatoleTaubman,AndreasLund,AxelBøyum,EivindSander,FredrikHaslund,GardB.Eidsvold,IdaEliseBroch,Ingvar,JonØigarden,MajorBjørnSunde,MajorOttoStoltmann,MortenSvartveit,NiniHaslundGleditsch,NordahlGrieg,OddHenry,OscarTorp,SvenNordin,ThorbjørnHarr", Gulltransporten,Released,Norwegian,123500000.0,420430064.8,NO -The Magician's Elephant,2023-03-17,73.0,"Animation,Adventure,Family,Fantasy","Peter is searching for his long-lost sister when he crosses paths with a fortune teller in the market square. His one one question is: is his sister still alive? The answer, that he must find a mysterious elephant and the magician who will conjure it, sets Peter off on a journey to complete three seemingly impossible tasks that will change the face of his town forever.","AasifMandvi,BenedictWong,BrianTyreeHenry,GloriaMatienne(voice),KirbyHowell-Baptiste,LeoMatienne(voice),MadamLaVaughn(voice),Magician(voice),MandyPatinkin,MirandaRichardson,Narrator/FortuneTeller(voice),NatasiaDemetriou,NoahJupe,Peter(voice),SianClifford,TheCountess(voice),TheKing(voice),Vilna(voice)",The Magician's Elephant,Released,English,82600000.0,473097256.6,AU -Plane,2023-01-27,69.0,"Action,Adventure,Thriller","After a heroic job of successfully landing his storm-damaged aircraft in a war zone, a fearless pilot finds himself between the agendas of multiple militias planning to take the plane and its passengers hostage.","Bonnie,BrodieTorrance,DaniellaPineda,Dele,EvanDaneTaylor,GerardButler,Hampton,JoeySlotnick,Junmar,LouisGaspare,MikeColter,PaulBen-Victor,RemiAdeleke,Scarsdale,Shellback,Sinclair,TonyGoldwyn,YosonAn",Plane,Released,English,25000000.0,51000000.0,GB -The Passion of the Christ,2004-02-25,74.0,Drama,A graphic portrayal of the last twelve hours of Jesus of Nazareth's life.,"Anás,Caifás,ChristoJivkov,FrancescoDeVito,HristoShopov,Jesus,JimCaviezel,Juan,Judas,LucaLionello,Magdalena,MaiaMorgenstern,Maria,MattiaSbragia,MonicaBellucci,Pedro,PoncioPilato,ToniBertorelli",The Passion of the Christ,Released,English,25000000.0,622313635.0,AU -Batman: The Doom That Came to Gotham,2023-03-28,64.0,"Animation,Fantasy,Horror,Action,Mystery","Explorer Bruce Wayne accidentally unleashes an ancient evil, and returns to Gotham after being away for two decades. There, Batman battles Lovecraftian supernatural forces and encounters allies and enemies such as Green Arrow, Ra's al Ghul, Mr. Freeze, Killer Croc, Two-Face and James Gordon.","Batman/BruceWayne(voice),ChristopherGorham,DarinDePaul,DavidDastmalchian,DavidGiuntoli,EmilyO'Brien,Grendon(voice),HarveyDent/Two-Face(voice),JamesGordon(voice),JohnDiMaggio,KaranBrar,NavidNegahban,OliverQueen/GreenArrow(voice),PatrickFabian,Ra'salGhul(voice),Sanjay'Jay'Tawde(voice),TaliaalGhul/MarthaWayne(voice),ThomasWayne(voice)",Batman: The Doom That Came to Gotham,Released,English,145760000.0,321934831.2,US +names,date_x,score,genre,overview,crew,orig_title,status,orig_lang,budget_x,revenue,country +Creed III,2023-03-02,73.0,"Drama,Action","After dominating the boxing world, Adonis Creed has been thriving in both his career and family life. When a childhood friend and former boxing prodigy, Damien Anderson, resurfaces after serving a long sentence in prison, he is eager to prove that he deserves his shot in the ring. The face-off between former friends is more than just a fight. To settle the score, Adonis must put his future on the line to battle Damien — a fighter who has nothing to lose.","AdonisCreed,AmaraCreed,BiancaTaylor,DamienAnderson,FelixChavez,FlorianMunteanu,JonathanMajors,JoséBenavidezJr.,LauraChavez,MaryAnneCreed,MichaelB.Jordan,MilaDavis-Kent,PhyliciaRashād,SelenisLeyva,TessaThompson,Tony'LittleDuke'Evers,ViktorDrago,WoodHarris",Creed III,Released,English,75000000.0,271616668.0,AU +Avatar: The Way of Water,2022-12-15,78.0,"Science Fiction,Adventure,Action","Set more than a decade after the events of the first film, learn the story of the Sully family (Jake, Neytiri, and their kids), the trouble that follows them, the lengths they go to keep each other safe, the battles they fight to stay alive, and the tragedies they endure.","CCHPounder,CliffCurtis,ColonelMilesQuaritch,EdieFalco,GeneralFrancesArdmore,JakeSully,JoelDavidMoore,KateWinslet,Kiri/Dr.GraceAugustine,Mo'at,Neytiri,NormSpellman,Ronal,SamWorthington,SigourneyWeaver,StephenLang,Tonowari,ZoeSaldaña",Avatar: The Way of Water,Released,English,460000000.0,2316794914.0,AU +The Super Mario Bros. Movie,2023-04-05,76.0,"Animation,Adventure,Family,Fantasy,Comedy","While working underground to fix a water main, Brooklyn plumbers—and brothers—Mario and Luigi are transported down a mysterious pipe and wander into a magical new world. But when the brothers are separated, Mario embarks on an epic quest to find Luigi.","AnyaTaylor-Joy,Bowser(voice),CharlieDay,ChrisPratt,CrankyKong(voice),DonkeyKong(voice),FredArmisen,JackBlack,Kamek(voice),Keegan-MichaelKey,KevinMichaelRichardson,Luigi(voice),Mario(voice),PrincessPeach(voice),SebastianManiscalco,SethRogen,Spike(voice),Toad(voice)",The Super Mario Bros. Movie,Released,English,100000000.0,724459031.0,AU +Mummies,2023-01-05,70.0,"Animation,Comedy,Family,Adventure,Fantasy","Through a series of unfortunate events, three mummies end up in present-day London and embark on a wacky and hilarious journey in search of an old ring belonging to the Royal Family, stolen by ambitious archaeologist Lord Carnaby.","AleixEstadella,AnaEstherAlborg,Carnaby(voice),Danny(voice),Dennis(voice),Ed(voice),JaumeSolà,JoséJavierSerranoRodríguez,JoséLuisMediavilla,LuisPérezReina,Madre(voice),MaríaLuisaSolá,MaríaMoscardó,Nefer(voice),Sekhem(voice),Thut(voice),Usi(voice),ÓscarBarberán", Momias,Released,"Spanish,Castilian",12300000.0,34200000.0,AU +Supercell,2023-03-17,61.0,Action,"Good-hearted teenager William always lived in hope of following in his late father’s footsteps and becoming a storm chaser. His father’s legacy has now been turned into a storm-chasing tourist business, managed by the greedy and reckless Zane Rogers, who is now using William as the main attraction to lead a group of unsuspecting adventurers deep into the eye of the most dangerous supercell ever seen.","AlecBaldwin,Amy,AnjulNigam,AnneHeche,BillBrody,DanielDiemer,DrQuinnBrody,HarperHunter,JohnnyWactor,JordanKristineSeamón,Martin,PrayaLundberg,Ramesh,RichardGunn,RoyCameron,SkeetUlrich,WilliamBrody,ZaneRogers",Supercell,Released,English,77000000.0,340941958.6,US +Cocaine Bear,2023-02-23,66.0,"Thriller,Comedy,Crime","Inspired by a true story, an oddball group of cops, criminals, tourists and teens converge in a Georgia forest where a 500-pound black bear goes on a murderous rampage after unintentionally ingesting cocaine.","AldenEhrenreich,Bob,ChristianConvery,Daveed,Eddie,Henry,IsiahWhitlockJr.,JesseTylerFerguson,KeriRussell,KristoferHivju,MargoMartindale,O'SheaJacksonJr.,Olaf(Kristoffer),Peter,RangerLiz,RayLiotta,Sari,Syd",Cocaine Bear,Released,English,35000000.0,80000000.0,AU +John Wick: Chapter 4,2023-03-23,80.0,"Action,Thriller,Crime","With the price on his head ever increasing, John Wick uncovers a path to defeating The High Table. But before he can earn his freedom, Wick must face off against a new enemy with powerful alliances across the globe and forces that turn old friends into foes.","BillSkarsgård,BoweryKing,Caine,Charon,ClancyBrown,DonnieYen,HiroyukiSanada,IanMcShane,JohnWick,KeanuReeves,LanceReddick,LaurenceFishburne,MarquisdeGramont,MrNobody,ShamierAnderson,Shimazu,TheHarbinger,Winston",John Wick: Chapter 4,Released,English,100000000.0,351349364.0,AU +Puss in Boots: The Last Wish,2022-12-26,83.0,"Animation,Family,Fantasy,Adventure,Comedy","Puss in Boots discovers that his passion for adventure has taken its toll: He has burned through eight of his nine lives, leaving him with only one life left. Puss sets out on an epic journey to find the mythical Last Wish and restore his nine lives.","AntonioBanderas,BabyBear(voice),FlorencePugh,Goldilocks(voice),HarveyGuillén,JackHorner(voice),JohnMulaney,KittySoftpaws(voice),MamaBear(voice),OliviaColman,PapaBear(voice),Perrito(voice),PussinBoots(voice),RayWinstone,SalmaHayek,SamsonKayo,WagnerMoura,Wolf(voice)",Puss in Boots: The Last Wish,Released,English,90000000.0,483480577.0,AU +Attack on Titan,2022-09-30,59.0,"Action,Science Fiction","As viable water is depleted on Earth, a mission is sent to Saturn's moon Titan to retrieve sustainable H2O reserves from its alien inhabitants. But just as the humans acquire the precious resource, they are attacked by Titan rebels, who don't trust that the Earthlings will leave in peace.","AdrianNaidu,AllisonQuince,AnthonyJensen,Computer(voice),ErinCoker,HeidiQuince,JackPearson,JennyTran,Jowers,JustinTanks,KaranSagoo,KimCosta,MarkMorales,MaxReece,NatalieStorrs,NeliSabour,PaulBianchi,SaoirseParker",Attack on Titan,Released,English,71000000.0,254946484.2,US +The Park,2023-03-02,58.0,"Action,Drama,Horror,Science Fiction,Thriller","A dystopian coming-of-age movie focused on three kids who find themselves in an abandoned amusement park, aiming to unite whoever remains. With dangers lurking around every corner, they will do whatever it takes to survive their hellish Neverland.","Bennett,BillySlaughter,Bui,CarliMcIntyre,CarminaGaray,ChloeGuidry,Ines,Jack,Kuan,LauraCoover,LegendJayJones,MartinParker,NhedrickJabier,PresleyRichardson,Reporter,Rue,SeanPapajohn,SlingshotGang",The Park,Released,English,119200000.0,488962491.0,US +Winnie the Pooh: Blood and Honey,2023-02-14,58.0,"Horror,Thriller","Christopher Robin is headed off to college and he has abandoned his old friends, Pooh and Piglet, which then leads to the duo embracing their inner monsters.","Alice,AmberDoig-Thorne,Charlene,ChrisCordell,ChristopherRobin,CraigDavidDowsett,DanielleRonald,DanielleScott,Lara,Maria,MariaTaylor,MayKelly,NatashaTosini,NikolaiLeon,Piglet,PoohBear,Tina,Zoe",Winnie the Pooh: Blood and Honey,Released,English,100000.0,3200000.0,AU +The Exorcist,2022-11-02,55.0,Horror,"Ophelia, a young nun recently arriving in the town of San Ramon, is forced to perform an exorcism on a pregnant woman in danger of dying. Just when she thinks her possession has ended, she discovers that the evil presence hasn't disappeared yet. The director of the award-winning Here Comes the Devil and Late Phases adds a new twist to possession movies in one of this year's Latin American horror surprises.",",DianaBracho,Fabian,Julia,JulioBracho,MaríaEvoli,NormaLazareno,Ofelia,PadreVictor,PilarSantacruz,RamónMedína,SalvadorSánchez,Sandra,SeñorRamirez,SeñoraRamirez,TinaRomero", La Exorcista,Released,"Spanish,Castilian",12000000.0,428214478.0,MX +Murder Mystery 2,2023-03-31,65.0,"Comedy,Mystery,Action","After starting their own detective agency, Nick and Audrey Spitz land a career-making case when their billionaire pal is kidnapped from his wedding.","AdamSandler,AdeelAkhtar,Audrey,Claudette,ColonelUlenga,CountessSekou,DanyBoon,InspectorDelacroix,JenniferAniston,JodieTurner-Smith,JohnKani,KuhooVerma,MarkStrong,Miller,MélanieLaurent,Nick,Saira,TheMaharajah",Murder Mystery 2,Released,English,43800000.0,489543087.4,AU +Black Panther: Wakanda Forever,2022-11-10,73.0,"Action,Adventure,Science Fiction","Queen Ramonda, Shuri, M’Baku, Okoye and the Dora Milaje fight to protect their nation from intervening world powers in the wake of King T’Challa’s death. As the Wakandans strive to embrace their next chapter, the heroes must band together with the help of War Dog Nakia and Everett Ross and forge a new path for the kingdom of Wakanda.","Aneka,AngelaBassett,Ayo,DanaiGurira,DominiqueThorne,FlorenceKasumba,LetitiaWright,LupitaNyong'o,M'Baku,MichaelaCoel,Nakia,Namor,Okoye,Ramonda,RiriWilliams/Ironheart,Shuri/BlackPanther,TenochHuertaMejía,WinstonDuke",Black Panther: Wakanda Forever,Released,English,250000000.0,854041058.0,AU +The Pope's Exorcist,2023-04-06,72.0,"Horror,Mystery","Father Gabriele Amorth, Chief Exorcist of the Vatican, investigates a young boy's terrifying possession and ends up uncovering a centuries-old conspiracy the Vatican has desperately tried to keep hidden.","AlexEssoe,Amy,BishopBarbuto,BishopLumumba,CornellJohn,DanielZovatto,Demon(voice),Enzo,FatherEsquibel,FatherGabrieleAmorth,FrancoNero,Julia,LaurelMarsden,PabloRaybould,RalphIneson,RiverHawkins,RussellCrowe,ThePope",The Pope's Exorcist,Released,English,92000000.0,462216471.8,AU +Prizefighter: The Life of Jem Belcher,2022-07-22,62.0,"Drama,History","At the turn of the 19th century, Pugilism was the sport of kings and a gifted young boxer fought his way to becoming champion of England.","BillWarr,JackSlack,JemBelcher,JodhiMay,JulianGlover,LordAshford,LordRushworth,MartonCsokas,MaryBelcher,MattHookings,RayWinstone,RussellCrowe,StevenBerkoff,Walter",Prizefighter: The Life of Jem Belcher,Released,English,77600000.0,562112551.0,GB +Knock at the Cabin,2023-02-02,64.0,"Thriller,Mystery,Horror,Fantasy","While vacationing at a remote cabin, a young girl and her two fathers are taken hostage by four armed strangers who demand that the family make an unthinkable choice to avert the apocalypse. With limited access to the outside world, the family must decide what they believe before all is lost.","AbbyQuinn,Adriane,Andrew,Andrew'sMom,BenAldridge,ClareLouiseFrost,DaveBautista,Eric,InfomercialCo-Host,JonathanGroff,KristenCui,Leonard,McKennaKerrigan,NikkiAmuka-Bird,Redmond,RupertGrint,Sabrina,Wen",Knock at the Cabin,Released,English,20000000.0,54629497.0,AU +The Devil Conspiracy,2023-01-13,65.0,"Horror,Thriller","The hottest biotech company in the world has discovered they can clone history’s most influential people from the dead. Now, they are auctioning clones of Michelangelo, Galileo, Vivaldi, and others for tens of millions of dollars to the world’s ultra-rich. But when they steal the Shroud of Turin and clone the DNA of Jesus Christ, all hell breaks loose.","AliceOrr-Ewing,ArchangelMichael,BeastoftheGround,Brenda,BrianCaspe,CardinalVincini,Dr.Laurent,EvelineHall,FatherMarconi/Michael,JamesFaulkner,JoeAnderson,JoeDoyle,Laura,Liz,Lucifer,PeterMensah,SpencerWilding,VictoriaChilap",The Devil Conspiracy,Released,English,54060000.0,492646395.4,US +Cazadora,2023-01-19,57.0,Thriller,"In a dystopian future, a mother and her teenage son go hunting in the mountains and encounter a stranger who threatens to upend their relationship.","AlexandraVonHummel,Emilia,FelipeValenzuela,Mateo,NataliaReddersen,Rena,Salvador,WillySemler",Cazadora,Released,"Spanish,Castilian",110400000.0,318117812.6,CL +Gold Run,2022-12-15,65.0,"War,Action,Adventure,Thriller","Fredrik isn’t the bravest of men, but now he is faced with a great responsibility and an enormous task - to get the entire Norwegian gold reserve away from the Nazis during the invasion of Norway.","AnatoleTaubman,AndreasLund,AxelBøyum,EivindSander,FredrikHaslund,GardB.Eidsvold,IdaEliseBroch,Ingvar,JonØigarden,MajorBjørnSunde,MajorOttoStoltmann,MortenSvartveit,NiniHaslundGleditsch,NordahlGrieg,OddHenry,OscarTorp,SvenNordin,ThorbjørnHarr", Gulltransporten,Released,Norwegian,123500000.0,420430064.8,NO +The Magician's Elephant,2023-03-17,73.0,"Animation,Adventure,Family,Fantasy","Peter is searching for his long-lost sister when he crosses paths with a fortune teller in the market square. His one one question is: is his sister still alive? The answer, that he must find a mysterious elephant and the magician who will conjure it, sets Peter off on a journey to complete three seemingly impossible tasks that will change the face of his town forever.","AasifMandvi,BenedictWong,BrianTyreeHenry,GloriaMatienne(voice),KirbyHowell-Baptiste,LeoMatienne(voice),MadamLaVaughn(voice),Magician(voice),MandyPatinkin,MirandaRichardson,Narrator/FortuneTeller(voice),NatasiaDemetriou,NoahJupe,Peter(voice),SianClifford,TheCountess(voice),TheKing(voice),Vilna(voice)",The Magician's Elephant,Released,English,82600000.0,473097256.6,AU +Plane,2023-01-27,69.0,"Action,Adventure,Thriller","After a heroic job of successfully landing his storm-damaged aircraft in a war zone, a fearless pilot finds himself between the agendas of multiple militias planning to take the plane and its passengers hostage.","Bonnie,BrodieTorrance,DaniellaPineda,Dele,EvanDaneTaylor,GerardButler,Hampton,JoeySlotnick,Junmar,LouisGaspare,MikeColter,PaulBen-Victor,RemiAdeleke,Scarsdale,Shellback,Sinclair,TonyGoldwyn,YosonAn",Plane,Released,English,25000000.0,51000000.0,GB +The Passion of the Christ,2004-02-25,74.0,Drama,A graphic portrayal of the last twelve hours of Jesus of Nazareth's life.,"Anás,Caifás,ChristoJivkov,FrancescoDeVito,HristoShopov,Jesus,JimCaviezel,Juan,Judas,LucaLionello,Magdalena,MaiaMorgenstern,Maria,MattiaSbragia,MonicaBellucci,Pedro,PoncioPilato,ToniBertorelli",The Passion of the Christ,Released,English,25000000.0,622313635.0,AU +Batman: The Doom That Came to Gotham,2023-03-28,64.0,"Animation,Fantasy,Horror,Action,Mystery","Explorer Bruce Wayne accidentally unleashes an ancient evil, and returns to Gotham after being away for two decades. There, Batman battles Lovecraftian supernatural forces and encounters allies and enemies such as Green Arrow, Ra's al Ghul, Mr. Freeze, Killer Croc, Two-Face and James Gordon.","Batman/BruceWayne(voice),ChristopherGorham,DarinDePaul,DavidDastmalchian,DavidGiuntoli,EmilyO'Brien,Grendon(voice),HarveyDent/Two-Face(voice),JamesGordon(voice),JohnDiMaggio,KaranBrar,NavidNegahban,OliverQueen/GreenArrow(voice),PatrickFabian,Ra'salGhul(voice),Sanjay'Jay'Tawde(voice),TaliaalGhul/MarthaWayne(voice),ThomasWayne(voice)",Batman: The Doom That Came to Gotham,Released,English,145760000.0,321934831.2,US diff --git a/src/routes/accounts.py b/src/routes/accounts.py index 82729aac..b9d02059 100644 --- a/src/routes/accounts.py +++ b/src/routes/accounts.py @@ -1,13 +1,25 @@ from datetime import datetime, timezone from typing import cast -from fastapi import APIRouter, Depends, status, HTTPException +from fastapi import ( + APIRouter, + Depends, + status, + HTTPException, + BackgroundTasks, + Request, +) from sqlalchemy import select, delete from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import joinedload -from config import get_jwt_auth_manager, get_settings, BaseAppSettings, get_accounts_email_notificator +from config import ( + get_jwt_auth_manager, + get_settings, + BaseAppSettings, + get_accounts_email_notificator, +) from database import ( get_db, UserModel, @@ -36,58 +48,24 @@ router = APIRouter() +def _as_utc(dt: datetime) -> datetime: + if dt.tzinfo is None: + return dt.replace(tzinfo=timezone.utc) + return dt + + @router.post( "/register/", response_model=UserRegistrationResponseSchema, - summary="User Registration", - description="Register a new user with an email and password.", status_code=status.HTTP_201_CREATED, - responses={ - 409: { - "description": "Conflict - User with this email already exists.", - "content": { - "application/json": { - "example": { - "detail": "A user with this email test@example.com already exists." - } - } - }, - }, - 500: { - "description": "Internal Server Error - An error occurred during user creation.", - "content": { - "application/json": { - "example": { - "detail": "An error occurred during user creation." - } - } - }, - }, - } ) async def register_user( - user_data: UserRegistrationRequestSchema, - db: AsyncSession = Depends(get_db), + user_data: UserRegistrationRequestSchema, + background_tasks: BackgroundTasks, + request: Request, + db: AsyncSession = Depends(get_db), + email_sender: EmailSenderInterface = Depends(get_accounts_email_notificator), ) -> UserRegistrationResponseSchema: - """ - Endpoint for user registration. - - Registers a new user, hashes their password, and assigns them to the default user group. - If a user with the same email already exists, an HTTP 409 error is raised. - In case of any unexpected issues during the creation process, an HTTP 500 error is returned. - - Args: - user_data (UserRegistrationRequestSchema): The registration details including email and password. - db (AsyncSession): The asynchronous database session. - - Returns: - UserRegistrationResponseSchema: The newly created user's details. - - Raises: - HTTPException: - - 409 Conflict if a user with the same email exists. - - 500 Internal Server Error if an error occurs during user creation. - """ stmt = select(UserModel).where(UserModel.email == user_data.email) result = await db.execute(stmt) existing_user = result.scalars().first() @@ -110,313 +88,197 @@ async def register_user( new_user = UserModel.create( email=str(user_data.email), raw_password=user_data.password, - group_id=user_group.id, + group_id=cast(int, user_group.id), ) db.add(new_user) await db.flush() - activation_token = ActivationTokenModel(user_id=new_user.id) + activation_token = ActivationTokenModel(user_id=cast(int, new_user.id)) db.add(activation_token) await db.commit() await db.refresh(new_user) + await db.refresh(activation_token) except SQLAlchemyError as e: await db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="An error occurred during user creation." ) from e - else: - return UserRegistrationResponseSchema.model_validate(new_user) + + activation_link = ( + f"{request.base_url}api/v1/accounts/activate/" + f"?email={new_user.email}&token={activation_token.token}" + ) + background_tasks.add_task( + email_sender.send_activation_email, + str(new_user.email), + activation_link, + ) + + return UserRegistrationResponseSchema.model_validate(new_user) @router.post( "/activate/", response_model=MessageResponseSchema, - summary="Activate User Account", - description="Activate a user's account using their email and activation token.", status_code=status.HTTP_200_OK, - responses={ - 400: { - "description": "Bad Request - The activation token is invalid or expired, " - "or the user account is already active.", - "content": { - "application/json": { - "examples": { - "invalid_token": { - "summary": "Invalid Token", - "value": { - "detail": "Invalid or expired activation token." - } - }, - "already_active": { - "summary": "Account Already Active", - "value": { - "detail": "User account is already active." - } - }, - } - } - }, - }, - }, ) async def activate_account( - activation_data: UserActivationRequestSchema, - db: AsyncSession = Depends(get_db), + activation_data: UserActivationRequestSchema, + background_tasks: BackgroundTasks, + request: Request, + db: AsyncSession = Depends(get_db), + email_sender: EmailSenderInterface = Depends(get_accounts_email_notificator), ) -> MessageResponseSchema: - """ - Endpoint to activate a user's account. - - This endpoint verifies the activation token for a user by checking that the token record exists - and that it has not expired. If the token is valid and the user's account is not already active, - the user's account is activated and the activation token is deleted. If the token is invalid, expired, - or if the account is already active, an HTTP 400 error is raised. - - Args: - activation_data (UserActivationRequestSchema): Contains the user's email and activation token. - db (AsyncSession): The asynchronous database session. - - Returns: - MessageResponseSchema: A response message confirming successful activation. - - Raises: - HTTPException: - - 400 Bad Request if the activation token is invalid or expired. - - 400 Bad Request if the user account is already active. - """ stmt = ( - select(ActivationTokenModel) - .options(joinedload(ActivationTokenModel.user)) - .join(UserModel) - .where( - UserModel.email == activation_data.email, - ActivationTokenModel.token == activation_data.token - ) + select(UserModel) + .options(joinedload(UserModel.activation_token)) + .where(UserModel.email == activation_data.email) ) result = await db.execute(stmt) - token_record = result.scalars().first() + user = result.scalars().first() - now_utc = datetime.now(timezone.utc) - if not token_record or cast(datetime, token_record.expires_at).replace(tzinfo=timezone.utc) < now_utc: - if token_record: - await db.delete(token_record) - await db.commit() - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Invalid or expired activation token." - ) + if user is None: + raise HTTPException(status_code=400, + detail="Invalid or expired activation token.") - user = token_record.user if user.is_active: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="User account is already active." - ) + raise HTTPException(status_code=400, + detail="User account is already active.") + + token_record = user.activation_token + if token_record is None or token_record.token != activation_data.token: + raise HTTPException(status_code=400, + detail="Invalid or expired activation token.") + + if _as_utc(token_record.expires_at) <= datetime.now(timezone.utc): + raise HTTPException(status_code=400, + detail="Invalid or expired activation token.") user.is_active = True await db.delete(token_record) await db.commit() + login_link = f"{request.base_url}api/v1/accounts/login/" + background_tasks.add_task( + email_sender.send_activation_complete_email, + str(user.email), + login_link, + ) + return MessageResponseSchema(message="User account activated successfully.") @router.post( "/password-reset/request/", response_model=MessageResponseSchema, - summary="Request Password Reset Token", - description=( - "Allows a user to request a password reset token. If the user exists and is active, " - "a new token will be generated and any existing tokens will be invalidated." - ), status_code=status.HTTP_200_OK, ) async def request_password_reset_token( - data: PasswordResetRequestSchema, - db: AsyncSession = Depends(get_db), + data: PasswordResetRequestSchema, + background_tasks: BackgroundTasks, + request: Request, + db: AsyncSession = Depends(get_db), + email_sender: EmailSenderInterface = Depends(get_accounts_email_notificator), ) -> MessageResponseSchema: - """ - Endpoint to request a password reset token. - - If the user exists and is active, invalidates any existing password reset tokens and generates a new one. - Always responds with a success message to avoid leaking user information. - - Args: - data (PasswordResetRequestSchema): The request data containing the user's email. - db (AsyncSession): The asynchronous database session. + generic = MessageResponseSchema( + message="If you are registered, you will receive an email with instructions.") - Returns: - MessageResponseSchema: A success message indicating that instructions will be sent. - """ - stmt = select(UserModel).filter_by(email=data.email) + stmt = select(UserModel).where(UserModel.email == data.email) result = await db.execute(stmt) user = result.scalars().first() - if not user or not user.is_active: - return MessageResponseSchema( - message="If you are registered, you will receive an email with instructions." - ) + if user is None or not user.is_active: + return generic await db.execute(delete(PasswordResetTokenModel).where(PasswordResetTokenModel.user_id == user.id)) reset_token = PasswordResetTokenModel(user_id=cast(int, user.id)) db.add(reset_token) await db.commit() + await db.refresh(reset_token) - return MessageResponseSchema( - message="If you are registered, you will receive an email with instructions." + reset_link = ( + f"{request.base_url}api/v1/accounts/reset-password/complete/" + f"?email={user.email}&token={reset_token.token}" + ) + background_tasks.add_task( + email_sender.send_password_reset_email, + str(user.email), + reset_link, ) + return generic + @router.post( "/reset-password/complete/", response_model=MessageResponseSchema, - summary="Reset User Password", - description="Reset a user's password if a valid token is provided.", status_code=status.HTTP_200_OK, - responses={ - 400: { - "description": ( - "Bad Request - The provided email or token is invalid, " - "the token has expired, or the user account is not active." - ), - "content": { - "application/json": { - "examples": { - "invalid_email_or_token": { - "summary": "Invalid Email or Token", - "value": { - "detail": "Invalid email or token." - } - }, - "expired_token": { - "summary": "Expired Token", - "value": { - "detail": "Invalid email or token." - } - } - } - } - }, - }, - 500: { - "description": "Internal Server Error - An error occurred while resetting the password.", - "content": { - "application/json": { - "example": { - "detail": "An error occurred while resetting the password." - } - } - }, - }, - }, ) async def reset_password( - data: PasswordResetCompleteRequestSchema, - db: AsyncSession = Depends(get_db), + data: PasswordResetCompleteRequestSchema, + background_tasks: BackgroundTasks, + request: Request, + db: AsyncSession = Depends(get_db), + email_sender: EmailSenderInterface = Depends(get_accounts_email_notificator), ) -> MessageResponseSchema: - """ - Endpoint for resetting a user's password. - - Validates the token and updates the user's password if the token is valid and not expired. - Deletes the token after a successful password reset. - - Args: - data (PasswordResetCompleteRequestSchema): The request data containing the user's email, - token, and new password. - db (AsyncSession): The asynchronous database session. - - Returns: - MessageResponseSchema: A response message indicating successful password reset. - - Raises: - HTTPException: - - 400 Bad Request if the email or token is invalid, or the token has expired. - - 500 Internal Server Error if an error occurs during the password reset process. - """ - stmt = select(UserModel).filter_by(email=data.email) + stmt = select(UserModel).where(UserModel.email == data.email) result = await db.execute(stmt) user = result.scalars().first() - if not user or not user.is_active: + if user is None or not user.is_active: raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, + status_code=400, detail="Invalid email or token." ) - stmt = select(PasswordResetTokenModel).filter_by(user_id=user.id) + stmt = select(PasswordResetTokenModel).where(PasswordResetTokenModel.user_id == user.id) result = await db.execute(stmt) token_record = result.scalars().first() - if not token_record or token_record.token != data.token: + if token_record is None or token_record.token != data.token: if token_record: - await db.run_sync(lambda s: s.delete(token_record)) + await db.delete(token_record) await db.commit() raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, + status_code=400, detail="Invalid email or token." ) - expires_at = cast(datetime, token_record.expires_at).replace(tzinfo=timezone.utc) - if expires_at < datetime.now(timezone.utc): - await db.run_sync(lambda s: s.delete(token_record)) + if _as_utc(token_record.expires_at) < datetime.now(timezone.utc): + await db.delete(token_record) await db.commit() raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, + status_code=400, detail="Invalid email or token." ) try: user.password = data.password - await db.run_sync(lambda s: s.delete(token_record)) + await db.delete(token_record) await db.commit() except SQLAlchemyError: await db.rollback() raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + status_code=500, detail="An error occurred while resetting the password." ) + login_link = f"{request.base_url}api/v1/accounts/login/" + background_tasks.add_task( + email_sender.send_password_reset_complete_email, + str(user.email), + login_link, + ) + return MessageResponseSchema(message="Password reset successfully.") @router.post( "/login/", response_model=UserLoginResponseSchema, - summary="User Login", - description="Authenticate a user and return access and refresh tokens.", status_code=status.HTTP_201_CREATED, - responses={ - 401: { - "description": "Unauthorized - Invalid email or password.", - "content": { - "application/json": { - "example": { - "detail": "Invalid email or password." - } - } - }, - }, - 403: { - "description": "Forbidden - User account is not activated.", - "content": { - "application/json": { - "example": { - "detail": "User account is not activated." - } - } - }, - }, - 500: { - "description": "Internal Server Error - An error occurred while processing the request.", - "content": { - "application/json": { - "example": { - "detail": "An error occurred while processing the request." - } - } - }, - }, - }, ) async def login_user( login_data: UserLoginRequestSchema, @@ -424,27 +286,6 @@ async def login_user( settings: BaseAppSettings = Depends(get_settings), jwt_manager: JWTAuthManagerInterface = Depends(get_jwt_auth_manager), ) -> UserLoginResponseSchema: - """ - Endpoint for user login. - - Authenticates a user using their email and password. - If authentication is successful, creates a new refresh token and returns both access and refresh tokens. - - Args: - login_data (UserLoginRequestSchema): The login credentials. - db (AsyncSession): The asynchronous database session. - settings (BaseAppSettings): The application settings. - jwt_manager (JWTAuthManagerInterface): The JWT authentication manager. - - Returns: - UserLoginResponseSchema: A response containing the access and refresh tokens. - - Raises: - HTTPException: - - 401 Unauthorized if the email or password is invalid. - - 403 Forbidden if the user account is not activated. - - 500 Internal Server Error if an error occurs during token creation. - """ stmt = select(UserModel).filter_by(email=login_data.email) result = await db.execute(stmt) user = result.scalars().first() @@ -489,67 +330,13 @@ async def login_user( @router.post( "/refresh/", response_model=TokenRefreshResponseSchema, - summary="Refresh Access Token", - description="Refresh the access token using a valid refresh token.", status_code=status.HTTP_200_OK, - responses={ - 400: { - "description": "Bad Request - The provided refresh token is invalid or expired.", - "content": { - "application/json": { - "example": { - "detail": "Token has expired." - } - } - }, - }, - 401: { - "description": "Unauthorized - Refresh token not found.", - "content": { - "application/json": { - "example": { - "detail": "Refresh token not found." - } - } - }, - }, - 404: { - "description": "Not Found - The user associated with the token does not exist.", - "content": { - "application/json": { - "example": { - "detail": "User not found." - } - } - }, - }, - }, ) async def refresh_access_token( token_data: TokenRefreshRequestSchema, db: AsyncSession = Depends(get_db), jwt_manager: JWTAuthManagerInterface = Depends(get_jwt_auth_manager), ) -> TokenRefreshResponseSchema: - """ - Endpoint to refresh an access token. - - Validates the provided refresh token, extracts the user ID from it, and issues - a new access token. If the token is invalid or expired, an error is returned. - - Args: - token_data (TokenRefreshRequestSchema): Contains the refresh token. - db (AsyncSession): The asynchronous database session. - jwt_manager (JWTAuthManagerInterface): JWT authentication manager. - - Returns: - TokenRefreshResponseSchema: A new access token. - - Raises: - HTTPException: - - 400 Bad Request if the token is invalid or expired. - - 401 Unauthorized if the refresh token is not found. - - 404 Not Found if the user associated with the token does not exist. - """ try: decoded_token = jwt_manager.decode_refresh_token(token_data.refresh_token) user_id = decoded_token.get("user_id") diff --git a/src/routes/profiles.py b/src/routes/profiles.py index 0b7c3420..deee643f 100644 --- a/src/routes/profiles.py +++ b/src/routes/profiles.py @@ -1,5 +1,152 @@ -from fastapi import APIRouter +from fastapi import APIRouter, Depends, HTTPException, Request, status +from fastapi.exceptions import RequestValidationError +from pydantic import ValidationError +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import joinedload + +from config import get_jwt_auth_manager, get_s3_storage_client +from database import get_db, UserModel, UserProfileModel, UserGroupEnum +from database.models.accounts import GenderEnum +from exceptions import BaseSecurityError, S3FileUploadError +from schemas.profiles import ProfileCreateSchema, ProfileResponseSchema +from security.interfaces import JWTAuthManagerInterface +from storages import S3StorageInterface router = APIRouter() -# Write your code here + +def get_token(request: Request) -> str: + authorization: str | None = request.headers.get("Authorization") + + if not authorization: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Authorization header is missing" + ) + + scheme, _, token = authorization.partition(" ") + + if scheme.lower() != "bearer" or not token: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid Authorization header format. Expected 'Bearer '" + ) + + return token + + +async def get_profile_payload(request: Request) -> ProfileCreateSchema: + form = await request.form() + try: + return ProfileCreateSchema( + first_name=form.get("first_name"), + last_name=form.get("last_name"), + gender=form.get("gender"), + date_of_birth=form.get("date_of_birth"), + info=form.get("info"), + avatar=form.get("avatar"), + ) + except ValidationError as e: + raise RequestValidationError(e.errors()) + + +@router.post( + "/users/{user_id}/profile/", + response_model=ProfileResponseSchema, + status_code=status.HTTP_201_CREATED, +) +async def create_user_profile( + user_id: int, + request: Request, + db: AsyncSession = Depends(get_db), + jwt_manager: JWTAuthManagerInterface = Depends(get_jwt_auth_manager), + s3_client: S3StorageInterface = Depends(get_s3_storage_client), +) -> ProfileResponseSchema: + token = get_token(request) + + try: + token_data = jwt_manager.decode_access_token(token) + except BaseSecurityError as e: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(e)) + + current_user_id = token_data.get("user_id") + + payload = await get_profile_payload(request) + + stmt_current = ( + select(UserModel) + .options(joinedload(UserModel.group)) + .where(UserModel.id == current_user_id) + ) + res_current = await db.execute(stmt_current) + current_user = res_current.scalars().first() + + if current_user is None or not current_user.is_active: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User not found or not active.", + ) + + is_admin = current_user.group is not None and current_user.group.name == UserGroupEnum.ADMIN + if (current_user.id != user_id) and (not is_admin): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You don't have permission to edit this profile.", + ) + + stmt_target = select(UserModel).where(UserModel.id == user_id) + res_target = await db.execute(stmt_target) + target_user = res_target.scalars().first() + + if target_user is None or not target_user.is_active: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User not found or not active.", + ) + + stmt_profile = select(UserProfileModel).where(UserProfileModel.user_id == user_id) + res_profile = await db.execute(stmt_profile) + if res_profile.scalars().first() is not None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="User already has a profile.", + ) + + avatar_key = f"avatars/{user_id}_avatar.jpg" + avatar_bytes = await payload.avatar.read() + + try: + await s3_client.upload_file(avatar_key, avatar_bytes) + except S3FileUploadError: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to upload avatar. Please try again later.", + ) + + profile = UserProfileModel( + user_id=user_id, + first_name=payload.first_name.lower(), + last_name=payload.last_name.lower(), + gender=GenderEnum(payload.gender), + date_of_birth=payload.date_of_birth, + info=payload.info, + avatar=avatar_key, + ) + + db.add(profile) + await db.commit() + await db.refresh(profile) + + avatar_url = await s3_client.get_file_url(avatar_key) + + return ProfileResponseSchema( + id=profile.id, + user_id=profile.user_id, + first_name=profile.first_name, + last_name=profile.last_name, + gender=profile.gender.value, + date_of_birth=profile.date_of_birth, + info=profile.info, + avatar=avatar_url, + ) diff --git a/src/schemas/accounts.py b/src/schemas/accounts.py index 1a034936..a5a7a0a5 100644 --- a/src/schemas/accounts.py +++ b/src/schemas/accounts.py @@ -3,63 +3,83 @@ from database import accounts_validators -class BaseEmailPasswordSchema(BaseModel): +class MessageResponseSchema(BaseModel): + message: str + + +class UserRegistrationRequestSchema(BaseModel): email: EmailStr password: str - model_config = { - "from_attributes": True - } - @field_validator("email") @classmethod - def validate_email(cls, value): - return value.lower() + def normalize_email(cls, value: EmailStr) -> EmailStr: + return EmailStr(str(value).lower()) @field_validator("password") @classmethod - def validate_password(cls, value): - return accounts_validators.validate_password_strength(value) - + def validate_password(cls, value: str) -> str: + accounts_validators.validate_password_strength(value) + return value -class UserRegistrationRequestSchema(BaseEmailPasswordSchema): - pass - -class PasswordResetRequestSchema(BaseModel): +class UserRegistrationResponseSchema(BaseModel): + id: int email: EmailStr + model_config = {"from_attributes": True} + -class PasswordResetCompleteRequestSchema(BaseEmailPasswordSchema): +class UserActivationRequestSchema(BaseModel): + email: EmailStr token: str + @field_validator("email") + @classmethod + def normalize_email(cls, value: EmailStr) -> EmailStr: + return EmailStr(str(value).lower()) -class UserLoginRequestSchema(BaseEmailPasswordSchema): - pass +class PasswordResetRequestSchema(BaseModel): + email: EmailStr -class UserLoginResponseSchema(BaseModel): - access_token: str - refresh_token: str - token_type: str = "bearer" + @field_validator("email") + @classmethod + def normalize_email(cls, value: EmailStr) -> EmailStr: + return EmailStr(str(value).lower()) -class UserRegistrationResponseSchema(BaseModel): - id: int +class PasswordResetCompleteRequestSchema(BaseModel): email: EmailStr + token: str + password: str - model_config = { - "from_attributes": True - } + @field_validator("email") + @classmethod + def normalize_email(cls, value: EmailStr) -> EmailStr: + return EmailStr(str(value).lower()) + @field_validator("password") + @classmethod + def validate_password(cls, value: str) -> str: + accounts_validators.validate_password_strength(value) + return value -class UserActivationRequestSchema(BaseModel): + +class UserLoginRequestSchema(BaseModel): email: EmailStr - token: str + password: str + @field_validator("email") + @classmethod + def normalize_email(cls, value: EmailStr) -> EmailStr: + return EmailStr(str(value).lower()) -class MessageResponseSchema(BaseModel): - message: str + +class UserLoginResponseSchema(BaseModel): + access_token: str + refresh_token: str + token_type: str = "bearer" class TokenRefreshRequestSchema(BaseModel): diff --git a/src/schemas/profiles.py b/src/schemas/profiles.py index adbcbcee..912b9220 100644 --- a/src/schemas/profiles.py +++ b/src/schemas/profiles.py @@ -1,7 +1,7 @@ from datetime import date -from fastapi import UploadFile, Form, File, HTTPException -from pydantic import BaseModel, field_validator, HttpUrl +from fastapi import UploadFile +from pydantic import BaseModel, field_validator from validation import ( validate_name, @@ -10,4 +10,59 @@ validate_birth_date ) -# Write your code here + +class ProfileCreateSchema(BaseModel): + first_name: str + last_name: str + gender: str + date_of_birth: date + info: str + avatar: UploadFile + + @field_validator("first_name") + @classmethod + def first_name_must_be_english(cls, v: str) -> str: + validate_name(v) + return v + + @field_validator("last_name") + @classmethod + def last_name_must_be_english(cls, v: str) -> str: + validate_name(v) + return v + + @field_validator("gender") + @classmethod + def gender_must_be_valid(cls, v: str) -> str: + validate_gender(v) + return v + + @field_validator("date_of_birth") + @classmethod + def birth_date_must_be_valid(cls, v: date) -> date: + validate_birth_date(v) + return v + + @field_validator("info") + @classmethod + def info_cannot_be_empty(cls, v: str) -> str: + if v is None or v.strip() == "": + raise ValueError("Info field cannot be empty or contain only spaces.") + return v + + @field_validator("avatar") + @classmethod + def avatar_must_be_valid_image(cls, v: UploadFile) -> UploadFile: + validate_image(v) + return v + + +class ProfileResponseSchema(BaseModel): + id: int + user_id: int + first_name: str + last_name: str + gender: str + date_of_birth: date + info: str + avatar: str diff --git a/src/validation/profile.py b/src/validation/profile.py index bdc32dfd..9a0aba8e 100644 --- a/src/validation/profile.py +++ b/src/validation/profile.py @@ -32,8 +32,9 @@ def validate_image(avatar: UploadFile) -> None: def validate_gender(gender: str) -> None: - if gender not in GenderEnum.__members__.values(): - raise ValueError(f"Gender must be one of: {', '.join(g.value for g in GenderEnum)}") + allowed = [g.value for g in GenderEnum] + if gender not in allowed: + raise ValueError(f"Gender must be one of: {', '.join(allowed)}") def validate_birth_date(birth_date: date) -> None: From 97a05775561cbbbed76143cb2fbfc2b66799d29d Mon Sep 17 00:00:00 2001 From: Ann Riabykina Date: Wed, 4 Feb 2026 10:27:15 +0200 Subject: [PATCH 2/4] fixed routes --- src/routes/profiles.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/routes/profiles.py b/src/routes/profiles.py index deee643f..dd2deaa9 100644 --- a/src/routes/profiles.py +++ b/src/routes/profiles.py @@ -1,3 +1,5 @@ +import os + from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.exceptions import RequestValidationError from pydantic import ValidationError @@ -113,7 +115,11 @@ async def create_user_profile( detail="User already has a profile.", ) - avatar_key = f"avatars/{user_id}_avatar.jpg" + filename = payload.avatar.filename + _, ext = os.path.splitext(filename) + ext = ext.lower() + + avatar_key = f"avatars/{user_id}_avatar.{ext}" avatar_bytes = await payload.avatar.read() try: From 2254001647a2cb12b46b5367195c667eca03c70f Mon Sep 17 00:00:00 2001 From: Ann Riabykina Date: Wed, 4 Feb 2026 10:33:26 +0200 Subject: [PATCH 3/4] fixed routes --- src/routes/profiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/profiles.py b/src/routes/profiles.py index dd2deaa9..60c989a5 100644 --- a/src/routes/profiles.py +++ b/src/routes/profiles.py @@ -119,7 +119,7 @@ async def create_user_profile( _, ext = os.path.splitext(filename) ext = ext.lower() - avatar_key = f"avatars/{user_id}_avatar.{ext}" + avatar_key = f"avatars/{user_id}_avatar{ext}" avatar_bytes = await payload.avatar.read() try: From 6df3813c73a61f311946f89422ea840dabef3fa6 Mon Sep 17 00:00:00 2001 From: Ann Riabykina Date: Wed, 4 Feb 2026 11:03:10 +0200 Subject: [PATCH 4/4] fixed schemas --- src/schemas/accounts.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/schemas/accounts.py b/src/schemas/accounts.py index a5a7a0a5..0b70f028 100644 --- a/src/schemas/accounts.py +++ b/src/schemas/accounts.py @@ -13,8 +13,8 @@ class UserRegistrationRequestSchema(BaseModel): @field_validator("email") @classmethod - def normalize_email(cls, value: EmailStr) -> EmailStr: - return EmailStr(str(value).lower()) + def normalize_email(cls, value: EmailStr) -> str: + return str(value).lower() @field_validator("password") @classmethod @@ -36,8 +36,8 @@ class UserActivationRequestSchema(BaseModel): @field_validator("email") @classmethod - def normalize_email(cls, value: EmailStr) -> EmailStr: - return EmailStr(str(value).lower()) + def normalize_email(cls, value: EmailStr) -> str: + return str(value).lower() class PasswordResetRequestSchema(BaseModel): @@ -45,8 +45,8 @@ class PasswordResetRequestSchema(BaseModel): @field_validator("email") @classmethod - def normalize_email(cls, value: EmailStr) -> EmailStr: - return EmailStr(str(value).lower()) + def normalize_email(cls, value: EmailStr) -> str: + return str(value).lower() class PasswordResetCompleteRequestSchema(BaseModel): @@ -56,8 +56,8 @@ class PasswordResetCompleteRequestSchema(BaseModel): @field_validator("email") @classmethod - def normalize_email(cls, value: EmailStr) -> EmailStr: - return EmailStr(str(value).lower()) + def normalize_email(cls, value: EmailStr) -> str: + return str(value).lower() @field_validator("password") @classmethod @@ -72,8 +72,8 @@ class UserLoginRequestSchema(BaseModel): @field_validator("email") @classmethod - def normalize_email(cls, value: EmailStr) -> EmailStr: - return EmailStr(str(value).lower()) + def normalize_email(cls, value: EmailStr) -> str: + return str(value).lower() class UserLoginResponseSchema(BaseModel):