[{"data":1,"prerenderedAt":23140},["ShallowReactive",2],{"navigation_docs":3,"-python-fastapi-fastapi-auth-security":3541,"-python-fastapi-fastapi-auth-security-surround":23135},[4,1707,1912,2366,2547,2673,2880,3002,3052,3109,3143,3269,3346,3360,3467,3484,3537],{"title":5,"icon":6,"path":7,"stem":8,"children":9},"C#","i-devicon-csharp","\u002Fcsharp","01.csharp",[10,13,60,90,120,202,219,253,379,404,457,650,1364,1654,1703],{"title":11,"path":7,"stem":12},"C# та .NET","01.csharp\u002Findex",{"title":14,"icon":15,"path":16,"stem":17,"children":18,"page":59},"Fundamentals","i-lucide-book-open","\u002Fcsharp\u002Ffundamentals","01.csharp\u002F01.fundamentals",[19,23,27,31,35,39,43,47,51,55],{"title":20,"path":21,"stem":22},"Вступ до екосистеми .NET","\u002Fcsharp\u002Ffundamentals\u002Fintroduction-to-ecosystem","01.csharp\u002F01.fundamentals\u002F01.introduction-to-ecosystem",{"title":24,"path":25,"stem":26},"Структура програми на C#","\u002Fcsharp\u002Ffundamentals\u002Fprogram-structure","01.csharp\u002F01.fundamentals\u002F02.program-structure",{"title":28,"path":29,"stem":30},"Змінні та Типи Даних","\u002Fcsharp\u002Ffundamentals\u002Fvariables-data-types","01.csharp\u002F01.fundamentals\u002F03.variables-data-types",{"title":32,"path":33,"stem":34},"Масиви","\u002Fcsharp\u002Ffundamentals\u002Farrays","01.csharp\u002F01.fundamentals\u002F04.arrays",{"title":36,"path":37,"stem":38},"Strings & Text Handling","\u002Fcsharp\u002Ffundamentals\u002Fstrings-text-handling","01.csharp\u002F01.fundamentals\u002F05.strings-text-handling",{"title":40,"path":41,"stem":42},"Дати і Час","\u002Fcsharp\u002Ffundamentals\u002Fdates-time-handling","01.csharp\u002F01.fundamentals\u002F06.dates-time-handling",{"title":44,"path":45,"stem":46},"Потік Керування","\u002Fcsharp\u002Ffundamentals\u002Fcontrol-flow","01.csharp\u002F01.fundamentals\u002F07.control-flow",{"title":48,"path":49,"stem":50},"Методи","\u002Fcsharp\u002Ffundamentals\u002Fmethods","01.csharp\u002F01.fundamentals\u002F08.methods",{"title":52,"path":53,"stem":54},"Основи Відлагодження","\u002Fcsharp\u002Ffundamentals\u002Fdebugging-basics","01.csharp\u002F01.fundamentals\u002F09.debugging-basics",{"title":56,"path":57,"stem":58},"Інтерактивна Консоль (Classic)","\u002Fcsharp\u002Ffundamentals\u002Finteractive-console","01.csharp\u002F01.fundamentals\u002F10.interactive-console",false,{"title":61,"icon":62,"path":63,"stem":64,"children":65,"page":59},"OOP","i-lucide-box","\u002Fcsharp\u002Foop","01.csharp\u002F02.oop",[66,70,74,78,82,86],{"title":67,"path":68,"stem":69},"Package Management (Управління Пакетами)","\u002Fcsharp\u002Foop\u002Fpackage-management","01.csharp\u002F02.oop\u002F01.package-management",{"title":71,"path":72,"stem":73},"Класи та Об'єкти","\u002Fcsharp\u002Foop\u002Fclasses-objects","01.csharp\u002F02.oop\u002F02.classes-objects",{"title":75,"path":76,"stem":77},"Властивості та Поля","\u002Fcsharp\u002Foop\u002Fproperties-fields","01.csharp\u002F02.oop\u002F03.properties-fields",{"title":79,"path":80,"stem":81},"Стовпи ООП","\u002Fcsharp\u002Foop\u002Foop-pillars","01.csharp\u002F02.oop\u002F04.oop-pillars",{"title":83,"path":84,"stem":85},"Advanced Types","\u002Fcsharp\u002Foop\u002Fadvanced-types","01.csharp\u002F02.oop\u002F05.advanced-types",{"title":87,"path":88,"stem":89},"Namespaces (Простори Імен)","\u002Fcsharp\u002Foop\u002Fnamespaces","01.csharp\u002F02.oop\u002F06.namespaces",{"title":91,"icon":92,"path":93,"stem":94,"children":95,"page":59},"Advanced Core","i-lucide-zap","\u002Fcsharp\u002Fadvanced-core","01.csharp\u002F03.advanced-core",[96,100,104,108,112,116],{"title":97,"path":98,"stem":99},"Generics (Узагальнення)","\u002Fcsharp\u002Fadvanced-core\u002Fgenerics","01.csharp\u002F03.advanced-core\u002F01.generics",{"title":101,"path":102,"stem":103},"Делегати, Події та Лямбда-вирази","\u002Fcsharp\u002Fadvanced-core\u002Fdelegates-events-lambdas","01.csharp\u002F03.advanced-core\u002F02.delegates-events-lambdas",{"title":105,"path":106,"stem":107},"Interfaces Deep Dive (Інтерфейси: Поглиблений Розгляд)","\u002Fcsharp\u002Fadvanced-core\u002Finterfaces-deep-dive","01.csharp\u002F03.advanced-core\u002F03.interfaces-deep-dive",{"title":109,"path":110,"stem":111},"Обробка Винятків","\u002Fcsharp\u002Fadvanced-core\u002Fexception-handling","01.csharp\u002F03.advanced-core\u002F04.exception-handling",{"title":113,"path":114,"stem":115},"Pattern Matching","\u002Fcsharp\u002Fadvanced-core\u002Fpattern-matching","01.csharp\u002F03.advanced-core\u002F05.pattern-matching",{"title":117,"path":118,"stem":119},"Додаткові Можливості C#","\u002Fcsharp\u002Fadvanced-core\u002Fadditional-features","01.csharp\u002F03.advanced-core\u002F06.additional-features",{"title":121,"icon":122,"path":123,"stem":124,"children":125,"page":59},"Architecture Best Practices","i-lucide-building-2","\u002Fcsharp\u002Farchitecture-best-practices","01.csharp\u002F04.architecture-best-practices",[126,130,149,153,157,161,165,169],{"title":127,"path":128,"stem":129},"Software Design Principles (Частина 1)","\u002Fcsharp\u002Farchitecture-best-practices\u002Fsoftware-design-principles","01.csharp\u002F04.architecture-best-practices\u002F01.software-design-principles",{"title":131,"icon":132,"path":133,"stem":134,"children":135,"page":59},"Design Patterns","i-lucide-folder","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdesign-patterns","01.csharp\u002F04.architecture-best-practices\u002F02.design-patterns",[136],{"title":137,"icon":132,"path":138,"stem":139,"children":140,"page":59},"Creational","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdesign-patterns\u002Fcreational","01.csharp\u002F04.architecture-best-practices\u002F02.design-patterns\u002Fcreational",[141,145],{"title":142,"path":143,"stem":144},"Singleton (Одинак)","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdesign-patterns\u002Fcreational\u002Fsingleton","01.csharp\u002F04.architecture-best-practices\u002F02.design-patterns\u002Fcreational\u002F01.singleton",{"title":146,"path":147,"stem":148},"Builder (Будівельник)","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdesign-patterns\u002Fcreational\u002Fbuilder","01.csharp\u002F04.architecture-best-practices\u002F02.design-patterns\u002Fcreational\u002F02.builder",{"title":150,"path":151,"stem":152},"Building Professional CLIs","\u002Fcsharp\u002Farchitecture-best-practices\u002Fbuilding-professional-clis","01.csharp\u002F04.architecture-best-practices\u002F03.building-professional-clis",{"title":154,"path":155,"stem":156},"Validation & Flow Control","\u002Fcsharp\u002Farchitecture-best-practices\u002Fvalidation-flow-control","01.csharp\u002F04.architecture-best-practices\u002F04.validation-flow-control",{"title":158,"path":159,"stem":160},"The Modern .NET Host (Microsoft.Extensions)","\u002Fcsharp\u002Farchitecture-best-practices\u002Fmodern-dotnet-host","01.csharp\u002F04.architecture-best-practices\u002F05.modern-dotnet-host",{"title":162,"path":163,"stem":164},"Data Mapper: Repository та DAO патерни (Частина 1)","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdata-mapper-part1","01.csharp\u002F04.architecture-best-practices\u002F06.data-mapper-part1",{"title":166,"path":167,"stem":168},"Data Mapper: Repository та DAO патерни (Частина 2)","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdata-mapper-part2","01.csharp\u002F04.architecture-best-practices\u002F07.data-mapper-part2",{"title":170,"icon":132,"path":171,"stem":172,"children":173,"page":59},"Di Ioc","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdi-ioc","01.csharp\u002F04.architecture-best-practices\u002F08.di-ioc",[174,178,182,186,190,194,198],{"title":175,"path":176,"stem":177},"Проблема залежностей та Інверсія Контролю","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdi-ioc\u002Fthe-dependency-problem","01.csharp\u002F04.architecture-best-practices\u002F08.di-ioc\u002F01.the-dependency-problem",{"title":179,"path":180,"stem":181},"Будуємо власний Service Container","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdi-ioc\u002Fbuild-your-own-container","01.csharp\u002F04.architecture-best-practices\u002F08.di-ioc\u002F02.build-your-own-container",{"title":183,"path":184,"stem":185},"Service Locator: Паттерн та Анти-паттерн","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdi-ioc\u002Fservice-locator-pattern","01.csharp\u002F04.architecture-best-practices\u002F08.di-ioc\u002F03.service-locator-pattern",{"title":187,"path":188,"stem":189},"Паттерни Dependency Injection","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdi-ioc\u002Fdependency-injection-patterns","01.csharp\u002F04.architecture-best-practices\u002F08.di-ioc\u002F04.dependency-injection-patterns",{"title":191,"path":192,"stem":193},"Microsoft DI: IServiceCollection та IServiceProvider","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdi-ioc\u002Fmicrosoft-di-deep-dive","01.csharp\u002F04.architecture-best-practices\u002F08.di-ioc\u002F05.microsoft-di-deep-dive",{"title":195,"path":196,"stem":197},"Service Lifetimes та Scopes","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdi-ioc\u002Fservice-lifetimes-and-scopes","01.csharp\u002F04.architecture-best-practices\u002F08.di-ioc\u002F06.service-lifetimes-and-scopes",{"title":199,"path":200,"stem":201},"DI Анти-паттерни та Найкращі Практики","\u002Fcsharp\u002Farchitecture-best-practices\u002Fdi-ioc\u002Fdi-anti-patterns-and-best-practices","01.csharp\u002F04.architecture-best-practices\u002F08.di-ioc\u002F07.di-anti-patterns-and-best-practices",{"title":203,"icon":132,"path":204,"stem":205,"children":206,"page":59},"Standard Library","\u002Fcsharp\u002Fstandard-library","01.csharp\u002F05.standard-library",[207,211,215],{"title":208,"path":209,"stem":210},"Collections (Колекції)","\u002Fcsharp\u002Fstandard-library\u002Fcollections","01.csharp\u002F05.standard-library\u002F01.collections",{"title":212,"path":213,"stem":214},"High Performance Types (Високопродуктивні Типи)","\u002Fcsharp\u002Fstandard-library\u002Fhigh-performance-types","01.csharp\u002F05.standard-library\u002F02.high-performance-types",{"title":216,"path":217,"stem":218},"LINQ (Language Integrated Query)","\u002Fcsharp\u002Fstandard-library\u002Flinq","01.csharp\u002F05.standard-library\u002F03.linq",{"title":220,"icon":221,"path":222,"stem":223,"children":224,"page":59},"System Internals Concurrency","i-lucide-server","\u002Fcsharp\u002Fsystem-internals-concurrency","01.csharp\u002F06.system-internals-concurrency",[225,229,233,237,241,245,249],{"title":226,"path":227,"stem":228},"Memory Management","\u002Fcsharp\u002Fsystem-internals-concurrency\u002Fmemory-management","01.csharp\u002F06.system-internals-concurrency\u002F01.memory-management",{"title":230,"path":231,"stem":232},"Reflection API: System.Type та Метадані","\u002Fcsharp\u002Fsystem-internals-concurrency\u002Freflection-fundamentals","01.csharp\u002F06.system-internals-concurrency\u002F02.reflection-fundamentals",{"title":234,"path":235,"stem":236},"Attributes та Dynamic Language Runtime","\u002Fcsharp\u002Fsystem-internals-concurrency\u002Fattributes-dynamic","01.csharp\u002F06.system-internals-concurrency\u002F03.attributes-dynamic",{"title":238,"path":239,"stem":240},"Expression Trees: Швидка Альтернатива Рефлексії","\u002Fcsharp\u002Fsystem-internals-concurrency\u002Fexpression-trees-compiled","01.csharp\u002F06.system-internals-concurrency\u002F04.expression-trees-compiled",{"title":242,"path":243,"stem":244},"Source Generators: Compile-Time Code Generation","\u002Fcsharp\u002Fsystem-internals-concurrency\u002Fsource-generators","01.csharp\u002F06.system-internals-concurrency\u002F05.source-generators",{"title":246,"path":247,"stem":248},"Multithreading Fundamentals","\u002Fcsharp\u002Fsystem-internals-concurrency\u002Fmultithreading-fundamentals","01.csharp\u002F06.system-internals-concurrency\u002F06.multithreading-fundamentals",{"title":250,"path":251,"stem":252},"Synchronization Primitives","\u002Fcsharp\u002Fsystem-internals-concurrency\u002Fsynchronization-primitives","01.csharp\u002F06.system-internals-concurrency\u002F07.synchronization-primitives",{"title":254,"icon":255,"path":256,"stem":257,"children":258,"page":59},"System Programming Windows","i-lucide-cpu","\u002Fcsharp\u002Fsystem-programming-windows","01.csharp\u002F07.system-programming-windows",[259,263,267,271,275,279,283,287,291,295,299,303,307,311,315,319,323,327,331,335,339,343,347,351,355,359,363,367,371,375],{"title":260,"path":261,"stem":262},"Як Працює Операційна Система","\u002Fcsharp\u002Fsystem-programming-windows\u002Fhow-os-works","01.csharp\u002F07.system-programming-windows\u002F01.how-os-works",{"title":264,"path":265,"stem":266},"Процеси в .NET — API та Запуск","\u002Fcsharp\u002Fsystem-programming-windows\u002Fprocesses-in-dotnet","01.csharp\u002F07.system-programming-windows\u002F02.processes-in-dotnet",{"title":268,"path":269,"stem":270},"Процеси в .NET — IPC та Міжпроцесна Комунікація","\u002Fcsharp\u002Fsystem-programming-windows\u002F02a.processes-ipc","01.csharp\u002F07.system-programming-windows\u002F02a.processes-ipc",{"title":272,"path":273,"stem":274},"Application Domains та Збірки — AppDomain і AssemblyLoadContext","\u002Fcsharp\u002Fsystem-programming-windows\u002Fappdomains-assemblies","01.csharp\u002F07.system-programming-windows\u002F03.appdomains-assemblies",{"title":276,"path":277,"stem":278},"Application Domains та Збірки — Plug-in Система з Hot-Reload","\u002Fcsharp\u002Fsystem-programming-windows\u002F03a.appdomains-plugin-system","01.csharp\u002F07.system-programming-windows\u002F03a.appdomains-plugin-system",{"title":280,"path":281,"stem":282},"Потоки — Основи та API Thread","\u002Fcsharp\u002Fsystem-programming-windows\u002Fthread-fundamentals","01.csharp\u002F07.system-programming-windows\u002F04.thread-fundamentals",{"title":284,"path":285,"stem":286},"Потоки — Lifecycle, Пріоритети та Безпечне Завершення","\u002Fcsharp\u002Fsystem-programming-windows\u002F04a.thread-lifecycle-priorities","01.csharp\u002F07.system-programming-windows\u002F04a.thread-lifecycle-priorities",{"title":288,"path":289,"stem":290},"Проблеми Спільного Стану — Race Condition та Data Race","\u002Fcsharp\u002Fsystem-programming-windows\u002Fshared-state-problems","01.csharp\u002F07.system-programming-windows\u002F05.shared-state-problems",{"title":292,"path":293,"stem":294},"Проблеми Спільного Стану — Memory Model та volatile","\u002Fcsharp\u002Fsystem-programming-windows\u002F05a.shared-state-memory-model","01.csharp\u002F07.system-programming-windows\u002F05a.shared-state-memory-model",{"title":296,"path":297,"stem":298},"Синхронізація — Monitor, lock та еволюція примітивів","\u002Fcsharp\u002Fsystem-programming-windows\u002Fsynchronization-fundamentals","01.csharp\u002F07.system-programming-windows\u002F06.synchronization-fundamentals",{"title":300,"path":301,"stem":302},"Синхронізація — Наскрізний Приклад та Deadlock Detection","\u002Fcsharp\u002Fsystem-programming-windows\u002F06a.synchronization-walkthrough","01.csharp\u002F07.system-programming-windows\u002F06a.synchronization-walkthrough",{"title":304,"path":305,"stem":306},"Синхронізація — Mutex, Semaphore та Event-Based Primitives","\u002Fcsharp\u002Fsystem-programming-windows\u002Fsynchronization-advanced","01.csharp\u002F07.system-programming-windows\u002F07.synchronization-advanced",{"title":308,"path":309,"stem":310},"Синхронізація — Interlocked, Volatile та Lock-Free Структури","\u002Fcsharp\u002Fsystem-programming-windows\u002F07a.synchronization-advanced-walkthrough","01.csharp\u002F07.system-programming-windows\u002F07a.synchronization-advanced-walkthrough",{"title":312,"path":313,"stem":314},"Interlocked, CAS та Lock-Free Структури","\u002Fcsharp\u002Fsystem-programming-windows\u002Finterlocked-cas-lockfree","01.csharp\u002F07.system-programming-windows\u002F08.interlocked-cas-lockfree",{"title":316,"path":317,"stem":318},"Volatile, Memory Model та Spinning","\u002Fcsharp\u002Fsystem-programming-windows\u002F08a.volatile-memory-model","01.csharp\u002F07.system-programming-windows\u002F08a.volatile-memory-model",{"title":320,"path":321,"stem":322},"ThreadPool — Пул Потоків для Ефективного Виконання","\u002Fcsharp\u002Fsystem-programming-windows\u002Fthread-pool","01.csharp\u002F07.system-programming-windows\u002F09.thread-pool",{"title":324,"path":325,"stem":326},"ThreadPool — Просунуті Сценарії та Внутрішня Будова","\u002Fcsharp\u002Fsystem-programming-windows\u002F09a.thread-pool-advanced","01.csharp\u002F07.system-programming-windows\u002F09a.thread-pool-advanced",{"title":328,"path":329,"stem":330},"Concurrent та Immutable Collections","\u002Fcsharp\u002Fsystem-programming-windows\u002Fconcurrent-collections","01.csharp\u002F07.system-programming-windows\u002F10.concurrent-collections",{"title":332,"path":333,"stem":334},"TPL, Task та Композиція — Від Thread до Task","\u002Fcsharp\u002Fsystem-programming-windows\u002Ftpl-parallel-plinq","01.csharp\u002F07.system-programming-windows\u002F11.tpl-parallel-plinq",{"title":336,"path":337,"stem":338},"Parallel Class та PLINQ — Data Parallelism","\u002Fcsharp\u002Fsystem-programming-windows\u002F11a.tpl-parallel-plinq-advanced","01.csharp\u002F07.system-programming-windows\u002F11a.tpl-parallel-plinq-advanced",{"title":340,"path":341,"stem":342},"Async\u002FAwait — Фундамент Асинхронного Програмування","\u002Fcsharp\u002Fsystem-programming-windows\u002Fasync-fundamentals","01.csharp\u002F07.system-programming-windows\u002F12.async-fundamentals",{"title":344,"path":345,"stem":346},"SynchronizationContext та ConfigureAwait — Контекст Виконання","\u002Fcsharp\u002Fsystem-programming-windows\u002Fasync-context-configureawait","01.csharp\u002F07.system-programming-windows\u002F13.async-context-configureawait",{"title":348,"path":349,"stem":350},"Async — Просунуті Паттерни","\u002Fcsharp\u002Fsystem-programming-windows\u002Fasync-advanced","01.csharp\u002F07.system-programming-windows\u002F14.async-advanced",{"title":352,"path":353,"stem":354},"System.Threading.Channels — Async Producer-Consumer","\u002Fcsharp\u002Fsystem-programming-windows\u002Fchannels","01.csharp\u002F07.system-programming-windows\u002F15.channels",{"title":356,"path":357,"stem":358},"Асинхронна Синхронізація","\u002Fcsharp\u002Fsystem-programming-windows\u002Fasync-synchronization","01.csharp\u002F07.system-programming-windows\u002F16.async-synchronization",{"title":360,"path":361,"stem":362},"Unsafe Code та Вказівники","\u002Fcsharp\u002Fsystem-programming-windows\u002Funsafe-code","01.csharp\u002F07.system-programming-windows\u002F17.unsafe-code",{"title":364,"path":365,"stem":366},"P\u002FInvoke та Windows API — Міст між .NET та Native Code","\u002Fcsharp\u002Fsystem-programming-windows\u002Fpinvoke-winapi","01.csharp\u002F07.system-programming-windows\u002F18.pinvoke-winapi",{"title":368,"path":369,"stem":370},"Реєстр Windows — Центральна База Конфігурації Системи","\u002Fcsharp\u002Fsystem-programming-windows\u002Fwindows-registry","01.csharp\u002F07.system-programming-windows\u002F19.windows-registry",{"title":372,"path":373,"stem":374},"Windows Hooks, Hotkeys та Services — Глибока Інтеграція з ОС","\u002Fcsharp\u002Fsystem-programming-windows\u002Fwindows-hooks-services","01.csharp\u002F07.system-programming-windows\u002F20.windows-hooks-services",{"title":376,"path":377,"stem":378},"Системне Програмування C# (Windows) — 07.system-programming-windows","\u002Fcsharp\u002Fsystem-programming-windows\u002Fimplementation_plan","01.csharp\u002F07.system-programming-windows\u002Fimplementation_plan",{"title":380,"icon":132,"path":381,"stem":382,"children":383,"page":59},"Io","\u002Fcsharp\u002Fio","01.csharp\u002F08.io",[384,388,392,396,400],{"title":385,"path":386,"stem":387},"8.1.1. Основи роботи з файловою системою","\u002Fcsharp\u002Fio\u002Ffile-system-basics","01.csharp\u002F08.io\u002F01.file-system-basics",{"title":389,"path":390,"stem":391},"8.1.2. Потоки (Streams) та Серіалізація Даних","\u002Fcsharp\u002Fio\u002Fstreams-serialization","01.csharp\u002F08.io\u002F02.streams-serialization",{"title":393,"path":394,"stem":395},"8.2.1. JSON Serialization з System.Text.Json","\u002Fcsharp\u002Fio\u002Fjson-serialization","01.csharp\u002F08.io\u002F03.json-serialization",{"title":397,"path":398,"stem":399},"8.2.2. XML Serialization та LINQ to XML","\u002Fcsharp\u002Fio\u002Fxml-serialization","01.csharp\u002F08.io\u002F04.xml-serialization",{"title":401,"path":402,"stem":403},"8.2.3. Binary Serialization: MessagePack та Protocol Buffers","\u002Fcsharp\u002Fio\u002Fbinary-serialization","01.csharp\u002F08.io\u002F05.binary-serialization",{"title":405,"icon":132,"path":406,"stem":407,"children":408,"page":59},"Ado Net","\u002Fcsharp\u002Fado-net","01.csharp\u002F09.ado-net",[409,413,417,421,425,429,433,437,441,445,449,453],{"title":410,"path":411,"stem":412},"9.1. Введення в ADO.NET","\u002Fcsharp\u002Fado-net\u002Fintroduction-to-adonet","01.csharp\u002F09.ado-net\u002F01.introduction-to-adonet",{"title":414,"path":415,"stem":416},"9.2. Клас DbConnection — з'єднання з базою даних","\u002Fcsharp\u002Fado-net\u002Fconnection","01.csharp\u002F09.ado-net\u002F02.connection",{"title":418,"path":419,"stem":420},"9.3. Клас DbCommand — виконання SQL-запитів","\u002Fcsharp\u002Fado-net\u002Fcommand-and-queries","01.csharp\u002F09.ado-net\u002F03.command-and-queries",{"title":422,"path":423,"stem":424},"9.4. Клас DbDataReader — ефективне читання даних","\u002Fcsharp\u002Fado-net\u002Fdatareader","01.csharp\u002F09.ado-net\u002F04.datareader",{"title":426,"path":427,"stem":428},"9.5. Параметризовані запити та захист від SQL Injection","\u002Fcsharp\u002Fado-net\u002Fparameters-and-sql-injection","01.csharp\u002F09.ado-net\u002F05.parameters-and-sql-injection",{"title":430,"path":431,"stem":432},"9.6. Транзакції в ADO.NET","\u002Fcsharp\u002Fado-net\u002Ftransactions","01.csharp\u002F09.ado-net\u002F06.transactions",{"title":434,"path":435,"stem":436},"9.7. DbProviderFactory — провайдер-незалежний код","\u002Fcsharp\u002Fado-net\u002Fprovider-factory","01.csharp\u002F09.ado-net\u002F07.provider-factory",{"title":438,"path":439,"stem":440},"9.8. Асинхронний доступ до даних","\u002Fcsharp\u002Fado-net\u002Fasync-data-access","01.csharp\u002F09.ado-net\u002F08.async-data-access",{"title":442,"path":443,"stem":444},"9.9. Від'єднаний режим: DataSet, DataTable, DataRow","\u002Fcsharp\u002Fado-net\u002Fdisconnected-mode-dataset","01.csharp\u002F09.ado-net\u002F09.disconnected-mode-dataset",{"title":446,"path":447,"stem":448},"9.10. DataAdapter — міст між DataSet та базою даних","\u002Fcsharp\u002Fado-net\u002Fdata-adapter","01.csharp\u002F09.ado-net\u002F10.data-adapter",{"title":450,"path":451,"stem":452},"9.11. Data Mapper та Repository: Архітектура доступу до даних","\u002Fcsharp\u002Fado-net\u002Fdata-mapper-repository","01.csharp\u002F09.ado-net\u002F11.data-mapper-repository",{"title":454,"path":455,"stem":456},"9.12. Identity Map, Unit of Work та Specification Pattern","\u002Fcsharp\u002Fado-net\u002Fadvanced-patterns","01.csharp\u002F09.ado-net\u002F12.advanced-patterns",{"title":458,"icon":255,"path":459,"stem":460,"children":461,"page":59},"Ef Core","\u002Fcsharp\u002Fef-core","01.csharp\u002F10.ef-core",[462,466,470,474,478,482,486,490,494,498,502,506,510,514,518,522,526,532,538,542,546,550,554,558,562,566,570,574,578,582,586,590,594,598,602,606,610,614,618,622,626,630,634,638,642,646],{"title":463,"path":464,"stem":465},"Що таке ORM? Від SQL до об'єктів","\u002Fcsharp\u002Fef-core\u002Fwhat-is-orm","01.csharp\u002F10.ef-core\u002F01.what-is-orm",{"title":467,"path":468,"stem":469},"Перший проєкт — від нуля до CRUD","\u002Fcsharp\u002Fef-core\u002Ffirst-project","01.csharp\u002F10.ef-core\u002F02.first-project",{"title":471,"path":472,"stem":473},"DbContext — Серце EF Core","\u002Fcsharp\u002Fef-core\u002Fdbcontext-deep-dive","01.csharp\u002F10.ef-core\u002F03.dbcontext-deep-dive",{"title":475,"path":476,"stem":477},"Провайдери баз даних — Архітектура та Вибір СУБД","\u002Fcsharp\u002Fef-core\u002Fdatabase-providers","01.csharp\u002F10.ef-core\u002F04.database-providers",{"title":479,"path":480,"stem":481},"Конвенції EF Core — Магія без конфігурації","\u002Fcsharp\u002Fef-core\u002Fconventions","01.csharp\u002F10.ef-core\u002F05.conventions",{"title":483,"path":484,"stem":485},"Fluent API та Data Annotations — Явна конфігурація моделі","\u002Fcsharp\u002Fef-core\u002Ffluent-api-vs-annotations","01.csharp\u002F10.ef-core\u002F06.fluent-api-vs-annotations",{"title":487,"path":488,"stem":489},"Зв'язки — One-to-One та One-to-Many","\u002Fcsharp\u002Fef-core\u002Frelationships-basics","01.csharp\u002F10.ef-core\u002F07.relationships-basics",{"title":491,"path":492,"stem":493},"Зв'язки Advanced — Many-to-Many та Складні Сценарії","\u002Fcsharp\u002Fef-core\u002Frelationships-advanced","01.csharp\u002F10.ef-core\u002F08.relationships-advanced",{"title":495,"path":496,"stem":497},"Властивості — Типи, Конвертери, Компаратори (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fproperty-configuration-part1","01.csharp\u002F10.ef-core\u002F09.property-configuration-part1",{"title":499,"path":500,"stem":501},"Властивості — Value Comparers, Generators, Shadow Properties (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fproperty-configuration-part2","01.csharp\u002F10.ef-core\u002F09.property-configuration-part2",{"title":503,"path":504,"stem":505},"Складні типи — Owned Types та Complex Types (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fcomplex-types-owned-part1","01.csharp\u002F10.ef-core\u002F10.complex-types-owned-part1",{"title":507,"path":508,"stem":509},"Складні типи — Complex Types, Keyless Entities, Порівняння (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fcomplex-types-owned-part2","01.csharp\u002F10.ef-core\u002F10.complex-types-owned-part2",{"title":511,"path":512,"stem":513},"JSON Columns — Складні дані у JSON (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fjson-columns-part1","01.csharp\u002F10.ef-core\u002F11.json-columns-part1",{"title":515,"path":516,"stem":517},"JSON Columns — Value Comparers, Індекси, Провайдери (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fjson-columns-part2","01.csharp\u002F10.ef-core\u002F11.json-columns-part2",{"title":519,"path":520,"stem":521},"Успадкування — Абстрактні класи та TPH (Частина 1)","\u002Fcsharp\u002Fef-core\u002Finheritance-part1","01.csharp\u002F10.ef-core\u002F12.inheritance-part1",{"title":523,"path":524,"stem":525},"Успадкування — TPT, TPC та Порівняння Стратегій (Частина 2)","\u002Fcsharp\u002Fef-core\u002Finheritance-part2","01.csharp\u002F10.ef-core\u002F12.inheritance-part2",{"title":527,"path":528,"stem":529,"children":530},"Індекси, Обмеження та Схема (Частина 1)","\u002Fcsharp\u002Fef-core\u002Findexes-constraints-part1","01.csharp\u002F10.ef-core\u002F13.indexes-constraints-part1",[531],{"title":527,"path":528,"stem":529},{"title":533,"path":534,"stem":535,"children":536},"Індекси, Обмеження та Схема (Частина 2)","\u002Fcsharp\u002Fef-core\u002Findexes-constraints-part2","01.csharp\u002F10.ef-core\u002F13.indexes-constraints-part2",[537],{"title":533,"path":534,"stem":535},{"title":539,"path":540,"stem":541},"Seed Data — Початкові Дані (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fseeding-part1","01.csharp\u002F10.ef-core\u002F14.seeding-part1",{"title":543,"path":544,"stem":545},"Seed Data — SQL-скрипти, Bogus та Стратегії (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fseeding-part2","01.csharp\u002F10.ef-core\u002F14.seeding-part2",{"title":547,"path":548,"stem":549},"Global Query Filters — Глобальні Фільтри (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fglobal-query-filters-part1","01.csharp\u002F10.ef-core\u002F15.global-query-filters-part1",{"title":551,"path":552,"stem":553},"Global Query Filters — Підводні камені та Інтеграція (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fglobal-query-filters-part2","01.csharp\u002F10.ef-core\u002F15.global-query-filters-part2",{"title":555,"path":556,"stem":557},"LINQ-запити в EF Core (Частина 1)","\u002Fcsharp\u002Fef-core\u002Flinq-queries-part1","01.csharp\u002F10.ef-core\u002F16.linq-queries-part1",{"title":559,"path":560,"stem":561},"LINQ-запити в EF Core (Частина 2)","\u002Fcsharp\u002Fef-core\u002Flinq-queries-part2","01.csharp\u002F10.ef-core\u002F16.linq-queries-part2",{"title":563,"path":564,"stem":565},"Завантаження Пов'язаних Даних (Частина 1)","\u002Fcsharp\u002Fef-core\u002Floading-related-data-part1","01.csharp\u002F10.ef-core\u002F17.loading-related-data-part1",{"title":567,"path":568,"stem":569},"Завантаження Пов'язаних Даних (Частина 2)","\u002Fcsharp\u002Fef-core\u002Floading-related-data-part2","01.csharp\u002F10.ef-core\u002F17.loading-related-data-part2",{"title":571,"path":572,"stem":573},"Raw SQL, Views та Stored Procedures (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fraw-sql-part1","01.csharp\u002F10.ef-core\u002F18.raw-sql-part1",{"title":575,"path":576,"stem":577},"Raw SQL — Stored Procedures, DbFunction та Bulk Operations (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fraw-sql-part2","01.csharp\u002F10.ef-core\u002F18.raw-sql-part2",{"title":579,"path":580,"stem":581},"Продвинуті Запити — Compiled Queries, Bulk та Оптимізація (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fadvanced-queries-part1","01.csharp\u002F10.ef-core\u002F19.advanced-queries-part1",{"title":583,"path":584,"stem":585},"Продвинуті Запити — Query Tags, Bulk та Interceptors (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fadvanced-queries-part2","01.csharp\u002F10.ef-core\u002F19.advanced-queries-part2",{"title":587,"path":588,"stem":589},"Change Tracker — Відстеження Змін (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fchange-tracking-part1","01.csharp\u002F10.ef-core\u002F20.change-tracking-part1",{"title":591,"path":592,"stem":593},"Change Tracker — Графи Об'єктів та Disconnected (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fchange-tracking-part2","01.csharp\u002F10.ef-core\u002F20.change-tracking-part2",{"title":595,"path":596,"stem":597},"Збереження Даних та Транзакції (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fsaving-data-part1","01.csharp\u002F10.ef-core\u002F21.saving-data-part1",{"title":599,"path":600,"stem":601},"Збереження Даних — Concurrency та Outbox (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fsaving-data-part2","01.csharp\u002F10.ef-core\u002F21.saving-data-part2",{"title":603,"path":604,"stem":605},"Конкурентність та Блокування (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fconcurrency-part1","01.csharp\u002F10.ef-core\u002F22.concurrency-part1",{"title":607,"path":608,"stem":609},"Конкурентність — Дедлоки та Queue Processing (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fconcurrency-part2","01.csharp\u002F10.ef-core\u002F22.concurrency-part2",{"title":611,"path":612,"stem":613},"Міграції в EF Core — Основи (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fmigrations-basics-part1","01.csharp\u002F10.ef-core\u002F23.migrations-basics-part1",{"title":615,"path":616,"stem":617},"Міграції в EF Core — Основи (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fmigrations-basics-part2","01.csharp\u002F10.ef-core\u002F23.migrations-basics-part2",{"title":619,"path":620,"stem":621},"Міграції — Просунуті Сценарії (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fmigrations-advanced-part1","01.csharp\u002F10.ef-core\u002F24.migrations-advanced-part1",{"title":623,"path":624,"stem":625},"Міграції — Просунуті Сценарії (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fmigrations-advanced-part2","01.csharp\u002F10.ef-core\u002F24.migrations-advanced-part2",{"title":627,"path":628,"stem":629},"Управління Схемою та Database-First (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fschema-management-part1","01.csharp\u002F10.ef-core\u002F25.schema-management-part1",{"title":631,"path":632,"stem":633},"Управління Схемою та Database-First (Частина 2)","\u002Fcsharp\u002Fef-core\u002Fschema-management-part2","01.csharp\u002F10.ef-core\u002F25.schema-management-part2",{"title":635,"path":636,"stem":637},"Продуктивність EF Core — Основи (Частина 1)","\u002Fcsharp\u002Fef-core\u002Fperformance-fundamentals-part1","01.csharp\u002F10.ef-core\u002F26.performance-fundamentals-part1",{"title":639,"path":640,"stem":641},"Interceptors в EF Core (Частина 1)","\u002Fcsharp\u002Fef-core\u002Finterceptors-part1","01.csharp\u002F10.ef-core\u002F29.interceptors-part1",{"title":643,"path":644,"stem":645},"Interceptors в EF Core — Connection, Transaction та Materialization (Частина 2)","\u002Fcsharp\u002Fef-core\u002Finterceptors-part2","01.csharp\u002F10.ef-core\u002F29.interceptors-part2",{"title":647,"path":648,"stem":649},"План вивчення Entity Framework Core — Повний курс","\u002Fcsharp\u002Fef-core\u002Fimplementation_plan","01.csharp\u002F10.ef-core\u002Fimplementation_plan",{"title":651,"icon":652,"path":653,"stem":654,"children":655,"page":59},"ASP.NET","i-devicon-dotnetcore","\u002Fcsharp\u002Faspnet","01.csharp\u002F11.aspnet",[656,730,791,869,927,941,967,1057,1111,1182,1212,1289,1346],{"title":657,"icon":658,"path":659,"stem":660,"children":661,"page":59},"Minimal API","i-lucide-network","\u002Fcsharp\u002Faspnet\u002Fminimal-api","01.csharp\u002F11.aspnet\u002F01.minimal-api",[662,666,670,674,678,682,686,690,694,698,702,706,710,714,718,722,726],{"title":663,"path":664,"stem":665},"Вступ до ASP.NET та еволюція фреймворку","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fintroduction","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F01.introduction",{"title":667,"path":668,"stem":669},"Перший додаток на ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Ffirst-application","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F02.first-application",{"title":671,"path":672,"stem":673},"WebApplication, Builder та Dependency Injection","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fwebapplication-builder","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F03.webapplication-builder",{"title":675,"path":676,"stem":677},"Конвеєр запитів та Middleware","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Frequest-pipeline-middleware","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F04.request-pipeline-middleware",{"title":679,"path":680,"stem":681},"Маршрутизація в ASP.NET Core: Основи","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Frouting-basics","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F05.routing-basics",{"title":683,"path":684,"stem":685},"Маршрутизація в ASP.NET Core: Розширені можливості","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Frouting-advanced","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F06.routing-advanced",{"title":687,"path":688,"stem":689},"Статичні файли в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fstatic-files","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F07.static-files",{"title":691,"path":692,"stem":693},"Статичні Активи: MapStaticAssets (ASP.NET Core 9.0)","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fstatic-assets","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F08.static-assets",{"title":695,"path":696,"stem":697},"Конфігурація в ASP.NET Core: Основи","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fconfiguration-fundamentals","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F09.configuration-fundamentals",{"title":699,"path":700,"stem":701},"Конфігурація: Паттерн Options","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fconfiguration-options","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F10.configuration-options",{"title":703,"path":704,"stem":705},"Логування в ASP.NET Core: Основи","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Flogging-basics","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F11.logging-basics",{"title":707,"path":708,"stem":709},"Логування: Serilog та Middleware","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Flogging-advanced","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F12.logging-advanced",{"title":711,"path":712,"stem":713},"Управління станом: HttpContext.Items та Cookies","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fstate-management","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F13.state-management",{"title":715,"path":716,"stem":717},"Стан сесії: Sessions","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fsession-state","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F14.session-state",{"title":719,"path":720,"stem":721},"Структура проєкту: від хаосу до архітектури","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fproject-structure","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F15.project-structure",{"title":723,"path":724,"stem":725},"Scalar у Minimal API: повний проєкт і Fluent OpenAPI","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fscalar-openapi-fluent","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F16.scalar-openapi-fluent",{"title":727,"path":728,"stem":729},"Swagger \u002F Swashbuckle у Minimal API: окремий класичний шлях","\u002Fcsharp\u002Faspnet\u002Fminimal-api\u002Fswagger-swashbuckle","01.csharp\u002F11.aspnet\u002F01.minimal-api\u002F17.swagger-swashbuckle",{"title":731,"icon":658,"path":732,"stem":733,"children":734,"page":59},"API","\u002Fcsharp\u002Faspnet\u002Fapi","01.csharp\u002F11.aspnet\u002F02.api",[735,739,743,747,751,755,759,763,767,771,775,779,783,787],{"title":736,"path":737,"stem":738},"Що таке API. Клієнт-серверна архітектура","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fwhat-is-api","01.csharp\u002F11.aspnet\u002F02.api\u002F01.what-is-api",{"title":740,"path":741,"stem":742},"Формати даних: JSON, XML, TOML та бінарні формати","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fdata-formats","01.csharp\u002F11.aspnet\u002F02.api\u002F02.data-formats",{"title":744,"path":745,"stem":746},"Парадигми API та концепція REST","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fapi-paradigms-rest","01.csharp\u002F11.aspnet\u002F02.api\u002F03.api-paradigms-rest",{"title":748,"path":749,"stem":750},"HTTP-методи, статус-коди та заголовки","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fhttp-methods-status-codes","01.csharp\u002F11.aspnet\u002F02.api\u002F04.http-methods-status-codes",{"title":752,"path":753,"stem":754},"Організація HTTP API за принципами REST","\u002Fcsharp\u002Faspnet\u002Fapi\u002Frest-organizing","01.csharp\u002F11.aspnet\u002F02.api\u002F05.rest-organizing",{"title":756,"path":757,"stem":758},"Номенклатура URL та CRUD-операції","\u002Fcsharp\u002Faspnet\u002Fapi\u002Furl-nomenclature-crud","01.csharp\u002F11.aspnet\u002F02.api\u002F06.url-nomenclature-crud",{"title":760,"path":761,"stem":762},"Правила дизайну: іменування та стандарти","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fapi-design-naming","01.csharp\u002F11.aspnet\u002F02.api\u002F07.api-design-naming",{"title":764,"path":765,"stem":766},"Валідація, ліміти та обробка помилок","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fapi-design-validation","01.csharp\u002F11.aspnet\u002F02.api\u002F08.api-design-validation",{"title":768,"path":769,"stem":770},"Обробка помилок у Minimal API","\u002Fcsharp\u002Faspnet\u002Fapi\u002Ferror-handling-http","01.csharp\u002F11.aspnet\u002F02.api\u002F09.error-handling-http",{"title":772,"path":773,"stem":774},"Ідемпотентність та синхронізація стану","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fidempotency-sync","01.csharp\u002F11.aspnet\u002F02.api\u002F10.idempotency-sync",{"title":776,"path":777,"stem":778},"Пагінація та організація списків","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fpagination-lists","01.csharp\u002F11.aspnet\u002F02.api\u002F11.pagination-lists",{"title":780,"path":781,"stem":782},"Безпека API, кешування та інтернаціоналізація","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fsecurity-auth","01.csharp\u002F11.aspnet\u002F02.api\u002F12.security-auth",{"title":784,"path":785,"stem":786},"Процес проєктування API та документування","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fapi-design-process","01.csharp\u002F11.aspnet\u002F02.api\u002F13.api-design-process",{"title":788,"path":789,"stem":790},"OpenAPI: контракт, специфікація та документація API","\u002Fcsharp\u002Faspnet\u002Fapi\u002Fopenapi","01.csharp\u002F11.aspnet\u002F02.api\u002F14.openapi",{"title":792,"icon":793,"path":794,"stem":795,"children":796,"page":59},"Auth","i-lucide-shield-check","\u002Fcsharp\u002Faspnet\u002Fauth","01.csharp\u002F11.aspnet\u002F03.auth",[797,801,805,809,813,817,821,825,829,833,837,841,845,849,853,857,861,865],{"title":798,"path":799,"stem":800},"Основи аутентифікації та авторизації","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fauth-fundamentals","01.csharp\u002F11.aspnet\u002F03.auth\u002F01.auth-fundamentals",{"title":802,"path":803,"stem":804},"JWT-аутентифікація","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fjwt-authentication","01.csharp\u002F11.aspnet\u002F03.auth\u002F02.jwt-authentication",{"title":806,"path":807,"stem":808},"Авторизація: ролі, політики та resource-based доступ","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fauthorization-policies","01.csharp\u002F11.aspnet\u002F03.auth\u002F03.authorization-policies",{"title":810,"path":811,"stem":812},"Cookie-аутентифікація та ASP.NET Core Identity","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fcookie-auth-identity","01.csharp\u002F11.aspnet\u002F03.auth\u002F04.cookie-auth-identity",{"title":814,"path":815,"stem":816},"JWT + Refresh Tokens (HttpOnly Cookie)","\u002Fcsharp\u002Faspnet\u002Fauth\u002F04b.identity-auth-jwt","01.csharp\u002F11.aspnet\u002F03.auth\u002F04b.identity-auth-jwt",{"title":818,"path":819,"stem":820},"Identity: Підтвердження Email та Скидання Пароля","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fidentity-email-confirmation","01.csharp\u002F11.aspnet\u002F03.auth\u002F05.identity-email-confirmation",{"title":822,"path":823,"stem":824},"Identity: Двофакторна Аутентифікація (2FA)","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fidentity-two-factor","01.csharp\u002F11.aspnet\u002F03.auth\u002F06.identity-two-factor",{"title":826,"path":827,"stem":828},"Identity: Внутрішня Архітектура та Кастомізація","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fidentity-internals","01.csharp\u002F11.aspnet\u002F03.auth\u002F07.identity-internals",{"title":830,"path":831,"stem":832},"OAuth 2.0 та зовнішні провайдери","\u002Fcsharp\u002Faspnet\u002Fauth\u002Foauth-external-providers","01.csharp\u002F11.aspnet\u002F03.auth\u002F08.oauth-external-providers",{"title":834,"path":835,"stem":836},"Безпека на практиці: CORS, HTTPS та захист від атак","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fsecurity-hardening","01.csharp\u002F11.aspnet\u002F03.auth\u002F09.security-hardening",{"title":838,"path":839,"stem":840},"Теорія OAuth 2.0: Поняття, Аналогії та Флоу","\u002Fcsharp\u002Faspnet\u002Fauth\u002Foauth-theory","01.csharp\u002F11.aspnet\u002F03.auth\u002F10.oauth-theory",{"title":842,"path":843,"stem":844},"OIDC, OAuth 2.0 та Keycloak в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fauth\u002Foidc-keycloak","01.csharp\u002F11.aspnet\u002F03.auth\u002F10.oidc-keycloak",{"title":846,"path":847,"stem":848},"API Keys аутентифікація в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fapi-keys","01.csharp\u002F11.aspnet\u002F03.auth\u002F11.api-keys",{"title":850,"path":851,"stem":852},"Rate Limiting та Throttling в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fauth\u002Frate-limiting","01.csharp\u002F11.aspnet\u002F03.auth\u002F12.rate-limiting",{"title":854,"path":855,"stem":856},"Refresh Token Rotation в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fauth\u002Frefresh-token-rotation","01.csharp\u002F11.aspnet\u002F03.auth\u002F13.refresh-token-rotation",{"title":858,"path":859,"stem":860},"Certificate Authentication та mTLS в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fcertificate-auth","01.csharp\u002F11.aspnet\u002F03.auth\u002F14.certificate-auth",{"title":862,"path":863,"stem":864},"RBAC, ABAC та ReBAC в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fauth\u002Frbac-abac-rebac","01.csharp\u002F11.aspnet\u002F03.auth\u002F15.rbac-abac-rebac",{"title":866,"path":867,"stem":868},"Multi-tenancy та ізоляція даних в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fauth\u002Fmulti-tenancy","01.csharp\u002F11.aspnet\u002F03.auth\u002F16.multi-tenancy",{"title":870,"icon":871,"path":872,"stem":873,"children":874,"page":59},"Нотифікації","i-lucide-bell","\u002Fcsharp\u002Faspnet\u002Fnotifications","01.csharp\u002F11.aspnet\u002F04.notifications",[875,879,883,887,891,895,899,903,907,911,915,919,923],{"title":876,"path":877,"stem":878},"In-App нотифікації через базу даних","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fin-app-database-notifications","01.csharp\u002F11.aspnet\u002F04.notifications\u002F01.in-app-database-notifications",{"title":880,"path":881,"stem":882},"Polling: Регулярний запит оновлень","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fpolling","01.csharp\u002F11.aspnet\u002F04.notifications\u002F02.polling",{"title":884,"path":885,"stem":886},"Server-Sent Events: Однострімовий push від сервера","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fserver-sent-events","01.csharp\u002F11.aspnet\u002F04.notifications\u002F03.server-sent-events",{"title":888,"path":889,"stem":890},"WebSockets: Двостороннє з'єднання в реальному часі","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fwebsockets","01.csharp\u002F11.aspnet\u002F04.notifications\u002F04.websockets",{"title":892,"path":893,"stem":894},"SignalR: Абстракція над транспортами реального часу","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fsignalr","01.csharp\u002F11.aspnet\u002F04.notifications\u002F05.signalr",{"title":896,"path":897,"stem":898},"Background Services: Фонові задачі в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fbackground-services","01.csharp\u002F11.aspnet\u002F04.notifications\u002F06.background-services",{"title":900,"path":901,"stem":902},"Web Push нотифікації","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fweb-push","01.csharp\u002F11.aspnet\u002F04.notifications\u002F07.web-push",{"title":904,"path":905,"stem":906},"Email нотифікації","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Femail-notifications","01.csharp\u002F11.aspnet\u002F04.notifications\u002F08.email-notifications",{"title":908,"path":909,"stem":910},"Порівняння підходів: Як вибрати правильну технологію нотифікацій","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fchoosing-the-right-approach","01.csharp\u002F11.aspnet\u002F04.notifications\u002F09.choosing-the-right-approach",{"title":912,"path":913,"stem":914},"Hangfire: Надійне планування фонових задач","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fhangfire","01.csharp\u002F11.aspnet\u002F04.notifications\u002F10.hangfire",{"title":916,"path":917,"stem":918},"Практика: Конвертація зображень у WebP через Hangfire","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fhangfire-image-webp","01.csharp\u002F11.aspnet\u002F04.notifications\u002F11.hangfire-image-webp",{"title":920,"path":921,"stem":922},"Практика: Підготовка відео до HLS-стрімінгу через Hangfire","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Fhangfire-video-hls","01.csharp\u002F11.aspnet\u002F04.notifications\u002F12.hangfire-video-hls",{"title":924,"path":925,"stem":926},"Telegram-нотифікації: від одного повідомлення до масових розсилок і мульти-канального підходу","\u002Fcsharp\u002Faspnet\u002Fnotifications\u002Ftelegram-notifications","01.csharp\u002F11.aspnet\u002F04.notifications\u002F13.telegram-notifications",{"title":928,"icon":929,"path":930,"stem":931,"children":932,"page":59},"Інтернаціоналізація","i-lucide-languages","\u002Fcsharp\u002Faspnet\u002Fi18n","01.csharp\u002F11.aspnet\u002F05.i18n",[933,937],{"title":934,"path":935,"stem":936},"Інтернаціоналізація (i18n) у Minimal API: від A до Я","\u002Fcsharp\u002Faspnet\u002Fi18n\u002Finternationalization","01.csharp\u002F11.aspnet\u002F05.i18n\u002F01.internationalization",{"title":938,"path":939,"stem":940},"Humanizer: людиномовні рядки у .NET","\u002Fcsharp\u002Faspnet\u002Fi18n\u002Fhumanizer","01.csharp\u002F11.aspnet\u002F05.i18n\u002F02.humanizer",{"title":942,"icon":943,"path":944,"stem":945,"children":946,"page":59},"Кешування","i-lucide-layers","\u002Fcsharp\u002Faspnet\u002Fcaching","01.csharp\u002F11.aspnet\u002F06.caching",[947,951,955,959,963],{"title":948,"path":949,"stem":950},"Огляд кешування: чотири рівні і коли що обирати","\u002Fcsharp\u002Faspnet\u002Fcaching\u002Fcaching","01.csharp\u002F11.aspnet\u002F06.caching\u002F01.caching",{"title":952,"path":953,"stem":954},"IMemoryCache: кеш в оперативній пам'яті","\u002Fcsharp\u002Faspnet\u002Fcaching\u002Fmemory-cache","01.csharp\u002F11.aspnet\u002F06.caching\u002F02.memory-cache",{"title":956,"path":957,"stem":958},"IDistributedCache і Redis: розподілений кеш","\u002Fcsharp\u002Faspnet\u002Fcaching\u002Fdistributed-cache","01.csharp\u002F11.aspnet\u002F06.caching\u002F03.distributed-cache",{"title":960,"path":961,"stem":962},"Response Cache: HTTP-кешування через Cache-Control","\u002Fcsharp\u002Faspnet\u002Fcaching\u002Fresponse-cache","01.csharp\u002F11.aspnet\u002F06.caching\u002F04.response-cache",{"title":964,"path":965,"stem":966},"Output Cache: серверний кеш HTTP-відповідей (.NET 7+)","\u002Fcsharp\u002Faspnet\u002Fcaching\u002Foutput-cache","01.csharp\u002F11.aspnet\u002F06.caching\u002F05.output-cache",{"title":968,"icon":969,"path":970,"stem":971,"children":972,"page":59},"Тестування","i-lucide-test-tube","\u002Fcsharp\u002Faspnet\u002Ftesting","01.csharp\u002F11.aspnet\u002F07.testing",[973,977,981,985,989,993,997,1001,1005,1009,1013,1017,1021,1025,1029,1033,1037,1041,1045,1049,1053],{"title":974,"path":975,"stem":976},"Що таке тестування? Від інтуїції до науки","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fwhat-is-testing","01.csharp\u002F11.aspnet\u002F07.testing\u002F01.what-is-testing",{"title":978,"path":979,"stem":980},"Піраміда тестування — Стратегія, а не Догма","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Ftesting-pyramid","01.csharp\u002F11.aspnet\u002F07.testing\u002F02.testing-pyramid",{"title":982,"path":983,"stem":984},"Дві Школи Тестування — Лондон проти Детройту","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Ftesting-schools","01.csharp\u002F11.aspnet\u002F07.testing\u002F03.testing-schools",{"title":986,"path":987,"stem":988},"TDD та BDD — Тести як Дизайн-інструмент","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Ftdd-and-bdd","01.csharp\u002F11.aspnet\u002F07.testing\u002F04.tdd-and-bdd",{"title":990,"path":991,"stem":992},"Що саме тестувати — Техніки аналізу та Циклomatична складність","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fwhat-to-test","01.csharp\u002F11.aspnet\u002F07.testing\u002F05.what-to-test",{"title":994,"path":995,"stem":996},"Тестові Фреймворки — Навіщо вони і що всередині","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Ftest-frameworks","01.csharp\u002F11.aspnet\u002F07.testing\u002F06.test-frameworks",{"title":998,"path":999,"stem":1000},"xUnit — Факти, Теорії та Lifecycle тестів","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fxunit-basics","01.csharp\u002F11.aspnet\u002F07.testing\u002F07.xunit-basics",{"title":1002,"path":1003,"stem":1004},"xUnit Advanced — Fixtures, Кастомізація та Розширення","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fxunit-advanced","01.csharp\u002F11.aspnet\u002F07.testing\u002F08.xunit-advanced",{"title":1006,"path":1007,"stem":1008},"Moq — Глибоке занурення в мокування","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fmocking-with-moq","01.csharp\u002F11.aspnet\u002F07.testing\u002F09.mocking-with-moq",{"title":1010,"path":1011,"stem":1012},"Тестування Баз Даних — EF Core, SQLite та Testcontainers","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fdatabase-testing","01.csharp\u002F11.aspnet\u002F07.testing\u002F10.database-testing",{"title":1014,"path":1015,"stem":1016},"Integration Testing — Частина 1 [Теорія та WebApplicationFactory]","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fintegration-testing","01.csharp\u002F11.aspnet\u002F07.testing\u002F11.integration-testing",{"title":1018,"path":1019,"stem":1020},"Інтеграційне тестування — Практика","\u002Fcsharp\u002Faspnet\u002Ftesting\u002F11a.integration-testing-practice","01.csharp\u002F11.aspnet\u002F07.testing\u002F11a.integration-testing-practice",{"title":1022,"path":1023,"stem":1024},"Integration Testing — Частина 2 [Просунуті Сценарії та Testcontainers]","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fintegration-testing-advanced","01.csharp\u002F11.aspnet\u002F07.testing\u002F12.integration-testing-advanced",{"title":1026,"path":1027,"stem":1028},"Професійний Postman: Колекції, Змінні та GitHub Інтеграція","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fpostman-professional","01.csharp\u002F11.aspnet\u002F07.testing\u002F13.postman-professional",{"title":1030,"path":1031,"stem":1032},"HttpClient у Тестах Частина 1: Архітектура та MockHttpMessageHandler","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fhttpclient-testing","01.csharp\u002F11.aspnet\u002F07.testing\u002F14.httpclient-testing",{"title":1034,"path":1035,"stem":1036},"HttpClient у Тестах Частина 2: WireMock.Net та Resilience","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fwiremock-net","01.csharp\u002F11.aspnet\u002F07.testing\u002F15.wiremock-net",{"title":1038,"path":1039,"stem":1040},"Патерни та Анти-патерни Тестування: Test Smells","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Ftesting-patterns","01.csharp\u002F11.aspnet\u002F07.testing\u002F16.testing-patterns",{"title":1042,"path":1043,"stem":1044},"Просунуті інструменти: Time, Snapshots та Властивості","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fadvanced-testing-tools","01.csharp\u002F11.aspnet\u002F07.testing\u002F17.advanced-testing-tools",{"title":1046,"path":1047,"stem":1048},"Тестування Архітектури з NetArchTest","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Farchitecture-testing","01.csharp\u002F11.aspnet\u002F07.testing\u002F18.architecture-testing",{"title":1050,"path":1051,"stem":1052},"Тестування Продуктивності: BenchmarkDotNet, NBomber та k6","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fperformance-testing","01.csharp\u002F11.aspnet\u002F07.testing\u002F19.performance-testing",{"title":1054,"path":1055,"stem":1056},"Залишок плану для курсу \"Тестування ASP.NET Minimal API\"","\u002Fcsharp\u002Faspnet\u002Ftesting\u002Fremaining_plan","01.csharp\u002F11.aspnet\u002F07.testing\u002Fremaining_plan",{"title":1058,"icon":1059,"path":1060,"stem":1061,"children":1062,"page":59},"Платежі","i-lucide-credit-card","\u002Fcsharp\u002Faspnet\u002Fpayments","01.csharp\u002F11.aspnet\u002F08.payments",[1063,1067,1071,1075,1079,1083,1087,1091,1095,1099,1103,1107],{"title":1064,"path":1065,"stem":1066},"Основи платіжної інфраструктури","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fpayment-fundamentals","01.csharp\u002F11.aspnet\u002F08.payments\u002F01.payment-fundamentals",{"title":1068,"path":1069,"stem":1070},"Методи оплати в Україні","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fpayment-methods-ukraine","01.csharp\u002F11.aspnet\u002F08.payments\u002F02.payment-methods-ukraine",{"title":1072,"path":1073,"stem":1074},"PCI DSS та безпека платежів","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fpci-dss-security","01.csharp\u002F11.aspnet\u002F08.payments\u002F03.pci-dss-security",{"title":1076,"path":1077,"stem":1078},"Архітектура платіжної підсистеми","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fpayment-architecture","01.csharp\u002F11.aspnet\u002F08.payments\u002F04.payment-architecture",{"title":1080,"path":1081,"stem":1082},"Інтеграція LiqPay (ПриватБанк)","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fliqpay-integration","01.csharp\u002F11.aspnet\u002F08.payments\u002F05.liqpay-integration",{"title":1084,"path":1085,"stem":1086},"Інтеграція Monobank Acquiring API","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fmonobank-acquiring","01.csharp\u002F11.aspnet\u002F08.payments\u002F06.monobank-acquiring",{"title":1088,"path":1089,"stem":1090},"Інтеграція Stripe","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fstripe-integration","01.csharp\u002F11.aspnet\u002F08.payments\u002F07.stripe-integration",{"title":1092,"path":1093,"stem":1094},"Webhooks — глибоке занурення","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fwebhooks-deep-dive","01.csharp\u002F11.aspnet\u002F08.payments\u002F08.webhooks-deep-dive",{"title":1096,"path":1097,"stem":1098},"Підписки та рекурентні платежі","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fsubscriptions-recurring","01.csharp\u002F11.aspnet\u002F08.payments\u002F09.subscriptions-recurring",{"title":1100,"path":1101,"stem":1102},"Повернення коштів та диспути","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Frefunds-disputes","01.csharp\u002F11.aspnet\u002F08.payments\u002F10.refunds-disputes",{"title":1104,"path":1105,"stem":1106},"Тестування платіжних інтеграцій","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Ftesting-payments","01.csharp\u002F11.aspnet\u002F08.payments\u002F11.testing-payments",{"title":1108,"path":1109,"stem":1110},"Чекліст виходу в Production","\u002Fcsharp\u002Faspnet\u002Fpayments\u002Fproduction-checklist","01.csharp\u002F11.aspnet\u002F08.payments\u002F12.production-checklist",{"title":1112,"icon":1113,"items":1114,"path":1127,"stem":1128,"children":1129,"page":59},"Популярні бібліотеки","lucide:box",[1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126],"01.fluent-validation","02.mapster","03.erroror-result-pattern","04.serilog","05.mediatr","06.polly","07.health-checks","08.feature-management","09.fluent-email","10.quest-pdf","11.bogus","12.humanizer-guard","\u002Fcsharp\u002Faspnet\u002Flibraries","01.csharp\u002F11.aspnet\u002F09.libraries",[1130,1134,1138,1142,1146,1150,1154,1158,1162,1166,1170,1174,1178],{"title":1131,"path":1132,"stem":1133},"Валідація з FluentValidation в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Ffluent-validation","01.csharp\u002F11.aspnet\u002F09.libraries\u002F01.fluent-validation",{"title":1135,"path":1136,"stem":1137},"Маппінг об","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fmapster","01.csharp\u002F11.aspnet\u002F09.libraries\u002F02.mapster",{"title":1139,"path":1140,"stem":1141},"Обробка помилок з ErrorOr та Result Pattern в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Ferroror-result-pattern","01.csharp\u002F11.aspnet\u002F09.libraries\u002F03.erroror-result-pattern",{"title":1143,"path":1144,"stem":1145},"Структуроване логування з Serilog в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fserilog","01.csharp\u002F11.aspnet\u002F09.libraries\u002F04.serilog",{"title":1147,"path":1148,"stem":1149},"CQRS та Mediator з MediatR в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fmediatr","01.csharp\u002F11.aspnet\u002F09.libraries\u002F05.mediatr",{"title":1151,"path":1152,"stem":1153},"Відмовостійкість з Polly в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fpolly","01.csharp\u002F11.aspnet\u002F09.libraries\u002F06.polly",{"title":1155,"path":1156,"stem":1157},"Health Checks в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fhealth-checks","01.csharp\u002F11.aspnet\u002F09.libraries\u002F07.health-checks",{"title":1159,"path":1160,"stem":1161},"Feature Management та Feature Flags в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Ffeature-management","01.csharp\u002F11.aspnet\u002F09.libraries\u002F08.feature-management",{"title":1163,"path":1164,"stem":1165},"Відправка Email з FluentEmail в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Ffluent-email","01.csharp\u002F11.aspnet\u002F09.libraries\u002F09.fluent-email",{"title":1167,"path":1168,"stem":1169},"Генерація PDF з QuestPDF в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fquest-pdf","01.csharp\u002F11.aspnet\u002F09.libraries\u002F10.quest-pdf",{"title":1171,"path":1172,"stem":1173},"Генерація тестових даних з Bogus в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fbogus","01.csharp\u002F11.aspnet\u002F09.libraries\u002F11.bogus",{"title":1175,"path":1176,"stem":1177},"Humanizer та Guard Clauses в ASP.NET Core","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fhumanizer-guard","01.csharp\u002F11.aspnet\u002F09.libraries\u002F12.humanizer-guard",{"title":1179,"path":1180,"stem":1181},"План модуля 10.libraries — Популярні бібліотеки ASP.NET","\u002Fcsharp\u002Faspnet\u002Flibraries\u002Fplan","01.csharp\u002F11.aspnet\u002F09.libraries\u002Fplan",{"title":1183,"icon":1184,"path":1185,"stem":1186,"children":1187,"page":59},"Razor Pages","i-lucide-layout-template","\u002Fcsharp\u002Faspnet\u002Frazor-pages","01.csharp\u002F11.aspnet\u002F10.razor-pages",[1188,1192,1196,1200,1204,1208],{"title":1189,"path":1190,"stem":1191},"Від Minimal API до Razor Pages: концептуальний перехід","\u002Fcsharp\u002Faspnet\u002Frazor-pages\u002Ffrom-minimal-api","01.csharp\u002F11.aspnet\u002F10.razor-pages\u002F01.from-minimal-api",{"title":1193,"path":1194,"stem":1195},"PageModel: логіка сторінки Razor Pages","\u002Fcsharp\u002Faspnet\u002Frazor-pages\u002Fpage-model","01.csharp\u002F11.aspnet\u002F10.razor-pages\u002F02.page-model",{"title":1197,"path":1198,"stem":1199},"Razor синтаксис: шаблонізатор у .cshtml","\u002Fcsharp\u002Faspnet\u002Frazor-pages\u002Frazor-syntax","01.csharp\u002F11.aspnet\u002F10.razor-pages\u002F03.razor-syntax",{"title":1201,"path":1202,"stem":1203},"Tag Helpers: типізований HTML","\u002Fcsharp\u002Faspnet\u002Frazor-pages\u002Ftag-helpers","01.csharp\u002F11.aspnet\u002F10.razor-pages\u002F04.tag-helpers",{"title":1205,"path":1206,"stem":1207},"Форми і валідація: повний цикл обробки даних","\u002Fcsharp\u002Faspnet\u002Frazor-pages\u002Fforms-validation","01.csharp\u002F11.aspnet\u002F10.razor-pages\u002F05.forms-validation",{"title":1209,"path":1210,"stem":1211},"Практичний проєкт: TaskManager на Razor Pages","\u002Fcsharp\u002Faspnet\u002Frazor-pages\u002Fproject-task-manager","01.csharp\u002F11.aspnet\u002F10.razor-pages\u002F06.project-task-manager",{"title":1213,"path":1214,"stem":1215,"children":1216,"page":59},"ASP.NET Core MVC","\u002Fcsharp\u002Faspnet\u002Fmvc","01.csharp\u002F11.aspnet\u002F11.mvc",[1217,1221,1225,1229,1233,1237,1241,1245,1249,1253,1257,1261,1265,1269,1273,1277,1281,1285],{"title":1218,"path":1219,"stem":1220},"Патерн MVC: архітектура, що змінила веб","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fmvc-pattern","01.csharp\u002F11.aspnet\u002F11.mvc\u002F01.mvc-pattern",{"title":1222,"path":1223,"stem":1224},"Від Razor Pages до MVC: концептуальний перехід","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Ffrom-razor-pages","01.csharp\u002F11.aspnet\u002F11.mvc\u002F02.from-razor-pages",{"title":1226,"path":1227,"stem":1228},"Controllers та Actions: серце MVC","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fcontrollers-actions","01.csharp\u002F11.aspnet\u002F11.mvc\u002F03.controllers-actions",{"title":1230,"path":1231,"stem":1232},"Маршрутизація в MVC: Convention vs Attribute Routing","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Frouting-mvc","01.csharp\u002F11.aspnet\u002F11.mvc\u002F04.routing-mvc",{"title":1234,"path":1235,"stem":1236},"Model Binding: від HTTP до C#","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fmodel-binding","01.csharp\u002F11.aspnet\u002F11.mvc\u002F05.model-binding",{"title":1238,"path":1239,"stem":1240},"Views, ViewData, ViewBag, TempData і ViewModel","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fviews-viewdata-tempdata","01.csharp\u002F11.aspnet\u002F11.mvc\u002F06.views-viewdata-tempdata",{"title":1242,"path":1243,"stem":1244},"Filters: аспектно-орієнтоване програмування в MVC","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Ffilters","01.csharp\u002F11.aspnet\u002F11.mvc\u002F07.filters",{"title":1246,"path":1247,"stem":1248},"Areas: структурування великих застосунків","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fareas","01.csharp\u002F11.aspnet\u002F11.mvc\u002F08.areas",{"title":1250,"path":1251,"stem":1252},"View Components: повторювані незалежні блоки UI","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fview-components","01.csharp\u002F11.aspnet\u002F11.mvc\u002F09.view-components",{"title":1254,"path":1255,"stem":1256},"Display та Editor Templates","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fdisplay-editor-templates","01.csharp\u002F11.aspnet\u002F11.mvc\u002F10.display-editor-templates",{"title":1258,"path":1259,"stem":1260},"Валідація: IValidatableObject та FluentValidation","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fvalidation-advanced","01.csharp\u002F11.aspnet\u002F11.mvc\u002F11.validation-advanced",{"title":1262,"path":1263,"stem":1264},"HTMX: інтерактивність через HTML-атрибути","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fhtmx","01.csharp\u002F11.aspnet\u002F11.mvc\u002F12.htmx",{"title":1266,"path":1267,"stem":1268},"HTMX у ASP.NET Core MVC: серверна інтеграція","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fajax-htmx-mvc","01.csharp\u002F11.aspnet\u002F11.mvc\u002F13.ajax-htmx-mvc",{"title":1270,"path":1271,"stem":1272},"Практичний проєкт: Каталог товарів з HTMX","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fhtmx-project","01.csharp\u002F11.aspnet\u002F11.mvc\u002F14.htmx-project",{"title":1274,"path":1275,"stem":1276},"Завантаження та обробка файлів","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Ffile-upload","01.csharp\u002F11.aspnet\u002F11.mvc\u002F15.file-upload",{"title":1278,"path":1279,"stem":1280},"Глобалізація та Локалізація MVC","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fglobalization-localization","01.csharp\u002F11.aspnet\u002F11.mvc\u002F16.globalization-localization",{"title":1282,"path":1283,"stem":1284},"Підсумковий проєкт: Блог-платформа","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fmvc-project","01.csharp\u002F11.aspnet\u002F11.mvc\u002F17.mvc-project",{"title":1286,"path":1287,"stem":1288},"План курсу: ASP.NET Core MVC","\u002Fcsharp\u002Faspnet\u002Fmvc\u002Fplan","01.csharp\u002F11.aspnet\u002F11.mvc\u002Fplan",{"title":1290,"path":1291,"stem":1292,"children":1293,"page":59},"Web Api","\u002Fcsharp\u002Faspnet\u002Fweb-api","01.csharp\u002F11.aspnet\u002F12.web-api",[1294,1298,1302,1306,1310,1314,1318,1322,1326,1330,1334,1338,1342],{"title":1295,"path":1296,"stem":1297},"Від Minimal API до Controller-based API","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Ffrom-minimal-api-to-controllers","01.csharp\u002F11.aspnet\u002F12.web-api\u002F01.from-minimal-api-to-controllers",{"title":1299,"path":1300,"stem":1301},"ControllerBase, ActionResult\u003CT> та Response Types","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fcontroller-base-actionresult","01.csharp\u002F11.aspnet\u002F12.web-api\u002F02.controller-base-actionresult",{"title":1303,"path":1304,"stem":1305},"Content Negotiation - JSON, XML та власні форматери","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fcontent-negotiation","01.csharp\u002F11.aspnet\u002F12.web-api\u002F03.content-negotiation",{"title":1307,"path":1308,"stem":1309},"Версіонування API","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fapi-versioning","01.csharp\u002F11.aspnet\u002F12.web-api\u002F04.api-versioning",{"title":1311,"path":1312,"stem":1313},"ProblemDetails та структурована обробка помилок","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fproblemdetails-error-handling","01.csharp\u002F11.aspnet\u002F12.web-api\u002F05.problemdetails-error-handling",{"title":1315,"path":1316,"stem":1317},"Фільтри у Web API контексті","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Ffilters-for-api","01.csharp\u002F11.aspnet\u002F12.web-api\u002F06.filters-for-api",{"title":1319,"path":1320,"stem":1321},"Пагінація, фільтрація та сортування","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fpagination-filtering-sorting","01.csharp\u002F11.aspnet\u002F12.web-api\u002F07.pagination-filtering-sorting",{"title":1323,"path":1324,"stem":1325},"HATEOAS та Resource Expansion","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fhateoas-resource-expansion","01.csharp\u002F11.aspnet\u002F12.web-api\u002F08.hateoas-resource-expansion",{"title":1327,"path":1328,"stem":1329},"Гібридна архітектура - Minimal API + Controllers","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fminimal-api-vs-controllers-hybrid","01.csharp\u002F11.aspnet\u002F12.web-api\u002F09.minimal-api-vs-controllers-hybrid",{"title":1331,"path":1332,"stem":1333},"Документація API - Swashbuckle, NSwag та генерація клієнтів","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fapi-documentation-generation","01.csharp\u002F11.aspnet\u002F12.web-api\u002F10.api-documentation-generation",{"title":1335,"path":1336,"stem":1337},"Health Checks та моніторинг API","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fhealth-checks-monitoring","01.csharp\u002F11.aspnet\u002F12.web-api\u002F11.health-checks-monitoring",{"title":1339,"path":1340,"stem":1341},"Підсумковий проєкт - Production-Ready REST API","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fweb-api-project","01.csharp\u002F11.aspnet\u002F12.web-api\u002F12.web-api-project",{"title":1343,"path":1344,"stem":1345},"План курсу: ASP.NET Core Web API (Controllers)","\u002Fcsharp\u002Faspnet\u002Fweb-api\u002Fplan","01.csharp\u002F11.aspnet\u002F12.web-api\u002Fplan",{"title":1347,"icon":1348,"path":1349,"stem":1350,"children":1351,"page":59},"Моніторинг","i-lucide-activity","\u002Fcsharp\u002Faspnet\u002Fmonitoring","01.csharp\u002F11.aspnet\u002F13.monitoring",[1352,1356,1360],{"title":1353,"path":1354,"stem":1355},"Спостережуваність: від console.log до production-систем","\u002Fcsharp\u002Faspnet\u002Fmonitoring\u002Fobservability-intro","01.csharp\u002F11.aspnet\u002F13.monitoring\u002F01.observability-intro",{"title":1357,"path":1358,"stem":1359},"Health Checks: перший рівень observability","\u002Fcsharp\u002Faspnet\u002Fmonitoring\u002Fhealth-checks","01.csharp\u002F11.aspnet\u002F13.monitoring\u002F02.health-checks",{"title":1361,"path":1362,"stem":1363},"Вбудовані метрики .NET 10 та System.Diagnostics.Metrics","\u002Fcsharp\u002Faspnet\u002Fmonitoring\u002Fdotnet-metrics","01.csharp\u002F11.aspnet\u002F13.monitoring\u002F03.dotnet-metrics",{"title":1365,"icon":1366,"path":1367,"stem":1368,"children":1369,"page":59},"Desktop UI","i-lucide-app-window","\u002Fcsharp\u002Fdesktop-ui","01.csharp\u002F12.desktop-ui",[1370,1374,1378,1382,1386,1390,1394,1398,1402,1406,1410,1414,1418,1422,1426,1430,1434,1438,1442,1446,1450,1454,1458,1462,1466,1470,1474,1478,1482,1486,1490,1494,1498,1502,1506,1510,1514,1518,1522,1526,1530,1534,1538,1542,1546,1550,1554,1558,1562,1566,1570,1574,1578,1582,1586,1590,1594,1598,1602,1606,1610,1614,1618,1622,1626,1630,1634,1638,1642,1646,1650],{"title":1371,"path":1372,"stem":1373},"Що таке десктопна розробка?","\u002Fcsharp\u002Fdesktop-ui\u002Fwhat-is-desktop-dev","01.csharp\u002F12.desktop-ui\u002F01.what-is-desktop-dev",{"title":1375,"path":1376,"stem":1377},"Архітектура WPF — як влаштований графічний інтерфейс","\u002Fcsharp\u002Fdesktop-ui\u002Fwpf-architecture","01.csharp\u002F12.desktop-ui\u002F02.wpf-architecture",{"title":1379,"path":1380,"stem":1381},"Перший WPF-проєкт — від нуля до вікна","\u002Fcsharp\u002Fdesktop-ui\u002Ffirst-wpf-app","01.csharp\u002F12.desktop-ui\u002F03.first-wpf-app",{"title":1383,"path":1384,"stem":1385},"Перший Avalonia-проєкт: WPF для всіх платформ","\u002Fcsharp\u002Fdesktop-ui\u002F03a.first-avalonia-app","01.csharp\u002F12.desktop-ui\u002F03a.first-avalonia-app",{"title":1387,"path":1388,"stem":1389},"XAML: декларативний інтерфейс","\u002Fcsharp\u002Fdesktop-ui\u002Fxaml-basics","01.csharp\u002F12.desktop-ui\u002F04.xaml-basics",{"title":1391,"path":1392,"stem":1393},"Fluent UI у WPF — сучасний дизайн Windows 11","\u002Fcsharp\u002Fdesktop-ui\u002F04a.wpf-fluent-ui","01.csharp\u002F12.desktop-ui\u002F04a.wpf-fluent-ui",{"title":1395,"path":1396,"stem":1397},"WPF UI — сучасна бібліотека Fluent контролів","\u002Fcsharp\u002Fdesktop-ui\u002F04b.wpf-ui-library","01.csharp\u002F12.desktop-ui\u002F04b.wpf-ui-library",{"title":1399,"path":1400,"stem":1401},"HandyControl — велика бібліотека UI контролів для WPF","\u002Fcsharp\u002Fdesktop-ui\u002F04c.handycontrol-library","01.csharp\u002F12.desktop-ui\u002F04c.handycontrol-library",{"title":1403,"path":1404,"stem":1405},"Простори імен та ресурси XAML","\u002Fcsharp\u002Fdesktop-ui\u002Fxaml-namespaces-resources","01.csharp\u002F12.desktop-ui\u002F05.xaml-namespaces-resources",{"title":1407,"path":1408,"stem":1409},"XAML в Avalonia: ключові відмінності від WPF","\u002Fcsharp\u002Fdesktop-ui\u002F05a.avalonia-xaml-differences","01.csharp\u002F12.desktop-ui\u002F05a.avalonia-xaml-differences",{"title":1411,"path":1412,"stem":1413},"Розширення розмітки XAML (Markup Extensions)","\u002Fcsharp\u002Fdesktop-ui\u002Fxaml-markup-extensions","01.csharp\u002F12.desktop-ui\u002F06.xaml-markup-extensions",{"title":1415,"path":1416,"stem":1417},"Панелі Layout: StackPanel, WrapPanel, DockPanel","\u002Fcsharp\u002Fdesktop-ui\u002Flayout-panels-part1","01.csharp\u002F12.desktop-ui\u002F07.layout-panels-part1",{"title":1419,"path":1420,"stem":1421},"Grid, Canvas, UniformGrid","\u002Fcsharp\u002Fdesktop-ui\u002Flayout-panels-part2","01.csharp\u002F12.desktop-ui\u002F07.layout-panels-part2",{"title":1423,"path":1424,"stem":1425},"Просунуті техніки Layout","\u002Fcsharp\u002Fdesktop-ui\u002Flayout-advanced","01.csharp\u002F12.desktop-ui\u002F08.layout-advanced",{"title":1427,"path":1428,"stem":1429},"Адаптивний Layout та найкращі практики","\u002Fcsharp\u002Fdesktop-ui\u002Flayout-responsive","01.csharp\u002F12.desktop-ui\u002F09.layout-responsive",{"title":1431,"path":1432,"stem":1433},"Layout в Avalonia: відмінності та нові можливості","\u002Fcsharp\u002Fdesktop-ui\u002F09a.layout-avalonia","01.csharp\u002F12.desktop-ui\u002F09a.layout-avalonia",{"title":1435,"path":1436,"stem":1437},"Button, Image, ProgressBar та інші базові контроли","\u002Fcsharp\u002Fdesktop-ui\u002Fbasic-controls","01.csharp\u002F12.desktop-ui\u002F10.basic-controls",{"title":1439,"path":1440,"stem":1441},"Контроли в Avalonia: відмінності від WPF","\u002Fcsharp\u002Fdesktop-ui\u002F10a.controls-avalonia","01.csharp\u002F12.desktop-ui\u002F10a.controls-avalonia",{"title":1443,"path":1444,"stem":1445},"Текстові контроли — TextBlock, TextBox, RichTextBox","\u002Fcsharp\u002Fdesktop-ui\u002Ftext-controls","01.csharp\u002F12.desktop-ui\u002F11.text-controls",{"title":1447,"path":1448,"stem":1449},"Контроли вибору — CheckBox, RadioButton, ComboBox, ListBox, DatePicker","\u002Fcsharp\u002Fdesktop-ui\u002Fselection-controls","01.csharp\u002F12.desktop-ui\u002F12.selection-controls",{"title":1451,"path":1452,"stem":1453},"Content Model — GroupBox, Expander, TabControl, StatusBar","\u002Fcsharp\u002Fdesktop-ui\u002Fcontent-controls","01.csharp\u002F12.desktop-ui\u002F13.content-controls",{"title":1455,"path":1456,"stem":1457},"UI\u002FUX принципи десктопних застосунків","\u002Fcsharp\u002Fdesktop-ui\u002F13a.ui-ux-principles","01.csharp\u002F12.desktop-ui\u002F13a.ui-ux-principles",{"title":1459,"path":1460,"stem":1461},"Dependency Properties — Концепція та Value Resolution","\u002Fcsharp\u002Fdesktop-ui\u002Fdependency-properties-part1","01.csharp\u002F12.desktop-ui\u002F14.dependency-properties-part1",{"title":1463,"path":1464,"stem":1465},"Avalonia Property System — StyledProperty та DirectProperty","\u002Fcsharp\u002Fdesktop-ui\u002F14a.avalonia-property-system","01.csharp\u002F12.desktop-ui\u002F14a.avalonia-property-system",{"title":1467,"path":1468,"stem":1469},"Attached Properties — Властивості без меж","\u002Fcsharp\u002Fdesktop-ui\u002Fattached-properties","01.csharp\u002F12.desktop-ui\u002F15.attached-properties",{"title":1471,"path":1472,"stem":1473},"Routed Events — Маршрутизація подій у WPF","\u002Fcsharp\u002Fdesktop-ui\u002Frouted-events","01.csharp\u002F12.desktop-ui\u002F16.routed-events",{"title":1475,"path":1476,"stem":1477},"Data Binding — Від Code-Behind до Декларативності","\u002Fcsharp\u002Fdesktop-ui\u002Fdata-binding-basics-part1","01.csharp\u002F12.desktop-ui\u002F17.data-binding-basics-part1",{"title":1479,"path":1480,"stem":1481},"INotifyPropertyChanged — Живе оновлення UI","\u002Fcsharp\u002Fdesktop-ui\u002Fdata-binding-basics-part2","01.csharp\u002F12.desktop-ui\u002F17.data-binding-basics-part2",{"title":1483,"path":1484,"stem":1485},"Compiled Bindings в Avalonia — Безпека на етапі компіляції","\u002Fcsharp\u002Fdesktop-ui\u002F17a.avalonia-compiled-bindings","01.csharp\u002F12.desktop-ui\u002F17a.avalonia-compiled-bindings",{"title":1487,"path":1488,"stem":1489},"Просунутий Data Binding — ElementName, RelativeSource, MultiBinding","\u002Fcsharp\u002Fdesktop-ui\u002Fdata-binding-advanced","01.csharp\u002F12.desktop-ui\u002F18.data-binding-advanced",{"title":1491,"path":1492,"stem":1493},"Value Converters — Перетворення типів даних у Data Binding","\u002Fcsharp\u002Fdesktop-ui\u002Fvalue-converters","01.csharp\u002F12.desktop-ui\u002F19.value-converters",{"title":1495,"path":1496,"stem":1497},"Data Templates — Візуалізація об'єктів у WPF","\u002Fcsharp\u002Fdesktop-ui\u002Fdata-templates","01.csharp\u002F12.desktop-ui\u002F20.data-templates",{"title":1499,"path":1500,"stem":1501},"Collections Binding Part 1 — ObservableCollection та ItemsControl","\u002Fcsharp\u002Fdesktop-ui\u002Fcollections-binding-part1","01.csharp\u002F12.desktop-ui\u002F21.collections-binding-part1",{"title":1503,"path":1504,"stem":1505},"Collections Binding Part 2 — ICollectionView, Filtering, Sorting та Virtualization","\u002Fcsharp\u002Fdesktop-ui\u002Fcollections-binding-part2","01.csharp\u002F12.desktop-ui\u002F21.collections-binding-part2",{"title":1507,"path":1508,"stem":1509},"MVVM Pattern — Від Spaghetti Code до архітектури","\u002Fcsharp\u002Fdesktop-ui\u002Fmvvm-pattern","01.csharp\u002F12.desktop-ui\u002F22.mvvm-pattern",{"title":1511,"path":1512,"stem":1513},"ViewModel Implementation — Від BaseViewModel до валідації","\u002Fcsharp\u002Fdesktop-ui\u002Fviewmodel-implementation","01.csharp\u002F12.desktop-ui\u002F23.viewmodel-implementation",{"title":1515,"path":1516,"stem":1517},"Commands — Від event handlers до декларативних команд","\u002Fcsharp\u002Fdesktop-ui\u002Fcommands","01.csharp\u002F12.desktop-ui\u002F24.commands",{"title":1519,"path":1520,"stem":1521},"MVVM Toolkit — MVVM без boilerplate через Source Generators","\u002Fcsharp\u002Fdesktop-ui\u002Fmvvm-toolkit","01.csharp\u002F12.desktop-ui\u002F25.mvvm-toolkit",{"title":1523,"path":1524,"stem":1525},"Messenger Pattern — Комунікація між ViewModel без прямих посилань","\u002Fcsharp\u002Fdesktop-ui\u002Fmessenger-pattern","01.csharp\u002F12.desktop-ui\u002F26.messenger-pattern",{"title":1527,"path":1528,"stem":1529},"Стилі WPF — CSS для десктопу","\u002Fcsharp\u002Fdesktop-ui\u002Fstyles-basics","01.csharp\u002F12.desktop-ui\u002F27.styles-basics",{"title":1531,"path":1532,"stem":1533},"CSS-like стилі Avalonia","\u002Fcsharp\u002Fdesktop-ui\u002F27a.avalonia-css-styling","01.csharp\u002F12.desktop-ui\u002F27a.avalonia-css-styling",{"title":1535,"path":1536,"stem":1537},"Control Templates — Частина 1. Концепція та TemplateBinding","\u002Fcsharp\u002Fdesktop-ui\u002Fcontrol-templates-part1","01.csharp\u002F12.desktop-ui\u002F28.control-templates-part1",{"title":1539,"path":1540,"stem":1541},"Control Templates — Частина 2. Named Parts та ContentPresenter","\u002Fcsharp\u002Fdesktop-ui\u002Fcontrol-templates-part2","01.csharp\u002F12.desktop-ui\u002F28.control-templates-part2",{"title":1543,"path":1544,"stem":1545},"Control Themes в Avalonia — нова ера стилізації","\u002Fcsharp\u002Fdesktop-ui\u002F28a.avalonia-control-themes","01.csharp\u002F12.desktop-ui\u002F28a.avalonia-control-themes",{"title":1547,"path":1548,"stem":1549},"Triggers та Visual State Manager у WPF","\u002Fcsharp\u002Fdesktop-ui\u002Ftriggers-visual-states","01.csharp\u002F12.desktop-ui\u002F29.triggers-visual-states",{"title":1551,"path":1552,"stem":1553},"Pseudo-classes в Avalonia — замість WPF Triggers","\u002Fcsharp\u002Fdesktop-ui\u002F29a.avalonia-pseudo-classes","01.csharp\u002F12.desktop-ui\u002F29a.avalonia-pseudo-classes",{"title":1555,"path":1556,"stem":1557},"Теми та ресурсні словники у WPF","\u002Fcsharp\u002Fdesktop-ui\u002Fresources-themes","01.csharp\u002F12.desktop-ui\u002F30.resources-themes",{"title":1559,"path":1560,"stem":1561},"Avalonia Themes — Fluent Design та система тематизації","\u002Fcsharp\u002Fdesktop-ui\u002F30a.avalonia-themes-fluent","01.csharp\u002F12.desktop-ui\u002F30a.avalonia-themes-fluent",{"title":1563,"path":1564,"stem":1565},"Контроли колекцій — глибоке занурення","\u002Fcsharp\u002Fdesktop-ui\u002Fcollection-controls","01.csharp\u002F12.desktop-ui\u002F31.collection-controls",{"title":1567,"path":1568,"stem":1569},"DataGrid — колонки та базове відображення","\u002Fcsharp\u002Fdesktop-ui\u002Fdatagrid-part1","01.csharp\u002F12.desktop-ui\u002F32.datagrid-part1",{"title":1571,"path":1572,"stem":1573},"DataGrid — сортування, фільтрація, редагування","\u002Fcsharp\u002Fdesktop-ui\u002Fdatagrid-part2","01.csharp\u002F12.desktop-ui\u002F32.datagrid-part2",{"title":1575,"path":1576,"stem":1577},"TreeView та GridView","\u002Fcsharp\u002Fdesktop-ui\u002Ftreeview-listview","01.csharp\u002F12.desktop-ui\u002F33.treeview-listview",{"title":1579,"path":1580,"stem":1581},"Меню, Toolbar, ContextMenu, StatusBar","\u002Fcsharp\u002Fdesktop-ui\u002Fmenus-toolbars","01.csharp\u002F12.desktop-ui\u002F34.menus-toolbars",{"title":1583,"path":1584,"stem":1585},"Навігація та керування вікнами. Частина 1: вікна та сторінки","\u002Fcsharp\u002Fdesktop-ui\u002Fnavigation-windows-part1","01.csharp\u002F12.desktop-ui\u002F35.navigation-windows-part1",{"title":1587,"path":1588,"stem":1589},"Навігація та керування вікнами. Частина 2: MVVM-навігація","\u002Fcsharp\u002Fdesktop-ui\u002Fnavigation-windows-part2","01.csharp\u002F12.desktop-ui\u002F35.navigation-windows-part2",{"title":1591,"path":1592,"stem":1593},"Avalonia — Навігація та діалоги","\u002Fcsharp\u002Fdesktop-ui\u002F35a.avalonia-navigation-dialogs","01.csharp\u002F12.desktop-ui\u002F35a.avalonia-navigation-dialogs",{"title":1595,"path":1596,"stem":1597},"Діалоги та File Pickers у WPF","\u002Fcsharp\u002Fdesktop-ui\u002Fdialogs-file-pickers","01.csharp\u002F12.desktop-ui\u002F36.dialogs-file-pickers",{"title":1599,"path":1600,"stem":1601},"UserControl: компонентний підхід у WPF","\u002Fcsharp\u002Fdesktop-ui\u002Fuser-controls","01.csharp\u002F12.desktop-ui\u002F37.user-controls",{"title":1603,"path":1604,"stem":1605},"Custom Controls: Lookless Controls у WPF","\u002Fcsharp\u002Fdesktop-ui\u002Fcustom-controls","01.csharp\u002F12.desktop-ui\u002F38.custom-controls",{"title":1607,"path":1608,"stem":1609},"Avalonia TemplatedControl — Lookless Controls","\u002Fcsharp\u002Fdesktop-ui\u002F38a.avalonia-templated-controls","01.csharp\u002F12.desktop-ui\u002F38a.avalonia-templated-controls",{"title":1611,"path":1612,"stem":1613},"Анімації у WPF: Storyboard та Easing Functions","\u002Fcsharp\u002Fdesktop-ui\u002Fanimations-transitions","01.csharp\u002F12.desktop-ui\u002F39.animations-transitions",{"title":1615,"path":1616,"stem":1617},"Анімації в Avalonia","\u002Fcsharp\u002Fdesktop-ui\u002F39a.avalonia-animations","01.csharp\u002F12.desktop-ui\u002F39a.avalonia-animations",{"title":1619,"path":1620,"stem":1621},"2D Графіка та Мультимедіа у WPF","\u002Fcsharp\u002Fdesktop-ui\u002Fmedia-graphics","01.csharp\u002F12.desktop-ui\u002F40.media-graphics",{"title":1623,"path":1624,"stem":1625},"Dependency Injection у WPF та Avalonia","\u002Fcsharp\u002Fdesktop-ui\u002Fdi-integration","01.csharp\u002F12.desktop-ui\u002F41.di-integration",{"title":1627,"path":1628,"stem":1629},"SQLite та EF Core у десктопних додатках","\u002Fcsharp\u002Fdesktop-ui\u002Fdata-persistence-part1","01.csharp\u002F12.desktop-ui\u002F42.data-persistence-part1",{"title":1631,"path":1632,"stem":1633},"Repository Pattern та Unit of Work","\u002Fcsharp\u002Fdesktop-ui\u002Fdata-persistence-part2","01.csharp\u002F12.desktop-ui\u002F43.data-persistence-part2",{"title":1635,"path":1636,"stem":1637},"Тестування ViewModels","\u002Fcsharp\u002Fdesktop-ui\u002Fviewmodel-testing","01.csharp\u002F12.desktop-ui\u002F44.viewmodel-testing",{"title":1639,"path":1640,"stem":1641},"Avalonia Headless Testing — тестування UI без вікон","\u002Fcsharp\u002Fdesktop-ui\u002F44a.avalonia-headless-testing","01.csharp\u002F12.desktop-ui\u002F44a.avalonia-headless-testing",{"title":1643,"path":1644,"stem":1645},"Кросплатформна розробка з Avalonia","\u002Fcsharp\u002Fdesktop-ui\u002Favalonia-cross-platform","01.csharp\u002F12.desktop-ui\u002F45.avalonia-cross-platform",{"title":1647,"path":1648,"stem":1649},"Пакування та розгортання Avalonia додатків","\u002Fcsharp\u002Fdesktop-ui\u002Favalonia-packaging-deployment","01.csharp\u002F12.desktop-ui\u002F46.avalonia-packaging-deployment",{"title":1651,"path":1652,"stem":1653},"Розгортання WPF застосунків","\u002Fcsharp\u002Fdesktop-ui\u002Fwpf-packaging-deployment","01.csharp\u002F12.desktop-ui\u002F47.wpf-packaging-deployment",{"title":1655,"icon":658,"path":1656,"stem":1657,"children":1658,"page":59},"Network Programming","\u002Fcsharp\u002Fnetwork-programming","01.csharp\u002F13.network-programming",[1659,1663,1667,1671,1675,1679,1683,1687,1691,1695,1699],{"title":1660,"path":1661,"stem":1662},"Основи комп'ютерних мереж","\u002Fcsharp\u002Fnetwork-programming\u002Ffoundations","01.csharp\u002F13.network-programming\u002F01.foundations",{"title":1664,"path":1665,"stem":1666},"Модель OSI та стек TCP\u002FIP","\u002Fcsharp\u002Fnetwork-programming\u002Fosi-model","01.csharp\u002F13.network-programming\u002F02.osi-model",{"title":1668,"path":1669,"stem":1670},"IP-протокол та адресація","\u002Fcsharp\u002Fnetwork-programming\u002Fip-addressing","01.csharp\u002F13.network-programming\u002F03.ip-addressing",{"title":1672,"path":1673,"stem":1674},"UDP — протокол без з'єднання","\u002Fcsharp\u002Fnetwork-programming\u002Fudp","01.csharp\u002F13.network-programming\u002F05.udp",{"title":1676,"path":1677,"stem":1678},"UDP Broadcast та Multicast","\u002Fcsharp\u002Fnetwork-programming\u002Fudp-broadcast-multicast","01.csharp\u002F13.network-programming\u002F06.udp-broadcast-multicast",{"title":1680,"path":1681,"stem":1682},"HTTP — протокол вебу","\u002Fcsharp\u002Fnetwork-programming\u002Fhttp-fundamentals","01.csharp\u002F13.network-programming\u002F07.http-fundamentals",{"title":1684,"path":1685,"stem":1686},"HttpListener — вбудований HTTP-сервер .NET","\u002Fcsharp\u002Fnetwork-programming\u002F07a.http-listener","01.csharp\u002F13.network-programming\u002F07a.http-listener",{"title":1688,"path":1689,"stem":1690},"HTTP Advanced — cookies, аутентифікація та HTTPS","\u002Fcsharp\u002Fnetwork-programming\u002Fhttp-advanced","01.csharp\u002F13.network-programming\u002F08.http-advanced",{"title":1692,"path":1693,"stem":1694},"SMTP та протоколи електронної пошти","\u002Fcsharp\u002Fnetwork-programming\u002Fsmtp","01.csharp\u002F13.network-programming\u002F09.smtp",{"title":1696,"path":1697,"stem":1698},"WebSocket — повнодуплексний протокол реального часу","\u002Fcsharp\u002Fnetwork-programming\u002Fwebsockets","01.csharp\u002F13.network-programming\u002F10.websockets",{"title":1700,"path":1701,"stem":1702},"TLS\u002FSSL — криптографічний захист мережевих з'єднань","\u002Fcsharp\u002Fnetwork-programming\u002Ftls-ssl","01.csharp\u002F13.network-programming\u002F11.tls-ssl",{"title":1704,"path":1705,"stem":1706},"C# & .NET: The Ultimate Roadmap","\u002Fcsharp\u002Froadmap","01.csharp\u002Froadmap",{"title":1708,"icon":1709,"path":1710,"stem":1711,"children":1712,"page":59},"C++","i-devicon-cplusplus","\u002Fcpp","02.cpp",[1713,1717,1721,1725,1729,1733,1737,1741,1745,1748,1752,1756,1760,1764,1768,1772,1776,1780,1784,1788,1792,1796,1800,1804,1808,1812,1816,1820,1824,1828,1832,1836,1840,1844,1848,1852,1856,1860,1864,1868,1872,1876,1880,1884,1888,1892,1896,1900,1904,1908],{"title":1714,"path":1715,"stem":1716},"Вступ у програмування та алгоритми","\u002Fcpp\u002Fintro-algorithms","02.cpp\u002F01.intro-algorithms",{"title":1718,"path":1719,"stem":1720},"Code Style: угоди про оформлення коду","\u002Fcpp\u002Fcode-style","02.cpp\u002F02.code-style",{"title":1722,"path":1723,"stem":1724},"Середовище розробки та перший проєкт","\u002Fcpp\u002Fide-setup","02.cpp\u002F03.ide-setup",{"title":1726,"path":1727,"stem":1728},"Вивід даних на екран","\u002Fcpp\u002Fdata-output","02.cpp\u002F04.data-output",{"title":1730,"path":1731,"stem":1732},"Типи даних, змінні та константи","\u002Fcpp\u002Fdata-types-variables","02.cpp\u002F05.data-types-variables",{"title":1734,"path":1735,"stem":1736},"Ввід даних з клавіатури","\u002Fcpp\u002Fdata-input","02.cpp\u002F06.data-input",{"title":1738,"path":1739,"stem":1740},"Оператори, перетворення типів та логічні операції","\u002Fcpp\u002Foperators-type-conversion","02.cpp\u002F07.operators-type-conversion",{"title":1742,"path":1743,"stem":1744},"Цикли","\u002Fcpp\u002Floops","02.cpp\u002F08.loops",{"title":32,"path":1746,"stem":1747},"\u002Fcpp\u002Farrays","02.cpp\u002F09.arrays",{"title":1749,"path":1750,"stem":1751},"Алгоритми сортування та аналіз складності","\u002Fcpp\u002Fsorting","02.cpp\u002F10.sorting",{"title":1753,"path":1754,"stem":1755},"Алгоритми пошуку","\u002Fcpp\u002Fsearching","02.cpp\u002F11.searching",{"title":1757,"path":1758,"stem":1759},"Функції: основи","\u002Fcpp\u002Ffunctions-basics","02.cpp\u002F12.functions-basics",{"title":1761,"path":1762,"stem":1763},"Функції: прототипи, область видимості та додаткові можливості","\u002Fcpp\u002Ffunctions-scope","02.cpp\u002F13.functions-scope",{"title":1765,"path":1766,"stem":1767},"Функції: перевантаження та шаблони","\u002Fcpp\u002Ffunctions-overloading-templates","02.cpp\u002F14.functions-overloading-templates",{"title":1769,"path":1770,"stem":1771},"Вказівники: основи","\u002Fcpp\u002Fpointers-basics","02.cpp\u002F15.pointers-basics",{"title":1773,"path":1774,"stem":1775},"Посилання (References)","\u002Fcpp\u002Freferences","02.cpp\u002F16.references",{"title":1777,"path":1778,"stem":1779},"Вказівники, const і масиви","\u002Fcpp\u002Fpointers-const-arrays","02.cpp\u002F17.pointers-const-arrays",{"title":1781,"path":1782,"stem":1783},"Адресна арифметика","\u002Fcpp\u002Fpointer-arithmetic","02.cpp\u002F18.pointer-arithmetic",{"title":1785,"path":1786,"stem":1787},"Динамічна пам'ять","\u002Fcpp\u002Fdynamic-memory","02.cpp\u002F19.dynamic-memory",{"title":1789,"path":1790,"stem":1791},"Вказівники типу void","\u002Fcpp\u002Fvoid-pointers","02.cpp\u002F20.void-pointers",{"title":1793,"path":1794,"stem":1795},"Вказівники на вказівники","\u002Fcpp\u002Fpointers-to-pointers","02.cpp\u002F21.pointers-to-pointers",{"title":1797,"path":1798,"stem":1799},"Оператор доступу до членів через вказівник (->)","\u002Fcpp\u002Fmember-access-operator","02.cpp\u002F22.member-access-operator",{"title":1801,"path":1802,"stem":1803},"Цикл for-each (Range-based for)","\u002Fcpp\u002Fforeach-loop","02.cpp\u002F23.foreach-loop",{"title":1805,"path":1806,"stem":1807},"Вказівники на функції","\u002Fcpp\u002Ffunction-pointers","02.cpp\u002F24.function-pointers",{"title":1809,"path":1810,"stem":1811},"Лямбда-вирази","\u002Fcpp\u002Flambdas","02.cpp\u002F25.lambdas",{"title":1813,"path":1814,"stem":1815},"Лямбда-захоплення","\u002Fcpp\u002Flambda-captures","02.cpp\u002F26.lambda-captures",{"title":1817,"path":1818,"stem":1819},"Еліпсис","\u002Fcpp\u002Fellipsis","02.cpp\u002F27.ellipsis",{"title":1821,"path":1822,"stem":1823},"Безпечні альтернативи еліпсису","\u002Fcpp\u002F27a.ellipsis","02.cpp\u002F27a.ellipsis",{"title":1825,"path":1826,"stem":1827},"Аргументи командного рядка","\u002Fcpp\u002Fcommand-line-arguments","02.cpp\u002F28.command-line-arguments",{"title":1829,"path":1830,"stem":1831},"Перерахування (enum)","\u002Fcpp\u002Fenum","02.cpp\u002F29.enum",{"title":1833,"path":1834,"stem":1835},"Класи-перерахування (enum class)","\u002Fcpp\u002Fenum-class","02.cpp\u002F30.enum-class",{"title":1837,"path":1838,"stem":1839},"Псевдоніми типів (typedef і using)","\u002Fcpp\u002Ftype-aliases","02.cpp\u002F31.type-aliases",{"title":1841,"path":1842,"stem":1843},"Системи числення та двійкова арифметика","\u002Fcpp\u002Fnumber-systems","02.cpp\u002F32.number-systems",{"title":1845,"path":1846,"stem":1847},"Структури (struct): агрегування даних","\u002Fcpp\u002Fstruct","02.cpp\u002F33.struct",{"title":1849,"path":1850,"stem":1851},"Структури у функціях","\u002Fcpp\u002Fstruct-functions","02.cpp\u002F34.struct-functions",{"title":1853,"path":1854,"stem":1855},"Масиви структур і вкладені структури","\u002Fcpp\u002Fstruct-arrays","02.cpp\u002F35.struct-arrays",{"title":1857,"path":1858,"stem":1859},"Патерни struct та межі застосування","\u002Fcpp\u002Fstruct-patterns","02.cpp\u002F36.struct-patterns",{"title":1861,"path":1862,"stem":1863},"Символи та таблиця ASCII","\u002Fcpp\u002Fascii-characters","02.cpp\u002F37.ascii-characters",{"title":1865,"path":1866,"stem":1867},"Unicode та кодування UTF","\u002Fcpp\u002Funicode-utf","02.cpp\u002F38.unicode-utf",{"title":1869,"path":1870,"stem":1871},"C-style рядки","\u002Fcpp\u002Fc-strings","02.cpp\u002F39.c-strings",{"title":1873,"path":1874,"stem":1875},"Вступ до std::string","\u002Fcpp\u002Fstd-string-intro","02.cpp\u002F40.std-string-intro",{"title":1877,"path":1878,"stem":1879},"Довжина, ємність та доступ до символів std::string","\u002Fcpp\u002Fstd-string-capacity-access","02.cpp\u002F41.std-string-capacity-access",{"title":1881,"path":1882,"stem":1883},"Модифікація std::string: присвоювання, додавання, вставка, видалення та заміна","\u002Fcpp\u002Fstd-string-modification","02.cpp\u002F42.std-string-modification",{"title":1885,"path":1886,"stem":1887},"Пошук у std::string: find, npos та практичні патерни","\u002Fcpp\u002Fstd-string-search","02.cpp\u002F43.std-string-search",{"title":1889,"path":1890,"stem":1891},"std::string_view: невласницький погляд на рядок без копіювання","\u002Fcpp\u002Fstd-string-view","02.cpp\u002F44.std-string-view",{"title":1893,"path":1894,"stem":1895},"Об'єднання (union): один блок пам'яті, кілька інтерпретацій","\u002Fcpp\u002Funion","02.cpp\u002F45.union",{"title":1897,"path":1898,"stem":1899},"Організація коду: файли, препроцесор, простори імен","\u002Fcpp\u002Fmultifile-programs","02.cpp\u002F46.multifile-programs",{"title":1901,"path":1902,"stem":1903},"Робота з файлами: C-стиль (stdio.h)","\u002Fcpp\u002Fc-style-files","02.cpp\u002F47.c-style-files",{"title":1905,"path":1906,"stem":1907},"Робота з файлами: C++-стиль (fstream)","\u002Fcpp\u002Fcpp-style-files","02.cpp\u002F48.cpp-style-files",{"title":1909,"path":1910,"stem":1911},"План навчання: Курс C++ — Продовження (Статті 29–60+)","\u002Fcpp\u002Fcurriculum-plan","02.cpp\u002Fcurriculum-plan",{"title":1913,"icon":1914,"path":1915,"stem":1916,"children":1917,"page":59},"JavaScript","i-devicon-javascript","\u002Fjavascript","03.javascript",[1918,1944,1998,2020,2324,2362],{"title":1919,"icon":1920,"path":1921,"stem":1922,"children":1923,"page":59},"Events","i-lucide-mouse-pointer-click","\u002Fjavascript\u002Fevents","03.javascript\u002F01.events",[1924,1928,1932,1936,1940],{"title":1925,"path":1926,"stem":1927},"Вступ до подій браузера","\u002Fjavascript\u002Fevents\u002Fintro","03.javascript\u002F01.events\u002F01.intro",{"title":1929,"path":1930,"stem":1931},"Бульбашковий механізм (Bubbling) та занурення (Capturing)","\u002Fjavascript\u002Fevents\u002Fbubbling-capturing","03.javascript\u002F01.events\u002F02.bubbling-capturing",{"title":1933,"path":1934,"stem":1935},"Делегування подій (Event Delegation)","\u002Fjavascript\u002Fevents\u002Fdelegate-events","03.javascript\u002F01.events\u002F03.delegate-events",{"title":1937,"path":1938,"stem":1939},"Типові дії браузера та preventDefault()","\u002Fjavascript\u002Fevents\u002Fprevent-default","03.javascript\u002F01.events\u002F04.prevent-default",{"title":1941,"path":1942,"stem":1943},"Запуск користувацьких подій (Custom Events)","\u002Fjavascript\u002Fevents\u002Fcustom-events","03.javascript\u002F01.events\u002F05.custom-events",{"title":1945,"icon":1946,"path":1947,"stem":1948,"children":1949,"page":59},"Network","i-lucide-globe","\u002Fjavascript\u002Fnetwork","03.javascript\u002F02.network",[1950,1954,1958,1962,1966,1970,1974,1978,1982,1986,1990,1994],{"title":1951,"path":1952,"stem":1953},"Fetch API - Сучасний підхід до HTTP-запитів","\u002Fjavascript\u002Fnetwork\u002F01-fetch-api","03.javascript\u002F02.network\u002F01-fetch-api",{"title":1955,"path":1956,"stem":1957},"FormData - Робота з формами та файлами","\u002Fjavascript\u002Fnetwork\u002F02-formdata","03.javascript\u002F02.network\u002F02-formdata",{"title":1959,"path":1960,"stem":1961},"Відстеження прогресу завантаження","\u002Fjavascript\u002Fnetwork\u002F03-download-progress","03.javascript\u002F02.network\u002F03-download-progress",{"title":1963,"path":1964,"stem":1965},"Переривання fetch-запитів","\u002Fjavascript\u002Fnetwork\u002F04-abort-requests","03.javascript\u002F02.network\u002F04-abort-requests",{"title":1967,"path":1968,"stem":1969},"CORS - Запити між різними джерелами","\u002Fjavascript\u002Fnetwork\u002F05-cors","03.javascript\u002F02.network\u002F05-cors",{"title":1971,"path":1972,"stem":1973},"Fetch API - Повний довідник опцій","\u002Fjavascript\u002Fnetwork\u002F06-fetch-options","03.javascript\u002F02.network\u002F06-fetch-options",{"title":1975,"path":1976,"stem":1977},"URL Objects - Робота з посиланнями","\u002Fjavascript\u002Fnetwork\u002F07-url-objects","03.javascript\u002F02.network\u002F07-url-objects",{"title":1979,"path":1980,"stem":1981},"XMLHttpRequest - AJAX та низькорівневі запити","\u002Fjavascript\u002Fnetwork\u002F08-xmlhttprequest","03.javascript\u002F02.network\u002F08-xmlhttprequest",{"title":1983,"path":1984,"stem":1985},"Відновлюване завантаження файлів","\u002Fjavascript\u002Fnetwork\u002F09-resumable-upload","03.javascript\u002F02.network\u002F09-resumable-upload",{"title":1987,"path":1988,"stem":1989},"Cookies, document.cookie та світ після \"Cookiepocalypse\"","\u002Fjavascript\u002Fnetwork\u002F10-cookies","03.javascript\u002F02.network\u002F10-cookies",{"title":1991,"path":1992,"stem":1993},"js-cookie: Керування Cookies без Болю","\u002Fjavascript\u002Fnetwork\u002F11-js-cookie","03.javascript\u002F02.network\u002F11-js-cookie",{"title":1995,"path":1996,"stem":1997},"Axios: Потужний HTTP-клієнт для JavaScript","\u002Fjavascript\u002Fnetwork\u002F12-axios","03.javascript\u002F02.network\u002F12-axios",{"title":1999,"icon":2000,"path":2001,"stem":2002,"children":2003,"page":59},"Bom","i-lucide-monitor","\u002Fjavascript\u002Fbom","03.javascript\u002F03.bom",[2004,2008,2012,2016],{"title":2005,"path":2006,"stem":2007},"LocalStorage, SessionStorage та patterns збереження даних","\u002Fjavascript\u002Fbom\u002F01-localstorage","03.javascript\u002F03.bom\u002F01-localstorage",{"title":2009,"path":2010,"stem":2011},"Location Object - Керування адресою сторінки","\u002Fjavascript\u002Fbom\u002F02-location-object","03.javascript\u002F03.bom\u002F02-location-object",{"title":2013,"path":2014,"stem":2015},"History API - Керування історією браузера","\u002Fjavascript\u002Fbom\u002F03-history-api","03.javascript\u002F03.bom\u002F03-history-api",{"title":2017,"path":2018,"stem":2019},"Navigator Object - Ідентифікація та Можливості Пристрою","\u002Fjavascript\u002Fbom\u002F04-navigator-object","03.javascript\u002F03.bom\u002F04-navigator-object",{"title":2021,"icon":2022,"path":2023,"stem":2024,"children":2025},"React","i-devicon-react","\u002Fjavascript\u002Freact","03.javascript\u002F04.react\u002Findex",[2026,2027,2031,2035,2039,2043,2106,2141,2293],{"title":2021,"path":2023,"stem":2024},{"title":2028,"path":2029,"stem":2030},"Робота з Формами в React","\u002Fjavascript\u002Freact\u002Freact-forms","03.javascript\u002F04.react\u002F01.react-forms",{"title":2032,"path":2033,"stem":2034},"React Hook Form: Професійна Робота з Формами","\u002Fjavascript\u002Freact\u002Freact-hook-form","03.javascript\u002F04.react\u002F02.react-hook-form",{"title":2036,"path":2037,"stem":2038},"React Hook Form: Глибоке Розуміння Архітектури та Оптимізації","\u002Fjavascript\u002Freact\u002Freact-hook-form-new","03.javascript\u002F04.react\u002F02.react-hook-form-new",{"title":2040,"path":2041,"stem":2042},"Axios та React: Професійна Архітектура Запитів","\u002Fjavascript\u002Freact\u002Fdata-fetching-axios","03.javascript\u002F04.react\u002F03.data-fetching-axios",{"title":2044,"icon":132,"path":2045,"stem":2046,"children":2047},"Tanstack Query","\u002Fjavascript\u002Freact\u002Ftanstack-query","03.javascript\u002F04.react\u002F04.tanstack-query\u002Findex",[2048,2050,2054,2058,2062,2066,2070,2074,2078,2082,2086,2090,2094,2098,2102],{"title":2049,"path":2045,"stem":2046},"TanStack Query: Майстерність Керування Станом Сервера",{"title":2051,"path":2052,"stem":2053},"Парадигма Server State: Чому useEffect недостатньо","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fserver-state-paradigm","03.javascript\u002F04.react\u002F04.tanstack-query\u002F01.server-state-paradigm",{"title":2055,"path":2056,"stem":2057},"Встановлення та Налаштування: Фундамент","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Finstallation-and-devtools","03.javascript\u002F04.react\u002F04.tanstack-query\u002F02.installation-and-devtools",{"title":2059,"path":2060,"stem":2061},"Основи Запитів та Магія Ключів","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fquery-basics-and-keys","03.javascript\u002F04.react\u002F04.tanstack-query\u002F03.query-basics-and-keys",{"title":2063,"path":2064,"stem":2065},"Синхронізація Даних: Життєвий Цикл Запиту","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fdata-synchronization","03.javascript\u002F04.react\u002F04.tanstack-query\u002F04.data-synchronization",{"title":2067,"path":2068,"stem":2069},"Мутації та Інвалідація: Зміна Даних","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fmutations-and-invalidation","03.javascript\u002F04.react\u002F04.tanstack-query\u002F05.mutations-and-invalidation",{"title":2071,"path":2072,"stem":2073},"Оптимістичні Оновлення: Швидше за Світло","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Foptimistic-updates","03.javascript\u002F04.react\u002F04.tanstack-query\u002F06.optimistic-updates",{"title":2075,"path":2076,"stem":2077},"Пагінація та Infinite Scroll","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fpagination-and-load-more","03.javascript\u002F04.react\u002F04.tanstack-query\u002F07.pagination-and-load-more",{"title":2079,"path":2080,"stem":2081},"Просунуті Патерни та Оптимізація","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fadvanced-patterns","03.javascript\u002F04.react\u002F04.tanstack-query\u002F08.advanced-patterns",{"title":2083,"path":2084,"stem":2085},"Архітектура та Best Practices","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Farchitecture-and-best-practices","03.javascript\u002F04.react\u002F04.tanstack-query\u002F09.architecture-and-best-practices",{"title":2087,"path":2088,"stem":2089},"Server-Side Rendering (SSR) та Гідратація","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fserver-side-rendering","03.javascript\u002F04.react\u002F04.tanstack-query\u002F10.server-side-rendering",{"title":2091,"path":2092,"stem":2093},"Стратегії Тестування","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Ftesting-strategies","03.javascript\u002F04.react\u002F04.tanstack-query\u002F11.testing-strategies",{"title":2095,"path":2096,"stem":2097},"Аутентифікація та Обробка Помилок","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fauthentication-and-errors","03.javascript\u002F04.react\u002F04.tanstack-query\u002F12.authentication-and-errors",{"title":2099,"path":2100,"stem":2101},"React Suspense та Майбутнє","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Freact-suspense","03.javascript\u002F04.react\u002F04.tanstack-query\u002F13.react-suspense",{"title":2103,"path":2104,"stem":2105},"Глибоке Занурення в Продуктивність","\u002Fjavascript\u002Freact\u002Ftanstack-query\u002Fperformance-deep-dive","03.javascript\u002F04.react\u002F04.tanstack-query\u002F14.performance-deep-dive",{"title":2107,"icon":2022,"path":2108,"stem":2109,"children":2110},"React Router","\u002Fjavascript\u002Freact\u002Freact-router","03.javascript\u002F04.react\u002F05.react-router\u002Findex",[2111,2113,2117,2121,2125,2129,2133,2137],{"title":2112,"path":2108,"stem":2109},"React Router: Навігаційна система сучасного вебу",{"title":2114,"path":2115,"stem":2116},"Налаштування та Базовий Роутинг","\u002Fjavascript\u002Freact\u002Freact-router\u002Fsetup-and-basic-routing","03.javascript\u002F04.react\u002F05.react-router\u002F01.setup-and-basic-routing",{"title":2118,"path":2119,"stem":2120},"Динамічна Навігація","\u002Fjavascript\u002Freact\u002Freact-router\u002Fnavigation-and-links","03.javascript\u002F04.react\u002F05.react-router\u002F02.navigation-and-links",{"title":2122,"path":2123,"stem":2124},"Вкладені Маршрути та Макети","\u002Fjavascript\u002Freact\u002Freact-router\u002Fnested-routes-and-layouts","03.javascript\u002F04.react\u002F05.react-router\u002F03.nested-routes-and-layouts",{"title":2126,"path":2127,"stem":2128},"Динамічні Маршрути та Параметри","\u002Fjavascript\u002Freact\u002Freact-router\u002Fdynamic-routing","03.javascript\u002F04.react\u002F05.react-router\u002F04.dynamic-routing",{"title":2130,"path":2131,"stem":2132},"Data APIs: Loaders та Actions","\u002Fjavascript\u002Freact\u002Freact-router\u002Fdata-loading","03.javascript\u002F04.react\u002F05.react-router\u002F05.data-loading",{"title":2134,"path":2135,"stem":2136},"Просунуті Патерни","\u002Fjavascript\u002Freact\u002Freact-router\u002Fadvanced-patterns","03.javascript\u002F04.react\u002F05.react-router\u002F06.advanced-patterns",{"title":2138,"path":2139,"stem":2140},"Legacy Routing: Компонентний підхід","\u002Fjavascript\u002Freact\u002Freact-router\u002Flegacy-routing","03.javascript\u002F04.react\u002F05.react-router\u002F07.legacy-routing",{"title":2142,"icon":132,"path":2143,"stem":2144,"children":2145},"Redux","\u002Fjavascript\u002Freact\u002Fredux","03.javascript\u002F04.react\u002F06.redux\u002Findex",[2146,2148,2164,2193,2202,2223,2239,2268],{"title":2147,"path":2143,"stem":2144},"Redux: Еволюція управління станом",{"title":14,"icon":15,"path":2149,"stem":2150,"children":2151,"page":59},"\u002Fjavascript\u002Freact\u002Fredux\u002Ffundamentals","03.javascript\u002F04.react\u002F06.redux\u002F01.fundamentals",[2152,2156,2160],{"title":2153,"path":2154,"stem":2155},"Вступ до State Management","\u002Fjavascript\u002Freact\u002Fredux\u002Ffundamentals\u002Fintro-state-management","03.javascript\u002F04.react\u002F06.redux\u002F01.fundamentals\u002F01.intro-state-management",{"title":2157,"path":2158,"stem":2159},"Філософія Redux та Три Принципи","\u002Fjavascript\u002Freact\u002Fredux\u002Ffundamentals\u002Fredux-philosophy","03.javascript\u002F04.react\u002F06.redux\u002F01.fundamentals\u002F02.redux-philosophy",{"title":2161,"path":2162,"stem":2163},"Чисті функції та Іммутабельність","\u002Fjavascript\u002Freact\u002Fredux\u002Ffundamentals\u002Fpure-functions-immutability","03.javascript\u002F04.react\u002F06.redux\u002F01.fundamentals\u002F03.pure-functions-immutability",{"title":2165,"icon":132,"path":2166,"stem":2167,"children":2168,"page":59},"Classic Redux","\u002Fjavascript\u002Freact\u002Fredux\u002Fclassic-redux","03.javascript\u002F04.react\u002F06.redux\u002F02.classic-redux",[2169,2173,2177,2181,2185,2189],{"title":2170,"path":2171,"stem":2172},"Створення Store (Classic Redux)","\u002Fjavascript\u002Freact\u002Fredux\u002Fclassic-redux\u002Fstore-setup","03.javascript\u002F04.react\u002F06.redux\u002F02.classic-redux\u002F01.store-setup",{"title":2174,"path":2175,"stem":2176},"Actions, Constants та Action Creators","\u002Fjavascript\u002Freact\u002Fredux\u002Fclassic-redux\u002Factions-constants","03.javascript\u002F04.react\u002F06.redux\u002F02.classic-redux\u002F02.actions-constants",{"title":2178,"path":2179,"stem":2180},"Логіка Reducers","\u002Fjavascript\u002Freact\u002Fredux\u002Fclassic-redux\u002Freducers","03.javascript\u002F04.react\u002F06.redux\u002F02.classic-redux\u002F03.reducers",{"title":2182,"path":2183,"stem":2184},"Комбінування Reducers (Root Reducer)","\u002Fjavascript\u002Freact\u002Fredux\u002Fclassic-redux\u002Fdata-flow","03.javascript\u002F04.react\u002F06.redux\u002F02.classic-redux\u002F04.data-flow",{"title":2186,"path":2187,"stem":2188},"Підключення до React (React-Redux)","\u002Fjavascript\u002Freact\u002Fredux\u002Fclassic-redux\u002Freact-redux-connection","03.javascript\u002F04.react\u002F06.redux\u002F02.classic-redux\u002F05.react-redux-connection",{"title":2190,"path":2191,"stem":2192},"Middleware та Асинхронність (Redux Thunk)","\u002Fjavascript\u002Freact\u002Fredux\u002Fclassic-redux\u002Fmiddleware-thunk","03.javascript\u002F04.react\u002F06.redux\u002F02.classic-redux\u002F06.middleware-thunk",{"title":2194,"icon":132,"path":2195,"stem":2196,"children":2197,"page":59},"Transition To Rtk","\u002Fjavascript\u002Freact\u002Fredux\u002Ftransition-to-rtk","03.javascript\u002F04.react\u002F06.redux\u002F03.transition-to-rtk",[2198],{"title":2199,"path":2200,"stem":2201},"Проблеми класичного Redux","\u002Fjavascript\u002Freact\u002Fredux\u002Ftransition-to-rtk\u002Fproblems-with-classic","03.javascript\u002F04.react\u002F06.redux\u002F03.transition-to-rtk\u002F01.problems-with-classic",{"title":2203,"icon":132,"path":2204,"stem":2205,"children":2206,"page":59},"Redux Toolkit","\u002Fjavascript\u002Freact\u002Fredux\u002Fredux-toolkit","03.javascript\u002F04.react\u002F06.redux\u002F04.redux-toolkit",[2207,2211,2215,2219],{"title":2208,"path":2209,"stem":2210},"Налаштування Store з configureStore","\u002Fjavascript\u002Freact\u002Fredux\u002Fredux-toolkit\u002Fconfigure-store","03.javascript\u002F04.react\u002F06.redux\u002F04.redux-toolkit\u002F01.configure-store",{"title":2212,"path":2213,"stem":2214},"createSlice: Революція в Redux","\u002Fjavascript\u002Freact\u002Fredux\u002Fredux-toolkit\u002Fcreate-slice","03.javascript\u002F04.react\u002F06.redux\u002F04.redux-toolkit\u002F02.create-slice",{"title":2216,"path":2217,"stem":2218},"Асинхронність з createAsyncThunk","\u002Fjavascript\u002Freact\u002Fredux\u002Fredux-toolkit\u002Fasync-thunks","03.javascript\u002F04.react\u002F06.redux\u002F04.redux-toolkit\u002F03.async-thunks",{"title":2220,"path":2221,"stem":2222},"04. Entity Adapter: Керування нормалізованим станом","\u002Fjavascript\u002Freact\u002Fredux\u002Fredux-toolkit\u002Fentity-adapter","03.javascript\u002F04.react\u002F06.redux\u002F04.redux-toolkit\u002F04.entity-adapter",{"title":2224,"icon":92,"path":2225,"stem":2226,"children":2227,"page":59},"Advanced","\u002Fjavascript\u002Freact\u002Fredux\u002Fadvanced","03.javascript\u002F04.react\u002F06.redux\u002F05.advanced",[2228,2232,2236],{"title":2229,"path":2230,"stem":2231},"Мемоізація та Селектори: Повний Гайд по Reselect","\u002Fjavascript\u002Freact\u002Fredux\u002Fadvanced\u002Fselectors-reselect","03.javascript\u002F04.react\u002F06.redux\u002F05.advanced\u002F01.selectors-reselect",{"title":2233,"path":2234,"stem":2235},"RTK Query: Архітектура Серверного Кешу","\u002Fjavascript\u002Freact\u002Fredux\u002Fadvanced\u002Frtk-query-intro","03.javascript\u002F04.react\u002F06.redux\u002F05.advanced\u002F02.rtk-query-intro",{"title":2083,"path":2237,"stem":2238},"\u002Fjavascript\u002Freact\u002Fredux\u002Fadvanced\u002Farchitecture-best-practices","03.javascript\u002F04.react\u002F06.redux\u002F05.advanced\u002F03.architecture-best-practices",{"title":2240,"icon":132,"path":2241,"stem":2242,"children":2243,"page":59},"Project Kanban","\u002Fjavascript\u002Freact\u002Fredux\u002Fproject-kanban","03.javascript\u002F04.react\u002F06.redux\u002F06.project-kanban",[2244,2248,2252,2256,2260,2264],{"title":2245,"path":2246,"stem":2247},"Проєкт: Kanban Board (Trello Clone)","\u002Fjavascript\u002Freact\u002Fredux\u002Fproject-kanban\u002Fproject-overview","03.javascript\u002F04.react\u002F06.redux\u002F06.project-kanban\u002F01.project-overview",{"title":2249,"path":2250,"stem":2251},"Налаштування та Типізація","\u002Fjavascript\u002Freact\u002Fredux\u002Fproject-kanban\u002Fsetup-and-types","03.javascript\u002F04.react\u002F06.redux\u002F06.project-kanban\u002F02.setup-and-types",{"title":2253,"path":2254,"stem":2255},"Board Slice: Серце Дошки","\u002Fjavascript\u002Freact\u002Fredux\u002Fproject-kanban\u002Fboard-slice","03.javascript\u002F04.react\u002F06.redux\u002F06.project-kanban\u002F03.board-slice",{"title":2257,"path":2258,"stem":2259},"Логіка Drag & Drop","\u002Fjavascript\u002Freact\u002Fredux\u002Fproject-kanban\u002Fdrag-and-drop-logic","03.javascript\u002F04.react\u002F06.redux\u002F06.project-kanban\u002F04.drag-and-drop-logic",{"title":2261,"path":2262,"stem":2263},"Інтеграція з RTK Query","\u002Fjavascript\u002Freact\u002Fredux\u002Fproject-kanban\u002Frtk-query-integration","03.javascript\u002F04.react\u002F06.redux\u002F06.project-kanban\u002F05.rtk-query-integration",{"title":2265,"path":2266,"stem":2267},"Optimistic Updates","\u002Fjavascript\u002Freact\u002Fredux\u002Fproject-kanban\u002Foptimistic-updates","03.javascript\u002F04.react\u002F06.redux\u002F06.project-kanban\u002F06.optimistic-updates",{"title":2269,"icon":132,"path":2270,"stem":2271,"children":2272,"page":59},"Testing","\u002Fjavascript\u002Freact\u002Fredux\u002Ftesting","03.javascript\u002F04.react\u002F06.redux\u002F07.testing",[2273,2277,2281,2285,2289],{"title":2274,"path":2275,"stem":2276},"Тестування Redux","\u002Fjavascript\u002Freact\u002Fredux\u002Ftesting\u002Fintro-testing","03.javascript\u002F04.react\u002F06.redux\u002F07.testing\u002F01.intro-testing",{"title":2278,"path":2279,"stem":2280},"Тестування Reducers","\u002Fjavascript\u002Freact\u002Fredux\u002Ftesting\u002Ftesting-reducers","03.javascript\u002F04.react\u002F06.redux\u002F07.testing\u002F02.testing-reducers",{"title":2282,"path":2283,"stem":2284},"Тестування Селекторів","\u002Fjavascript\u002Freact\u002Fredux\u002Ftesting\u002Ftesting-selectors","03.javascript\u002F04.react\u002F06.redux\u002F07.testing\u002F03.testing-selectors",{"title":2286,"path":2287,"stem":2288},"Тестування Компонентів (Integration)","\u002Fjavascript\u002Freact\u002Fredux\u002Ftesting\u002Ftesting-components","03.javascript\u002F04.react\u002F06.redux\u002F07.testing\u002F04.testing-components",{"title":2290,"path":2291,"stem":2292},"Тестування Async Thunks","\u002Fjavascript\u002Freact\u002Fredux\u002Ftesting\u002Ftesting-thunks","03.javascript\u002F04.react\u002F06.redux\u002F07.testing\u002F05.testing-thunks",{"title":2294,"icon":132,"path":2295,"stem":2296,"children":2297},"Ui Libraries","\u002Fjavascript\u002Freact\u002Fui-libraries","03.javascript\u002F04.react\u002F07.ui-libraries\u002Findex",[2298,2300,2304,2308,2312,2316,2320],{"title":2299,"path":2295,"stem":2296},"UI Бібліотеки в React",{"title":2301,"path":2302,"stem":2303},"Вступ до UI Бібліотек: Навіщо Винаходити Велосипед Двічі?","\u002Fjavascript\u002Freact\u002Fui-libraries\u002Fintroduction-to-ui-libraries","03.javascript\u002F04.react\u002F07.ui-libraries\u002F01.introduction-to-ui-libraries",{"title":2305,"path":2306,"stem":2307},"Філософія shadcn\u002Fui: \"Not a Component Library\"","\u002Fjavascript\u002Freact\u002Fui-libraries\u002Fshadcn-philosophy","03.javascript\u002F04.react\u002F07.ui-libraries\u002F02.shadcn-philosophy",{"title":2309,"path":2310,"stem":2311},"Установка та Налаштування shadcn\u002Fui","\u002Fjavascript\u002Freact\u002Fui-libraries\u002Fshadcn-installation","03.javascript\u002F04.react\u002F07.ui-libraries\u002F03.shadcn-installation",{"title":2313,"path":2314,"stem":2315},"Базові Компоненти shadcn\u002Fui: Фундамент Інтерфейсу","\u002Fjavascript\u002Freact\u002Fui-libraries\u002Fshadcn-components-basics","03.javascript\u002F04.react\u002F07.ui-libraries\u002F04.shadcn-components-basics",{"title":2317,"path":2318,"stem":2319},"Компоненти Форм: Побудова Інтерактивних Form","\u002Fjavascript\u002Freact\u002Fui-libraries\u002Fshadcn-components-forms","03.javascript\u002F04.react\u002F07.ui-libraries\u002F05.shadcn-components-forms",{"title":2321,"path":2322,"stem":2323},"Складні Компоненти: Dialog, Dropdown, Table та Command","\u002Fjavascript\u002Freact\u002Fui-libraries\u002Fshadcn-components-advanced","03.javascript\u002F04.react\u002F07.ui-libraries\u002F06.shadcn-components-advanced",{"title":2325,"icon":2326,"path":2327,"stem":2328,"children":2329,"page":59},"TypeScript","i-devicon-typescript","\u002Fjavascript\u002Ftypescript","03.javascript\u002F05.typescript",[2330,2334,2338,2342,2346,2350,2354,2358],{"title":2331,"path":2332,"stem":2333},"TypeScript: Броня для вашого коду","\u002Fjavascript\u002Ftypescript\u002Fintro-and-basic-types","03.javascript\u002F05.typescript\u002F01.intro-and-basic-types",{"title":2335,"path":2336,"stem":2337},"Майстерність Моделювання Даних: Інтерфейси та Просунуті Типи","\u002Fjavascript\u002Ftypescript\u002Finterfaces-and-advanced-types","03.javascript\u002F05.typescript\u002F02.interfaces-and-advanced-types",{"title":2339,"path":2340,"stem":2341},"Алхімія Типів: Generics та Utility Types","\u002Fjavascript\u002Ftypescript\u002Fgenerics-and-utilities","03.javascript\u002F05.typescript\u002F03.generics-and-utilities",{"title":2343,"path":2344,"stem":2345},"Архітектура та Шаблони: Класи в TypeScript","\u002Fjavascript\u002Ftypescript\u002Fclasses-and-oop","03.javascript\u002F05.typescript\u002F04.classes-and-oop",{"title":2347,"path":2348,"stem":2349},"Продакшн та Екосистема: Advanced Config & Workflow","\u002Fjavascript\u002Ftypescript\u002Fadvanced-patterns-and-config","03.javascript\u002F05.typescript\u002F05.advanced-patterns-and-config",{"title":2351,"path":2352,"stem":2353},"TypeScript у світі React","\u002Fjavascript\u002Ftypescript\u002Freact-basics","03.javascript\u002F05.typescript\u002F06.react-basics",{"title":2355,"path":2356,"stem":2357},"React + TypeScript: Продвинуті патерни","\u002Fjavascript\u002Ftypescript\u002Freact-advanced","03.javascript\u002F05.typescript\u002F07.react-advanced",{"title":2359,"path":2360,"stem":2361},"React + TypeScript: Екосистема та бібліотеки","\u002Fjavascript\u002Ftypescript\u002Freact-ecosystem","03.javascript\u002F05.typescript\u002F08.react-ecosystem",{"title":2363,"path":2364,"stem":2365},"Atomic Design","\u002Fjavascript\u002Fatomic-design","03.javascript\u002F2.atomic-design",{"title":2367,"icon":2368,"path":2369,"stem":2370,"children":2371,"page":59},"Java","i-devicon-java","\u002Fjava","04.java",[2372,2375,2378,2382,2386,2390,2394],{"title":162,"path":2373,"stem":2374},"\u002Fjava\u002Fdata-mapper-part1","04.java\u002F01.data-mapper-part1",{"title":166,"path":2376,"stem":2377},"\u002Fjava\u002Fdata-mapper-part2","04.java\u002F02.data-mapper-part2",{"title":2379,"path":2380,"stem":2381},"Service Layer: Організація бізнес-логіки","\u002Fjava\u002Fservice-layer","04.java\u002F03.service-layer",{"title":2383,"path":2384,"stem":2385},"Rich Domain Model та State Pattern","\u002Fjava\u002Frich-domain-model","04.java\u002F04.rich-domain-model",{"title":2387,"path":2388,"stem":2389},"Патерни для складної бізнес-логіки","\u002Fjava\u002Fbusiness-logic-patterns","04.java\u002F05.business-logic-patterns",{"title":2391,"path":2392,"stem":2393},"Обробка помилок та валідація","\u002Fjava\u002Ferror-handling-validation","04.java\u002F06.error-handling-validation",{"title":2395,"path":2396,"stem":2397,"children":2398,"page":59},"Проектування баз даних","\u002Fjava\u002Fpr2","04.java\u002Fpr2",[2399,2403,2407,2411,2415,2419,2423,2427,2431,2435,2439,2443,2447,2451,2455,2459,2463,2467,2471,2475,2479,2483,2487,2491,2495,2499,2503,2507,2511,2515,2519,2523,2527,2531,2535,2539,2543],{"title":2400,"path":2401,"stem":2402},"Концептуальне моделювання: Мистецтво розуміння предметної області","\u002Fjava\u002Fpr2\u002Fconceptual-modeling","04.java\u002Fpr2\u002F01.conceptual-modeling",{"title":2404,"path":2405,"stem":2406},"Логічне моделювання: Від бізнес-ідей до структур даних","\u002Fjava\u002Fpr2\u002Flogical-modeling","04.java\u002Fpr2\u002F02.logical-modeling",{"title":2408,"path":2409,"stem":2410},"Нормалізація: Гігієна даних та боротьба з аномаліями","\u002Fjava\u002Fpr2\u002Fnormalization","04.java\u002Fpr2\u002F03.normalization",{"title":2412,"path":2413,"stem":2414},"Фізична схема: Від абстракції до DDL","\u002Fjava\u002Fpr2\u002Fphysical-schema","04.java\u002Fpr2\u002F04.physical-schema",{"title":2416,"path":2417,"stem":2418},"Архітектурна класифікація таблиць","\u002Fjava\u002Fpr2\u002Ftable-classification","04.java\u002Fpr2\u002F05.table-classification",{"title":2420,"path":2421,"stem":2422},"Database Migrations: Версіонування схеми з Flyway","\u002Fjava\u002Fpr2\u002Fdatabase-migrations","04.java\u002Fpr2\u002F06.database-migrations",{"title":2424,"path":2425,"stem":2426},"А що, якби це була не реляційна БД?","\u002Fjava\u002Fpr2\u002Fbeyond-relational","04.java\u002Fpr2\u002F07.beyond-relational",{"title":2428,"path":2429,"stem":2430},"Object-Relational Impedance Mismatch: Два світи, що не хочуть дружити","\u002Fjava\u002Fpr2\u002Fimpedance-mismatch","04.java\u002Fpr2\u002F09.impedance-mismatch",{"title":2432,"path":2433,"stem":2434},"JDBC: Перший контакт із базою даних","\u002Fjava\u002Fpr2\u002Fjdbc-fundamentals","04.java\u002Fpr2\u002F10.jdbc-fundamentals",{"title":2436,"path":2437,"stem":2438},"Якість коду: Spotless, SpotBugs та SonarQube","\u002Fjava\u002Fpr2\u002F10a.code-quality","04.java\u002Fpr2\u002F10a.code-quality",{"title":2440,"path":2441,"stem":2442},"Connection Pool: Патерн Object Pool для JDBC-з'єднань","\u002Fjava\u002Fpr2\u002Fconnection-pool","04.java\u002Fpr2\u002F11.connection-pool",{"title":2444,"path":2445,"stem":2446},"Row Data Gateway: Об'єкт як обгортка рядка таблиці","\u002Fjava\u002Fpr2\u002Frow-data-gateway","04.java\u002Fpr2\u002F12.row-data-gateway",{"title":2448,"path":2449,"stem":2450},"Table Data Gateway: Фасад таблиці як архітектурний відступ","\u002Fjava\u002Fpr2\u002Ftable-data-gateway","04.java\u002Fpr2\u002F13.table-data-gateway",{"title":2452,"path":2453,"stem":2454},"Repository + Data Mapper: Правильна шарова архітектура з JDBC","\u002Fjava\u002Fpr2\u002Frepository-data-mapper","04.java\u002Fpr2\u002F14.repository-data-mapper",{"title":2456,"path":2457,"stem":2458},"Identity Map: Кешування сутностей у рамках сесії","\u002Fjava\u002Fpr2\u002Fidentity-map","04.java\u002Fpr2\u002F15.identity-map",{"title":2460,"path":2461,"stem":2462},"Unit of Work: Відстеження змін і координація JDBC-транзакцій","\u002Fjava\u002Fpr2\u002Funit-of-work","04.java\u002Fpr2\u002F16.unit-of-work",{"title":2464,"path":2465,"stem":2466},"Strategy: Замінювані SQL-стратегії для підтримки різних СУБД","\u002Fjava\u002Fpr2\u002Fstrategy-sql","04.java\u002Fpr2\u002F17.strategy-sql",{"title":2468,"path":2469,"stem":2470},"Proxy: Lazy Loading для One-To-Many колекцій","\u002Fjava\u002Fpr2\u002Fproxy-lazy-loading","04.java\u002Fpr2\u002F18.proxy-lazy-loading",{"title":2472,"path":2473,"stem":2474},"Generic Repository через Java Reflection: анотації та динамічний SQL","\u002Fjava\u002Fpr2\u002Fgeneric-repository-reflection","04.java\u002Fpr2\u002F19.generic-repository-reflection",{"title":2476,"path":2477,"stem":2478},"Specification Pattern: Композиція бізнес-правил для складних запитів","\u002Fjava\u002Fpr2\u002Fspecification-pattern","04.java\u002Fpr2\u002F20.specification-pattern",{"title":2480,"path":2481,"stem":2482},"Розширені можливості Specification Pattern: підзапити, агрегації та гібридний підхід","\u002Fjava\u002Fpr2\u002F20a.advanced-specifications","04.java\u002Fpr2\u002F20a.advanced-specifications",{"title":2484,"path":2485,"stem":2486},"Асинхронність у JDBC: Від блокуючих викликів до CompletableFuture","\u002Fjava\u002Fpr2\u002Fasynchronous-jdbc","04.java\u002Fpr2\u002F21.asynchronous-jdbc",{"title":2488,"path":2489,"stem":2490},"Інтеграційне тестування JDBC-репозиторіїв: Embedded H2 та патерн AAA","\u002Fjava\u002Fpr2\u002Fintegration-testing-h2","04.java\u002Fpr2\u002F22.integration-testing-h2",{"title":2492,"path":2493,"stem":2494},"Testcontainers: Тестування з реальною PostgreSQL у Docker-контейнерах","\u002Fjava\u002Fpr2\u002Fintegration-testing-testcontainers","04.java\u002Fpr2\u002F23.integration-testing-testcontainers",{"title":2496,"path":2497,"stem":2498},"Google Guice: Впровадження залежностей у JavaFX-проєкті","\u002Fjava\u002Fpr2\u002Fdependency-injection-guice","04.java\u002Fpr2\u002F24.dependency-injection-guice",{"title":2500,"path":2501,"stem":2502},"JavaFX: Основи побудови графічних інтерфейсів","\u002Fjava\u002Fpr2\u002Fjavafx-fundamentals","04.java\u002Fpr2\u002F25.javafx-fundamentals",{"title":2504,"path":2505,"stem":2506},"Properties та Bindings: Реактивність у JavaFX","\u002Fjava\u002Fpr2\u002Fjavafx-properties-bindings","04.java\u002Fpr2\u002F26.javafx-properties-bindings",{"title":2508,"path":2509,"stem":2510},"MVC vs MVP vs MVVM: Еволюція архітектурних патернів UI","\u002Fjava\u002Fpr2\u002Fui-architecture-patterns","04.java\u002Fpr2\u002F27.ui-architecture-patterns",{"title":2512,"path":2513,"stem":2514},"MVVM на практиці: Побудова ViewModel","\u002Fjava\u002Fpr2\u002Fmvvm-viewmodel-implementation","04.java\u002Fpr2\u002F28.mvvm-viewmodel-implementation",{"title":2516,"path":2517,"stem":2518},"View та Controller: Зв'язування з ViewModel через FXML","\u002Fjava\u002Fpr2\u002Fmvvm-view-controller","04.java\u002Fpr2\u002F29.mvvm-view-controller",{"title":2520,"path":2521,"stem":2522},"Інтеграція MVVM з Guice: Автоматична ін'єкція залежностей","\u002Fjava\u002Fpr2\u002Fmvvm-guice-integration","04.java\u002Fpr2\u002F30.mvvm-guice-integration",{"title":2524,"path":2525,"stem":2526},"Валідація та обробка помилок у MVVM","\u002Fjava\u002Fpr2\u002Fmvvm-validation-error-handling","04.java\u002Fpr2\u002F31.mvvm-validation-error-handling",{"title":2528,"path":2529,"stem":2530},"Навігація та управління екранами у JavaFX MVVM","\u002Fjava\u002Fpr2\u002Fmvvm-navigation-screen-management","04.java\u002Fpr2\u002F32.mvvm-navigation-screen-management",{"title":2532,"path":2533,"stem":2534},"Тестування JavaFX MVVM-додатків","\u002Fjava\u002Fpr2\u002Fmvvm-testing","04.java\u002Fpr2\u002F33.mvvm-testing",{"title":2536,"path":2537,"stem":2538},"Стилізація та теми у JavaFX: CSS та User Experience","\u002Fjava\u002Fpr2\u002Fjavafx-styling-themes","04.java\u002Fpr2\u002F34.javafx-styling-themes",{"title":2540,"path":2541,"stem":2542},"AtlantaFX: Сучасні теми для JavaFX додатків","\u002Fjava\u002Fpr2\u002Fatlantafx-modern-themes","04.java\u002Fpr2\u002F35.atlantafx-modern-themes",{"title":2544,"path":2545,"stem":2546},"Пакування та розповсюдження JavaFX-додатків","\u002Fjava\u002Fpr2\u002Fjar-packaging-distribution","04.java\u002Fpr2\u002F36.jar-packaging-distribution",{"title":2548,"icon":2549,"path":2550,"stem":2551,"children":2552,"page":59},"Python","i-devicon-python","\u002Fpython","05.python",[2553,2557,2560,2564,2568,2572,2576,2580,2584,2588,2592,2596,2600,2604,2608,2669],{"title":2554,"path":2555,"stem":2556},"Модулі, Пакети та Віртуальні Середовища","\u002Fpython\u002Fmodules-packages-venv","05.python\u002F00.modules-packages-venv",{"title":71,"path":2558,"stem":2559},"\u002Fpython\u002Fclasses-objects","05.python\u002F01.classes-objects",{"title":2561,"path":2562,"stem":2563},"Інкапсуляція, Керування Доступом та Властивості","\u002Fpython\u002Fencapsulation","05.python\u002F02.encapsulation",{"title":2565,"path":2566,"stem":2567},"Наслідування, MRO та суперсила super()","\u002Fpython\u002Finheritance-mro","05.python\u002F03.inheritance-mro",{"title":2569,"path":2570,"stem":2571},"Абстракція — ABC проти Статичних Протоколів (PEP 544)","\u002Fpython\u002Fabstraction-protocols","05.python\u002F04.abstraction-protocols",{"title":2573,"path":2574,"stem":2575},"Магічні методи (Dunder) та Емуляція протоколів","\u002Fpython\u002Fdunder-methods","05.python\u002F05.dunder-methods",{"title":2577,"path":2578,"stem":2579},"Декоратори та Керування життєвим циклом методів","\u002Fpython\u002Fdecorators-static-class","05.python\u002F06.decorators-static-class",{"title":2581,"path":2582,"stem":2583},"Дескриптори — Магія доступу до атрибутів","\u002Fpython\u002Fdescriptors","05.python\u002F07.descriptors",{"title":2585,"path":2586,"stem":2587},"Метакласи — Динамічне створення класів під капотом CPython","\u002Fpython\u002Fmetaclasses","05.python\u002F08.metaclasses",{"title":2589,"path":2590,"stem":2591},"Dataclasses, NamedTuple та сучасні контейнери Python","\u002Fpython\u002Fmodern-containers","05.python\u002F09.modern-containers",{"title":2593,"path":2594,"stem":2595},"GIL та модель конкурентності CPython — фундамент перед потоками і процесами","\u002Fpython\u002Fgil-concurrency-intro","05.python\u002F11.gil-concurrency-intro",{"title":2597,"path":2598,"stem":2599},"Threading — конкурентність для I\u002FO-bound задач","\u002Fpython\u002Fthreading","05.python\u002F12.threading",{"title":2601,"path":2602,"stem":2603},"Multiprocessing — справжній паралелізм для CPU-bound задач","\u002Fpython\u002Fmultiprocessing","05.python\u002F13.multiprocessing",{"title":2605,"path":2606,"stem":2607},"asyncio — кооперативна конкурентність та event loop","\u002Fpython\u002Fasyncio","05.python\u002F14.asyncio",{"title":2609,"icon":92,"path":2610,"stem":2611,"children":2612,"page":59},"FastAPI","\u002Fpython\u002Ffastapi","05.python\u002Ffastapi",[2613,2617,2621,2625,2629,2633,2637,2641,2645,2649,2653,2657,2661,2665],{"title":2614,"path":2615,"stem":2616},"Глибокий Typing та Pydantic v2 — від анотацій до валідації","\u002Fpython\u002Ffastapi\u002Ftyping-pydantic","05.python\u002Ffastapi\u002F15.typing-pydantic",{"title":2618,"path":2619,"stem":2620},"WSGI, ASGI та Python Web-екосистема","\u002Fpython\u002Ffastapi\u002Fwsgi-asgi-ecosystem","05.python\u002Ffastapi\u002F16.wsgi-asgi-ecosystem",{"title":2622,"path":2623,"stem":2624},"FastAPI: Перший додаток, Uvicorn та OpenAPI","\u002Fpython\u002Ffastapi\u002Ffastapi-intro","05.python\u002Ffastapi\u002F17.fastapi-intro",{"title":2626,"path":2627,"stem":2628},"Маршрутизація, параметри запитів та APIRouter","\u002Fpython\u002Ffastapi\u002Ffastapi-routing-params","05.python\u002Ffastapi\u002F18.fastapi-routing-params",{"title":2630,"path":2631,"stem":2632},"Pydantic v2 у FastAPI — схеми, валідація та серіалізація","\u002Fpython\u002Ffastapi\u002Ffastapi-pydantic-schemas","05.python\u002Ffastapi\u002F19.fastapi-pydantic-schemas",{"title":2634,"path":2635,"stem":2636},"Dependency Injection — серце архітектури FastAPI","\u002Fpython\u002Ffastapi\u002Ffastapi-dependency-injection","05.python\u002Ffastapi\u002F20.fastapi-dependency-injection",{"title":2638,"path":2639,"stem":2640},"Обробка помилок, Middleware та CORS у FastAPI","\u002Fpython\u002Ffastapi\u002Ffastapi-errors-middleware","05.python\u002Ffastapi\u002F21.fastapi-errors-middleware",{"title":2642,"path":2643,"stem":2644},"SQLAlchemy 2.0 — ORM, Core та Async Engine","\u002Fpython\u002Ffastapi\u002Fsqlalchemy-orm","05.python\u002Ffastapi\u002F22.sqlalchemy-orm",{"title":2646,"path":2647,"stem":2648},"Alembic — міграції бази даних","\u002Fpython\u002Ffastapi\u002Falembic-migrations","05.python\u002Ffastapi\u002F23.alembic-migrations",{"title":2650,"path":2651,"stem":2652},"Repository Pattern та Clean Architecture у FastAPI","\u002Fpython\u002Ffastapi\u002Ffastapi-repository-clean-arch","05.python\u002Ffastapi\u002F24.fastapi-repository-clean-arch",{"title":2654,"path":2655,"stem":2656},"Автентифікація та авторизація — JWT, OAuth2, RBAC","\u002Fpython\u002Ffastapi\u002Ffastapi-auth-security","05.python\u002Ffastapi\u002F25.fastapi-auth-security",{"title":2658,"path":2659,"stem":2660},"Тестування FastAPI — pytest, httpx, fixtures","\u002Fpython\u002Ffastapi\u002Ffastapi-testing","05.python\u002Ffastapi\u002F26.fastapi-testing",{"title":2662,"path":2663,"stem":2664},"Async на повну — Background Tasks, WebSockets, SSE та Celery","\u002Fpython\u002Ffastapi\u002Ffastapi-async-advanced","05.python\u002Ffastapi\u002F27.fastapi-async-advanced",{"title":2666,"path":2667,"stem":2668},"Deployment — Docker, CI\u002FCD, Monitoring","\u002Fpython\u002Ffastapi\u002Ffastapi-deployment","05.python\u002Ffastapi\u002F28.fastapi-deployment",{"title":2670,"path":2671,"stem":2672},"[object Object]","\u002Fpython\u002Foop-plan","05.python\u002Foop-plan",{"title":2674,"icon":2675,"path":2676,"stem":2677,"children":2678,"page":59},"Бази даних","i-lucide-database","\u002Fdatabases","06.databases",[2679,2709,2732,2769,2798,2816,2850,2862,2871],{"title":2680,"icon":2681,"path":2682,"stem":2683,"children":2684,"page":59},"Intro","i-lucide-play","\u002Fdatabases\u002Fintro","06.databases\u002F01.intro",[2685,2689,2693,2697,2701,2705],{"title":2686,"path":2687,"stem":2688},"Введення в теорію баз даних","\u002Fdatabases\u002Fintro\u002Fintroduction-to-databases","06.databases\u002F01.intro\u002F01.introduction-to-databases",{"title":2690,"path":2691,"stem":2692},"Реляційна модель даних","\u002Fdatabases\u002Fintro\u002Frelational-model-theory","06.databases\u002F01.intro\u002F02.relational-model-theory",{"title":2694,"path":2695,"stem":2696},"ER-моделювання","\u002Fdatabases\u002Fintro\u002Fer-modeling","06.databases\u002F01.intro\u002F03.er-modeling",{"title":2698,"path":2699,"stem":2700},"Логічне проектування БД","\u002Fdatabases\u002Fintro\u002Flogical-schema","06.databases\u002F01.intro\u002F04.logical-schema",{"title":2702,"path":2703,"stem":2704},"Класифікація таблиць","\u002Fdatabases\u002Fintro\u002Ftable-classification","06.databases\u002F01.intro\u002F05.table-classification",{"title":2706,"path":2707,"stem":2708},"PlantUML для баз даних","\u002Fdatabases\u002Fintro\u002Fplantuml-diagrams","06.databases\u002F01.intro\u002F06.plantuml-diagrams",{"title":2710,"icon":2675,"path":2711,"stem":2712,"children":2713,"page":59},"MS SQL Server Start","\u002Fdatabases\u002Fms-sql-server-start","06.databases\u002F02.ms-sql-server-start",[2714,2718,2724,2728],{"title":2715,"path":2716,"stem":2717},"Типи даних у MS SQL Server","\u002Fdatabases\u002Fms-sql-server-start\u002Fdata-types","06.databases\u002F02.ms-sql-server-start\u002F01.data-types",{"title":2719,"path":2720,"stem":2721,"children":2722},"Індекси у MS SQL Server","\u002Fdatabases\u002Fms-sql-server-start\u002Fsql-indexes","06.databases\u002F02.ms-sql-server-start\u002F02.sql-indexes",[2723],{"title":2719,"path":2720,"stem":2721},{"title":2725,"path":2726,"stem":2727},"Системні бази даних MS SQL Server","\u002Fdatabases\u002Fms-sql-server-start\u002Fsystem-databases","06.databases\u002F02.ms-sql-server-start\u002F03.system-databases",{"title":2729,"path":2730,"stem":2731},"Огляд мови SQL та запитів","\u002Fdatabases\u002Fms-sql-server-start\u002Fsql-queries-overview","06.databases\u002F02.ms-sql-server-start\u002F04.sql-queries-overview",{"title":2733,"icon":2675,"path":2734,"stem":2735,"children":2736,"page":59},"SQL","\u002Fdatabases\u002Fsql","06.databases\u002F03.sql",[2737,2741,2745,2749,2753,2757,2761,2765],{"title":2738,"path":2739,"stem":2740},"Налаштування демонстраційної бази даних","\u002Fdatabases\u002Fsql\u002Fsample-database-setup","06.databases\u002F03.sql\u002F00.sample-database-setup",{"title":2742,"path":2743,"stem":2744},"DDL - Створення таблиць (CREATE TABLE)","\u002Fdatabases\u002Fsql\u002Fddl-create-table","06.databases\u002F03.sql\u002F01.ddl-create-table",{"title":2746,"path":2747,"stem":2748},"DDL - Зміна та видалення таблиць (ALTER, DROP)","\u002Fdatabases\u002Fsql\u002Fddl-alter-drop-table","06.databases\u002F03.sql\u002F02.ddl-alter-drop-table",{"title":2750,"path":2751,"stem":2752},"SELECT запити - Основи","\u002Fdatabases\u002Fsql\u002Fselect-queries-fundamentals","06.databases\u002F03.sql\u002F03.select-queries-fundamentals",{"title":2754,"path":2755,"stem":2756},"SELECT запити - Розширені можливості","\u002Fdatabases\u002Fsql\u002Fselect-queries-advanced","06.databases\u002F03.sql\u002F04.select-queries-advanced",{"title":2758,"path":2759,"stem":2760},"INSERT запити - Додавання даних","\u002Fdatabases\u002Fsql\u002Finsert-queries","06.databases\u002F03.sql\u002F05.insert-queries",{"title":2762,"path":2763,"stem":2764},"UPDATE та DELETE запити","\u002Fdatabases\u002Fsql\u002Fupdate-delete-queries","06.databases\u002F03.sql\u002F06.update-delete-queries",{"title":2766,"path":2767,"stem":2768},"Транзакції в SQL","\u002Fdatabases\u002Fsql\u002Ftransactions","06.databases\u002F03.sql\u002F07.transactions",{"title":2770,"icon":2675,"path":2771,"stem":2772,"children":2773,"page":59},"Multi Table Databases","\u002Fdatabases\u002Fmulti-table-databases","06.databases\u002F04.multi-table-databases",[2774,2778,2782,2786,2790,2794],{"title":2775,"path":2776,"stem":2777},"Зв'язки та нормалізація БД","\u002Fdatabases\u002Fmulti-table-databases\u002Frelationships-and-normalization","06.databases\u002F04.multi-table-databases\u002F00.relationships-and-normalization",{"title":2779,"path":2780,"stem":2781},"INNER JOIN - З'єднання таблиць","\u002Fdatabases\u002Fmulti-table-databases\u002Finner-join","06.databases\u002F04.multi-table-databases\u002F01.inner-join",{"title":2783,"path":2784,"stem":2785},"OUTER JOINs - LEFT, RIGHT, FULL","\u002Fdatabases\u002Fmulti-table-databases\u002Fouter-joins","06.databases\u002F04.multi-table-databases\u002F02.outer-joins",{"title":2787,"path":2788,"stem":2789},"CROSS та SELF JOINs","\u002Fdatabases\u002Fmulti-table-databases\u002Fcross-self-joins","06.databases\u002F04.multi-table-databases\u002F03.cross-self-joins",{"title":2791,"path":2792,"stem":2793},"Підзапити (Subqueries)","\u002Fdatabases\u002Fmulti-table-databases\u002Fsubqueries","06.databases\u002F04.multi-table-databases\u002F04.subqueries",{"title":2795,"path":2796,"stem":2797},"Агрегації з JOIN","\u002Fdatabases\u002Fmulti-table-databases\u002Faggregations-with-joins","06.databases\u002F04.multi-table-databases\u002F05.aggregations-with-joins",{"title":2799,"icon":2800,"path":2801,"stem":2802,"children":2803,"page":59},"Aggregate Functions","i-lucide-calculator","\u002Fdatabases\u002Faggregate-functions","06.databases\u002F05.aggregate-functions",[2804,2808,2812],{"title":2805,"path":2806,"stem":2807},"Функції агрегування в MS SQL Server","\u002Fdatabases\u002Faggregate-functions\u002Fintroduction-aggregate-functions","06.databases\u002F05.aggregate-functions\u002F01.introduction-aggregate-functions",{"title":2809,"path":2810,"stem":2811},"Групування даних в MS SQL Server","\u002Fdatabases\u002Faggregate-functions\u002Fgrouping-data","06.databases\u002F05.aggregate-functions\u002F02.grouping-data",{"title":2813,"path":2814,"stem":2815},"Підзапити з агрегатними функціями","\u002Fdatabases\u002Faggregate-functions\u002Fsubqueries-aggregates","06.databases\u002F05.aggregate-functions\u002F03.subqueries-aggregates",{"title":2817,"icon":2818,"path":2819,"stem":2820,"children":2821,"page":59},"Тригери та зберігаємі процедури","i-lucide-database-zap","\u002Fdatabases\u002Ftriggers-stored-procedures","06.databases\u002F07.triggers-stored-procedures",[2822,2826,2830,2834,2838,2842,2846],{"title":2823,"path":2824,"stem":2825},"DML-тригери","\u002Fdatabases\u002Ftriggers-stored-procedures\u002Fdml-triggers","06.databases\u002F07.triggers-stored-procedures\u002F01.dml-triggers",{"title":2827,"path":2828,"stem":2829},"DDL-тригери","\u002Fdatabases\u002Ftriggers-stored-procedures\u002Fddl-triggers","06.databases\u002F07.triggers-stored-procedures\u002F02.ddl-triggers",{"title":2831,"path":2832,"stem":2833},"Transact-SQL розширення","\u002Fdatabases\u002Ftriggers-stored-procedures\u002Ftransact-sql-extensions","06.databases\u002F07.triggers-stored-procedures\u002F03.transact-sql-extensions",{"title":2835,"path":2836,"stem":2837},"Транзакції","\u002Fdatabases\u002Ftriggers-stored-procedures\u002Ftransactions","06.databases\u002F07.triggers-stored-procedures\u002F04.transactions",{"title":2839,"path":2840,"stem":2841},"Зберігаємі процедури","\u002Fdatabases\u002Ftriggers-stored-procedures\u002Fstored-procedures","06.databases\u002F07.triggers-stored-procedures\u002F05.stored-procedures",{"title":2843,"path":2844,"stem":2845},"Користувацькі функції","\u002Fdatabases\u002Ftriggers-stored-procedures\u002Fuser-defined-functions","06.databases\u002F07.triggers-stored-procedures\u002F06.user-defined-functions",{"title":2847,"path":2848,"stem":2849},"Безпека баз даних","\u002Fdatabases\u002Ftriggers-stored-procedures\u002Fsecurity","06.databases\u002F07.triggers-stored-procedures\u002F08.security",{"title":2847,"icon":793,"path":2851,"stem":2852,"children":2853,"page":59},"\u002Fdatabases\u002Fsecurity","06.databases\u002F08.security",[2854,2858],{"title":2855,"path":2856,"stem":2857},"Вступ до безпеки баз даних","\u002Fdatabases\u002Fsecurity\u002Fintroduction","06.databases\u002F08.security\u002F01.introduction",{"title":2859,"path":2860,"stem":2861},"Системні представлення та метадані","\u002Fdatabases\u002Fsecurity\u002Fsystem-views","06.databases\u002F08.security\u002F02.system-views",{"title":2863,"icon":2864,"path":2865,"stem":2866,"children":2867,"page":59},"Резервне копіювання та відновлення","i-lucide-database-backup","\u002Fdatabases\u002Fbackup-recovery","06.databases\u002F09.backup-recovery",[2868],{"title":2863,"path":2869,"stem":2870},"\u002Fdatabases\u002Fbackup-recovery\u002Fbackup-restore","06.databases\u002F09.backup-recovery\u002F01.backup-restore",{"title":2872,"icon":2873,"path":2874,"stem":2875,"children":2876,"page":59},"Повнотекстовий пошук","i-lucide-search","\u002Fdatabases\u002Ffull-text-search","06.databases\u002F10.full-text-search",[2877],{"title":2872,"path":2878,"stem":2879},"\u002Fdatabases\u002Ffull-text-search\u002Ffull-text-search","06.databases\u002F10.full-text-search\u002F01.full-text-search",{"title":2881,"icon":2882,"path":2883,"stem":2884,"children":2885,"page":59},"Tools","i-lucide-wrench","\u002Ftools","07.tools",[2886,2962],{"title":2887,"icon":2888,"path":2889,"stem":2890,"children":2891},"Docker","i-simple-icons-docker","\u002Ftools\u002Fdocker","07.tools\u002F01.docker\u002Findex",[2892,2894,2898,2902,2906,2910,2914,2918,2922,2926,2930,2934,2938,2942,2946,2950,2954,2958],{"title":2893,"path":2889,"stem":2890},"Docker: від нуля до production",{"title":2895,"path":2896,"stem":2897},"Контейнеризація — від проблеми до рішення","\u002Ftools\u002Fdocker\u002Fcontainerization-concept","07.tools\u002F01.docker\u002F01.containerization-concept",{"title":2899,"path":2900,"stem":2901},"Docker — що це і навіщо?","\u002Ftools\u002Fdocker\u002Fdocker-what-and-why","07.tools\u002F01.docker\u002F02.docker-what-and-why",{"title":2903,"path":2904,"stem":2905},"Архітектура Docker Engine","\u002Ftools\u002Fdocker\u002Fdocker-architecture","07.tools\u002F01.docker\u002F03.docker-architecture",{"title":2907,"path":2908,"stem":2909},"Встановлення Docker","\u002Ftools\u002Fdocker\u002Finstallation","07.tools\u002F01.docker\u002F04.installation",{"title":2911,"path":2912,"stem":2913},"Перший контейнер — docker run","\u002Ftools\u002Fdocker\u002Ffirst-container","07.tools\u002F01.docker\u002F05.first-container",{"title":2915,"path":2916,"stem":2917},"Життєвий цикл контейнера","\u002Ftools\u002Fdocker\u002Fcontainer-lifecycle","07.tools\u002F01.docker\u002F06.container-lifecycle",{"title":2919,"path":2920,"stem":2921},"Docker Images — фундаментальні концепції","\u002Ftools\u002Fdocker\u002Fdocker-images-fundamentals","07.tools\u002F01.docker\u002F07.docker-images-fundamentals",{"title":2923,"path":2924,"stem":2925},"Dockerfile — основи","\u002Ftools\u002Fdocker\u002Fdockerfile-basics","07.tools\u002F01.docker\u002F08.dockerfile-basics",{"title":2927,"path":2928,"stem":2929},"Dockerfile — просунуті техніки","\u002Ftools\u002Fdocker\u002Fdockerfile-advanced","07.tools\u002F01.docker\u002F09.dockerfile-advanced",{"title":2931,"path":2932,"stem":2933},"Build Context та кешування шарів","\u002Ftools\u002Fdocker\u002Fbuild-context-and-cache","07.tools\u002F01.docker\u002F10.build-context-and-cache",{"title":2935,"path":2936,"stem":2937},"Реєстри Docker-образів","\u002Ftools\u002Fdocker\u002Fimage-registries","07.tools\u002F01.docker\u002F11.image-registries",{"title":2939,"path":2940,"stem":2941},"Контейнеризація .NET додатків","\u002Ftools\u002Fdocker\u002Fdotnet-containerization","07.tools\u002F01.docker\u002F12.dotnet-containerization",{"title":2943,"path":2944,"stem":2945},"Томи та збереження даних","\u002Ftools\u002Fdocker\u002Fvolumes-and-data","07.tools\u002F01.docker\u002F13.volumes-and-data",{"title":2947,"path":2948,"stem":2949},"Основи мережі в Docker","\u002Ftools\u002Fdocker\u002Fnetworking-basics","07.tools\u002F01.docker\u002F14.networking-basics",{"title":2951,"path":2952,"stem":2953},"Змінні оточення та конфігурація","\u002Ftools\u002Fdocker\u002Fenvironment-and-configuration","07.tools\u002F01.docker\u002F15.environment-and-configuration",{"title":2955,"path":2956,"stem":2957},"Docker Compose — оркестрація контейнерів","\u002Ftools\u002Fdocker\u002Fdocker-compose-basics","07.tools\u002F01.docker\u002F16.docker-compose-basics",{"title":2959,"path":2960,"stem":2961},"Docker Compose — Multi-Service застосунки","\u002Ftools\u002Fdocker\u002Fcompose-multi-service","07.tools\u002F01.docker\u002F17.compose-multi-service",{"title":2963,"icon":2964,"path":2965,"stem":2966,"children":2967},"Kubernetes","simple-icons:kubernetes","\u002Ftools\u002Fkubernetes","07.tools\u002F02.kubernetes\u002Findex",[2968,2970,2974,2978,2982,2986,2990,2994,2998],{"title":2969,"path":2965,"stem":2966},"Kubernetes: від розробки до production",{"title":2971,"path":2972,"stem":2973},"Kubernetes — коли Docker Compose більше не вистачає","\u002Ftools\u002Fkubernetes\u002Fwhy-kubernetes","07.tools\u002F02.kubernetes\u002F01.why-kubernetes",{"title":2975,"path":2976,"stem":2977},"Архітектура Kubernetes — анатомія кластера","\u002Ftools\u002Fkubernetes\u002Fkubernetes-architecture","07.tools\u002F02.kubernetes\u002F02.kubernetes-architecture",{"title":2979,"path":2980,"stem":2981},"Локальне середовище — minikube, kind та k3s","\u002Ftools\u002Fkubernetes\u002Flocal-environment","07.tools\u002F02.kubernetes\u002F03.local-environment",{"title":2983,"path":2984,"stem":2985},"Pod — атомарна одиниця Kubernetes","\u002Ftools\u002Fkubernetes\u002Fpods-and-containers","07.tools\u002F02.kubernetes\u002F04.pods-and-containers",{"title":2987,"path":2988,"stem":2989},"Патерни використання Pod","\u002Ftools\u002Fkubernetes\u002Fpod-patterns","07.tools\u002F02.kubernetes\u002F05.pod-patterns",{"title":2991,"path":2992,"stem":2993},"Deployment — декларативне управління Pod","\u002Ftools\u002Fkubernetes\u002Fdeployment-basics","07.tools\u002F02.kubernetes\u002F06.deployment-basics",{"title":2995,"path":2996,"stem":2997},"Rolling Updates та управління життєвим циклом Deployment","\u002Ftools\u002Fkubernetes\u002Fdeployment-rolling-updates","07.tools\u002F02.kubernetes\u002F07.deployment-rolling-updates",{"title":2999,"path":3000,"stem":3001},"Service — мережева абстракція для Pod","\u002Ftools\u002Fkubernetes\u002Fservices-networking","07.tools\u002F02.kubernetes\u002F08.services-networking",{"title":3003,"icon":3004,"path":3005,"stem":3006,"children":3007,"page":59},"Software Engineering","i-lucide-code-2","\u002Fsoftware-engineering","09.software-engineering",[3008,3012,3016,3020,3024,3028,3032,3036,3040,3044,3048],{"title":3009,"path":3010,"stem":3011},"1. Аналіз предметної області. Експертні знання та складність","\u002Fsoftware-engineering\u002Fintro-subdomains","09.software-engineering\u002F01.intro-subdomains",{"title":3013,"path":3014,"stem":3015},"2. Обмежені контексти. Інтеграція обмежених контекстів","\u002Fsoftware-engineering\u002Fintegrating-limited-contexts","09.software-engineering\u002F02.integrating-limited-contexts",{"title":3017,"path":3018,"stem":3019},"3. Реалізація простої бізнес-логіки","\u002Fsoftware-engineering\u002Fsimple","09.software-engineering\u002F03.simple",{"title":3021,"path":3022,"stem":3023},"4. Опрацювання складної бізнес-логіки","\u002Fsoftware-engineering\u002Fcomplex-business-logic","09.software-engineering\u002F04.complex-business-logic",{"title":3025,"path":3026,"stem":3027},"5. Моделювання фактора часу. Подієво-орієнтована архітектура.","\u002Fsoftware-engineering\u002Fmodelling-the-time-factor","09.software-engineering\u002F05.modelling-the-time-factor",{"title":3029,"path":3030,"stem":3031},"6. Архітектурні патерни","\u002Fsoftware-engineering\u002Farchitectural-patterns","09.software-engineering\u002F06.architectural-patterns",{"title":3033,"path":3034,"stem":3035},"Паттерни взаємодії","\u002Fsoftware-engineering\u002Fpatterns-of-interaction","09.software-engineering\u002F07.patterns-of-interaction",{"title":3037,"path":3038,"stem":3039},"Евристика проєктування","\u002Fsoftware-engineering\u002Fdesign-heuristics","09.software-engineering\u002F08.design-heuristics",{"title":3041,"path":3042,"stem":3043},"Еволюція проєктних рішень","\u002Fsoftware-engineering\u002Fevolution-of-design-solutions","09.software-engineering\u002F09.evolution-of-design-solutions",{"title":3045,"path":3046,"stem":3047},"EventStorming","\u002Fsoftware-engineering\u002Feventstorming","09.software-engineering\u002F10.eventstorming",{"title":3049,"path":3050,"stem":3051},"DDD на практиці","\u002Fsoftware-engineering\u002Fddd-in-practice","09.software-engineering\u002F11.ddd-in-practice",{"title":3053,"icon":943,"path":3054,"stem":3055,"children":3056,"page":59},"DDD","\u002Fddd","10.ddd",[3057,3061,3065,3069,3073,3077,3081,3085,3089,3093,3097,3101,3105],{"title":3058,"path":3059,"stem":3060},"Аналіз предметної області","\u002Fddd\u002Fdomain-analysis","10.ddd\u002F01.domain-analysis",{"title":3062,"path":3063,"stem":3064},"Експертні знання про предметну область","\u002Fddd\u002Fdomain-expert-knowledge","10.ddd\u002F02.domain-expert-knowledge",{"title":3066,"path":3067,"stem":3068},"Як осмислити складність предметної області","\u002Fddd\u002Fmanaging-domain-complexity","10.ddd\u002F03.managing-domain-complexity",{"title":3070,"path":3071,"stem":3072},"Інтеграція обмежених контекстів","\u002Fddd\u002Fbounded-context-integration","10.ddd\u002F04.bounded-context-integration",{"title":3074,"path":3075,"stem":3076},"Реалізація простої бізнес-логіки","\u002Fddd\u002Fsimple-business-logic","10.ddd\u002F05.simple-business-logic",{"title":3078,"path":3079,"stem":3080},"Обробка складної бізнес-логіки","\u002Fddd\u002Fcomplex-business-logic","10.ddd\u002F06.complex-business-logic",{"title":3082,"path":3083,"stem":3084},"Моделювання фактора часу","\u002Fddd\u002Ftime-modeling","10.ddd\u002F07.time-modeling",{"title":3086,"path":3087,"stem":3088},"Глава 8. Архітектурні Патерни","\u002Fddd\u002Farchitectural-patterns","10.ddd\u002F08.architectural-patterns",{"title":3090,"path":3091,"stem":3092},"Глава 9. Патерни Взаємодії","\u002Fddd\u002Finteraction-patterns","10.ddd\u002F09.interaction-patterns",{"title":3094,"path":3095,"stem":3096},"Глава 10. Проектні Евристики","\u002Fddd\u002Fdesign-heuristics","10.ddd\u002F10.design-heuristics",{"title":3098,"path":3099,"stem":3100},"Глава 11. Еволюція Проектних Рішень","\u002Fddd\u002Fevolution-of-design-decisions","10.ddd\u002F11.evolution-of-design-decisions",{"title":3102,"path":3103,"stem":3104},"Глава 12. EventStorming","\u002Fddd\u002Fevent-storming","10.ddd\u002F12.event-storming",{"title":3106,"path":3107,"stem":3108},"Глава 13. DDD на Практиці","\u002Fddd\u002Fddd-in-practice","10.ddd\u002F13.ddd-in-practice",{"title":3110,"icon":3111,"path":3112,"stem":3113,"children":3114,"page":59},"Media Streaming","i-lucide-video","\u002Fmedia-streaming","11.media-streaming",[3115,3119,3123,3127,3131,3135,3139],{"title":3116,"path":3117,"stem":3118},"01. Магія Стрімінгу: Що відбувається, коли ви натискаєте \"Play\"","\u002Fmedia-streaming\u002Fintroduction","11.media-streaming\u002F01.introduction",{"title":3120,"path":3121,"stem":3122},"02. Анатомія Медіа: Кодеки, Контейнери та Стиснення","\u002Fmedia-streaming\u002Faudio-video-anatomy","11.media-streaming\u002F02.audio-video-anatomy",{"title":3124,"path":3125,"stem":3126},"03. The Gym: FFmpeg Deep Dive","\u002Fmedia-streaming\u002Fffmpeg-gym","11.media-streaming\u002F03.ffmpeg-gym",{"title":3128,"path":3129,"stem":3130},"04. HLS Protocol: HTTP Live Streaming у Деталях","\u002Fmedia-streaming\u002Fhls-protocol","11.media-streaming\u002F04.hls-protocol",{"title":3132,"path":3133,"stem":3134},"05. DASH Protocol: Відкритий Стандарт","\u002Fmedia-streaming\u002Fdash-protocol","11.media-streaming\u002F05.dash-protocol",{"title":3136,"path":3137,"stem":3138},"06. Масштабування: CDN та Adaptive Bitrate","\u002Fmedia-streaming\u002Fcdn-and-adaptive-bitrate","11.media-streaming\u002F06.cdn-and-adaptive-bitrate",{"title":3140,"path":3141,"stem":3142},"07. Війна із Затримкою (Latency)","\u002Fmedia-streaming\u002Frealtime-latency","11.media-streaming\u002F07.realtime-latency",{"title":3144,"icon":3145,"path":3146,"stem":3147,"children":3148,"page":59},"HTML & CSS","i-devicon-html5","\u002Fhtml-css","12.html-css",[3149,3153,3157,3161,3165,3169,3173,3177,3181,3185,3189,3193,3197,3201,3205,3209,3213,3217,3221,3225,3229,3233,3237,3241,3245,3249,3253,3257,3261,3265],{"title":3150,"path":3151,"stem":3152},"Вступ до HTML. Структура документа","\u002Fhtml-css\u002Fintro-html-structure","12.html-css\u002F01.intro-html-structure",{"title":3154,"path":3155,"stem":3156},"Форматування тексту в HTML","\u002Fhtml-css\u002Fhtml-text-formatting","12.html-css\u002F02.html-text-formatting",{"title":3158,"path":3159,"stem":3160},"Посилання та зображення в HTML","\u002Fhtml-css\u002Fhtml-links-images","12.html-css\u002F03.html-links-images",{"title":3162,"path":3163,"stem":3164},"Списки та таблиці в HTML","\u002Fhtml-css\u002Fhtml-lists-tables","12.html-css\u002F04.html-lists-tables",{"title":3166,"path":3167,"stem":3168},"Форми в HTML","\u002Fhtml-css\u002Fhtml-forms","12.html-css\u002F05.html-forms",{"title":3170,"path":3171,"stem":3172},"Семантичні елементи HTML5","\u002Fhtml-css\u002Fhtml-semantic-elements","12.html-css\u002F06.html-semantic-elements",{"title":3174,"path":3175,"stem":3176},"Мультимедіа та розширені елементи HTML","\u002Fhtml-css\u002Fhtml-multimedia-advanced","12.html-css\u002F07.html-multimedia-advanced",{"title":3178,"path":3179,"stem":3180},"Мікророзмітка та SEO в HTML","\u002Fhtml-css\u002Fhtml-microdata-seo","12.html-css\u002F08.html-microdata-seo",{"title":3182,"path":3183,"stem":3184},"Вступ до CSS. Селектори та специфічність","\u002Fhtml-css\u002Fcss-intro-selectors","12.html-css\u002F09.css-intro-selectors",{"title":3186,"path":3187,"stem":3188},"Блокова модель CSS. Відступи. Box Sizing","\u002Fhtml-css\u002Fcss-box-model","12.html-css\u002F10.css-box-model",{"title":3190,"path":3191,"stem":3192},"Розміри у CSS: повний довідник одиниць і ключових слів","\u002Fhtml-css\u002F10a.css-sizing","12.html-css\u002F10a.css-sizing",{"title":3194,"path":3195,"stem":3196},"Типографіка в CSS. Шрифти та текст","\u002Fhtml-css\u002Fcss-typography","12.html-css\u002F11.css-typography",{"title":3198,"path":3199,"stem":3200},"Кольори та фони в CSS","\u002Fhtml-css\u002Fcss-colors-backgrounds","12.html-css\u002F12.css-colors-backgrounds",{"title":3202,"path":3203,"stem":3204},"Тіні та фільтри в CSS","\u002Fhtml-css\u002F12b.css-shadows-filters","12.html-css\u002F12b.css-shadows-filters",{"title":3206,"path":3207,"stem":3208},"CSS Flexbox: Фундамент гнучких макетів","\u002Fhtml-css\u002Fcss-flexbox-fundamentals","12.html-css\u002F13.css-flexbox-fundamentals",{"title":3210,"path":3211,"stem":3212},"CSS Flexbox: Вирівнювання та Позиціонування","\u002Fhtml-css\u002Fcss-flexbox-alignment-sizing-and-patterns","12.html-css\u002F14.css-flexbox-alignment-sizing-and-patterns",{"title":3214,"path":3215,"stem":3216},"CSS Grid. Двовимірний макет. Частина 1","\u002Fhtml-css\u002Fcss-layout-grid","12.html-css\u002F15.css-layout-grid",{"title":3218,"path":3219,"stem":3220},"CSS Grid. Двовимірний макет. Частина 2","\u002Fhtml-css\u002Fcss-layout-grid-advanced","12.html-css\u002F16.css-layout-grid-advanced",{"title":3222,"path":3223,"stem":3224},"Позиціонування в CSS. Z-index. Stacking Context","\u002Fhtml-css\u002Fcss-positioning","12.html-css\u002F17.css-positioning",{"title":3226,"path":3227,"stem":3228},"CSS Анімації та Переходи","\u002Fhtml-css\u002Fcss-animations-transitions","12.html-css\u002F18.css-animations-transitions",{"title":3230,"path":3231,"stem":3232},"Адаптивний дизайн. Media Queries. Частина 1","\u002Fhtml-css\u002Fcss-responsive-media-queries","12.html-css\u002F19.css-responsive-media-queries",{"title":3234,"path":3235,"stem":3236},"Адаптивний дизайн. Частина 2: clamp(), Container Queries, @layer","\u002Fhtml-css\u002Fcss-responsive-advanced","12.html-css\u002F20.css-responsive-advanced",{"title":3238,"path":3239,"stem":3240},"CSS Custom Properties. Методології. Сучасний CSS","\u002Fhtml-css\u002Fcss-variables-methodologies","12.html-css\u002F21.css-variables-methodologies",{"title":3242,"path":3243,"stem":3244},"Сучасний CSS 2023–2025: Нові можливості","\u002Fhtml-css\u002Fcss-modern-features","12.html-css\u002F22.css-modern-features",{"title":3246,"path":3247,"stem":3248},"CSS Nesting, @layer, @scope та @property: нативний препроцесор","\u002Fhtml-css\u002F22a.css-nesting-modern-syntax","12.html-css\u002F22a.css-nesting-modern-syntax",{"title":3250,"path":3251,"stem":3252},"CSS для форм та інтерактивних станів","\u002Fhtml-css\u002Fcss-forms-interactive-states","12.html-css\u002F23.css-forms-interactive-states",{"title":3254,"path":3255,"stem":3256},"Доступність у CSS (CSS Accessibility)","\u002Fhtml-css\u002Fcss-accessibility","12.html-css\u002F24.css-accessibility",{"title":3258,"path":3259,"stem":3260},"CSS-функції та сучасні sizing primitives","\u002Fhtml-css\u002Fcss-functions-sizing","12.html-css\u002F25.css-functions-sizing",{"title":3262,"path":3263,"stem":3264},"Rendering Pipeline і CSS Performance","\u002Fhtml-css\u002Fcss-rendering-performance","12.html-css\u002F26.css-rendering-performance",{"title":3266,"path":3267,"stem":3268},"CSS Best Practices: типові ситуації та правильні рішення","\u002Fhtml-css\u002Fcss-best-practices","12.html-css\u002F27.css-best-practices",{"title":3270,"path":3271,"stem":3272,"children":3273,"page":59},"AWS","\u002Faws","13.aws",[3274,3278,3282,3286,3290,3294,3298,3302,3306,3310,3314,3318,3322,3326,3330,3334,3338,3342],{"title":3275,"path":3276,"stem":3277},"Реєстрація AWS акаунту та студентські програми","\u002Faws\u002Faccount-registration","13.aws\u002F00.account-registration",{"title":3279,"path":3280,"stem":3281},"Вступ до хмарних обчислень та AWS","\u002Faws\u002Fintroduction-to-cloud","13.aws\u002F01.introduction-to-cloud",{"title":3283,"path":3284,"stem":3285},"AWS IAM — Identity and Access Management","\u002Faws\u002Fiam","13.aws\u002F02.iam",{"title":3287,"path":3288,"stem":3289},"AWS IAM CLI — Довідник команд","\u002Faws\u002F02a.iam-doc","13.aws\u002F02a.iam-doc",{"title":3291,"path":3292,"stem":3293},"Docker та контейнеризація в AWS — ECR, ECS та Fargate","\u002Faws\u002Fdocker-ecs","13.aws\u002F03.docker-ecs",{"title":3295,"path":3296,"stem":3297},"AWS ECR \u002F ECS CLI — Довідник команд","\u002Faws\u002F03a.docker-ecs-doc","13.aws\u002F03a.docker-ecs-doc",{"title":3299,"path":3300,"stem":3301},"Amazon EC2 — Elastic Compute Cloud","\u002Faws\u002Fec2","13.aws\u002F04.ec2",{"title":3303,"path":3304,"stem":3305},"AWS EC2 CLI — Довідник команд","\u002Faws\u002F04a.ec2-doc","13.aws\u002F04a.ec2-doc",{"title":3307,"path":3308,"stem":3309},"Elastic Load Balancing та Auto Scaling","\u002Faws\u002Falb-asg","13.aws\u002F05.alb-asg",{"title":3311,"path":3312,"stem":3313},"Amazon S3 — Simple Storage Service","\u002Faws\u002Fs3","13.aws\u002F06.s3",{"title":3315,"path":3316,"stem":3317},"Amazon CloudFront — Content Delivery Network","\u002Faws\u002Fcloudfront","13.aws\u002F07.cloudfront",{"title":3319,"path":3320,"stem":3321},"Amazon RDS — Relational Database Service","\u002Faws\u002Frds","13.aws\u002F08.rds",{"title":3323,"path":3324,"stem":3325},"Amazon DynamoDB — NoSQL Database","\u002Faws\u002Fdynamodb","13.aws\u002F09.dynamodb",{"title":3327,"path":3328,"stem":3329},"AWS Lambda та Serverless Compute","\u002Faws\u002Flambda","13.aws\u002F10.lambda",{"title":3331,"path":3332,"stem":3333},"Amazon Bedrock - Foundation Models, RAG та Agents","\u002Faws\u002Fbedrock","13.aws\u002F22.bedrock",{"title":3335,"path":3336,"stem":3337},"Amazon Rekognition - Комп'ютерний зір","\u002Faws\u002Frekognition","13.aws\u002F23.rekognition",{"title":3339,"path":3340,"stem":3341},"Amazon Textract - Інтелектуальний аналіз документів","\u002Faws\u002Ftextract","13.aws\u002F24.textract",{"title":3343,"path":3344,"stem":3345},"Amazon Polly, Transcribe, Comprehend та Translate","\u002Faws\u002Faudio-nlp-services","13.aws\u002F25.audio-nlp-services",{"title":3347,"path":3348,"stem":3349,"children":3350,"page":59},"Ai Development","\u002Fai-development","14.ai-development",[3351],{"title":3352,"path":3353,"stem":3354,"children":3355,"page":59},"Foundations","\u002Fai-development\u002Ffoundations","14.ai-development\u002F00.foundations",[3356],{"title":3357,"path":3358,"stem":3359},"Модуль 0.1. Що таке Large Language Model (LLM)","\u002Fai-development\u002Ffoundations\u002Fwhat-is-llm","14.ai-development\u002F00.foundations\u002F01.what-is-llm",{"title":3361,"path":3362,"stem":3363,"children":3364,"page":59},"React Native","\u002Freact-native","15.react-native",[3365,3386],{"title":3366,"path":3367,"stem":3368,"children":3369,"page":59},"Version1","\u002Freact-native\u002Fversion1","15.react-native\u002Fversion1",[3370,3374,3378,3382],{"title":3371,"path":3372,"stem":3373},"Що таке React Native","\u002Freact-native\u002Fversion1\u002Fwhat-is-react-native","15.react-native\u002Fversion1\u002F01.what-is-react-native",{"title":3375,"path":3376,"stem":3377},"Як влаштований React Native «під капотом»","\u002Freact-native\u002Fversion1\u002Freact-native-architecture","15.react-native\u002Fversion1\u002F02.react-native-architecture",{"title":3379,"path":3380,"stem":3381},"Від React до React Native: ключові відмінності","\u002Freact-native\u002Fversion1\u002Freact-native-vs-react","15.react-native\u002Fversion1\u002F03.react-native-vs-react",{"title":3383,"path":3384,"stem":3385},"View та Text — фундаментальні будівельні блоки","\u002Freact-native\u002Fversion1\u002Fview-and-text","15.react-native\u002Fversion1\u002F04.view-and-text",{"title":3387,"path":3388,"stem":3389,"children":3390,"page":59},"Version2","\u002Freact-native\u002Fversion2","15.react-native\u002Fversion2",[3391,3395,3399,3403,3407,3411,3415,3419,3423,3427,3431,3435,3439,3443,3447,3451,3455,3459,3463],{"title":3392,"path":3393,"stem":3394},"React Native — навіщо він потрібен","\u002Freact-native\u002Fversion2\u002Fwhy-react-native","15.react-native\u002Fversion2\u002F01.why-react-native",{"title":3396,"path":3397,"stem":3398},"Налаштування середовища розробки","\u002Freact-native\u002Fversion2\u002Fenvironment-setup","15.react-native\u002Fversion2\u002F02.environment-setup",{"title":3400,"path":3401,"stem":3402},"Expo і класичний React Native — два шляхи одного фреймворку","\u002Freact-native\u002Fversion2\u002Fexpo-vs-cli-landscape","15.react-native\u002Fversion2\u002F03.expo-vs-cli-landscape",{"title":3404,"path":3405,"stem":3406},"Середовище Expo — перший запуск","\u002Freact-native\u002Fversion2\u002Fexpo-setup-and-tooling","15.react-native\u002Fversion2\u002F04.expo-setup-and-tooling",{"title":3408,"path":3409,"stem":3410},"Структура проєкту та перші UI-примітиви","\u002Freact-native\u002Fversion2\u002Fproject-structure-and-conventions","15.react-native\u002Fversion2\u002F05.project-structure-and-conventions",{"title":3412,"path":3413,"stem":3414},"Базові компоненти React Native","\u002Freact-native\u002Fversion2\u002Fcore-components","15.react-native\u002Fversion2\u002F06.core-components",{"title":3416,"path":3417,"stem":3418},"Flexbox і layout у React Native","\u002Freact-native\u002Fversion2\u002Fflexbox-and-layout","15.react-native\u002Fversion2\u002F07.flexbox-and-layout",{"title":3420,"path":3421,"stem":3422},"StyleSheet і теми (light \u002F dark)","\u002Freact-native\u002Fversion2\u002Fstylesheet-and-theming","15.react-native\u002Fversion2\u002F08.stylesheet-and-theming",{"title":3424,"path":3425,"stem":3426},"Списки та віртуалізація","\u002Freact-native\u002Fversion2\u002Flists-and-virtualization","15.react-native\u002Fversion2\u002F09.lists-and-virtualization",{"title":3428,"path":3429,"stem":3430},"Форми, ввід і валідація","\u002Freact-native\u002Fversion2\u002Fforms-input-validation","15.react-native\u002Fversion2\u002F10.forms-input-validation",{"title":3432,"path":3433,"stem":3434},"Основи Expo Router","\u002Freact-native\u002Fversion2\u002Fexpo-router-basics","15.react-native\u002Fversion2\u002F11.expo-router-basics",{"title":3436,"path":3437,"stem":3438},"Вкладені навігатори та модалки","\u002Freact-native\u002Fversion2\u002Fnested-navigators-and-modals","15.react-native\u002Fversion2\u002F12.nested-navigators-and-modals",{"title":3440,"path":3441,"stem":3442},"Deep linking і параметри маршрутів","\u002Freact-native\u002Fversion2\u002Fdeep-linking-and-params","15.react-native\u002Fversion2\u002F13.deep-linking-and-params",{"title":3444,"path":3445,"stem":3446},"Робота з мережею та життєвий цикл застосунку","\u002Freact-native\u002Fversion2\u002Fnetworking-and-app-lifecycle","15.react-native\u002Fversion2\u002F14.networking-and-app-lifecycle",{"title":3448,"path":3449,"stem":3450},"Redux Toolkit на мобільних платформах","\u002Freact-native\u002Fversion2\u002Fredux-toolkit-on-mobile","15.react-native\u002Fversion2\u002F15.redux-toolkit-on-mobile",{"title":3452,"path":3453,"stem":3454},"RTK Query на mobile","\u002Freact-native\u002Fversion2\u002Frtk-query-mobile","15.react-native\u002Fversion2\u002F16.rtk-query-mobile",{"title":3456,"path":3457,"stem":3458},"Автентифікація та Безпечне Зберігання","\u002Freact-native\u002Fversion2\u002Fauth-secure-storage","15.react-native\u002Fversion2\u002F18.auth-secure-storage",{"title":3460,"path":3461,"stem":3462},"Локальне сховище — AsyncStorage, MMKV, SQLite","\u002Freact-native\u002Fversion2\u002Flocal-storage-mmkv-sqlite","15.react-native\u002Fversion2\u002F19.local-storage-mmkv-sqlite",{"title":3464,"path":3465,"stem":3466},"Офлайн-First і синхронізація","\u002Freact-native\u002Fversion2\u002Foffline-first-sync","15.react-native\u002Fversion2\u002F20.offline-first-sync",{"title":3468,"path":3469,"stem":3470,"children":3471,"page":59},"Ai Python","\u002Fai-python","16.ai-python",[3472,3476,3480],{"title":3473,"path":3474,"stem":3475},"Вступ до штучного інтелекту та машинного навчання","\u002Fai-python\u002Fintro-to-ai","16.ai-python\u002F01.intro-to-ai",{"title":3477,"path":3478,"stem":3479},"Знайомство з Jupyter Notebook та JupyterLab","\u002Fai-python\u002Fjupyter-notebook","16.ai-python\u002F02.jupyter-notebook",{"title":3481,"path":3482,"stem":3483},"Робота з даними з використанням NumPy і pandas","\u002Fai-python\u002Fnumpy-pandas","16.ai-python\u002F03.numpy-pandas",{"title":3485,"path":3486,"stem":3487,"children":3488,"page":59},"Tailwind","\u002Ftailwind","21.tailwind",[3489,3493,3497,3501,3505,3509,3513,3517,3521,3525,3529,3533],{"title":3490,"path":3491,"stem":3492},"Що таке Tailwind CSS і навіщо він потрібен","\u002Ftailwind\u002Ftailwind-intro-philosophy","21.tailwind\u002F01.tailwind-intro-philosophy",{"title":3494,"path":3495,"stem":3496},"Встановлення та налаштування Tailwind CSS v4","\u002Ftailwind\u002Ftailwind-installation-setup","21.tailwind\u002F02.tailwind-installation-setup",{"title":3498,"path":3499,"stem":3500},"Utility-класи: основи та система Tailwind","\u002Ftailwind\u002Ftailwind-utility-classes-core","21.tailwind\u002F03.tailwind-utility-classes-core",{"title":3502,"path":3503,"stem":3504},"Layout: Flexbox та Grid через Tailwind","\u002Ftailwind\u002Ftailwind-flexbox-grid","21.tailwind\u002F04.tailwind-flexbox-grid",{"title":3506,"path":3507,"stem":3508},"Кастомізація теми через @theme у Tailwind v4","\u002Ftailwind\u002Ftailwind-theme-customization","21.tailwind\u002F05.tailwind-theme-customization",{"title":3510,"path":3511,"stem":3512},"Варіанти: hover, focus, responsive, dark mode та нові v4","\u002Ftailwind\u002Ftailwind-variants-states","21.tailwind\u002F06.tailwind-variants-states",{"title":3514,"path":3515,"stem":3516},"Типографіка та система кольорів у Tailwind v4","\u002Ftailwind\u002Ftailwind-typography-colors","21.tailwind\u002F07.tailwind-typography-colors",{"title":3518,"path":3519,"stem":3520},"Компоненти та повторюваність: @apply, @utility та патерни","\u002Ftailwind\u002Ftailwind-components-patterns","21.tailwind\u002F08.tailwind-components-patterns",{"title":3522,"path":3523,"stem":3524},"Темна тема та система дизайн-токенів у Tailwind v4","\u002Ftailwind\u002Ftailwind-dark-mode-theming","21.tailwind\u002F09.tailwind-dark-mode-theming",{"title":3526,"path":3527,"stem":3528},"Довільні значення та контейнерні запити у Tailwind v4","\u002Ftailwind\u002Ftailwind-arbitrary-container-queries","21.tailwind\u002F10.tailwind-arbitrary-container-queries",{"title":3530,"path":3531,"stem":3532},"Анімації, трансформації та 3D у Tailwind v4","\u002Ftailwind\u002Ftailwind-animations-transforms","21.tailwind\u002F11.tailwind-animations-transforms",{"title":3534,"path":3535,"stem":3536},"Tailwind CLI, PostCSS та інтеграція з фреймворками","\u002Ftailwind\u002Ftailwind-cli-tooling","21.tailwind\u002F12.tailwind-cli-tooling",{"title":3538,"path":3539,"stem":3540},"Тестування компонентів діаграм","\u002Ftest-components","98.test-components",{"id":3542,"title":2654,"body":3543,"description":23129,"extension":23130,"links":23131,"meta":23132,"navigation":3729,"path":2655,"seo":23133,"stem":2656,"__hash__":23134},"docs\u002F05.python\u002Ffastapi\u002F25.fastapi-auth-security.md",{"type":3544,"value":3545,"toc":23056},"minimark",[3546,3550,3555,3572,3575,3602,3613,3653,3656,3660,3665,3676,3683,3686,3847,3854,3858,3861,4114,4121,4128,4193,4208,4212,4223,4234,4237,4260,4262,4268,4279,4295,4303,4362,4367,4381,4387,4391,4401,4715,4725,4727,4736,4747,4782,4787,4793,4803,4841,4848,4970,5018,5020,5024,5027,5031,5042,5230,5256,5260,5270,5284,5287,5291,5298,5301,5309,5393,5448,5463,5467,5474,5584,5595,5599,5602,5609,5612,5729,5736,5766,5814,6024,6035,6037,6041,6045,6052,6055,6096,6115,6119,6126,6132,6135,6141,6412,6436,6440,6451,6605,6620,6624,6630,6833,6838,6869,6873,6880,6884,6897,6903,6908,6917,6922,6936,6947,6951,6961,6996,7003,7149,7153,7177,7342,7346,7499,7517,7519,7526,7533,7586,7593,7599,8037,8041,8047,8968,8974,9115,9136,9185,9187,9191,9195,9202,9221,9227,9234,9244,9315,9330,9337,9346,9398,9405,9421,9557,9560,10224,10227,10384,10411,10415,10422,11750,11763,11765,11769,11776,11780,11786,11790,11917,11921,16410,16414,16568,16570,16574,16578,16589,16596,16639,16643,16656,16771,16798,16802,16808,17130,17137,17147,17289,17974,17978,18831,18837,18998,19029,19033,19051,19096,19127,19131,19233,19235,19239,19243,19250,19256,19271,19275,19337,19341,19344,19708,20282,20285,20592,20595,21123,21143,21145,21149,21153,21156,21263,21267,21273,21650,21654,21660,21883,21900,21904,21915,21981,22368,22371,22551,22556,22604,22606,22610,22614,22620,22625,22669,22688,22692,22697,22701,22745,22764,22774,22778,22787,22791,22820,22855,22881,22893,22895,22899,22902,23002,23006,23052],[3547,3548,2654],"h1",{"id":3549},"автентифікація-та-авторизація-jwt-oauth2-rbac",[3551,3552,3554],"h2",{"id":3553},"чому-безпека-є-першим-пріоритетом","Чому безпека є першим пріоритетом",[3556,3557,3558,3559,3563,3564,3567,3568,3571],"p",{},"Кожен застосунок, що обробляє дані користувачів, рано чи пізно зіткнеться з одним і тим самим фундаментальним питанням: ",[3560,3561,3562],"strong",{},"хто ти такий і що тобі дозволено робити?"," Саме ці два питання формують два наріжні камені безпеки будь-якої системи — ",[3560,3565,3566],{},"автентифікацію"," (authentication) та ",[3560,3569,3570],{},"авторизацію"," (authorization).",[3556,3573,3574],{},"На перший погляд, їхні назви схожі. Але різниця принципова:",[3576,3577,3578,3591],"card-group",{},[3579,3580,3583,3584,3587,3588],"card",{"icon":3581,"title":3582},"i-heroicons-identification","Автентифікація (Authentication)","Процес ",[3560,3585,3586],{},"підтвердження особи",". Система перевіряє, що запитувач є саме тим, за кого себе видає. Класичний приклад — введення логіну та пароля. Відповідь на питання: ",[3560,3589,3590],{},"«Хто ти?»",[3579,3592,3583,3595,3598,3599],{"icon":3593,"title":3594},"i-heroicons-shield-check","Авторизація (Authorization)",[3560,3596,3597],{},"перевірки прав доступу",". Після того, як особа підтверджена, система вирішує, що їй дозволено робити. Відповідь на питання: ",[3560,3600,3601],{},"«Що тобі можна?»",[3556,3603,3604,3605,3608,3609,3612],{},"Типова послідовність виглядає так: спочатку система ",[3560,3606,3607],{},"автентифікує"," — перевіряє логін і пароль, видає токен. Потім — при кожному наступному запиті — ",[3560,3610,3611],{},"авторизує"," — перевіряє токен і вирішує, чи має цей користувач доступ до конкретного ресурсу.",[3614,3615,3616,3621],"note",{},[3556,3617,3618],{},[3560,3619,3620],{},"Контекст для тих, хто прийшов з ASP.NET:",[3556,3622,3623,3624,3628,3629,3632,3633,3636,3637,3640,3641,3644,3645,3648,3649,3652],{},"В екосистемі ASP.NET Core ці концепції реалізовані через ",[3625,3626,3627],"code",{},"[Authorize]"," атрибут, middleware ",[3625,3630,3631],{},"UseAuthentication()"," \u002F ",[3625,3634,3635],{},"UseAuthorization()",", схеми ",[3625,3638,3639],{},"JwtBearerDefaults.AuthenticationScheme",", Identity framework та claims-based модель. У FastAPI підхід концептуально ідентичний, але реалізований через систему ",[3560,3642,3643],{},"Dependency Injection"," (",[3625,3646,3647],{},"Depends()","), ",[3560,3650,3651],{},"Security Schemes"," та декоратори роутів. Цю статтю побудовано так, щоб виявити паралелі та відмінності між двома підходами.",[3654,3655],"hr",{},[3551,3657,3659],{"id":3658},"частина-1-хешування-паролів-фундамент-безпеки","Частина 1. Хешування паролів — фундамент безпеки",[3661,3662,3664],"h3",{"id":3663},"чому-не-можна-зберігати-паролі-відкритим-текстом","Чому не можна зберігати паролі відкритим текстом",[3556,3666,3667,3668,3671,3672,3675],{},"Уявіть таку ситуацію: ваша база даних була скомпрометована — зловмисник отримав дамп таблиці ",[3625,3669,3670],{},"users",". Якщо паролі зберігалися відкритим текстом (або навіть у форматі Base64), він отримав одразу ",[3560,3673,3674],{},"всі паролі всіх користувачів",". Враховуючи, що більшість людей використовують один пароль на декількох сайтах, збитки будуть катастрофічними.",[3556,3677,3678,3679,3682],{},"Правильне рішення — зберігати не сам пароль, а його ",[3560,3680,3681],{},"криптографічний хеш",". При вході користувач знову вводить пароль, система знову хешує його і порівнює два хеші. Якщо вони збігаються — пароль правильний.",[3556,3684,3685],{},"Але не всі хеш-функції однаково безпечні. Розглянемо еволюцію підходів:",[3687,3688,3689],"plant-uml",{},[3690,3691,3696],"pre",{"className":3692,"code":3693,"language":3694,"meta":3695,"style":3695},"language-plantuml shiki shiki-themes light-plus dark-plus dark-plus","@startuml\nskinparam style plain\nskinparam backgroundColor #ffffff\nskinparam defaultFontSize 13\n\nrectangle \"❌ MD5 \u002F SHA-1 \u002F SHA-256\\n(НЕ для паролів)\" as bad #ffcccc {\n  note as n1\n    Призначені для швидкості\n    GPU: мільярди хешів\u002Fсекунду\n    Rainbow tables атаки\n    Не містять salt за замовчуванням\n  end note\n}\n\nrectangle \"✅ bcrypt \u002F Argon2 \u002F scrypt\\n(Правильний вибір)\" as good #ccffcc {\n  note as n2\n    Спеціально повільні (work factor)\n    Вбудований унікальний salt\n    Захист від GPU-атак\n    Налаштована складність\n  end note\n}\n\nbad -down-> good : Еволюція\n@enduml\n","plantuml","",[3625,3697,3698,3706,3712,3718,3724,3731,3737,3743,3749,3755,3761,3767,3773,3779,3784,3790,3796,3802,3808,3814,3820,3825,3830,3835,3841],{"__ignoreMap":3695},[3699,3700,3703],"span",{"class":3701,"line":3702},"line",1,[3699,3704,3705],{},"@startuml\n",[3699,3707,3709],{"class":3701,"line":3708},2,[3699,3710,3711],{},"skinparam style plain\n",[3699,3713,3715],{"class":3701,"line":3714},3,[3699,3716,3717],{},"skinparam backgroundColor #ffffff\n",[3699,3719,3721],{"class":3701,"line":3720},4,[3699,3722,3723],{},"skinparam defaultFontSize 13\n",[3699,3725,3727],{"class":3701,"line":3726},5,[3699,3728,3730],{"emptyLinePlaceholder":3729},true,"\n",[3699,3732,3734],{"class":3701,"line":3733},6,[3699,3735,3736],{},"rectangle \"❌ MD5 \u002F SHA-1 \u002F SHA-256\\n(НЕ для паролів)\" as bad #ffcccc {\n",[3699,3738,3740],{"class":3701,"line":3739},7,[3699,3741,3742],{},"  note as n1\n",[3699,3744,3746],{"class":3701,"line":3745},8,[3699,3747,3748],{},"    Призначені для швидкості\n",[3699,3750,3752],{"class":3701,"line":3751},9,[3699,3753,3754],{},"    GPU: мільярди хешів\u002Fсекунду\n",[3699,3756,3758],{"class":3701,"line":3757},10,[3699,3759,3760],{},"    Rainbow tables атаки\n",[3699,3762,3764],{"class":3701,"line":3763},11,[3699,3765,3766],{},"    Не містять salt за замовчуванням\n",[3699,3768,3770],{"class":3701,"line":3769},12,[3699,3771,3772],{},"  end note\n",[3699,3774,3776],{"class":3701,"line":3775},13,[3699,3777,3778],{},"}\n",[3699,3780,3782],{"class":3701,"line":3781},14,[3699,3783,3730],{"emptyLinePlaceholder":3729},[3699,3785,3787],{"class":3701,"line":3786},15,[3699,3788,3789],{},"rectangle \"✅ bcrypt \u002F Argon2 \u002F scrypt\\n(Правильний вибір)\" as good #ccffcc {\n",[3699,3791,3793],{"class":3701,"line":3792},16,[3699,3794,3795],{},"  note as n2\n",[3699,3797,3799],{"class":3701,"line":3798},17,[3699,3800,3801],{},"    Спеціально повільні (work factor)\n",[3699,3803,3805],{"class":3701,"line":3804},18,[3699,3806,3807],{},"    Вбудований унікальний salt\n",[3699,3809,3811],{"class":3701,"line":3810},19,[3699,3812,3813],{},"    Захист від GPU-атак\n",[3699,3815,3817],{"class":3701,"line":3816},20,[3699,3818,3819],{},"    Налаштована складність\n",[3699,3821,3823],{"class":3701,"line":3822},21,[3699,3824,3772],{},[3699,3826,3828],{"class":3701,"line":3827},22,[3699,3829,3778],{},[3699,3831,3833],{"class":3701,"line":3832},23,[3699,3834,3730],{"emptyLinePlaceholder":3729},[3699,3836,3838],{"class":3701,"line":3837},24,[3699,3839,3840],{},"bad -down-> good : Еволюція\n",[3699,3842,3844],{"class":3701,"line":3843},25,[3699,3845,3846],{},"@enduml\n",[3556,3848,3849,3850,3853],{},"Ключова ідея: хеш-функції для паролів ",[3560,3851,3852],{},"навмисно повільні",". Це не баг — це фіча. Якщо перевірка одного пароля займає 100 мілісекунд, то атака перебором 1 мільярду паролів займе ~115 днів навіть на одному ядрі. На відміну від SHA-256, де GPU може перевіряти мільярди варіантів за секунду.",[3661,3855,3857],{"id":3856},"порівняльна-таблиця-прості-хеш-функції-vs-спеціалізовані","Порівняльна таблиця: прості хеш-функції vs спеціалізовані",[3556,3859,3860],{},"Щоб зробити відмінності максимально наочними, розглянемо конкретні числа. Усі вимірювання — на сучасному споживчому CPU (Intel\u002FAMD 2023 р.) та GPU RTX 4090.",[3862,3863,3864,3895],"table",{},[3865,3866,3867],"thead",{},[3868,3869,3870,3874,3877,3880,3883,3886,3889,3892],"tr",{},[3871,3872,3873],"th",{},"Алгоритм",[3871,3875,3876],{},"Призначення",[3871,3878,3879],{},"Час одного хешу (CPU)",[3871,3881,3882],{},"Хешів\u002Fсек (CPU)",[3871,3884,3885],{},"Хешів\u002Fсек (RTX 4090)",[3871,3887,3888],{},"Вбудований salt",[3871,3890,3891],{},"Захист від GPU",[3871,3893,3894],{},"Налаш- тована складність",[3896,3897,3898,3925,3950,3975,4000,4030,4057,4085],"tbody",{},[3868,3899,3900,3906,3909,3912,3915,3918,3921,3923],{},[3901,3902,3903],"td",{},[3560,3904,3905],{},"MD5",[3901,3907,3908],{},"Контрольна сума файлів",[3901,3910,3911],{},"~1 нс",[3901,3913,3914],{},"~30 млрд",[3901,3916,3917],{},"~300 млрд",[3901,3919,3920],{},"❌",[3901,3922,3920],{},[3901,3924,3920],{},[3868,3926,3927,3932,3935,3938,3941,3944,3946,3948],{},[3901,3928,3929],{},[3560,3930,3931],{},"SHA-1",[3901,3933,3934],{},"Цифровий підпис (застарілий)",[3901,3936,3937],{},"~2 нс",[3901,3939,3940],{},"~15 млрд",[3901,3942,3943],{},"~150 млрд",[3901,3945,3920],{},[3901,3947,3920],{},[3901,3949,3920],{},[3868,3951,3952,3957,3960,3963,3966,3969,3971,3973],{},[3901,3953,3954],{},[3560,3955,3956],{},"SHA-256",[3901,3958,3959],{},"Цілісність даних, TLS",[3901,3961,3962],{},"~4 нс",[3901,3964,3965],{},"~500 млн",[3901,3967,3968],{},"~22 млрд",[3901,3970,3920],{},[3901,3972,3920],{},[3901,3974,3920],{},[3868,3976,3977,3982,3985,3988,3991,3994,3996,3998],{},[3901,3978,3979],{},[3560,3980,3981],{},"SHA-512",[3901,3983,3984],{},"Цілісність, підписи",[3901,3986,3987],{},"~6 нс",[3901,3989,3990],{},"~300 млн",[3901,3992,3993],{},"~9 млрд",[3901,3995,3920],{},[3901,3997,3920],{},[3901,3999,3920],{},[3868,4001,4002,4008,4013,4016,4019,4022,4025,4028],{},[3901,4003,4004,4007],{},[3560,4005,4006],{},"bcrypt"," (cost=10)",[3901,4009,4010],{},[3560,4011,4012],{},"Паролі",[3901,4014,4015],{},"~40 мс",[3901,4017,4018],{},"~25",[3901,4020,4021],{},"~300",[3901,4023,4024],{},"✅",[3901,4026,4027],{},"⚠️ частково",[3901,4029,4024],{},[3868,4031,4032,4037,4042,4045,4048,4051,4053,4055],{},[3901,4033,4034,4036],{},[3560,4035,4006],{}," (cost=12)",[3901,4038,4039],{},[3560,4040,4041],{},"Паролі (рекомендовано)",[3901,4043,4044],{},"~160 мс",[3901,4046,4047],{},"~6",[3901,4049,4050],{},"~80",[3901,4052,4024],{},[3901,4054,4027],{},[3901,4056,4024],{},[3868,4058,4059,4065,4069,4072,4075,4078,4080,4083],{},[3901,4060,4061,4064],{},[3560,4062,4063],{},"scrypt"," (N=32768)",[3901,4066,4067],{},[3560,4068,4012],{},[3901,4070,4071],{},"~100 мс",[3901,4073,4074],{},"~10",[3901,4076,4077],{},"~15",[3901,4079,4024],{},[3901,4081,4082],{},"✅ сильний",[3901,4084,4024],{},[3868,4086,4087,4093,4098,4101,4104,4107,4109,4112],{},[3901,4088,4089,4092],{},[3560,4090,4091],{},"Argon2id"," (t=2, m=64MB)",[3901,4094,4095],{},[3560,4096,4097],{},"Паролі (найкращий вибір)",[3901,4099,4100],{},"~120 мс",[3901,4102,4103],{},"~8",[3901,4105,4106],{},"~12",[3901,4108,4024],{},[3901,4110,4111],{},"✅ дуже сильний",[3901,4113,4024],{},[3614,4115,4116,4117,4120],{},"Зверніть увагу на колонку ",[3560,4118,4119],{},"«Хешів\u002Fсек (RTX 4090)»",": для SHA-256 GPU дає прискорення у ~44 рази відносно CPU. Для bcrypt — лише у ~13 разів. Для Argon2id — лише у ~1.5 рази. Саме ця різниця й робить спеціалізовані алгоритми практично невразливими до GPU-атак.",[3556,4122,4123,4124,4127],{},"Щоб перетворити ці числа на реальне відчуття загрози, розглянемо сценарій атаки: зловмисник отримав ваш дамп БД і хоче зламати пароль ",[3625,4125,4126],{},"P@ssw0rd!"," (9 символів, змішаний регістр + цифри + спецсимвол, ~3 квадрильйони комбінацій):",[3862,4129,4130,4142],{},[3865,4131,4132],{},[3868,4133,4134,4136,4139],{},[3871,4135,3873],{},[3871,4137,4138],{},"Швидкість GPU (RTX 4090)",[3871,4140,4141],{},"Час повного перебору",[3896,4143,4144,4156,4168,4181],{},[3868,4145,4146,4148,4151],{},[3901,4147,3905],{},[3901,4149,4150],{},"300 млрд\u002Fсек",[3901,4152,4153],{},[3560,4154,4155],{},"~2.8 години",[3868,4157,4158,4160,4163],{},[3901,4159,3956],{},[3901,4161,4162],{},"22 млрд\u002Fсек",[3901,4164,4165],{},[3560,4166,4167],{},"~38 годин",[3868,4169,4170,4173,4176],{},[3901,4171,4172],{},"bcrypt cost=12",[3901,4174,4175],{},"80\u002Fсек",[3901,4177,4178],{},[3560,4179,4180],{},"~1.2 мільярда років",[3868,4182,4183,4185,4188],{},[3901,4184,4091],{},[3901,4186,4187],{},"12\u002Fсек",[3901,4189,4190],{},[3560,4191,4192],{},"~7.9 мільярда років",[4194,4195,4196,4197,4200,4201,4200,4204,4207],"caution",{},"Наведені часи для bcrypt та Argon2id — теоретичний максимум для одного GPU. На практиці зловмисники використовують словникові атаки з реальними паролями людей (типу ",[3625,4198,4199],{},"password123",", ",[3625,4202,4203],{},"qwerty",[3625,4205,4206],{},"iloveyou","), а не повний перебір. Тому надійний, довгий та унікальний пароль — така ж важлива умова безпеки, як і правильний алгоритм хешування.",[3661,4209,4211],{"id":4210},"salt-захист-від-таблиць-передобчислень","Salt: захист від таблиць передобчислень",[3556,4213,4214,4215,4218,4219,4222],{},"Ще одна концепція, яку важливо розуміти — ",[3560,4216,4217],{},"сіль (salt)",". Це випадковий рядок, який додається до пароля перед хешуванням. Кожен пароль отримує ",[3560,4220,4221],{},"унікальний"," salt.",[3556,4224,4225,4226,4229,4230,4233],{},"Навіщо це потрібно? Без salt два користувачі з однаковим паролем ",[3625,4227,4228],{},"secret123"," матимуть однаковий хеш у базі даних. Зловмисник це помітить і зрозуміє, що паролі однакові. Більше того, він може заздалегідь побудувати так звану ",[3560,4231,4232],{},"rainbow table"," — таблицю відповідності «пароль → хеш» для мільйонів популярних паролів і миттєво знайти збіг.",[3556,4235,4236],{},"Salt вирішує цю проблему: навіть якщо два користувачі мають однаковий пароль, їхні хеші будуть абсолютно різними, оскільки salt у кожного унікальний.",[4238,4239,4240,4246,4251,4256],"field-group",{},[4241,4242,4245],"field",{"name":4243,"type":4244},"password","str","Оригінальний пароль користувача у відкритому вигляді. Ніколи не зберігається.",[4241,4247,4250],{"name":4248,"type":4249},"salt","bytes","Випадковий рядок байтів, що генерується при кожному хешуванні. Зберігається разом із хешем (зазвичай як префікс).",[4241,4252,4255],{"name":4253,"type":4254},"work_factor \u002F cost","int","Параметр, що визначає обчислювальну складність операції хешування. Чим вище — тим повільніше і безпечніше. Для bcrypt типові значення: 10–12. Можна збільшувати з часом при зростанні потужностей CPU.",[4241,4257,4259],{"name":4258,"type":4244},"hash","Результат хешування: рядок, що містить у собі алгоритм, work factor, salt та власне хеш — все в одному. Зберігається у базі даних.",[3654,4261],{},[3661,4263,4265,4266],{"id":4264},"хешування-паролів-у-python-перехід-на-чистий-bcrypt","Хешування паролів у Python: перехід на чистий ",[3625,4267,4006],{},[3556,4269,4270,4271,4274,4275,4278],{},"Історично стандартним вибором в екосистемі Python для роботи з паролями була бібліотека ",[3625,4272,4273],{},"passlib"," (зокрема її клас ",[3625,4276,4277],{},"CryptContext","). Вона дозволяла зручно керувати багатьма алгоритмами.",[3556,4280,4281,4282,4284,4285,4288,4289,4291,4292],{},"Однак на сьогодні бібліотека ",[3625,4283,4273],{}," є ",[3560,4286,4287],{},"фактично закинутою (unmaintained)"," і не оновлюється. Це призводить до серйозних проблем із сумісністю: при використанні нових версій бібліотеки ",[3625,4290,4006],{}," (від 4.0.0 і вище) виникає помилка:\n",[3625,4293,4294],{},"AttributeError: module 'bcrypt' has no attribute '__about__'",[3556,4296,4297,4298,4302],{},"Тому сучасним рекомендованим підходом є використання офіційної бібліотеки ",[3560,4299,4300],{},[3625,4301,4006],{}," напряму.",[4304,4305,4306,4329,4346],"tabs",{},[4307,4308,4310],"tabs-item",{"label":4309},"pip",[3690,4311,4315],{"className":4312,"code":4313,"language":4314,"meta":3695,"style":3695},"language-bash shiki shiki-themes light-plus dark-plus dark-plus","pip install bcrypt\n","bash",[3625,4316,4317],{"__ignoreMap":3695},[3699,4318,4319,4322,4326],{"class":3701,"line":3702},[3699,4320,4309],{"class":4321},"s8Opu",[3699,4323,4325],{"class":4324},"sbdoH"," install",[3699,4327,4328],{"class":4324}," bcrypt\n",[4307,4330,4332],{"label":4331},"uv",[3690,4333,4335],{"className":4312,"code":4334,"language":4314,"meta":3695,"style":3695},"uv add bcrypt\n",[3625,4336,4337],{"__ignoreMap":3695},[3699,4338,4339,4341,4344],{"class":3701,"line":3702},[3699,4340,4331],{"class":4321},[3699,4342,4343],{"class":4324}," add",[3699,4345,4328],{"class":4324},[4307,4347,4349],{"label":4348},"poetry",[3690,4350,4352],{"className":4312,"code":4351,"language":4314,"meta":3695,"style":3695},"poetry add bcrypt\n",[3625,4353,4354],{"__ignoreMap":3695},[3699,4355,4356,4358,4360],{"class":3701,"line":3702},[3699,4357,4348],{"class":4321},[3699,4359,4343],{"class":4324},[3699,4361,4328],{"class":4324},[4363,4364,4366],"h4",{"id":4365},"обмеження-bcrypt-у-72-байти","Обмеження bcrypt у 72 байти",[3556,4368,4369,4370,4373,4374,4376,4377,4380],{},"Алгоритм bcrypt має вбудоване обмеження: максимальна довжина вхідних даних для хешування становить ",[3560,4371,4372],{},"72 байти",". Якщо передати довший пароль, сучасна бібліотека ",[3625,4375,4006],{}," згенерує помилку ",[3625,4378,4379],{},"ValueError",".",[3556,4382,4383,4384,4380],{},"Щоб підтримувати паролі будь-якої довжини і гарантувати безпеку, перед передачею пароля до bcrypt його кодують у UTF-8 і обрізають до перших 72 байтів: ",[3625,4385,4386],{},"plain_password.encode(\"utf-8\")[:72]",[4363,4388,4390],{"id":4389},"реалізація-у-проєкті-taskforge","Реалізація у проєкті TaskForge",[3556,4392,4393,4394,4397,4398,4400],{},"Ось як виглядає рекомендована реалізація модуля ",[3625,4395,4396],{},"app\u002Fcore\u002Fsecurity.py"," з прямим використанням ",[3625,4399,4006],{},":",[3690,4402,4406],{"className":4403,"code":4404,"language":4405,"meta":3695,"style":3695},"language-python shiki shiki-themes light-plus dark-plus dark-plus","# app\u002Fcore\u002Fsecurity.py\nimport bcrypt\n\n\ndef hash_password(plain_password: str) -> str:\n    \"\"\"\n    Хешує пароль за допомогою bcrypt з автоматично згенерованою сіллю.\n    Пароль попередньо обрізається до 72 байтів (ліміт алгоритму bcrypt).\n\n    Args:\n        plain_password: Пароль у відкритому вигляді.\n\n    Returns:\n        Рядок хешу, що містить версію, work factor (rounds), salt та сам хеш.\n        Приклад: \"$2b$12$...\"\n    \"\"\"\n    # Обрізаємо пароль до 72 байтів для запобігання ValueError\n    pwd_bytes = plain_password.encode(\"utf-8\")[:72]\n    \n    # Генеруємо сіль із рекомендованим work factor (rounds=12)\n    salt = bcrypt.gensalt(rounds=12)\n    \n    # Хешуємо та декодуємо результат у рядок UTF-8\n    return bcrypt.hashpw(pwd_bytes, salt).decode(\"utf-8\")\n\n\ndef verify_password(plain_password: str, hashed_password: str) -> bool:\n    \"\"\"\n    Перевіряє відповідність пароля збереженому хешу.\n\n    Args:\n        plain_password: Пароль у відкритому вигляді (від користувача).\n        hashed_password: Хеш із бази даних.\n\n    Returns:\n        True, якщо пароль відповідає хешу. False — інакше.\n    \"\"\"\n    pwd_bytes = plain_password.encode(\"utf-8\")[:72]\n    hashed_bytes = hashed_password.encode(\"utf-8\")\n    \n    return bcrypt.checkpw(pwd_bytes, hashed_bytes)\n","python",[3625,4407,4408,4414,4423,4427,4431,4461,4466,4471,4476,4480,4485,4490,4494,4499,4504,4509,4513,4518,4536,4541,4546,4563,4567,4572,4584,4588,4593,4625,4630,4636,4641,4646,4652,4658,4663,4668,4674,4679,4692,4702,4707],{"__ignoreMap":3695},[3699,4409,4410],{"class":3701,"line":3702},[3699,4411,4413],{"class":4412},"spJ8K","# app\u002Fcore\u002Fsecurity.py\n",[3699,4415,4416,4420],{"class":3701,"line":3708},[3699,4417,4419],{"class":4418},"s8xlr","import",[3699,4421,4328],{"class":4422},"sHH4Y",[3699,4424,4425],{"class":3701,"line":3714},[3699,4426,3730],{"emptyLinePlaceholder":3729},[3699,4428,4429],{"class":3701,"line":3720},[3699,4430,3730],{"emptyLinePlaceholder":3729},[3699,4432,4433,4437,4440,4443,4447,4450,4453,4456,4458],{"class":3701,"line":3726},[3699,4434,4436],{"class":4435},"su1O8","def",[3699,4438,4439],{"class":4321}," hash_password",[3699,4441,4442],{"class":4422},"(",[3699,4444,4446],{"class":4445},"siwwj","plain_password",[3699,4448,4449],{"class":4422},": ",[3699,4451,4244],{"class":4452},"sN1BT",[3699,4454,4455],{"class":4422},") -> ",[3699,4457,4244],{"class":4452},[3699,4459,4460],{"class":4422},":\n",[3699,4462,4463],{"class":3701,"line":3733},[3699,4464,4465],{"class":4324},"    \"\"\"\n",[3699,4467,4468],{"class":3701,"line":3739},[3699,4469,4470],{"class":4324},"    Хешує пароль за допомогою bcrypt з автоматично згенерованою сіллю.\n",[3699,4472,4473],{"class":3701,"line":3745},[3699,4474,4475],{"class":4324},"    Пароль попередньо обрізається до 72 байтів (ліміт алгоритму bcrypt).\n",[3699,4477,4478],{"class":3701,"line":3751},[3699,4479,3730],{"emptyLinePlaceholder":3729},[3699,4481,4482],{"class":3701,"line":3757},[3699,4483,4484],{"class":4324},"    Args:\n",[3699,4486,4487],{"class":3701,"line":3763},[3699,4488,4489],{"class":4324},"        plain_password: Пароль у відкритому вигляді.\n",[3699,4491,4492],{"class":3701,"line":3769},[3699,4493,3730],{"emptyLinePlaceholder":3729},[3699,4495,4496],{"class":3701,"line":3775},[3699,4497,4498],{"class":4324},"    Returns:\n",[3699,4500,4501],{"class":3701,"line":3781},[3699,4502,4503],{"class":4324},"        Рядок хешу, що містить версію, work factor (rounds), salt та сам хеш.\n",[3699,4505,4506],{"class":3701,"line":3786},[3699,4507,4508],{"class":4324},"        Приклад: \"$2b$12$...\"\n",[3699,4510,4511],{"class":3701,"line":3792},[3699,4512,4465],{"class":4324},[3699,4514,4515],{"class":3701,"line":3798},[3699,4516,4517],{"class":4412},"    # Обрізаємо пароль до 72 байтів для запобігання ValueError\n",[3699,4519,4520,4523,4526,4529,4533],{"class":3701,"line":3804},[3699,4521,4522],{"class":4422},"    pwd_bytes = plain_password.encode(",[3699,4524,4525],{"class":4324},"\"utf-8\"",[3699,4527,4528],{"class":4422},")[:",[3699,4530,4532],{"class":4531},"sJj4R","72",[3699,4534,4535],{"class":4422},"]\n",[3699,4537,4538],{"class":3701,"line":3810},[3699,4539,4540],{"class":4422},"    \n",[3699,4542,4543],{"class":3701,"line":3816},[3699,4544,4545],{"class":4412},"    # Генеруємо сіль із рекомендованим work factor (rounds=12)\n",[3699,4547,4548,4551,4554,4557,4560],{"class":3701,"line":3822},[3699,4549,4550],{"class":4422},"    salt = bcrypt.gensalt(",[3699,4552,4553],{"class":4445},"rounds",[3699,4555,4556],{"class":4422},"=",[3699,4558,4559],{"class":4531},"12",[3699,4561,4562],{"class":4422},")\n",[3699,4564,4565],{"class":3701,"line":3827},[3699,4566,4540],{"class":4422},[3699,4568,4569],{"class":3701,"line":3832},[3699,4570,4571],{"class":4412},"    # Хешуємо та декодуємо результат у рядок UTF-8\n",[3699,4573,4574,4577,4580,4582],{"class":3701,"line":3837},[3699,4575,4576],{"class":4418},"    return",[3699,4578,4579],{"class":4422}," bcrypt.hashpw(pwd_bytes, salt).decode(",[3699,4581,4525],{"class":4324},[3699,4583,4562],{"class":4422},[3699,4585,4586],{"class":3701,"line":3843},[3699,4587,3730],{"emptyLinePlaceholder":3729},[3699,4589,4591],{"class":3701,"line":4590},26,[3699,4592,3730],{"emptyLinePlaceholder":3729},[3699,4594,4596,4598,4601,4603,4605,4607,4609,4611,4614,4616,4618,4620,4623],{"class":3701,"line":4595},27,[3699,4597,4436],{"class":4435},[3699,4599,4600],{"class":4321}," verify_password",[3699,4602,4442],{"class":4422},[3699,4604,4446],{"class":4445},[3699,4606,4449],{"class":4422},[3699,4608,4244],{"class":4452},[3699,4610,4200],{"class":4422},[3699,4612,4613],{"class":4445},"hashed_password",[3699,4615,4449],{"class":4422},[3699,4617,4244],{"class":4452},[3699,4619,4455],{"class":4422},[3699,4621,4622],{"class":4452},"bool",[3699,4624,4460],{"class":4422},[3699,4626,4628],{"class":3701,"line":4627},28,[3699,4629,4465],{"class":4324},[3699,4631,4633],{"class":3701,"line":4632},29,[3699,4634,4635],{"class":4324},"    Перевіряє відповідність пароля збереженому хешу.\n",[3699,4637,4639],{"class":3701,"line":4638},30,[3699,4640,3730],{"emptyLinePlaceholder":3729},[3699,4642,4644],{"class":3701,"line":4643},31,[3699,4645,4484],{"class":4324},[3699,4647,4649],{"class":3701,"line":4648},32,[3699,4650,4651],{"class":4324},"        plain_password: Пароль у відкритому вигляді (від користувача).\n",[3699,4653,4655],{"class":3701,"line":4654},33,[3699,4656,4657],{"class":4324},"        hashed_password: Хеш із бази даних.\n",[3699,4659,4661],{"class":3701,"line":4660},34,[3699,4662,3730],{"emptyLinePlaceholder":3729},[3699,4664,4666],{"class":3701,"line":4665},35,[3699,4667,4498],{"class":4324},[3699,4669,4671],{"class":3701,"line":4670},36,[3699,4672,4673],{"class":4324},"        True, якщо пароль відповідає хешу. False — інакше.\n",[3699,4675,4677],{"class":3701,"line":4676},37,[3699,4678,4465],{"class":4324},[3699,4680,4682,4684,4686,4688,4690],{"class":3701,"line":4681},38,[3699,4683,4522],{"class":4422},[3699,4685,4525],{"class":4324},[3699,4687,4528],{"class":4422},[3699,4689,4532],{"class":4531},[3699,4691,4535],{"class":4422},[3699,4693,4695,4698,4700],{"class":3701,"line":4694},39,[3699,4696,4697],{"class":4422},"    hashed_bytes = hashed_password.encode(",[3699,4699,4525],{"class":4324},[3699,4701,4562],{"class":4422},[3699,4703,4705],{"class":3701,"line":4704},40,[3699,4706,4540],{"class":4422},[3699,4708,4710,4712],{"class":3701,"line":4709},41,[3699,4711,4576],{"class":4418},[3699,4713,4714],{"class":4422}," bcrypt.checkpw(pwd_bytes, hashed_bytes)\n",[4716,4717,4718,4721,4722,4724],"tip",{},[3560,4719,4720],{},"Чому саме rounds=12?","\nПараметр ",[3625,4723,4553],{}," визначає обчислювальну складність операції хешування (кількість ітерацій дорівнює $2^{12} = 4096$). Значення 12 є оптимальним стандартом на сьогодні: воно займає близько 150-200 мс на сучасному процесорі, що ускладнює brute-force атаки на відеокартах, але є непомітним для користувача під час авторизації.",[3654,4726],{},[3661,4728,4730,4731,4733,4734],{"id":4729},"спадкова-альтернатива-використання-passlib-та-cryptcontext","Спадкова альтернатива: використання ",[3625,4732,4273],{}," та ",[3625,4735,4277],{},[3556,4737,4738,4739,4741,4742,4744,4745,4380],{},"Якщо у вашому проєкті вже використовується ",[3625,4740,4273],{}," і швидка міграція на прямий ",[3625,4743,4006],{}," неможлива, ви можете налаштувати контекст через ",[3625,4746,4277],{},[4194,4748,4749,4755],{},[3556,4750,4751,4752,4754],{},"При виникненні помилки ",[3625,4753,4294],{}," у вас є два варіанти швидкого вирішення:",[4756,4757,4758,4769],"ol",{},[4759,4760,4761,4764,4765,4768],"li",{},[3560,4762,4763],{},"Знизити версію bcrypt:"," встановити старішу версію бібліотеки (наприклад, ",[3625,4766,4767],{},"pip install bcrypt==3.2.2","). Цей варіант є тимчасовим фіксом, але не рекомендується з міркувань безпеки.",[4759,4770,4771,4774,4775,4778,4779,4380],{},[3560,4772,4773],{},"Перейти на Argon2id:"," встановити ",[3625,4776,4777],{},"argon2-cffi"," і використовувати схему ",[3625,4780,4781],{},"\"argon2\"",[3556,4783,4784,4785,4400],{},"Для ознайомлення нижче наведено опис роботи з ",[3625,4786,4277],{},[4363,4788,4790,4791],{"id":4789},"клас-cryptcontext","Клас ",[3625,4792,4277],{},[3556,4794,4795,4796,4284,4798,4802],{},"Центральним об'єктом ",[3625,4797,4273],{},[3560,4799,4800],{},[3625,4801,4277],{}," — контекст, що інкапсулює вибір алгоритму, параметри та логіку верифікації.",[4238,4804,4805,4820,4828,4837],{},[4241,4806,4808,4811],{"name":4277,"type":4807},"клас passlib.context",[3556,4809,4810],{},"Менеджер криптографічного контексту для хешування паролів. Підтримує кілька схем одночасно та автоматичне оновлення застарілих хешів.",[3556,4812,4813,4816,4817],{},[3560,4814,4815],{},"Імпорт:"," ",[3625,4818,4819],{},"from passlib.context import CryptContext",[4241,4821,4824,4825,4380],{"name":4822,"type":4823},"schemes","list[str]","Список підтримуваних алгоритмів. Перший у списку є алгоритмом за замовчуванням для нових хешів. Приклад: ",[3625,4826,4827],{},"[\"bcrypt\", \"argon2\"]",[4241,4829,4832,4833,4836],{"name":4830,"type":4831},"deprecated","str | list[str]","Алгоритми, що вважаються застарілими. Паролі, захешовані цими алгоритмами, будуть позначені як такі, що потребують перехешування. Значення ",[3625,4834,4835],{},"\"auto\""," автоматично позначає всі схеми, крім активної.",[4241,4838,4840],{"name":4839,"type":4254},"bcrypt\\_\\_rounds","Work factor для bcrypt. За замовчуванням 12. Кожна одиниця подвоює час обчислення. Для production рекомендовано 12–14.",[3556,4842,4843],{},[3560,4844,4845,4846,4400],{},"Ключові методи ",[3625,4847,4277],{},[4238,4849,4850,4881,4914],{},[4241,4851,4854,4861],{"name":4852,"type":4853},"hash(secret)","метод → str",[3556,4855,4856,4857,4860],{},"Хешує пароль ",[3625,4858,4859],{},"secret"," з використанням активного алгоритму та автоматично генерованим salt. Повертає повний рядок хешу, що містить всю необхідну інформацію.",[3690,4862,4864],{"className":4403,"code":4863,"language":4405,"meta":3695,"style":3695},"hashed = pwd_context.hash(\"mysecretpassword\")\n# Результат: \"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW\"\n",[3625,4865,4866,4876],{"__ignoreMap":3695},[3699,4867,4868,4871,4874],{"class":3701,"line":3702},[3699,4869,4870],{"class":4422},"hashed = pwd_context.hash(",[3699,4872,4873],{"class":4324},"\"mysecretpassword\"",[3699,4875,4562],{"class":4422},[3699,4877,4878],{"class":3701,"line":3708},[3699,4879,4880],{"class":4412},"# Результат: \"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW\"\n",[4241,4882,4885,4894],{"name":4883,"type":4884},"verify(secret, hash)","метод → bool",[3556,4886,4887,4888,4890,4891,4893],{},"Перевіряє, чи відповідає пароль ",[3625,4889,4859],{}," раніше збереженому хешу ",[3625,4892,4258],{},". Автоматично визначає алгоритм та параметри з самого рядка хешу.",[3690,4895,4897],{"className":4403,"code":4896,"language":4405,"meta":3695,"style":3695},"is_valid = pwd_context.verify(\"mysecretpassword\", hashed_from_db)\n# True або False\n",[3625,4898,4899,4909],{"__ignoreMap":3695},[3699,4900,4901,4904,4906],{"class":3701,"line":3702},[3699,4902,4903],{"class":4422},"is_valid = pwd_context.verify(",[3699,4905,4873],{"class":4324},[3699,4907,4908],{"class":4422},", hashed_from_db)\n",[3699,4910,4911],{"class":3701,"line":3708},[3699,4912,4913],{"class":4412},"# True або False\n",[4241,4915,4918,4939],{"name":4916,"type":4917},"verify_and_update(secret, hash)","метод → tuple[bool, str | None]",[3556,4919,4920,4921,4924,4925,4928,4929,4932,4933,4935,4936,4380],{},"Розширена версія ",[3625,4922,4923],{},"verify",". Повертає кортеж ",[3625,4926,4927],{},"(is_valid, new_hash)",". Якщо хеш застарів (використовує deprecated алгоритм або застарілі параметри), ",[3625,4930,4931],{},"new_hash"," буде новим хешем — зберегти у БД. Якщо хеш актуальний — ",[3625,4934,4931],{}," буде ",[3625,4937,4938],{},"None",[3690,4940,4942],{"className":4403,"code":4941,"language":4405,"meta":3695,"style":3695},"is_valid, new_hash = pwd_context.verify_and_update(password, hash_from_db)\nif new_hash:\n    # зберегти new_hash у базі даних\n    await user_repo.update_password(user_id, new_hash)\n",[3625,4943,4944,4949,4957,4962],{"__ignoreMap":3695},[3699,4945,4946],{"class":3701,"line":3702},[3699,4947,4948],{"class":4422},"is_valid, new_hash = pwd_context.verify_and_update(password, hash_from_db)\n",[3699,4950,4951,4954],{"class":3701,"line":3708},[3699,4952,4953],{"class":4418},"if",[3699,4955,4956],{"class":4422}," new_hash:\n",[3699,4958,4959],{"class":3701,"line":3714},[3699,4960,4961],{"class":4412},"    # зберегти new_hash у базі даних\n",[3699,4963,4964,4967],{"class":3701,"line":3720},[3699,4965,4966],{"class":4418},"    await",[3699,4968,4969],{"class":4422}," user_repo.update_password(user_id, new_hash)\n",[4716,4971,4972,4977,5007],{},[3556,4973,4974],{},[3560,4975,4976],{},"Порівняння з ASP.NET Identity:",[3556,4978,4979,4980,4983,4984,4987,4988,4991,4992,4995,4996,4999,5000,5003,5004,4380],{},"В ASP.NET Identity за хешування паролів відповідає клас ",[3625,4981,4982],{},"PasswordHasher\u003CTUser>",", що реалізує інтерфейс ",[3625,4985,4986],{},"IPasswordHasher\u003CTUser>",". За замовчуванням він використовує ",[3560,4989,4990],{},"PBKDF2"," з HMAC-SHA512, 100 000 ітерацій і 128-бітний salt. Метод ",[3625,4993,4994],{},"HashPassword()"," аналогічний ",[3625,4997,4998],{},"pwd_context.hash()",", а ",[3625,5001,5002],{},"VerifyHashedPassword()"," — ",[3625,5005,5006],{},"pwd_context.verify()",[3556,5008,5009,5010,5013,5014,5017],{},"Ключова відмінність: ASP.NET Identity повертає ",[3625,5011,5012],{},"PasswordVerificationResult.SuccessRehashNeeded"," якщо хеш застарів — аналог ",[3625,5015,5016],{},"verify_and_update()"," у passlib.",[3654,5019],{},[3661,5021,5023],{"id":5022},"поглиблено-як-влаштований-bcrypt-зсередини","Поглиблено: як влаштований bcrypt зсередини",[3556,5025,5026],{},"Щоб bcrypt не залишався для вас «чорною скринькою», варто зрозуміти, як він влаштований. Це допоможе приймати свідомі архітектурні рішення: який work factor обрати, чому bcrypt важко атакувати GPU, що саме зберігається у рядку хешу.",[4363,5028,5030],{"id":5029},"походження-шифр-blowfish","Походження: шифр Blowfish",[3556,5032,5033,5034,5037,5038,5041],{},"bcrypt був запропонований у 1999 році Niels Provos та David Mazières. В основі алгоритму лежить ",[3560,5035,5036],{},"симетричний блоковий шифр Blowfish",", розроблений Брюсом Шнаєром у 1993 році. Але bcrypt не просто шифрує пароль через Blowfish — він використовує спеціально модифіковану, навмисно дорогу версію процедури розгортання ключів, що отримала назву ",[3560,5039,5040],{},"Eksblowfish"," (Expensive Key Schedule Blowfish).",[3687,5043,5044],{},[3690,5045,5047],{"className":3692,"code":5046,"language":3694,"meta":3695,"style":3695},"@startuml\nskinparam style plain\nskinparam backgroundColor #ffffff\nskinparam defaultFontSize 12\nskinparam padding 8\n\ntitle Алгоритм bcrypt: загальна схема\n\nstart\n\n:Вхідні дані:\n- password (до 72 байт)\n- salt (128 біт \u002F 16 байт)\n- cost factor (N);\n\n:EksBlowfishSetup(cost, salt, password)\n────────────────────────────────\n① Ініціалізація P-масиву (18 × 32-біт)\n   та S-boxes (4 × 256 × 32-біт)\n   константами числа π;\n\n:② Розгортання ключа з паролем:\nExpandKey(state, 0, password)\n— XOR P-масиву з байтами пароля;\n\n:③ Дорогий цикл: 2^cost ітерацій\nExpandKey(state, 0, salt)\nExpandKey(state, 0, password)\n— повторювати 2^N разів;\n\n:④ Шифрування магічного рядка:\nctext = \"OrpheanBeholderScryDoubt\"\n— 64-бітними блоками Blowfish\n— 64 рази;\n\n:Вихід: 24 байти (192 біти) хешу;\n\nstop\n@enduml\n",[3625,5048,5049,5053,5057,5061,5066,5071,5075,5080,5084,5089,5093,5098,5103,5108,5113,5117,5122,5127,5132,5137,5142,5146,5151,5156,5161,5165,5170,5175,5179,5184,5188,5193,5198,5203,5208,5212,5217,5221,5226],{"__ignoreMap":3695},[3699,5050,5051],{"class":3701,"line":3702},[3699,5052,3705],{},[3699,5054,5055],{"class":3701,"line":3708},[3699,5056,3711],{},[3699,5058,5059],{"class":3701,"line":3714},[3699,5060,3717],{},[3699,5062,5063],{"class":3701,"line":3720},[3699,5064,5065],{},"skinparam defaultFontSize 12\n",[3699,5067,5068],{"class":3701,"line":3726},[3699,5069,5070],{},"skinparam padding 8\n",[3699,5072,5073],{"class":3701,"line":3733},[3699,5074,3730],{"emptyLinePlaceholder":3729},[3699,5076,5077],{"class":3701,"line":3739},[3699,5078,5079],{},"title Алгоритм bcrypt: загальна схема\n",[3699,5081,5082],{"class":3701,"line":3745},[3699,5083,3730],{"emptyLinePlaceholder":3729},[3699,5085,5086],{"class":3701,"line":3751},[3699,5087,5088],{},"start\n",[3699,5090,5091],{"class":3701,"line":3757},[3699,5092,3730],{"emptyLinePlaceholder":3729},[3699,5094,5095],{"class":3701,"line":3763},[3699,5096,5097],{},":Вхідні дані:\n",[3699,5099,5100],{"class":3701,"line":3769},[3699,5101,5102],{},"- password (до 72 байт)\n",[3699,5104,5105],{"class":3701,"line":3775},[3699,5106,5107],{},"- salt (128 біт \u002F 16 байт)\n",[3699,5109,5110],{"class":3701,"line":3781},[3699,5111,5112],{},"- cost factor (N);\n",[3699,5114,5115],{"class":3701,"line":3786},[3699,5116,3730],{"emptyLinePlaceholder":3729},[3699,5118,5119],{"class":3701,"line":3792},[3699,5120,5121],{},":EksBlowfishSetup(cost, salt, password)\n",[3699,5123,5124],{"class":3701,"line":3798},[3699,5125,5126],{},"────────────────────────────────\n",[3699,5128,5129],{"class":3701,"line":3804},[3699,5130,5131],{},"① Ініціалізація P-масиву (18 × 32-біт)\n",[3699,5133,5134],{"class":3701,"line":3810},[3699,5135,5136],{},"   та S-boxes (4 × 256 × 32-біт)\n",[3699,5138,5139],{"class":3701,"line":3816},[3699,5140,5141],{},"   константами числа π;\n",[3699,5143,5144],{"class":3701,"line":3822},[3699,5145,3730],{"emptyLinePlaceholder":3729},[3699,5147,5148],{"class":3701,"line":3827},[3699,5149,5150],{},":② Розгортання ключа з паролем:\n",[3699,5152,5153],{"class":3701,"line":3832},[3699,5154,5155],{},"ExpandKey(state, 0, password)\n",[3699,5157,5158],{"class":3701,"line":3837},[3699,5159,5160],{},"— XOR P-масиву з байтами пароля;\n",[3699,5162,5163],{"class":3701,"line":3843},[3699,5164,3730],{"emptyLinePlaceholder":3729},[3699,5166,5167],{"class":3701,"line":4590},[3699,5168,5169],{},":③ Дорогий цикл: 2^cost ітерацій\n",[3699,5171,5172],{"class":3701,"line":4595},[3699,5173,5174],{},"ExpandKey(state, 0, salt)\n",[3699,5176,5177],{"class":3701,"line":4627},[3699,5178,5155],{},[3699,5180,5181],{"class":3701,"line":4632},[3699,5182,5183],{},"— повторювати 2^N разів;\n",[3699,5185,5186],{"class":3701,"line":4638},[3699,5187,3730],{"emptyLinePlaceholder":3729},[3699,5189,5190],{"class":3701,"line":4643},[3699,5191,5192],{},":④ Шифрування магічного рядка:\n",[3699,5194,5195],{"class":3701,"line":4648},[3699,5196,5197],{},"ctext = \"OrpheanBeholderScryDoubt\"\n",[3699,5199,5200],{"class":3701,"line":4654},[3699,5201,5202],{},"— 64-бітними блоками Blowfish\n",[3699,5204,5205],{"class":3701,"line":4660},[3699,5206,5207],{},"— 64 рази;\n",[3699,5209,5210],{"class":3701,"line":4665},[3699,5211,3730],{"emptyLinePlaceholder":3729},[3699,5213,5214],{"class":3701,"line":4670},[3699,5215,5216],{},":Вихід: 24 байти (192 біти) хешу;\n",[3699,5218,5219],{"class":3701,"line":4676},[3699,5220,3730],{"emptyLinePlaceholder":3729},[3699,5222,5223],{"class":3701,"line":4681},[3699,5224,5225],{},"stop\n",[3699,5227,5228],{"class":3701,"line":4694},[3699,5229,3846],{},[3556,5231,5232,5233,5236,5237,5240,5241,5244,5245,5248,5249,5252,5253,4380],{},"Зверніть увагу на крок ③: саме там знаходиться весь «секрет» повільності bcrypt. Цикл виконується ",[3560,5234,5235],{},"2^N разів",", де N — ваш ",[3625,5238,5239],{},"cost"," (work factor). При ",[3625,5242,5243],{},"cost=12"," це 4096 ітерацій; при ",[3625,5246,5247],{},"cost=13"," — вже 8192. Кожне збільшення cost на 1 ",[3560,5250,5251],{},"подвоює"," час обчислення. Цей підхід ще називають ",[3560,5254,5255],{},"key stretching",[4363,5257,5259],{"id":5258},"ліміт-у-72-байти-підводний-камінь","Ліміт у 72 байти — підводний камінь",[3556,5261,5262,5263,5266,5267,4380],{},"Одна з небагатьох справжніх слабкостей bcrypt, яку важливо знати: алгоритм ",[3560,5264,5265],{},"обрізає пароль до 72 байтів",". Якщо користувач має пароль довший за 72 символи ASCII (або менше символів Unicode, якщо вони багатобайтні), все, що виходить за межі, ",[3560,5268,5269],{},"ігнорується",[3556,5271,5272,5273,5276,5277,5280,5281,4380],{},"На практиці це означає, що паролі ",[3625,5274,5275],{},"correcthorsebatterystaple12345678901234567890123456789012345678901234"," (73 символи) та ",[3625,5278,5279],{},"correcthorsebatterystaple1234567890123456789012345678901234567890123"," (72 символи) дадуть ",[3560,5282,5283],{},"однаковий хеш",[4194,5285,5286],{},"Якщо ваш додаток дозволяє паролі довші за 72 символи і використовує bcrypt — повідомте користувачів про це обмеження. Практичне рішення: попередньо хешувати пароль через SHA-256 і передавати хеш у bcrypt. Але це ускладнює систему. Для нових проєктів розгляньте Argon2id, що не має такого обмеження.",[4363,5288,5290],{"id":5289},"анатомія-рядка-хешу-bcrypt","Анатомія рядка хешу bcrypt",[3556,5292,5293,5294,5297],{},"Результат хешування через bcrypt — це не просто набір байтів. Це ",[3560,5295,5296],{},"самодостатній рядок",", що кодує у собі всю інформацію, необхідну для верифікації. Зберігаючи цей рядок у базі даних, вам більше нічого не потрібно.",[3556,5299,5300],{},"Розберемо приклад реального хешу:",[3690,5302,5307],{"className":5303,"code":5305,"language":5306},[5304],"language-text","$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW\n","text",[3625,5308,5305],{"__ignoreMap":3695},[3687,5310,5311],{},[3690,5312,5314],{"className":3692,"code":5313,"language":3694,"meta":3695,"style":3695},"@startuml\nskinparam style plain\nskinparam backgroundColor #ffffff\nskinparam defaultFontSize 13\n\nrectangle \"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW\" as full #eeeeee\n\nrectangle \"\u003Cb>$2b$\u003C\u002Fb>\\nВерсія алгоритму\" as v #cce5ff\nrectangle \"\u003Cb>12\u003C\u002Fb>\\nWork factor\\n(cost = 12,\\nтобто 2^12 = 4096 ітерацій)\" as c #ccffcc\nrectangle \"\u003Cb>EixZaYVK1fsbw1ZfbX3OXe\u003C\u002Fb>\\nSalt (22 символи Base64\\n= 128 біт)\" as s #fff3cc\nrectangle \"\u003Cb>PaWxn96p36WQoeG6Lruj3vjPGga31lW\u003C\u002Fb>\\nХеш (31 символ Base64\\n= 184 біти з 192)\" as h #ffcccc\n\nfull -down-> v\nfull -down-> c\nfull -down-> s\nfull -down-> h\n@enduml\n",[3625,5315,5316,5320,5324,5328,5332,5336,5341,5345,5350,5355,5360,5365,5369,5374,5379,5384,5389],{"__ignoreMap":3695},[3699,5317,5318],{"class":3701,"line":3702},[3699,5319,3705],{},[3699,5321,5322],{"class":3701,"line":3708},[3699,5323,3711],{},[3699,5325,5326],{"class":3701,"line":3714},[3699,5327,3717],{},[3699,5329,5330],{"class":3701,"line":3720},[3699,5331,3723],{},[3699,5333,5334],{"class":3701,"line":3726},[3699,5335,3730],{"emptyLinePlaceholder":3729},[3699,5337,5338],{"class":3701,"line":3733},[3699,5339,5340],{},"rectangle \"$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW\" as full #eeeeee\n",[3699,5342,5343],{"class":3701,"line":3739},[3699,5344,3730],{"emptyLinePlaceholder":3729},[3699,5346,5347],{"class":3701,"line":3745},[3699,5348,5349],{},"rectangle \"\u003Cb>$2b$\u003C\u002Fb>\\nВерсія алгоритму\" as v #cce5ff\n",[3699,5351,5352],{"class":3701,"line":3751},[3699,5353,5354],{},"rectangle \"\u003Cb>12\u003C\u002Fb>\\nWork factor\\n(cost = 12,\\nтобто 2^12 = 4096 ітерацій)\" as c #ccffcc\n",[3699,5356,5357],{"class":3701,"line":3757},[3699,5358,5359],{},"rectangle \"\u003Cb>EixZaYVK1fsbw1ZfbX3OXe\u003C\u002Fb>\\nSalt (22 символи Base64\\n= 128 біт)\" as s #fff3cc\n",[3699,5361,5362],{"class":3701,"line":3763},[3699,5363,5364],{},"rectangle \"\u003Cb>PaWxn96p36WQoeG6Lruj3vjPGga31lW\u003C\u002Fb>\\nХеш (31 символ Base64\\n= 184 біти з 192)\" as h #ffcccc\n",[3699,5366,5367],{"class":3701,"line":3769},[3699,5368,3730],{"emptyLinePlaceholder":3729},[3699,5370,5371],{"class":3701,"line":3775},[3699,5372,5373],{},"full -down-> v\n",[3699,5375,5376],{"class":3701,"line":3781},[3699,5377,5378],{},"full -down-> c\n",[3699,5380,5381],{"class":3701,"line":3786},[3699,5382,5383],{},"full -down-> s\n",[3699,5385,5386],{"class":3701,"line":3792},[3699,5387,5388],{},"full -down-> h\n",[3699,5390,5391],{"class":3701,"line":3798},[3699,5392,3846],{},[4238,5394,5395,5426,5430,5439],{},[4241,5396,5399,5400,5403,5404,5407,5408,3632,5411,5414,5415,5003,5417,5420,5421,5423,5424,4380],{"name":5397,"type":5398},"$2b$","Версія алгоритму (4 символи)","Префікс, що ідентифікує алгоритм та його версію. Існують варіанти ",[3625,5401,5402],{},"$2$"," (оригінальний), ",[3625,5405,5406],{},"$2a$"," (виправлена специфікація 2011 р.), ",[3625,5409,5410],{},"$2x$",[3625,5412,5413],{},"$2y$"," (OpenBSD-специфічні виправлення) та ",[3625,5416,5397],{},[3560,5418,5419],{},"рекомендована поточна версія",", виправлена у 2014 р. Бібліотека ",[3625,5422,4273],{}," за замовчуванням генерує ",[3625,5425,5397],{},[4241,5427,5429],{"name":4559,"type":5428},"Work factor (2 символи)","Числова вартість — параметр N у формулі 2^N ітерацій. Зберігається у хеші, щоб при верифікації алгоритм знав, скільки ітерацій використовувалося при хешуванні. Дозволяє поступово збільшувати складність у майбутньому без інвалідації старих хешів.",[4241,5431,5434,5435,5438],{"name":5432,"type":5433},"EixZaYVK1fsbw1ZfbX3OXe","Salt (22 символи Radix-64)","Випадково згенерований 128-бітний salt, закодований у модифікованій Base64 (алфавіт ",[3625,5436,5437],{},".\u002FABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789","). Рівно 22 символи = 132 біти, з яких реально використовуються 128 (останні 4 біти ігноруються через особливості кодування).",[4241,5440,5443,5444,5447],{"name":5441,"type":5442},"PaWxn96p36WQoeG6Lruj3vjPGga31lW","Хеш (31 символ Radix-64)","Власне криптографічний хеш — результат 64-разового Blowfish-шифрування магічного рядка ",[3625,5445,5446],{},"OrpheanBeholderScryDoubt",". 31 символ кодує 184 біти з 192 (останні 8 біт відкидаються). Саме цей сегмент порівнюється при верифікації.",[3556,5449,5450,5451,5454,5455,5458,5459,5462],{},"Весь рядок разом має ",[3560,5452,5453],{},"60 символів",". Це завжди фіксована довжина для bcrypt — незалежно від довжини вхідного пароля. Ця властивість сама по собі корисна: розмір стовпця у базі даних завжди відомий наперед (",[3625,5456,5457],{},"CHAR(60)"," або ",[3625,5460,5461],{},"VARCHAR(60)",").",[4363,5464,5466],{"id":5465},"work-factor-скільки-обрати","Work factor: скільки обрати?",[3556,5468,5469,5470,5473],{},"Вибір work factor — це баланс між безпекою та продуктивністю. Мета: щоб одне хешування займало ",[3560,5471,5472],{},"~100–250 мс"," на вашому production-сервері. Цього достатньо, щоб зробити перебір практично неможливим, але не занадто довго для легітимних користувачів.",[3862,5475,5476,5492],{},[3865,5477,5478],{},[3868,5479,5480,5483,5486,5489],{},[3871,5481,5482],{},"Work factor (cost)",[3871,5484,5485],{},"Ітерацій (2^N)",[3871,5487,5488],{},"Час на сучасному CPU",[3871,5490,5491],{},"Хешів\u002Fсек (атака)",[3896,5493,5494,5507,5521,5542,5556,5570],{},[3868,5495,5496,5499,5502,5504],{},[3901,5497,5498],{},"10",[3901,5500,5501],{},"1 024",[3901,5503,4015],{},[3901,5505,5506],{},"~25\u002Fс",[3868,5508,5509,5512,5515,5518],{},[3901,5510,5511],{},"11",[3901,5513,5514],{},"2 048",[3901,5516,5517],{},"~80 мс",[3901,5519,5520],{},"~12\u002Fс",[3868,5522,5523,5527,5532,5536],{},[3901,5524,5525],{},[3560,5526,4559],{},[3901,5528,5529],{},[3560,5530,5531],{},"4 096",[3901,5533,5534],{},[3560,5535,4044],{},[3901,5537,5538,5541],{},[3560,5539,5540],{},"~6\u002Fс"," ← рекомендовано",[3868,5543,5544,5547,5550,5553],{},[3901,5545,5546],{},"13",[3901,5548,5549],{},"8 192",[3901,5551,5552],{},"~320 мс",[3901,5554,5555],{},"~3\u002Fс",[3868,5557,5558,5561,5564,5567],{},[3901,5559,5560],{},"14",[3901,5562,5563],{},"16 384",[3901,5565,5566],{},"~640 мс",[3901,5568,5569],{},"~1.5\u002Fс",[3868,5571,5572,5575,5578,5581],{},[3901,5573,5574],{},"16",[3901,5576,5577],{},"65 536",[3901,5579,5580],{},"~2.5 с",[3901,5582,5583],{},"~0.4\u002Fс",[4716,5585,5586,5589,5590,5592,5593,4380],{},[3560,5587,5588],{},"Правило",": раз на кілька років переглядайте work factor. Оскільки CPU-потужності зростають, те, що вчора займало 150 мс, завтра займатиме 80 мс. Завдяки тому, що cost зберігається у хеші, ви можете плавно мігрувати: при наступному вході користувача перехешуйте його пароль з новим cost. ",[3625,5591,4273],{}," робить це автоматично через ",[3625,5594,5016],{},[4363,5596,5598],{"id":5597},"чому-bcrypt-важко-атакувати-gpu","Чому bcrypt важко атакувати GPU",[3556,5600,5601],{},"Сучасні GPU мають тисячі паралельних ядер і ідеально підходять для атак на SHA-256: можна перевіряти мільярди паролів за секунду. Bcrypt спеціально спроектований, щоб цьому протистояти.",[3556,5603,5604,5605,5608],{},"Головна причина — ",[3560,5606,5607],{},"великий розмір стану",". Алгоритм Eksblowfish оперує таблицями розміром ~4 КБ (P-масив + S-boxes). Кожне ядро GPU має дуже мало швидкої пам'яті (регістрів) — набагато менше, ніж потрібно для паралельного зберігання кількох станів bcrypt. Це змушує GPU або обчислювати bcrypt послідовно (не використовуючи паралелізм), або постійно читати з повільної відеопам'яті.",[3556,5610,5611],{},"Наслідок: bcrypt на GPU дає прискорення лише в 10–50 разів порівняно з CPU, а не в тисячі разів, як для SHA-256. Для практичних атак це фактично зводить nance GPU до нуля.",[3687,5613,5614],{},[3690,5615,5617],{"className":3692,"code":5616,"language":3694,"meta":3695,"style":3695},"@startuml\nskinparam style plain\nskinparam backgroundColor #ffffff\nskinparam defaultFontSize 12\n\ntitle Швидкість атак: bcrypt vs SHA-256 (cost=12)\n\nrectangle \"SHA-256\" as sha #ffcccc {\n  note as n1\n    CPU: ~500 мільйонів хешів\u002Fсек\n    GPU (RTX 4090): ~22 мільярди хешів\u002Fсек\n    Час на злам 8-символьного пароля: хвилини\n  end note\n}\n\nrectangle \"bcrypt (cost=12)\" as bc #ccffcc {\n  note as n2\n    CPU: ~6 хешів\u002Fсек\n    GPU (RTX 4090): ~100–300 хешів\u002Fсек\n    Час на злам 8-символьного пароля: роки\n  end note\n}\n\nsha -right[hidden]-> bc\n@enduml\n",[3625,5618,5619,5623,5627,5631,5635,5639,5644,5648,5653,5657,5662,5667,5672,5676,5680,5684,5689,5693,5698,5703,5708,5712,5716,5720,5725],{"__ignoreMap":3695},[3699,5620,5621],{"class":3701,"line":3702},[3699,5622,3705],{},[3699,5624,5625],{"class":3701,"line":3708},[3699,5626,3711],{},[3699,5628,5629],{"class":3701,"line":3714},[3699,5630,3717],{},[3699,5632,5633],{"class":3701,"line":3720},[3699,5634,5065],{},[3699,5636,5637],{"class":3701,"line":3726},[3699,5638,3730],{"emptyLinePlaceholder":3729},[3699,5640,5641],{"class":3701,"line":3733},[3699,5642,5643],{},"title Швидкість атак: bcrypt vs SHA-256 (cost=12)\n",[3699,5645,5646],{"class":3701,"line":3739},[3699,5647,3730],{"emptyLinePlaceholder":3729},[3699,5649,5650],{"class":3701,"line":3745},[3699,5651,5652],{},"rectangle \"SHA-256\" as sha #ffcccc {\n",[3699,5654,5655],{"class":3701,"line":3751},[3699,5656,3742],{},[3699,5658,5659],{"class":3701,"line":3757},[3699,5660,5661],{},"    CPU: ~500 мільйонів хешів\u002Fсек\n",[3699,5663,5664],{"class":3701,"line":3763},[3699,5665,5666],{},"    GPU (RTX 4090): ~22 мільярди хешів\u002Fсек\n",[3699,5668,5669],{"class":3701,"line":3769},[3699,5670,5671],{},"    Час на злам 8-символьного пароля: хвилини\n",[3699,5673,5674],{"class":3701,"line":3775},[3699,5675,3772],{},[3699,5677,5678],{"class":3701,"line":3781},[3699,5679,3778],{},[3699,5681,5682],{"class":3701,"line":3786},[3699,5683,3730],{"emptyLinePlaceholder":3729},[3699,5685,5686],{"class":3701,"line":3792},[3699,5687,5688],{},"rectangle \"bcrypt (cost=12)\" as bc #ccffcc {\n",[3699,5690,5691],{"class":3701,"line":3798},[3699,5692,3795],{},[3699,5694,5695],{"class":3701,"line":3804},[3699,5696,5697],{},"    CPU: ~6 хешів\u002Fсек\n",[3699,5699,5700],{"class":3701,"line":3810},[3699,5701,5702],{},"    GPU (RTX 4090): ~100–300 хешів\u002Fсек\n",[3699,5704,5705],{"class":3701,"line":3816},[3699,5706,5707],{},"    Час на злам 8-символьного пароля: роки\n",[3699,5709,5710],{"class":3701,"line":3822},[3699,5711,3772],{},[3699,5713,5714],{"class":3701,"line":3827},[3699,5715,3778],{},[3699,5717,5718],{"class":3701,"line":3832},[3699,5719,3730],{"emptyLinePlaceholder":3729},[3699,5721,5722],{"class":3701,"line":3837},[3699,5723,5724],{},"sha -right[hidden]-> bc\n",[3699,5726,5727],{"class":3701,"line":3843},[3699,5728,3846],{},[3661,5730,5732,5733,5735],{"id":5731},"бібліотека-argon2-cffi-переможець-phc","Бібліотека ",[3625,5734,4777],{}," — переможець PHC",[3556,5737,5738,5739,5741,5742,5745,5746,5753,5754,5757,5758,5761,5762,5765],{},"Якщо ",[3625,5740,4006],{}," є перевіреним вибором, то ",[3560,5743,5744],{},"Argon2"," — переможцем ",[5747,5748,5752],"a",{"href":5749,"rel":5750},"https:\u002F\u002Fwww.password-hashing.net\u002F",[5751],"nofollow","Password Hashing Competition 2015"," і рекомендованим стандартом OWASP для нових систем. Він має три варіанти: ",[3625,5755,5756],{},"argon2d"," (стійкий до GPU), ",[3625,5759,5760],{},"argon2i"," (стійкий до side-channel атак) та ",[3625,5763,5764],{},"argon2id"," (гібридний, рекомендований).",[4304,5767,5768,5784,5799],{},[4307,5769,5770],{"label":4309},[3690,5771,5773],{"className":4312,"code":5772,"language":4314,"meta":3695,"style":3695},"pip install argon2-cffi\n",[3625,5774,5775],{"__ignoreMap":3695},[3699,5776,5777,5779,5781],{"class":3701,"line":3702},[3699,5778,4309],{"class":4321},[3699,5780,4325],{"class":4324},[3699,5782,5783],{"class":4324}," argon2-cffi\n",[4307,5785,5786],{"label":4331},[3690,5787,5789],{"className":4312,"code":5788,"language":4314,"meta":3695,"style":3695},"uv add argon2-cffi\n",[3625,5790,5791],{"__ignoreMap":3695},[3699,5792,5793,5795,5797],{"class":3701,"line":3702},[3699,5794,4331],{"class":4321},[3699,5796,4343],{"class":4324},[3699,5798,5783],{"class":4324},[4307,5800,5801],{"label":4348},[3690,5802,5804],{"className":4312,"code":5803,"language":4314,"meta":3695,"style":3695},"poetry add argon2-cffi\n",[3625,5805,5806],{"__ignoreMap":3695},[3699,5807,5808,5810,5812],{"class":3701,"line":3702},[3699,5809,4348],{"class":4321},[3699,5811,4343],{"class":4324},[3699,5813,5783],{"class":4324},[3690,5815,5817],{"className":4403,"code":5816,"language":4405,"meta":3695,"style":3695},"# Альтернативна реалізація через argon2-cffi\nfrom argon2 import PasswordHasher\nfrom argon2.exceptions import VerifyMismatchError\n\n# PasswordHasher з параметрами за замовчуванням OWASP\nph = PasswordHasher(\n    time_cost=2,       # кількість ітерацій\n    memory_cost=65536, # 64 MB пам'яті (захист від GPU)\n    parallelism=2,     # паралельні потоки\n    hash_len=32,       # довжина хешу в байтах\n    salt_len=16,       # довжина salt\n)\n\nhashed = ph.hash(\"mysecretpassword\")\n\ntry:\n    ph.verify(hashed, \"mysecretpassword\")  # True або виняток\n    # Перевірка чи потрібне перехешування\n    if ph.check_needs_rehash(hashed):\n        new_hash = ph.hash(\"mysecretpassword\")\n        # Зберегти new_hash у БД\nexcept VerifyMismatchError:\n    # Неправильний пароль\n    pass\n",[3625,5818,5819,5824,5837,5849,5853,5858,5863,5879,5894,5909,5924,5938,5942,5946,5955,5959,5966,5979,5984,5992,6001,6006,6014,6019],{"__ignoreMap":3695},[3699,5820,5821],{"class":3701,"line":3702},[3699,5822,5823],{"class":4412},"# Альтернативна реалізація через argon2-cffi\n",[3699,5825,5826,5829,5832,5834],{"class":3701,"line":3708},[3699,5827,5828],{"class":4418},"from",[3699,5830,5831],{"class":4422}," argon2 ",[3699,5833,4419],{"class":4418},[3699,5835,5836],{"class":4422}," PasswordHasher\n",[3699,5838,5839,5841,5844,5846],{"class":3701,"line":3714},[3699,5840,5828],{"class":4418},[3699,5842,5843],{"class":4422}," argon2.exceptions ",[3699,5845,4419],{"class":4418},[3699,5847,5848],{"class":4422}," VerifyMismatchError\n",[3699,5850,5851],{"class":3701,"line":3720},[3699,5852,3730],{"emptyLinePlaceholder":3729},[3699,5854,5855],{"class":3701,"line":3726},[3699,5856,5857],{"class":4412},"# PasswordHasher з параметрами за замовчуванням OWASP\n",[3699,5859,5860],{"class":3701,"line":3733},[3699,5861,5862],{"class":4422},"ph = PasswordHasher(\n",[3699,5864,5865,5868,5870,5873,5876],{"class":3701,"line":3739},[3699,5866,5867],{"class":4445},"    time_cost",[3699,5869,4556],{"class":4422},[3699,5871,5872],{"class":4531},"2",[3699,5874,5875],{"class":4422},",       ",[3699,5877,5878],{"class":4412},"# кількість ітерацій\n",[3699,5880,5881,5884,5886,5889,5891],{"class":3701,"line":3745},[3699,5882,5883],{"class":4445},"    memory_cost",[3699,5885,4556],{"class":4422},[3699,5887,5888],{"class":4531},"65536",[3699,5890,4200],{"class":4422},[3699,5892,5893],{"class":4412},"# 64 MB пам'яті (захист від GPU)\n",[3699,5895,5896,5899,5901,5903,5906],{"class":3701,"line":3751},[3699,5897,5898],{"class":4445},"    parallelism",[3699,5900,4556],{"class":4422},[3699,5902,5872],{"class":4531},[3699,5904,5905],{"class":4422},",     ",[3699,5907,5908],{"class":4412},"# паралельні потоки\n",[3699,5910,5911,5914,5916,5919,5921],{"class":3701,"line":3757},[3699,5912,5913],{"class":4445},"    hash_len",[3699,5915,4556],{"class":4422},[3699,5917,5918],{"class":4531},"32",[3699,5920,5875],{"class":4422},[3699,5922,5923],{"class":4412},"# довжина хешу в байтах\n",[3699,5925,5926,5929,5931,5933,5935],{"class":3701,"line":3763},[3699,5927,5928],{"class":4445},"    salt_len",[3699,5930,4556],{"class":4422},[3699,5932,5574],{"class":4531},[3699,5934,5875],{"class":4422},[3699,5936,5937],{"class":4412},"# довжина salt\n",[3699,5939,5940],{"class":3701,"line":3769},[3699,5941,4562],{"class":4422},[3699,5943,5944],{"class":3701,"line":3775},[3699,5945,3730],{"emptyLinePlaceholder":3729},[3699,5947,5948,5951,5953],{"class":3701,"line":3781},[3699,5949,5950],{"class":4422},"hashed = ph.hash(",[3699,5952,4873],{"class":4324},[3699,5954,4562],{"class":4422},[3699,5956,5957],{"class":3701,"line":3786},[3699,5958,3730],{"emptyLinePlaceholder":3729},[3699,5960,5961,5964],{"class":3701,"line":3792},[3699,5962,5963],{"class":4418},"try",[3699,5965,4460],{"class":4422},[3699,5967,5968,5971,5973,5976],{"class":3701,"line":3798},[3699,5969,5970],{"class":4422},"    ph.verify(hashed, ",[3699,5972,4873],{"class":4324},[3699,5974,5975],{"class":4422},")  ",[3699,5977,5978],{"class":4412},"# True або виняток\n",[3699,5980,5981],{"class":3701,"line":3804},[3699,5982,5983],{"class":4412},"    # Перевірка чи потрібне перехешування\n",[3699,5985,5986,5989],{"class":3701,"line":3810},[3699,5987,5988],{"class":4418},"    if",[3699,5990,5991],{"class":4422}," ph.check_needs_rehash(hashed):\n",[3699,5993,5994,5997,5999],{"class":3701,"line":3816},[3699,5995,5996],{"class":4422},"        new_hash = ph.hash(",[3699,5998,4873],{"class":4324},[3699,6000,4562],{"class":4422},[3699,6002,6003],{"class":3701,"line":3822},[3699,6004,6005],{"class":4412},"        # Зберегти new_hash у БД\n",[3699,6007,6008,6011],{"class":3701,"line":3827},[3699,6009,6010],{"class":4418},"except",[3699,6012,6013],{"class":4422}," VerifyMismatchError:\n",[3699,6015,6016],{"class":3701,"line":3832},[3699,6017,6018],{"class":4412},"    # Неправильний пароль\n",[3699,6020,6021],{"class":3701,"line":3837},[3699,6022,6023],{"class":4418},"    pass\n",[4716,6025,6026,6027,5458,6029,6031,6032,6034],{},"Для нових проєктів пряме використання ",[3625,6028,4006],{},[3625,6030,4777],{}," є найкращим вибором. ",[3625,6033,4777],{}," (алгоритм Argon2id) є сучасним індустріальним стандартом, оскільки забезпечує максимальну стійкість і не має 72-байтового ліміту довжини пароля, властивого bcrypt.",[3654,6036],{},[3551,6038,6040],{"id":6039},"частина-2-jwt-json-web-token","Частина 2. JWT — JSON Web Token",[3661,6042,6044],{"id":6043},"проблема-яку-вирішує-jwt","Проблема, яку вирішує JWT",[3556,6046,6047,6048,6051],{},"Після того як користувач успішно ввів логін і пароль, виникає нова проблема: ",[3560,6049,6050],{},"як сервер ідентифікуватиме його при наступних запитах?"," HTTP — протокол без стану (stateless). Кожен запит незалежний від попередніх. Сервер не «пам'ятає», що саме цей клієнт вже автентифікувався.",[3556,6053,6054],{},"Існує два класичні підходи:",[3576,6056,6057,6076],{},[3579,6058,6061,6070],{"icon":6059,"title":6060},"i-heroicons-server","Session-based (сесії)",[3556,6062,6063,6064,6067,6068,4380],{},"Сервер зберігає сесію у пам'яті або БД. Клієнт отримує ",[3625,6065,6066],{},"session_id"," у cookie. При кожному запиті сервер шукає сесію за ",[3625,6069,6066],{},[3556,6071,6072,6075],{},[3560,6073,6074],{},"Мінус:"," стан зберігається на сервері → складно масштабувати горизонтально (кілька серверів мають бачити одну сесію → потрібен Redis або sticky sessions).",[3579,6077,6080,6090],{"icon":6078,"title":6079},"i-heroicons-key","Token-based (токени)",[3556,6081,6082,6083,6086,6087,4380],{},"Сервер видає ",[3560,6084,6085],{},"підписаний токен"," із вбудованими даними про користувача. Клієнт надсилає токен у кожному запиті. Сервер перевіряє підпис і читає дані ",[3560,6088,6089],{},"без звернення до БД",[3556,6091,6092,6095],{},[3560,6093,6094],{},"Перевага:"," повністю stateless — будь-який сервер у кластері може перевірити токен без спільного стану.",[3556,6097,6098,6101,6102,6107,6108,6111,6112,4380],{},[3560,6099,6100],{},"JWT (JSON Web Token)"," — найпоширеніший стандарт для token-based автентифікації. Він описаний у ",[5747,6103,6106],{"href":6104,"rel":6105},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc7519",[5751],"RFC 7519"," і є відкритим форматом для безпечної передачі інформації між сторонами у вигляді JSON-об'єкта. Ключова властивість: інформація у токені ",[3560,6109,6110],{},"верифікована та надійна",", оскільки він ",[3560,6113,6114],{},"цифрово підписаний",[3661,6116,6118],{"id":6117},"анатомія-jwt-токена","Анатомія JWT-токена",[3556,6120,6121,6122,6125],{},"JWT складається рівно з ",[3560,6123,6124],{},"трьох частин",", розділених крапкою:",[3690,6127,6130],{"className":6128,"code":6129,"language":5306},[5304],"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c\n",[3625,6131,6129],{"__ignoreMap":3695},[3556,6133,6134],{},"Візуально:",[3690,6136,6139],{"className":6137,"code":6138,"language":5306},[5304],"HEADER          PAYLOAD              SIGNATURE\neyJhbGci...  .  eyJzdWIiOiIx...  .  SflKxwRJSM...\n",[3625,6140,6138],{"__ignoreMap":3695},[3687,6142,6143],{},[3690,6144,6146],{"className":3692,"code":6145,"language":3694,"meta":3695,"style":3695},"@startuml\nskinparam style plain\nskinparam backgroundColor #ffffff\nskinparam defaultFontSize 13\n\npackage \"JWT Token\" #f8f8f8 {\n\n  rectangle \"HEADER\\n(Base64URL)\" as h #cce5ff {\n    note as nh\n      {\n        \"alg\": \"HS256\",\n        \"typ\": \"JWT\"\n      }\n      ──────────────\n      alg: алгоритм підпису\n      HS256 = HMAC-SHA256\n      RS256 = RSA-SHA256\n    end note\n  }\n\n  rectangle \"PAYLOAD\\n(Base64URL)\" as p #ccffcc {\n    note as np\n      {\n        \"sub\": \"user-uuid-123\",\n        \"email\": \"user@example.com\",\n        \"role\": \"admin\",\n        \"iat\": 1720000000,\n        \"exp\": 1720003600\n      }\n      ──────────────\n      sub: subject (ID користувача)\n      iat: issued at (коли видано)\n      exp: expiration (коли спливає)\n    end note\n  }\n\n  rectangle \"SIGNATURE\" as s #ffcccc {\n    note as ns\n      HMAC_SHA256(\n        base64url(header) + \".\" +\n        base64url(payload),\n        SECRET_KEY\n      )\n      ──────────────\n      Підтверджує цілісність:\n      будь-яка зміна payload\n      інвалідує підпис\n    end note\n  }\n\n  h -right-> p : \".\"\n  p -right-> s : \".\"\n}\n@enduml\n",[3625,6147,6148,6152,6156,6160,6164,6168,6173,6177,6182,6187,6192,6197,6202,6207,6212,6217,6222,6227,6232,6237,6241,6246,6251,6255,6260,6265,6270,6275,6280,6284,6288,6293,6298,6303,6307,6311,6315,6320,6325,6330,6335,6340,6346,6352,6357,6363,6369,6375,6380,6385,6390,6396,6402,6407],{"__ignoreMap":3695},[3699,6149,6150],{"class":3701,"line":3702},[3699,6151,3705],{},[3699,6153,6154],{"class":3701,"line":3708},[3699,6155,3711],{},[3699,6157,6158],{"class":3701,"line":3714},[3699,6159,3717],{},[3699,6161,6162],{"class":3701,"line":3720},[3699,6163,3723],{},[3699,6165,6166],{"class":3701,"line":3726},[3699,6167,3730],{"emptyLinePlaceholder":3729},[3699,6169,6170],{"class":3701,"line":3733},[3699,6171,6172],{},"package \"JWT Token\" #f8f8f8 {\n",[3699,6174,6175],{"class":3701,"line":3739},[3699,6176,3730],{"emptyLinePlaceholder":3729},[3699,6178,6179],{"class":3701,"line":3745},[3699,6180,6181],{},"  rectangle \"HEADER\\n(Base64URL)\" as h #cce5ff {\n",[3699,6183,6184],{"class":3701,"line":3751},[3699,6185,6186],{},"    note as nh\n",[3699,6188,6189],{"class":3701,"line":3757},[3699,6190,6191],{},"      {\n",[3699,6193,6194],{"class":3701,"line":3763},[3699,6195,6196],{},"        \"alg\": \"HS256\",\n",[3699,6198,6199],{"class":3701,"line":3769},[3699,6200,6201],{},"        \"typ\": \"JWT\"\n",[3699,6203,6204],{"class":3701,"line":3775},[3699,6205,6206],{},"      }\n",[3699,6208,6209],{"class":3701,"line":3781},[3699,6210,6211],{},"      ──────────────\n",[3699,6213,6214],{"class":3701,"line":3786},[3699,6215,6216],{},"      alg: алгоритм підпису\n",[3699,6218,6219],{"class":3701,"line":3792},[3699,6220,6221],{},"      HS256 = HMAC-SHA256\n",[3699,6223,6224],{"class":3701,"line":3798},[3699,6225,6226],{},"      RS256 = RSA-SHA256\n",[3699,6228,6229],{"class":3701,"line":3804},[3699,6230,6231],{},"    end note\n",[3699,6233,6234],{"class":3701,"line":3810},[3699,6235,6236],{},"  }\n",[3699,6238,6239],{"class":3701,"line":3816},[3699,6240,3730],{"emptyLinePlaceholder":3729},[3699,6242,6243],{"class":3701,"line":3822},[3699,6244,6245],{},"  rectangle \"PAYLOAD\\n(Base64URL)\" as p #ccffcc {\n",[3699,6247,6248],{"class":3701,"line":3827},[3699,6249,6250],{},"    note as np\n",[3699,6252,6253],{"class":3701,"line":3832},[3699,6254,6191],{},[3699,6256,6257],{"class":3701,"line":3837},[3699,6258,6259],{},"        \"sub\": \"user-uuid-123\",\n",[3699,6261,6262],{"class":3701,"line":3843},[3699,6263,6264],{},"        \"email\": \"user@example.com\",\n",[3699,6266,6267],{"class":3701,"line":4590},[3699,6268,6269],{},"        \"role\": \"admin\",\n",[3699,6271,6272],{"class":3701,"line":4595},[3699,6273,6274],{},"        \"iat\": 1720000000,\n",[3699,6276,6277],{"class":3701,"line":4627},[3699,6278,6279],{},"        \"exp\": 1720003600\n",[3699,6281,6282],{"class":3701,"line":4632},[3699,6283,6206],{},[3699,6285,6286],{"class":3701,"line":4638},[3699,6287,6211],{},[3699,6289,6290],{"class":3701,"line":4643},[3699,6291,6292],{},"      sub: subject (ID користувача)\n",[3699,6294,6295],{"class":3701,"line":4648},[3699,6296,6297],{},"      iat: issued at (коли видано)\n",[3699,6299,6300],{"class":3701,"line":4654},[3699,6301,6302],{},"      exp: expiration (коли спливає)\n",[3699,6304,6305],{"class":3701,"line":4660},[3699,6306,6231],{},[3699,6308,6309],{"class":3701,"line":4665},[3699,6310,6236],{},[3699,6312,6313],{"class":3701,"line":4670},[3699,6314,3730],{"emptyLinePlaceholder":3729},[3699,6316,6317],{"class":3701,"line":4676},[3699,6318,6319],{},"  rectangle \"SIGNATURE\" as s #ffcccc {\n",[3699,6321,6322],{"class":3701,"line":4681},[3699,6323,6324],{},"    note as ns\n",[3699,6326,6327],{"class":3701,"line":4694},[3699,6328,6329],{},"      HMAC_SHA256(\n",[3699,6331,6332],{"class":3701,"line":4704},[3699,6333,6334],{},"        base64url(header) + \".\" +\n",[3699,6336,6337],{"class":3701,"line":4709},[3699,6338,6339],{},"        base64url(payload),\n",[3699,6341,6343],{"class":3701,"line":6342},42,[3699,6344,6345],{},"        SECRET_KEY\n",[3699,6347,6349],{"class":3701,"line":6348},43,[3699,6350,6351],{},"      )\n",[3699,6353,6355],{"class":3701,"line":6354},44,[3699,6356,6211],{},[3699,6358,6360],{"class":3701,"line":6359},45,[3699,6361,6362],{},"      Підтверджує цілісність:\n",[3699,6364,6366],{"class":3701,"line":6365},46,[3699,6367,6368],{},"      будь-яка зміна payload\n",[3699,6370,6372],{"class":3701,"line":6371},47,[3699,6373,6374],{},"      інвалідує підпис\n",[3699,6376,6378],{"class":3701,"line":6377},48,[3699,6379,6231],{},[3699,6381,6383],{"class":3701,"line":6382},49,[3699,6384,6236],{},[3699,6386,6388],{"class":3701,"line":6387},50,[3699,6389,3730],{"emptyLinePlaceholder":3729},[3699,6391,6393],{"class":3701,"line":6392},51,[3699,6394,6395],{},"  h -right-> p : \".\"\n",[3699,6397,6399],{"class":3701,"line":6398},52,[3699,6400,6401],{},"  p -right-> s : \".\"\n",[3699,6403,6405],{"class":3701,"line":6404},53,[3699,6406,3778],{},[3699,6408,6410],{"class":3701,"line":6409},54,[3699,6411,3846],{},[3614,6413,6414,6417,6418,6421,6422,4200,6425,6428,6429,6432,6433,6435],{},[3560,6415,6416],{},"Base64URL"," — це варіант Base64, адаптований для URL: символи ",[3625,6419,6420],{},"+"," замінені на ",[3625,6423,6424],{},"-",[3625,6426,6427],{},"\u002F"," на ",[3625,6430,6431],{},"_",", і знаки ",[3625,6434,4556],{}," (padding) опускаються. Тому JWT можна безпечно передавати у HTTP-заголовках, query параметрах і cookie без URL-кодування.",[3661,6437,6439],{"id":6438},"claims-твердження-у-payload","Claims — твердження у Payload",[3556,6441,6442,6443,6446,6447,6450],{},"Частина ",[3560,6444,6445],{},"Payload"," містить набір пар ключ-значення, які називаються ",[3560,6448,6449],{},"claims"," (твердження). JWT-стандарт визначає три категорії:",[4238,6452,6453,6567,6587],{},[4241,6454,6456,6459],{"name":6455,"type":6106},"Registered claims (стандартні)",[3556,6457,6458],{},"Зарезервовані імена з чітко визначеною семантикою. Використання необов'язкове, але рекомендоване.",[3862,6460,6461,6474],{},[3865,6462,6463],{},[3868,6464,6465,6468,6471],{},[3871,6466,6467],{},"Claim",[3871,6469,6470],{},"Повна назва",[3871,6472,6473],{},"Значення",[3896,6475,6476,6489,6502,6515,6528,6541,6554],{},[3868,6477,6478,6483,6486],{},[3901,6479,6480],{},[3625,6481,6482],{},"iss",[3901,6484,6485],{},"Issuer",[3901,6487,6488],{},"Хто видав токен (назва сервісу)",[3868,6490,6491,6496,6499],{},[3901,6492,6493],{},[3625,6494,6495],{},"sub",[3901,6497,6498],{},"Subject",[3901,6500,6501],{},"Про кого токен (ID користувача)",[3868,6503,6504,6509,6512],{},[3901,6505,6506],{},[3625,6507,6508],{},"aud",[3901,6510,6511],{},"Audience",[3901,6513,6514],{},"Для кого токен (назва клієнта)",[3868,6516,6517,6522,6525],{},[3901,6518,6519],{},[3625,6520,6521],{},"exp",[3901,6523,6524],{},"Expiration Time",[3901,6526,6527],{},"Unix timestamp — коли токен спливає",[3868,6529,6530,6535,6538],{},[3901,6531,6532],{},[3625,6533,6534],{},"nbf",[3901,6536,6537],{},"Not Before",[3901,6539,6540],{},"Unix timestamp — з якого моменту токен дійсний",[3868,6542,6543,6548,6551],{},[3901,6544,6545],{},[3625,6546,6547],{},"iat",[3901,6549,6550],{},"Issued At",[3901,6552,6553],{},"Unix timestamp — коли токен видано",[3868,6555,6556,6561,6564],{},[3901,6557,6558],{},[3625,6559,6560],{},"jti",[3901,6562,6563],{},"JWT ID",[3901,6565,6566],{},"Унікальний ідентифікатор токена (для blacklist)",[4241,6568,6571,6572,6577,6578,4200,6581,4200,6584,4380],{"name":6569,"type":6570},"Public claims (публічні)","опційні","Claims, зареєстровані в ",[5747,6573,6576],{"href":6574,"rel":6575},"https:\u002F\u002Fwww.iana.org\u002Fassignments\u002Fjwt\u002Fjwt.xhtml",[5751],"IANA JSON Web Token Claims Registry"," для уникнення конфліктів імен між різними системами. Наприклад: ",[3625,6579,6580],{},"email",[3625,6582,6583],{},"name",[3625,6585,6586],{},"picture",[4241,6588,6591,6592,4200,6595,4200,6598,6601,6602,4380],{"name":6589,"type":6590},"Private claims (приватні)","довільні","Будь-які власні поля, погоджені між сторонами. Наприклад: ",[3625,6593,6594],{},"role",[3625,6596,6597],{},"project_id",[3625,6599,6600],{},"permissions",". Використовуйте обережно — не кладіть чутливі дані (пароль, платіжні дані), бо Payload лише закодований Base64URL, але ",[3560,6603,6604],{},"не зашифрований",[4194,6606,6607,6608,6611,6612,6615,6616,6619],{},"JWT ",[3560,6609,6610],{},"не є зашифрованим"," (якщо ви не використовуєте JWE — JSON Web Encryption). Будь-хто, хто отримає ваш токен, може ",[3560,6613,6614],{},"прочитати"," Payload, просто декодувавши Base64URL. Але ",[3560,6617,6618],{},"змінити"," без секретного ключа — не зможе: підпис стане недійсним. Тому: ніколи не кладіть у Payload паролі, номери карток або іншу чутливу інформацію.",[3661,6621,6623],{"id":6622},"access-token-refresh-token-правильний-flow","Access Token + Refresh Token — правильний flow",[3556,6625,6626,6627,4400],{},"Використовувати лише один токен з довгим терміном дії — небезпечно: якщо його вкрадуть, зловмисник матиме доступ до системи тривалий час. Галузевий стандарт — ",[3560,6628,6629],{},"двотокенна система",[3687,6631,6632],{},[3690,6633,6635],{"className":3692,"code":6634,"language":3694,"meta":3695,"style":3695},"@startuml\nskinparam style plain\nskinparam backgroundColor #ffffff\nskinparam defaultFontSize 12\n\nactor \"Клієнт\\n(браузер\u002Fмобайл)\" as client\nparticipant \"FastAPI\\nAuth Service\" as auth\ndatabase \"База\\nданих\" as db\n\n== Крок 1: Логін ==\n\nclient -> auth : POST \u002Fauth\u002Flogin\\n{email, password}\nauth -> db : SELECT user WHERE email=...\ndb --> auth : user з hashed_password\nauth -> auth : verify_password(password, hash)\nauth -> auth : Генерує access_token (15хв)\\n+ refresh_token (30 днів)\nauth -> db : INSERT refresh_tokens\\n(token_hash, user_id, expires_at)\nauth --> client : {access_token, refresh_token}\n\n== Крок 2: Захищені запити ==\n\nclient -> auth : GET \u002Fapi\u002Fprojects\\nAuthorization: Bearer \u003Caccess_token>\nauth -> auth : Верифікує JWT підпис\\nЧитає user_id з payload\nnote right: НЕ звертається до БД!\\nВсе є у токені.\nauth --> client : 200 OK + дані\n\n== Крок 3: Оновлення (access_token спік) ==\n\nclient -> auth : POST \u002Fauth\u002Frefresh\\n{refresh_token}\nauth -> db : SELECT WHERE token_hash=...\\nAND expires_at > NOW()\ndb --> auth : валідний refresh_token\nauth -> db : DELETE старий refresh_token\\n(ротація!)\nauth -> auth : Генерує НОВІ access + refresh tokens\nauth -> db : INSERT новий refresh_token\nauth --> client : {new_access_token, new_refresh_token}\n\n== Крок 4: Логаут ==\n\nclient -> auth : POST \u002Fauth\u002Flogout\\n{refresh_token}\nauth -> db : DELETE refresh_token\nauth --> client : 204 No Content\n@enduml\n",[3625,6636,6637,6641,6645,6649,6653,6657,6662,6667,6672,6676,6681,6685,6690,6695,6700,6705,6710,6715,6720,6724,6729,6733,6738,6743,6748,6753,6757,6762,6766,6771,6776,6781,6786,6791,6796,6801,6805,6810,6814,6819,6824,6829],{"__ignoreMap":3695},[3699,6638,6639],{"class":3701,"line":3702},[3699,6640,3705],{},[3699,6642,6643],{"class":3701,"line":3708},[3699,6644,3711],{},[3699,6646,6647],{"class":3701,"line":3714},[3699,6648,3717],{},[3699,6650,6651],{"class":3701,"line":3720},[3699,6652,5065],{},[3699,6654,6655],{"class":3701,"line":3726},[3699,6656,3730],{"emptyLinePlaceholder":3729},[3699,6658,6659],{"class":3701,"line":3733},[3699,6660,6661],{},"actor \"Клієнт\\n(браузер\u002Fмобайл)\" as client\n",[3699,6663,6664],{"class":3701,"line":3739},[3699,6665,6666],{},"participant \"FastAPI\\nAuth Service\" as auth\n",[3699,6668,6669],{"class":3701,"line":3745},[3699,6670,6671],{},"database \"База\\nданих\" as db\n",[3699,6673,6674],{"class":3701,"line":3751},[3699,6675,3730],{"emptyLinePlaceholder":3729},[3699,6677,6678],{"class":3701,"line":3757},[3699,6679,6680],{},"== Крок 1: Логін ==\n",[3699,6682,6683],{"class":3701,"line":3763},[3699,6684,3730],{"emptyLinePlaceholder":3729},[3699,6686,6687],{"class":3701,"line":3769},[3699,6688,6689],{},"client -> auth : POST \u002Fauth\u002Flogin\\n{email, password}\n",[3699,6691,6692],{"class":3701,"line":3775},[3699,6693,6694],{},"auth -> db : SELECT user WHERE email=...\n",[3699,6696,6697],{"class":3701,"line":3781},[3699,6698,6699],{},"db --> auth : user з hashed_password\n",[3699,6701,6702],{"class":3701,"line":3786},[3699,6703,6704],{},"auth -> auth : verify_password(password, hash)\n",[3699,6706,6707],{"class":3701,"line":3792},[3699,6708,6709],{},"auth -> auth : Генерує access_token (15хв)\\n+ refresh_token (30 днів)\n",[3699,6711,6712],{"class":3701,"line":3798},[3699,6713,6714],{},"auth -> db : INSERT refresh_tokens\\n(token_hash, user_id, expires_at)\n",[3699,6716,6717],{"class":3701,"line":3804},[3699,6718,6719],{},"auth --> client : {access_token, refresh_token}\n",[3699,6721,6722],{"class":3701,"line":3810},[3699,6723,3730],{"emptyLinePlaceholder":3729},[3699,6725,6726],{"class":3701,"line":3816},[3699,6727,6728],{},"== Крок 2: Захищені запити ==\n",[3699,6730,6731],{"class":3701,"line":3822},[3699,6732,3730],{"emptyLinePlaceholder":3729},[3699,6734,6735],{"class":3701,"line":3827},[3699,6736,6737],{},"client -> auth : GET \u002Fapi\u002Fprojects\\nAuthorization: Bearer \u003Caccess_token>\n",[3699,6739,6740],{"class":3701,"line":3832},[3699,6741,6742],{},"auth -> auth : Верифікує JWT підпис\\nЧитає user_id з payload\n",[3699,6744,6745],{"class":3701,"line":3837},[3699,6746,6747],{},"note right: НЕ звертається до БД!\\nВсе є у токені.\n",[3699,6749,6750],{"class":3701,"line":3843},[3699,6751,6752],{},"auth --> client : 200 OK + дані\n",[3699,6754,6755],{"class":3701,"line":4590},[3699,6756,3730],{"emptyLinePlaceholder":3729},[3699,6758,6759],{"class":3701,"line":4595},[3699,6760,6761],{},"== Крок 3: Оновлення (access_token спік) ==\n",[3699,6763,6764],{"class":3701,"line":4627},[3699,6765,3730],{"emptyLinePlaceholder":3729},[3699,6767,6768],{"class":3701,"line":4632},[3699,6769,6770],{},"client -> auth : POST \u002Fauth\u002Frefresh\\n{refresh_token}\n",[3699,6772,6773],{"class":3701,"line":4638},[3699,6774,6775],{},"auth -> db : SELECT WHERE token_hash=...\\nAND expires_at > NOW()\n",[3699,6777,6778],{"class":3701,"line":4643},[3699,6779,6780],{},"db --> auth : валідний refresh_token\n",[3699,6782,6783],{"class":3701,"line":4648},[3699,6784,6785],{},"auth -> db : DELETE старий refresh_token\\n(ротація!)\n",[3699,6787,6788],{"class":3701,"line":4654},[3699,6789,6790],{},"auth -> auth : Генерує НОВІ access + refresh tokens\n",[3699,6792,6793],{"class":3701,"line":4660},[3699,6794,6795],{},"auth -> db : INSERT новий refresh_token\n",[3699,6797,6798],{"class":3701,"line":4665},[3699,6799,6800],{},"auth --> client : {new_access_token, new_refresh_token}\n",[3699,6802,6803],{"class":3701,"line":4670},[3699,6804,3730],{"emptyLinePlaceholder":3729},[3699,6806,6807],{"class":3701,"line":4676},[3699,6808,6809],{},"== Крок 4: Логаут ==\n",[3699,6811,6812],{"class":3701,"line":4681},[3699,6813,3730],{"emptyLinePlaceholder":3729},[3699,6815,6816],{"class":3701,"line":4694},[3699,6817,6818],{},"client -> auth : POST \u002Fauth\u002Flogout\\n{refresh_token}\n",[3699,6820,6821],{"class":3701,"line":4704},[3699,6822,6823],{},"auth -> db : DELETE refresh_token\n",[3699,6825,6826],{"class":3701,"line":4709},[3699,6827,6828],{},"auth --> client : 204 No Content\n",[3699,6830,6831],{"class":3701,"line":6342},[3699,6832,3846],{},[3556,6834,6835],{},[3560,6836,6837],{},"Чому два токени, а не один?",[4238,6839,6840,6852],{},[4241,6841,6844,6845,6848,6849,6851],{"name":6842,"type":6843},"Access Token","короткоживучий (15 хв – 1 год)","Передається у кожному запиті через заголовок ",[3625,6846,6847],{},"Authorization: Bearer \u003Ctoken>",". Сервер перевіряє лише підпис — ",[3560,6850,6089],{},". Саме через це він повинен бути короткоживучим: якщо токен вкрадуть, він швидко протухне.",[4241,6853,6856,6857,6860,6861,6864,6865,6868],{"name":6854,"type":6855},"Refresh Token","довгоживучий (7–30 днів)","Зберігається ",[3560,6858,6859],{},"у базі даних"," (у хешованому вигляді). Використовується тільки для отримання нового access token. Передається лише на один ендпоінт ",[3625,6862,6863],{},"\u002Fauth\u002Frefresh",". Принцип ",[3560,6866,6867],{},"ротації",": при кожному використанні старий refresh token видаляється і видається новий — це дозволяє виявляти крадіжку токена (якщо хтось використав токен, що вже був використаний — це сигнал компрометації).",[3661,6870,6872],{"id":6871},"refresh-token-який-формат-обрати","Refresh Token: який формат обрати?",[3556,6874,6875,6876,6879],{},"Ось де криється важливий архітектурний нюанс, який часто залишається «за кадром» у більшості туторіалів. Те, що ми назвали «Refresh Token», ",[3560,6877,6878],{},"не зобов'язане бути JWT",". Насправді існує два принципово різних підходи, і вибір між ними має серйозні наслідки для безпеки та архітектури.",[4363,6881,6883],{"id":6882},"підхід-1-refresh-token-як-jwt","Підхід 1: Refresh Token як JWT",[3556,6885,6886,6887,6889,6890,6893,6894,4380],{},"Refresh token у форматі JWT — це той самий підписаний JSON Web Token, але з довшим ",[3625,6888,6521],{}," і claim ",[3625,6891,6892],{},"\"type\": \"refresh\"",". Він самодостатній: сервер може верифікувати його підпис ",[3560,6895,6896],{},"без звернення до бази даних",[3690,6898,6901],{"className":6899,"code":6900,"language":5306},[5304],"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\n.eyJzdWIiOiJ1c2VyLTEyMyIsInR5cGUiOiJyZWZyZXNoIiwiZXhwIjoxNzIwMjU5MjAwfQ\n.signature\n",[3625,6902,6900],{"__ignoreMap":3695},[3556,6904,6905],{},[3560,6906,6907],{},"Переваги:",[6909,6910,6911,6914],"ul",{},[4759,6912,6913],{},"Не потребує запису до БД при видачі — менше навантаження",[4759,6915,6916],{},"Stateless верифікація: будь-який сервер у кластері перевіряє самостійно",[3556,6918,6919],{},[3560,6920,6921],{},"Критична проблема:",[3556,6923,6924,6925,6928,6929,6931,6932,6935],{},"Якщо JWT refresh token вкрадений, його ",[3560,6926,6927],{},"неможливо відкликати достроково",". JWT дійсний до закінчення ",[3625,6930,6521],{}," — і крапка. Єдиний спосіб інвалідувати його — ведення ",[3560,6933,6934],{},"blacklist"," (список відкликаних токенів) у Redis або БД. Але тоді втрачається вся «stateless» перевага, і ви отримуєте найгіршу комбінацію: складність JWT плюс необхідність зберігати стан.",[6937,6938,6939,6940,6943,6944,6946],"warning",{},"Використання JWT як refresh token без механізму відкликання — поширена вразливість. Зловмисник, що отримав такий токен, матиме доступ до видачі нових access token протягом ",[3560,6941,6942],{},"30 днів"," (або скільки там у вас ",[3625,6945,6521],{},"). Ні logout, ні зміна пароля не допоможуть — JWT вже виданий і підписаний.",[4363,6948,6950],{"id":6949},"підхід-2-refresh-token-як-непрозорий-випадковий-рядок-рекомендовано","Підхід 2: Refresh Token як непрозорий випадковий рядок (рекомендовано)",[3556,6952,6953,6954,6957,6958,4380],{},"Refresh token — це просто ",[3560,6955,6956],{},"криптографічно безпечний випадковий рядок"," (opaque token), що не несе жодних вбудованих даних. Уся інформація про нього зберігається ",[3560,6959,6960],{},"виключно в базі даних",[3690,6962,6964],{"className":4403,"code":6963,"language":4405,"meta":3695,"style":3695},"import secrets\n\n# Генерація: 32 байти = 64 символи hex = 256 біт ентропії\nrefresh_token = secrets.token_urlsafe(32)\n# Наприклад: \"bX3R9qKm7Vn2pL8sA1jD5cW4uY6oE0tF\"\n",[3625,6965,6966,6973,6977,6982,6991],{"__ignoreMap":3695},[3699,6967,6968,6970],{"class":3701,"line":3702},[3699,6969,4419],{"class":4418},[3699,6971,6972],{"class":4422}," secrets\n",[3699,6974,6975],{"class":3701,"line":3708},[3699,6976,3730],{"emptyLinePlaceholder":3729},[3699,6978,6979],{"class":3701,"line":3714},[3699,6980,6981],{"class":4412},"# Генерація: 32 байти = 64 символи hex = 256 біт ентропії\n",[3699,6983,6984,6987,6989],{"class":3701,"line":3720},[3699,6985,6986],{"class":4422},"refresh_token = secrets.token_urlsafe(",[3699,6988,5918],{"class":4531},[3699,6990,4562],{"class":4422},[3699,6992,6993],{"class":3701,"line":3726},[3699,6994,6995],{"class":4412},"# Наприклад: \"bX3R9qKm7Vn2pL8sA1jD5cW4uY6oE0tF\"\n",[3556,6997,6998,6999,7002],{},"У базі даних зберігається не сам токен, а його ",[3560,7000,7001],{},"bcrypt-хеш"," (або SHA-256, якщо час хешування критичний), разом із метаданими:",[3690,7004,7008],{"className":7005,"code":7006,"language":7007,"meta":3695,"style":3695},"language-sql shiki shiki-themes light-plus dark-plus dark-plus","CREATE TABLE refresh_tokens (\n    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n    token_hash  TEXT NOT NULL UNIQUE,  -- bcrypt або sha256 від токена\n    user_id     UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n    expires_at  TIMESTAMPTZ NOT NULL,\n    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n    revoked_at  TIMESTAMPTZ,           -- для м'якого відкликання\n    user_agent  TEXT,                  -- для аудиту: який браузер\u002Fпристрій\n    ip_address  INET                   -- для аудиту\n);\n","sql",[3625,7009,7010,7024,7038,7058,7078,7090,7107,7120,7133,7144],{"__ignoreMap":3695},[3699,7011,7012,7015,7018,7021],{"class":3701,"line":3702},[3699,7013,7014],{"class":4435},"CREATE",[3699,7016,7017],{"class":4435}," TABLE",[3699,7019,7020],{"class":4321}," refresh_tokens",[3699,7022,7023],{"class":4422}," (\n",[3699,7025,7026,7029,7032,7035],{"class":3701,"line":3708},[3699,7027,7028],{"class":4422},"    id          UUID ",[3699,7030,7031],{"class":4435},"PRIMARY KEY",[3699,7033,7034],{"class":4435}," DEFAULT",[3699,7036,7037],{"class":4422}," gen_random_uuid(),\n",[3699,7039,7040,7043,7046,7049,7052,7055],{"class":3701,"line":3714},[3699,7041,7042],{"class":4422},"    token_hash  ",[3699,7044,7045],{"class":4435},"TEXT",[3699,7047,7048],{"class":4435}," NOT NULL",[3699,7050,7051],{"class":4435}," UNIQUE",[3699,7053,7054],{"class":4422},",  ",[3699,7056,7057],{"class":4412},"-- bcrypt або sha256 від токена\n",[3699,7059,7060,7063,7066,7069,7072,7075],{"class":3701,"line":3720},[3699,7061,7062],{"class":4422},"    user_id     UUID ",[3699,7064,7065],{"class":4435},"NOT NULL",[3699,7067,7068],{"class":4435}," REFERENCES",[3699,7070,7071],{"class":4422}," users(id) ",[3699,7073,7074],{"class":4435},"ON DELETE CASCADE",[3699,7076,7077],{"class":4422},",\n",[3699,7079,7080,7083,7086,7088],{"class":3701,"line":3726},[3699,7081,7082],{"class":4422},"    expires_at  ",[3699,7084,7085],{"class":4435},"TIMESTAMPTZ",[3699,7087,7048],{"class":4435},[3699,7089,7077],{"class":4422},[3699,7091,7092,7095,7097,7099,7101,7104],{"class":3701,"line":3733},[3699,7093,7094],{"class":4422},"    created_at  ",[3699,7096,7085],{"class":4435},[3699,7098,7048],{"class":4435},[3699,7100,7034],{"class":4435},[3699,7102,7103],{"class":4435}," NOW",[3699,7105,7106],{"class":4422},"(),\n",[3699,7108,7109,7112,7114,7117],{"class":3701,"line":3739},[3699,7110,7111],{"class":4422},"    revoked_at  ",[3699,7113,7085],{"class":4435},[3699,7115,7116],{"class":4422},",           ",[3699,7118,7119],{"class":4412},"-- для м'якого відкликання\n",[3699,7121,7122,7125,7127,7130],{"class":3701,"line":3745},[3699,7123,7124],{"class":4422},"    user_agent  ",[3699,7126,7045],{"class":4435},[3699,7128,7129],{"class":4422},",                  ",[3699,7131,7132],{"class":4412},"-- для аудиту: який браузер\u002Fпристрій\n",[3699,7134,7135,7138,7141],{"class":3701,"line":3751},[3699,7136,7137],{"class":4422},"    ip_address  ",[3699,7139,7140],{"class":4435},"INET",[3699,7142,7143],{"class":4412},"                   -- для аудиту\n",[3699,7145,7146],{"class":3701,"line":3757},[3699,7147,7148],{"class":4422},");\n",[3556,7150,7151],{},[3560,7152,6907],{},[6909,7154,7155,7161,7167],{},[4759,7156,7157,7160],{},[3560,7158,7159],{},"Миттєве відкликання",": достатньо DELETE або UPDATE запису — токен стає недійсним моментально",[4759,7162,7163,7166],{},[3560,7164,7165],{},"Аудит сесій",": можна показати користувачу список активних сесій (пристроїв) і дозволити завершити будь-яку",[4759,7168,7169,7172,7173,7176],{},[3560,7170,7171],{},"Виявлення крадіжки"," через ротацію: якщо хтось використав вже використаний токен — це сигнал компрометації, можна відкликати ",[3560,7174,7175],{},"всі"," сесії цього користувача",[3687,7178,7179],{},[3690,7180,7182],{"className":3692,"code":7181,"language":3694,"meta":3695,"style":3695},"@startuml\nskinparam style plain\nskinparam backgroundColor #ffffff\nskinparam defaultFontSize 12\n\ntitle Виявлення крадіжки refresh token через ротацію\n\nactor \"Легітимний\\nкористувач\" as user\nactor \"Зловмисник\" as attacker\nparticipant \"FastAPI\" as api\ndatabase \"БД\\nrefresh_tokens\" as db\n\n== Нормальна ротація ==\n\nuser -> api : POST \u002Fauth\u002Frefresh\\n{token: \"abc123\"}\napi -> db : SELECT WHERE token_hash = hash(\"abc123\")\ndb --> api : знайдено, дійсний\napi -> db : DELETE token \"abc123\"\\nINSERT token \"def456\"\napi --> user : {new_refresh: \"def456\", new_access: \"...\"}\n\n== Атака: зловмисник використовує вкрадений старий токен ==\n\nattacker -> api : POST \u002Fauth\u002Frefresh\\n{token: \"abc123\"}\napi -> db : SELECT WHERE token_hash = hash(\"abc123\")\ndb --> api : ❌ НЕ знайдено\\n(вже видалено при ротації!)\napi -> api : ⚠️ Можлива крадіжка токена!\\nВідкликати ВСІ сесії user_id\napi -> db : DELETE ALL WHERE user_id = ...\napi --> attacker : 401 Unauthorized\nnote over user, attacker\n  Легітимний користувач також\\n\n  втрачає доступ — але безпечніше\\n\n  так, ніж дати зловмиснику доступ\nend note\n@enduml\n",[3625,7183,7184,7188,7192,7196,7200,7204,7209,7213,7218,7223,7228,7233,7237,7242,7246,7251,7256,7261,7266,7271,7275,7280,7284,7289,7293,7298,7303,7308,7313,7318,7323,7328,7333,7338],{"__ignoreMap":3695},[3699,7185,7186],{"class":3701,"line":3702},[3699,7187,3705],{},[3699,7189,7190],{"class":3701,"line":3708},[3699,7191,3711],{},[3699,7193,7194],{"class":3701,"line":3714},[3699,7195,3717],{},[3699,7197,7198],{"class":3701,"line":3720},[3699,7199,5065],{},[3699,7201,7202],{"class":3701,"line":3726},[3699,7203,3730],{"emptyLinePlaceholder":3729},[3699,7205,7206],{"class":3701,"line":3733},[3699,7207,7208],{},"title Виявлення крадіжки refresh token через ротацію\n",[3699,7210,7211],{"class":3701,"line":3739},[3699,7212,3730],{"emptyLinePlaceholder":3729},[3699,7214,7215],{"class":3701,"line":3745},[3699,7216,7217],{},"actor \"Легітимний\\nкористувач\" as user\n",[3699,7219,7220],{"class":3701,"line":3751},[3699,7221,7222],{},"actor \"Зловмисник\" as attacker\n",[3699,7224,7225],{"class":3701,"line":3757},[3699,7226,7227],{},"participant \"FastAPI\" as api\n",[3699,7229,7230],{"class":3701,"line":3763},[3699,7231,7232],{},"database \"БД\\nrefresh_tokens\" as db\n",[3699,7234,7235],{"class":3701,"line":3769},[3699,7236,3730],{"emptyLinePlaceholder":3729},[3699,7238,7239],{"class":3701,"line":3775},[3699,7240,7241],{},"== Нормальна ротація ==\n",[3699,7243,7244],{"class":3701,"line":3781},[3699,7245,3730],{"emptyLinePlaceholder":3729},[3699,7247,7248],{"class":3701,"line":3786},[3699,7249,7250],{},"user -> api : POST \u002Fauth\u002Frefresh\\n{token: \"abc123\"}\n",[3699,7252,7253],{"class":3701,"line":3792},[3699,7254,7255],{},"api -> db : SELECT WHERE token_hash = hash(\"abc123\")\n",[3699,7257,7258],{"class":3701,"line":3798},[3699,7259,7260],{},"db --> api : знайдено, дійсний\n",[3699,7262,7263],{"class":3701,"line":3804},[3699,7264,7265],{},"api -> db : DELETE token \"abc123\"\\nINSERT token \"def456\"\n",[3699,7267,7268],{"class":3701,"line":3810},[3699,7269,7270],{},"api --> user : {new_refresh: \"def456\", new_access: \"...\"}\n",[3699,7272,7273],{"class":3701,"line":3816},[3699,7274,3730],{"emptyLinePlaceholder":3729},[3699,7276,7277],{"class":3701,"line":3822},[3699,7278,7279],{},"== Атака: зловмисник використовує вкрадений старий токен ==\n",[3699,7281,7282],{"class":3701,"line":3827},[3699,7283,3730],{"emptyLinePlaceholder":3729},[3699,7285,7286],{"class":3701,"line":3832},[3699,7287,7288],{},"attacker -> api : POST \u002Fauth\u002Frefresh\\n{token: \"abc123\"}\n",[3699,7290,7291],{"class":3701,"line":3837},[3699,7292,7255],{},[3699,7294,7295],{"class":3701,"line":3843},[3699,7296,7297],{},"db --> api : ❌ НЕ знайдено\\n(вже видалено при ротації!)\n",[3699,7299,7300],{"class":3701,"line":4590},[3699,7301,7302],{},"api -> api : ⚠️ Можлива крадіжка токена!\\nВідкликати ВСІ сесії user_id\n",[3699,7304,7305],{"class":3701,"line":4595},[3699,7306,7307],{},"api -> db : DELETE ALL WHERE user_id = ...\n",[3699,7309,7310],{"class":3701,"line":4627},[3699,7311,7312],{},"api --> attacker : 401 Unauthorized\n",[3699,7314,7315],{"class":3701,"line":4632},[3699,7316,7317],{},"note over user, attacker\n",[3699,7319,7320],{"class":3701,"line":4638},[3699,7321,7322],{},"  Легітимний користувач також\\n\n",[3699,7324,7325],{"class":3701,"line":4643},[3699,7326,7327],{},"  втрачає доступ — але безпечніше\\n\n",[3699,7329,7330],{"class":3701,"line":4648},[3699,7331,7332],{},"  так, ніж дати зловмиснику доступ\n",[3699,7334,7335],{"class":3701,"line":4654},[3699,7336,7337],{},"end note\n",[3699,7339,7340],{"class":3701,"line":4660},[3699,7341,3846],{},[4363,7343,7345],{"id":7344},"порівняльна-таблиця-jwt-vs-opaque-refresh-token","Порівняльна таблиця: JWT vs Opaque refresh token",[3862,7347,7348,7361],{},[3865,7349,7350],{},[3868,7351,7352,7355,7358],{},[3871,7353,7354],{},"Критерій",[3871,7356,7357],{},"JWT Refresh Token",[3871,7359,7360],{},"Opaque (випадковий)",[3896,7362,7363,7378,7396,7411,7424,7436,7448,7460,7473,7486],{},[3868,7364,7365,7370,7375],{},[3901,7366,7367],{},[3560,7368,7369],{},"Формат",[3901,7371,7372],{},[3625,7373,7374],{},"header.payload.signature",[3901,7376,7377],{},"Випадковий рядок (base64url)",[3868,7379,7380,7385,7393],{},[3901,7381,7382],{},[3560,7383,7384],{},"Вбудовані дані",[3901,7386,7387,7388,4200,7390,7392],{},"Так (",[3625,7389,6495],{},[3625,7391,6521],{},", claims)",[3901,7394,7395],{},"Ні — лише lookup key",[3868,7397,7398,7403,7406],{},[3901,7399,7400],{},[3560,7401,7402],{},"Зберігається у БД",[3901,7404,7405],{},"Не обов'язково (stateless)",[3901,7407,7408],{},[3560,7409,7410],{},"Так, завжди",[3868,7412,7413,7418,7421],{},[3901,7414,7415],{},[3560,7416,7417],{},"Верифікація",[3901,7419,7420],{},"Перевірка підпису (без БД)",[3901,7422,7423],{},"Пошук у БД за хешем",[3868,7425,7426,7430,7433],{},[3901,7427,7428],{},[3560,7429,7159],{},[3901,7431,7432],{},"❌ Ні (потрібен blacklist)",[3901,7434,7435],{},"✅ Так (DELETE рядка)",[3868,7437,7438,7442,7445],{},[3901,7439,7440],{},[3560,7441,7165],{},[3901,7443,7444],{},"❌ Важко",[3901,7446,7447],{},"✅ Легко (окремий рядок на сесію)",[3868,7449,7450,7454,7457],{},[3901,7451,7452],{},[3560,7453,7171],{},[3901,7455,7456],{},"❌ Без blacklist — неможливо",[3901,7458,7459],{},"✅ Через ротацію",[3868,7461,7462,7467,7470],{},[3901,7463,7464],{},[3560,7465,7466],{},"Навантаження на БД",[3901,7468,7469],{},"Менше",[3901,7471,7472],{},"1 запит при кожному refresh",[3868,7474,7475,7480,7483],{},[3901,7476,7477],{},[3560,7478,7479],{},"Складність реалізації",[3901,7481,7482],{},"Проста",[3901,7484,7485],{},"Середня",[3868,7487,7488,7493,7496],{},[3901,7489,7490],{},[3560,7491,7492],{},"Рекомендація OWASP",[3901,7494,7495],{},"⚠️ З blacklist",[3901,7497,7498],{},"✅ Preferred",[4716,7500,7501,7504,7505,7508,7509,7512,7513,7516],{},[3560,7502,7503],{},"Висновок для TaskForge:"," використовуємо ",[3560,7506,7507],{},"opaque refresh token"," (випадковий рядок через ",[3625,7510,7511],{},"secrets.token_urlsafe(32)","), збережений у таблиці ",[3625,7514,7515],{},"refresh_tokens"," у вигляді SHA-256 хешу (достатньо для lookup, bcrypt зайво повільний для цього випадку). Access token залишається JWT — він короткоживучий і stateless верифікація тут виправдана. Це поєднує найкращі властивості обох підходів.",[3654,7518],{},[3661,7520,5732,7522,7525],{"id":7521},"бібліотека-python-jose-створення-та-верифікація-jwt",[3625,7523,7524],{},"python-jose"," — створення та верифікація JWT",[3556,7527,7528,7532],{},[3560,7529,7530],{},[3625,7531,7524],{}," — найпопулярніша Python-бібліотека для роботи з JWT та JWS (JSON Web Signature). Підтримує широкий набір алгоритмів підпису: симетричні (HS256, HS512) та асиметричні (RS256, ES256).",[4304,7534,7535,7556,7571],{},[4307,7536,7537],{"label":4309},[3690,7538,7540],{"className":4312,"code":7539,"language":4314,"meta":3695,"style":3695},"pip install \"python-jose[cryptography]\"\n# [cryptography] — бекенд для підтримки RSA та EC алгоритмів\n",[3625,7541,7542,7551],{"__ignoreMap":3695},[3699,7543,7544,7546,7548],{"class":3701,"line":3702},[3699,7545,4309],{"class":4321},[3699,7547,4325],{"class":4324},[3699,7549,7550],{"class":4324}," \"python-jose[cryptography]\"\n",[3699,7552,7553],{"class":3701,"line":3708},[3699,7554,7555],{"class":4412},"# [cryptography] — бекенд для підтримки RSA та EC алгоритмів\n",[4307,7557,7558],{"label":4331},[3690,7559,7561],{"className":4312,"code":7560,"language":4314,"meta":3695,"style":3695},"uv add \"python-jose[cryptography]\"\n",[3625,7562,7563],{"__ignoreMap":3695},[3699,7564,7565,7567,7569],{"class":3701,"line":3702},[3699,7566,4331],{"class":4321},[3699,7568,4343],{"class":4324},[3699,7570,7550],{"class":4324},[4307,7572,7573],{"label":4348},[3690,7574,7576],{"className":4312,"code":7575,"language":4314,"meta":3695,"style":3695},"poetry add \"python-jose[cryptography]\"\n",[3625,7577,7578],{"__ignoreMap":3695},[3699,7579,7580,7582,7584],{"class":3701,"line":3702},[3699,7581,4348],{"class":4321},[3699,7583,4343],{"class":4324},[3699,7585,7550],{"class":4324},[4363,7587,7589,7590],{"id":7588},"модуль-josejwt","Модуль ",[3625,7591,7592],{},"jose.jwt",[3556,7594,7595,7596,7598],{},"Центральний інтерфейс бібліотеки — модуль ",[3625,7597,7592],{}," з двома ключовими функціями:",[4238,7600,7601,7784],{},[4241,7602,7605,7608,7613,7646,7655],{"name":7603,"type":7604},"jwt.encode(claims, key, algorithm)","функція → str",[3556,7606,7607],{},"Створює підписаний JWT-токен.",[3556,7609,7610],{},[3560,7611,7612],{},"Параметри:",[6909,7614,7615,7626,7635],{},[4759,7616,7617,3644,7619,7622,7623,7625],{},[3625,7618,6449],{},[3625,7620,7621],{},"dict",") — payload токена. Обов'язково включайте ",[3625,7624,6521],{}," (час спливання).",[4759,7627,7628,3644,7631,7634],{},[3625,7629,7630],{},"key",[3625,7632,7633],{},"str | bytes | dict",") — секретний ключ для HMAC або приватний ключ RSA\u002FEC.",[4759,7636,7637,3644,7640,7642,7643,4380],{},[3625,7638,7639],{},"algorithm",[3625,7641,4244],{},") — алгоритм підпису. За замовчуванням ",[3625,7644,7645],{},"\"HS256\"",[3556,7647,7648,7651,7652,4380],{},[3560,7649,7650],{},"Повертає:"," рядок токена у форматі ",[3625,7653,7654],{},"\"header.payload.signature\"",[3690,7656,7658],{"className":4403,"code":7657,"language":4405,"meta":3695,"style":3695},"from jose import jwt\nfrom datetime import datetime, timedelta, timezone\n\ntoken = jwt.encode(\n    claims={\n        \"sub\": \"user-uuid-abc123\",\n        \"email\": \"user@example.com\",\n        \"exp\": datetime.now(timezone.utc) + timedelta(minutes=15),\n        \"iat\": datetime.now(timezone.utc),\n    },\n    key=\"your-secret-key\",\n    algorithm=\"HS256\",\n)\n",[3625,7659,7660,7672,7684,7688,7693,7701,7713,7725,7744,7752,7757,7769,7780],{"__ignoreMap":3695},[3699,7661,7662,7664,7667,7669],{"class":3701,"line":3702},[3699,7663,5828],{"class":4418},[3699,7665,7666],{"class":4422}," jose ",[3699,7668,4419],{"class":4418},[3699,7670,7671],{"class":4422}," jwt\n",[3699,7673,7674,7676,7679,7681],{"class":3701,"line":3708},[3699,7675,5828],{"class":4418},[3699,7677,7678],{"class":4422}," datetime ",[3699,7680,4419],{"class":4418},[3699,7682,7683],{"class":4422}," datetime, timedelta, timezone\n",[3699,7685,7686],{"class":3701,"line":3714},[3699,7687,3730],{"emptyLinePlaceholder":3729},[3699,7689,7690],{"class":3701,"line":3720},[3699,7691,7692],{"class":4422},"token = jwt.encode(\n",[3699,7694,7695,7698],{"class":3701,"line":3726},[3699,7696,7697],{"class":4445},"    claims",[3699,7699,7700],{"class":4422},"={\n",[3699,7702,7703,7706,7708,7711],{"class":3701,"line":3733},[3699,7704,7705],{"class":4324},"        \"sub\"",[3699,7707,4449],{"class":4422},[3699,7709,7710],{"class":4324},"\"user-uuid-abc123\"",[3699,7712,7077],{"class":4422},[3699,7714,7715,7718,7720,7723],{"class":3701,"line":3739},[3699,7716,7717],{"class":4324},"        \"email\"",[3699,7719,4449],{"class":4422},[3699,7721,7722],{"class":4324},"\"user@example.com\"",[3699,7724,7077],{"class":4422},[3699,7726,7727,7730,7733,7736,7738,7741],{"class":3701,"line":3745},[3699,7728,7729],{"class":4324},"        \"exp\"",[3699,7731,7732],{"class":4422},": datetime.now(timezone.utc) + timedelta(",[3699,7734,7735],{"class":4445},"minutes",[3699,7737,4556],{"class":4422},[3699,7739,7740],{"class":4531},"15",[3699,7742,7743],{"class":4422},"),\n",[3699,7745,7746,7749],{"class":3701,"line":3751},[3699,7747,7748],{"class":4324},"        \"iat\"",[3699,7750,7751],{"class":4422},": datetime.now(timezone.utc),\n",[3699,7753,7754],{"class":3701,"line":3757},[3699,7755,7756],{"class":4422},"    },\n",[3699,7758,7759,7762,7764,7767],{"class":3701,"line":3763},[3699,7760,7761],{"class":4445},"    key",[3699,7763,4556],{"class":4422},[3699,7765,7766],{"class":4324},"\"your-secret-key\"",[3699,7768,7077],{"class":4422},[3699,7770,7771,7774,7776,7778],{"class":3701,"line":3769},[3699,7772,7773],{"class":4445},"    algorithm",[3699,7775,4556],{"class":4422},[3699,7777,7645],{"class":4324},[3699,7779,7077],{"class":4422},[3699,7781,7782],{"class":3701,"line":3775},[3699,7783,4562],{"class":4422},[4241,7785,7788,7791,7795,7843,7850,7855,7881],{"name":7786,"type":7787},"jwt.decode(token, key, algorithms)","функція → dict",[3556,7789,7790],{},"Верифікує підпис JWT та повертає розкодований payload.",[3556,7792,7793],{},[3560,7794,7612],{},[6909,7796,7797,7805,7812,7824,7832],{},[4759,7798,7799,3644,7802,7804],{},[3625,7800,7801],{},"token",[3625,7803,4244],{},") — рядок токена.",[4759,7806,7807,3644,7809,7811],{},[3625,7808,7630],{},[3625,7810,7633],{},") — секретний ключ (той самий, що при encode).",[4759,7813,7814,3644,7817,7819,7820,7823],{},[3625,7815,7816],{},"algorithms",[3625,7818,4823],{},") — список дозволених алгоритмів. ",[3560,7821,7822],{},"Завжди передавайте явно"," — це захист від атаки «algorithm confusion».",[4759,7825,7826,3644,7829,7831],{},[3625,7827,7828],{},"options",[3625,7830,7621],{},", опційно) — додаткові опції верифікації.",[4759,7833,7834,3644,7837,7839,7840,7842],{},[3625,7835,7836],{},"audience",[3625,7838,4244],{},", опційно) — очікуване значення ",[3625,7841,6508],{}," claim.",[3556,7844,7845,4816,7847,7849],{},[3560,7846,7650],{},[3625,7848,7621],{}," з claims токена.",[3556,7851,7852],{},[3560,7853,7854],{},"Викидає винятки:",[6909,7856,7857,7866,7872],{},[4759,7858,7859,7862,7863,7865],{},[3625,7860,7861],{},"jose.ExpiredSignatureError"," — токен протух (",[3625,7864,6521],{}," у минулому).",[4759,7867,7868,7871],{},[3625,7869,7870],{},"jose.JWTError"," — базовий виняток: невалідний підпис, некоректний формат.",[4759,7873,7874,7877,7878,7880],{},[3625,7875,7876],{},"jose.JWTClaimsError"," — невалідні claims (наприклад, ",[3625,7879,6508],{}," не збігається).",[3690,7882,7884],{"className":4403,"code":7883,"language":4405,"meta":3695,"style":3695},"from jose import jwt, JWTError, ExpiredSignatureError\n\ntry:\n    payload = jwt.decode(\n        token=token_string,\n        key=\"your-secret-key\",\n        algorithms=[\"HS256\"],  # завжди явний список!\n    )\n    user_id = payload[\"sub\"]\nexcept ExpiredSignatureError:\n    # токен протух — повернути 401 з підказкою оновити\n    raise HTTPException(status_code=401, detail=\"Token expired\")\nexcept JWTError:\n    # невалідний підпис або формат\n    raise HTTPException(status_code=401, detail=\"Invalid token\")\n",[3625,7885,7886,7897,7901,7907,7912,7920,7931,7947,7952,7962,7969,7974,8002,8009,8014],{"__ignoreMap":3695},[3699,7887,7888,7890,7892,7894],{"class":3701,"line":3702},[3699,7889,5828],{"class":4418},[3699,7891,7666],{"class":4422},[3699,7893,4419],{"class":4418},[3699,7895,7896],{"class":4422}," jwt, JWTError, ExpiredSignatureError\n",[3699,7898,7899],{"class":3701,"line":3708},[3699,7900,3730],{"emptyLinePlaceholder":3729},[3699,7902,7903,7905],{"class":3701,"line":3714},[3699,7904,5963],{"class":4418},[3699,7906,4460],{"class":4422},[3699,7908,7909],{"class":3701,"line":3720},[3699,7910,7911],{"class":4422},"    payload = jwt.decode(\n",[3699,7913,7914,7917],{"class":3701,"line":3726},[3699,7915,7916],{"class":4445},"        token",[3699,7918,7919],{"class":4422},"=token_string,\n",[3699,7921,7922,7925,7927,7929],{"class":3701,"line":3733},[3699,7923,7924],{"class":4445},"        key",[3699,7926,4556],{"class":4422},[3699,7928,7766],{"class":4324},[3699,7930,7077],{"class":4422},[3699,7932,7933,7936,7939,7941,7944],{"class":3701,"line":3739},[3699,7934,7935],{"class":4445},"        algorithms",[3699,7937,7938],{"class":4422},"=[",[3699,7940,7645],{"class":4324},[3699,7942,7943],{"class":4422},"],  ",[3699,7945,7946],{"class":4412},"# завжди явний список!\n",[3699,7948,7949],{"class":3701,"line":3745},[3699,7950,7951],{"class":4422},"    )\n",[3699,7953,7954,7957,7960],{"class":3701,"line":3751},[3699,7955,7956],{"class":4422},"    user_id = payload[",[3699,7958,7959],{"class":4324},"\"sub\"",[3699,7961,4535],{"class":4422},[3699,7963,7964,7966],{"class":3701,"line":3757},[3699,7965,6010],{"class":4418},[3699,7967,7968],{"class":4422}," ExpiredSignatureError:\n",[3699,7970,7971],{"class":3701,"line":3763},[3699,7972,7973],{"class":4412},"    # токен протух — повернути 401 з підказкою оновити\n",[3699,7975,7976,7979,7982,7985,7987,7990,7992,7995,7997,8000],{"class":3701,"line":3769},[3699,7977,7978],{"class":4418},"    raise",[3699,7980,7981],{"class":4422}," HTTPException(",[3699,7983,7984],{"class":4445},"status_code",[3699,7986,4556],{"class":4422},[3699,7988,7989],{"class":4531},"401",[3699,7991,4200],{"class":4422},[3699,7993,7994],{"class":4445},"detail",[3699,7996,4556],{"class":4422},[3699,7998,7999],{"class":4324},"\"Token expired\"",[3699,8001,4562],{"class":4422},[3699,8003,8004,8006],{"class":3701,"line":3775},[3699,8005,6010],{"class":4418},[3699,8007,8008],{"class":4422}," JWTError:\n",[3699,8010,8011],{"class":3701,"line":3781},[3699,8012,8013],{"class":4412},"    # невалідний підпис або формат\n",[3699,8015,8016,8018,8020,8022,8024,8026,8028,8030,8032,8035],{"class":3701,"line":3786},[3699,8017,7978],{"class":4418},[3699,8019,7981],{"class":4422},[3699,8021,7984],{"class":4445},[3699,8023,4556],{"class":4422},[3699,8025,7989],{"class":4531},[3699,8027,4200],{"class":4422},[3699,8029,7994],{"class":4445},[3699,8031,4556],{"class":4422},[3699,8033,8034],{"class":4324},"\"Invalid token\"",[3699,8036,4562],{"class":4422},[4363,8038,8040],{"id":8039},"повна-реалізація-у-taskforge","Повна реалізація у TaskForge",[3556,8042,8043,8044,8046],{},"Ось як виглядає модуль ",[3625,8045,4396],{}," з усіма функціями для роботи з токенами:",[3690,8048,8050],{"className":4403,"code":8049,"language":4405,"meta":3695,"style":3695},"# app\u002Fcore\u002Fsecurity.py\nimport bcrypt\nimport hashlib\nimport secrets\nfrom datetime import datetime, timedelta, timezone\nfrom typing import Any\n\nfrom jose import jwt, JWTError, ExpiredSignatureError\n\nfrom app.core.config import settings  # pydantic Settings\n\n# ─── Хешування паролів ────────────────────────────────────────────────────────\ndef hash_password(plain_password: str) -> str:\n    pwd_bytes = plain_password.encode(\"utf-8\")[:72]\n    salt = bcrypt.gensalt()\n    return bcrypt.hashpw(pwd_bytes, salt).decode(\"utf-8\")\n\n\ndef verify_password(plain_password: str, hashed_password: str) -> bool:\n    pwd_bytes = plain_password.encode(\"utf-8\")[:72]\n    return bcrypt.checkpw(pwd_bytes, hashed_password.encode(\"utf-8\"))\n\n\n# ─── Access JWT-токен ─────────────────────────────────────────────────────────\ndef create_access_token(\n    subject: str | Any,\n    extra_claims: dict | None = None,\n) -> str:\n    \"\"\"\n    Створює короткоживучий Access JWT-токен.\n\n    Args:\n        subject: Ідентифікатор суб'єкта (зазвичай user UUID або ID).\n                 Буде збережений у claim 'sub'.\n        extra_claims: Додаткові claims для вбудовування у payload\n                      (наприклад, {'email': '...', 'role': 'admin'}).\n\n    Returns:\n        Підписаний JWT-рядок у форматі header.payload.signature.\n    \"\"\"\n    now = datetime.now(timezone.utc)\n    expire = now + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)\n\n    payload = {\n        \"sub\": str(subject),\n        \"iat\": now,\n        \"exp\": expire,\n        \"type\": \"access\",  # захист від підміни: access token ≠ refresh token\n    }\n    if extra_claims:\n        payload.update(extra_claims)\n\n    return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM)\n\n\ndef decode_access_token(token: str) -> dict:\n    \"\"\"\n    Верифікує підпис Access JWT та повертає розкодований payload.\n\n    Args:\n        token: JWT-рядок.\n\n    Returns:\n        Словник з claims токена.\n\n    Raises:\n        ExpiredSignatureError: Токен протух (exp у минулому).\n        JWTError: Невалідний підпис або формат токена.\n    \"\"\"\n    return jwt.decode(\n        token,\n        settings.SECRET_KEY,\n        algorithms=[settings.ALGORITHM],\n    )\n\n\n# ─── Opaque Refresh Token ─────────────────────────────────────────────────────\ndef generate_refresh_token() -> str:\n    \"\"\"\n    Генерує криптографічно безпечний opaque refresh token.\n\n    Refresh token — це просто випадковий рядок з 256 бітами ентропії.\n    Він НЕ є JWT і не містить жодних вбудованих даних.\n    Вся інформація про токен зберігається в таблиці refresh_tokens у БД.\n\n    Returns:\n        URL-safe рядок з 43 символів (32 байти → base64url без padding).\n        Наприклад: \"bX3R9qKm7Vn2pL8sA1jD5cW4uY6oE0tF_kzQwPn1pI\"\n    \"\"\"\n    return secrets.token_urlsafe(32)\n\n\ndef hash_refresh_token(token: str) -> str:\n    \"\"\"\n    Повертає SHA-256 хеш refresh token для зберігання у БД.\n\n    Чому SHA-256, а не bcrypt?\n    - Refresh token вже має 256 біт ентропії (випадковий) —\n      rainbow table атаки принципово неможливі без знання токена.\n    - bcrypt навмисно повільний (~160 мс) — це зайво для lookup-операцій\n      у БД при кожному запиті refresh.\n    - SHA-256 тут достатньо: він захищає від витоку хешів з БД,\n      оскільки зворотно обчислити випадковий 256-бітний рядок — нереально.\n\n    Args:\n        token: Оригінальний refresh token у відкритому вигляді.\n\n    Returns:\n        HEX-рядок SHA-256 хешу (64 символи).\n    \"\"\"\n    return hashlib.sha256(token.encode()).hexdigest()\n\n\ndef verify_refresh_token(plain_token: str, stored_hash: str) -> bool:\n    \"\"\"\n    Перевіряє відповідність refresh token збереженому SHA-256 хешу.\n\n    Використовує secrets.compare_digest для захисту від timing attacks\n    (атака за часом виконання порівняння рядків).\n\n    Args:\n        plain_token: Токен у відкритому вигляді (від клієнта).\n        stored_hash: SHA-256 хеш з бази даних.\n\n    Returns:\n        True якщо токен відповідає хешу, False — інакше.\n    \"\"\"\n    expected_hash = hash_refresh_token(plain_token)\n    # secrets.compare_digest — постійний час порівняння, незалежно від відповіді\n    return secrets.compare_digest(expected_hash, stored_hash)\n",[3625,8051,8052,8056,8062,8069,8075,8085,8097,8101,8111,8115,8130,8134,8139,8159,8171,8176,8186,8190,8194,8222,8234,8246,8250,8254,8259,8269,8281,8302,8310,8314,8319,8323,8327,8332,8337,8342,8347,8351,8355,8360,8364,8369,8379,8383,8388,8399,8406,8413,8428,8433,8440,8445,8449,8461,8465,8470,8492,8497,8503,8508,8513,8519,8524,8529,8535,8540,8546,8552,8558,8563,8571,8577,8583,8591,8596,8601,8606,8612,8627,8632,8638,8643,8649,8655,8661,8666,8671,8677,8683,8688,8700,8705,8710,8732,8737,8743,8748,8754,8760,8766,8772,8778,8784,8790,8795,8800,8806,8811,8816,8822,8827,8835,8840,8845,8877,8882,8888,8893,8899,8905,8910,8915,8921,8927,8932,8937,8943,8948,8954,8960],{"__ignoreMap":3695},[3699,8053,8054],{"class":3701,"line":3702},[3699,8055,4413],{"class":4412},[3699,8057,8058,8060],{"class":3701,"line":3708},[3699,8059,4419],{"class":4418},[3699,8061,4328],{"class":4422},[3699,8063,8064,8066],{"class":3701,"line":3714},[3699,8065,4419],{"class":4418},[3699,8067,8068],{"class":4422}," hashlib\n",[3699,8070,8071,8073],{"class":3701,"line":3720},[3699,8072,4419],{"class":4418},[3699,8074,6972],{"class":4422},[3699,8076,8077,8079,8081,8083],{"class":3701,"line":3726},[3699,8078,5828],{"class":4418},[3699,8080,7678],{"class":4422},[3699,8082,4419],{"class":4418},[3699,8084,7683],{"class":4422},[3699,8086,8087,8089,8092,8094],{"class":3701,"line":3733},[3699,8088,5828],{"class":4418},[3699,8090,8091],{"class":4422}," typing ",[3699,8093,4419],{"class":4418},[3699,8095,8096],{"class":4422}," Any\n",[3699,8098,8099],{"class":3701,"line":3739},[3699,8100,3730],{"emptyLinePlaceholder":3729},[3699,8102,8103,8105,8107,8109],{"class":3701,"line":3745},[3699,8104,5828],{"class":4418},[3699,8106,7666],{"class":4422},[3699,8108,4419],{"class":4418},[3699,8110,7896],{"class":4422},[3699,8112,8113],{"class":3701,"line":3751},[3699,8114,3730],{"emptyLinePlaceholder":3729},[3699,8116,8117,8119,8122,8124,8127],{"class":3701,"line":3757},[3699,8118,5828],{"class":4418},[3699,8120,8121],{"class":4422}," app.core.config ",[3699,8123,4419],{"class":4418},[3699,8125,8126],{"class":4422}," settings  ",[3699,8128,8129],{"class":4412},"# pydantic Settings\n",[3699,8131,8132],{"class":3701,"line":3763},[3699,8133,3730],{"emptyLinePlaceholder":3729},[3699,8135,8136],{"class":3701,"line":3769},[3699,8137,8138],{"class":4412},"# ─── Хешування паролів ────────────────────────────────────────────────────────\n",[3699,8140,8141,8143,8145,8147,8149,8151,8153,8155,8157],{"class":3701,"line":3775},[3699,8142,4436],{"class":4435},[3699,8144,4439],{"class":4321},[3699,8146,4442],{"class":4422},[3699,8148,4446],{"class":4445},[3699,8150,4449],{"class":4422},[3699,8152,4244],{"class":4452},[3699,8154,4455],{"class":4422},[3699,8156,4244],{"class":4452},[3699,8158,4460],{"class":4422},[3699,8160,8161,8163,8165,8167,8169],{"class":3701,"line":3781},[3699,8162,4522],{"class":4422},[3699,8164,4525],{"class":4324},[3699,8166,4528],{"class":4422},[3699,8168,4532],{"class":4531},[3699,8170,4535],{"class":4422},[3699,8172,8173],{"class":3701,"line":3786},[3699,8174,8175],{"class":4422},"    salt = bcrypt.gensalt()\n",[3699,8177,8178,8180,8182,8184],{"class":3701,"line":3792},[3699,8179,4576],{"class":4418},[3699,8181,4579],{"class":4422},[3699,8183,4525],{"class":4324},[3699,8185,4562],{"class":4422},[3699,8187,8188],{"class":3701,"line":3798},[3699,8189,3730],{"emptyLinePlaceholder":3729},[3699,8191,8192],{"class":3701,"line":3804},[3699,8193,3730],{"emptyLinePlaceholder":3729},[3699,8195,8196,8198,8200,8202,8204,8206,8208,8210,8212,8214,8216,8218,8220],{"class":3701,"line":3810},[3699,8197,4436],{"class":4435},[3699,8199,4600],{"class":4321},[3699,8201,4442],{"class":4422},[3699,8203,4446],{"class":4445},[3699,8205,4449],{"class":4422},[3699,8207,4244],{"class":4452},[3699,8209,4200],{"class":4422},[3699,8211,4613],{"class":4445},[3699,8213,4449],{"class":4422},[3699,8215,4244],{"class":4452},[3699,8217,4455],{"class":4422},[3699,8219,4622],{"class":4452},[3699,8221,4460],{"class":4422},[3699,8223,8224,8226,8228,8230,8232],{"class":3701,"line":3816},[3699,8225,4522],{"class":4422},[3699,8227,4525],{"class":4324},[3699,8229,4528],{"class":4422},[3699,8231,4532],{"class":4531},[3699,8233,4535],{"class":4422},[3699,8235,8236,8238,8241,8243],{"class":3701,"line":3822},[3699,8237,4576],{"class":4418},[3699,8239,8240],{"class":4422}," bcrypt.checkpw(pwd_bytes, hashed_password.encode(",[3699,8242,4525],{"class":4324},[3699,8244,8245],{"class":4422},"))\n",[3699,8247,8248],{"class":3701,"line":3827},[3699,8249,3730],{"emptyLinePlaceholder":3729},[3699,8251,8252],{"class":3701,"line":3832},[3699,8253,3730],{"emptyLinePlaceholder":3729},[3699,8255,8256],{"class":3701,"line":3837},[3699,8257,8258],{"class":4412},"# ─── Access JWT-токен ─────────────────────────────────────────────────────────\n",[3699,8260,8261,8263,8266],{"class":3701,"line":3843},[3699,8262,4436],{"class":4435},[3699,8264,8265],{"class":4321}," create_access_token",[3699,8267,8268],{"class":4422},"(\n",[3699,8270,8271,8274,8276,8278],{"class":3701,"line":4590},[3699,8272,8273],{"class":4445},"    subject",[3699,8275,4449],{"class":4422},[3699,8277,4244],{"class":4452},[3699,8279,8280],{"class":4422}," | Any,\n",[3699,8282,8283,8286,8288,8290,8293,8295,8298,8300],{"class":3701,"line":4595},[3699,8284,8285],{"class":4445},"    extra_claims",[3699,8287,4449],{"class":4422},[3699,8289,7621],{"class":4452},[3699,8291,8292],{"class":4422}," | ",[3699,8294,4938],{"class":4435},[3699,8296,8297],{"class":4422}," = ",[3699,8299,4938],{"class":4435},[3699,8301,7077],{"class":4422},[3699,8303,8304,8306,8308],{"class":3701,"line":4627},[3699,8305,4455],{"class":4422},[3699,8307,4244],{"class":4452},[3699,8309,4460],{"class":4422},[3699,8311,8312],{"class":3701,"line":4632},[3699,8313,4465],{"class":4324},[3699,8315,8316],{"class":3701,"line":4638},[3699,8317,8318],{"class":4324},"    Створює короткоживучий Access JWT-токен.\n",[3699,8320,8321],{"class":3701,"line":4643},[3699,8322,3730],{"emptyLinePlaceholder":3729},[3699,8324,8325],{"class":3701,"line":4648},[3699,8326,4484],{"class":4324},[3699,8328,8329],{"class":3701,"line":4654},[3699,8330,8331],{"class":4324},"        subject: Ідентифікатор суб'єкта (зазвичай user UUID або ID).\n",[3699,8333,8334],{"class":3701,"line":4660},[3699,8335,8336],{"class":4324},"                 Буде збережений у claim 'sub'.\n",[3699,8338,8339],{"class":3701,"line":4665},[3699,8340,8341],{"class":4324},"        extra_claims: Додаткові claims для вбудовування у payload\n",[3699,8343,8344],{"class":3701,"line":4670},[3699,8345,8346],{"class":4324},"                      (наприклад, {'email': '...', 'role': 'admin'}).\n",[3699,8348,8349],{"class":3701,"line":4676},[3699,8350,3730],{"emptyLinePlaceholder":3729},[3699,8352,8353],{"class":3701,"line":4681},[3699,8354,4498],{"class":4324},[3699,8356,8357],{"class":3701,"line":4694},[3699,8358,8359],{"class":4324},"        Підписаний JWT-рядок у форматі header.payload.signature.\n",[3699,8361,8362],{"class":3701,"line":4704},[3699,8363,4465],{"class":4324},[3699,8365,8366],{"class":3701,"line":4709},[3699,8367,8368],{"class":4422},"    now = datetime.now(timezone.utc)\n",[3699,8370,8371,8374,8376],{"class":3701,"line":6342},[3699,8372,8373],{"class":4422},"    expire = now + timedelta(",[3699,8375,7735],{"class":4445},[3699,8377,8378],{"class":4422},"=settings.ACCESS_TOKEN_EXPIRE_MINUTES)\n",[3699,8380,8381],{"class":3701,"line":6348},[3699,8382,3730],{"emptyLinePlaceholder":3729},[3699,8384,8385],{"class":3701,"line":6354},[3699,8386,8387],{"class":4422},"    payload = {\n",[3699,8389,8390,8392,8394,8396],{"class":3701,"line":6359},[3699,8391,7705],{"class":4324},[3699,8393,4449],{"class":4422},[3699,8395,4244],{"class":4452},[3699,8397,8398],{"class":4422},"(subject),\n",[3699,8400,8401,8403],{"class":3701,"line":6365},[3699,8402,7748],{"class":4324},[3699,8404,8405],{"class":4422},": now,\n",[3699,8407,8408,8410],{"class":3701,"line":6371},[3699,8409,7729],{"class":4324},[3699,8411,8412],{"class":4422},": expire,\n",[3699,8414,8415,8418,8420,8423,8425],{"class":3701,"line":6377},[3699,8416,8417],{"class":4324},"        \"type\"",[3699,8419,4449],{"class":4422},[3699,8421,8422],{"class":4324},"\"access\"",[3699,8424,7054],{"class":4422},[3699,8426,8427],{"class":4412},"# захист від підміни: access token ≠ refresh token\n",[3699,8429,8430],{"class":3701,"line":6382},[3699,8431,8432],{"class":4422},"    }\n",[3699,8434,8435,8437],{"class":3701,"line":6387},[3699,8436,5988],{"class":4418},[3699,8438,8439],{"class":4422}," extra_claims:\n",[3699,8441,8442],{"class":3701,"line":6392},[3699,8443,8444],{"class":4422},"        payload.update(extra_claims)\n",[3699,8446,8447],{"class":3701,"line":6398},[3699,8448,3730],{"emptyLinePlaceholder":3729},[3699,8450,8451,8453,8456,8458],{"class":3701,"line":6404},[3699,8452,4576],{"class":4418},[3699,8454,8455],{"class":4422}," jwt.encode(payload, settings.SECRET_KEY, ",[3699,8457,7639],{"class":4445},[3699,8459,8460],{"class":4422},"=settings.ALGORITHM)\n",[3699,8462,8463],{"class":3701,"line":6409},[3699,8464,3730],{"emptyLinePlaceholder":3729},[3699,8466,8468],{"class":3701,"line":8467},55,[3699,8469,3730],{"emptyLinePlaceholder":3729},[3699,8471,8473,8475,8478,8480,8482,8484,8486,8488,8490],{"class":3701,"line":8472},56,[3699,8474,4436],{"class":4435},[3699,8476,8477],{"class":4321}," decode_access_token",[3699,8479,4442],{"class":4422},[3699,8481,7801],{"class":4445},[3699,8483,4449],{"class":4422},[3699,8485,4244],{"class":4452},[3699,8487,4455],{"class":4422},[3699,8489,7621],{"class":4452},[3699,8491,4460],{"class":4422},[3699,8493,8495],{"class":3701,"line":8494},57,[3699,8496,4465],{"class":4324},[3699,8498,8500],{"class":3701,"line":8499},58,[3699,8501,8502],{"class":4324},"    Верифікує підпис Access JWT та повертає розкодований payload.\n",[3699,8504,8506],{"class":3701,"line":8505},59,[3699,8507,3730],{"emptyLinePlaceholder":3729},[3699,8509,8511],{"class":3701,"line":8510},60,[3699,8512,4484],{"class":4324},[3699,8514,8516],{"class":3701,"line":8515},61,[3699,8517,8518],{"class":4324},"        token: JWT-рядок.\n",[3699,8520,8522],{"class":3701,"line":8521},62,[3699,8523,3730],{"emptyLinePlaceholder":3729},[3699,8525,8527],{"class":3701,"line":8526},63,[3699,8528,4498],{"class":4324},[3699,8530,8532],{"class":3701,"line":8531},64,[3699,8533,8534],{"class":4324},"        Словник з claims токена.\n",[3699,8536,8538],{"class":3701,"line":8537},65,[3699,8539,3730],{"emptyLinePlaceholder":3729},[3699,8541,8543],{"class":3701,"line":8542},66,[3699,8544,8545],{"class":4324},"    Raises:\n",[3699,8547,8549],{"class":3701,"line":8548},67,[3699,8550,8551],{"class":4324},"        ExpiredSignatureError: Токен протух (exp у минулому).\n",[3699,8553,8555],{"class":3701,"line":8554},68,[3699,8556,8557],{"class":4324},"        JWTError: Невалідний підпис або формат токена.\n",[3699,8559,8561],{"class":3701,"line":8560},69,[3699,8562,4465],{"class":4324},[3699,8564,8566,8568],{"class":3701,"line":8565},70,[3699,8567,4576],{"class":4418},[3699,8569,8570],{"class":4422}," jwt.decode(\n",[3699,8572,8574],{"class":3701,"line":8573},71,[3699,8575,8576],{"class":4422},"        token,\n",[3699,8578,8580],{"class":3701,"line":8579},72,[3699,8581,8582],{"class":4422},"        settings.SECRET_KEY,\n",[3699,8584,8586,8588],{"class":3701,"line":8585},73,[3699,8587,7935],{"class":4445},[3699,8589,8590],{"class":4422},"=[settings.ALGORITHM],\n",[3699,8592,8594],{"class":3701,"line":8593},74,[3699,8595,7951],{"class":4422},[3699,8597,8599],{"class":3701,"line":8598},75,[3699,8600,3730],{"emptyLinePlaceholder":3729},[3699,8602,8604],{"class":3701,"line":8603},76,[3699,8605,3730],{"emptyLinePlaceholder":3729},[3699,8607,8609],{"class":3701,"line":8608},77,[3699,8610,8611],{"class":4412},"# ─── Opaque Refresh Token ─────────────────────────────────────────────────────\n",[3699,8613,8615,8617,8620,8623,8625],{"class":3701,"line":8614},78,[3699,8616,4436],{"class":4435},[3699,8618,8619],{"class":4321}," generate_refresh_token",[3699,8621,8622],{"class":4422},"() -> ",[3699,8624,4244],{"class":4452},[3699,8626,4460],{"class":4422},[3699,8628,8630],{"class":3701,"line":8629},79,[3699,8631,4465],{"class":4324},[3699,8633,8635],{"class":3701,"line":8634},80,[3699,8636,8637],{"class":4324},"    Генерує криптографічно безпечний opaque refresh token.\n",[3699,8639,8641],{"class":3701,"line":8640},81,[3699,8642,3730],{"emptyLinePlaceholder":3729},[3699,8644,8646],{"class":3701,"line":8645},82,[3699,8647,8648],{"class":4324},"    Refresh token — це просто випадковий рядок з 256 бітами ентропії.\n",[3699,8650,8652],{"class":3701,"line":8651},83,[3699,8653,8654],{"class":4324},"    Він НЕ є JWT і не містить жодних вбудованих даних.\n",[3699,8656,8658],{"class":3701,"line":8657},84,[3699,8659,8660],{"class":4324},"    Вся інформація про токен зберігається в таблиці refresh_tokens у БД.\n",[3699,8662,8664],{"class":3701,"line":8663},85,[3699,8665,3730],{"emptyLinePlaceholder":3729},[3699,8667,8669],{"class":3701,"line":8668},86,[3699,8670,4498],{"class":4324},[3699,8672,8674],{"class":3701,"line":8673},87,[3699,8675,8676],{"class":4324},"        URL-safe рядок з 43 символів (32 байти → base64url без padding).\n",[3699,8678,8680],{"class":3701,"line":8679},88,[3699,8681,8682],{"class":4324},"        Наприклад: \"bX3R9qKm7Vn2pL8sA1jD5cW4uY6oE0tF_kzQwPn1pI\"\n",[3699,8684,8686],{"class":3701,"line":8685},89,[3699,8687,4465],{"class":4324},[3699,8689,8691,8693,8696,8698],{"class":3701,"line":8690},90,[3699,8692,4576],{"class":4418},[3699,8694,8695],{"class":4422}," secrets.token_urlsafe(",[3699,8697,5918],{"class":4531},[3699,8699,4562],{"class":4422},[3699,8701,8703],{"class":3701,"line":8702},91,[3699,8704,3730],{"emptyLinePlaceholder":3729},[3699,8706,8708],{"class":3701,"line":8707},92,[3699,8709,3730],{"emptyLinePlaceholder":3729},[3699,8711,8713,8715,8718,8720,8722,8724,8726,8728,8730],{"class":3701,"line":8712},93,[3699,8714,4436],{"class":4435},[3699,8716,8717],{"class":4321}," hash_refresh_token",[3699,8719,4442],{"class":4422},[3699,8721,7801],{"class":4445},[3699,8723,4449],{"class":4422},[3699,8725,4244],{"class":4452},[3699,8727,4455],{"class":4422},[3699,8729,4244],{"class":4452},[3699,8731,4460],{"class":4422},[3699,8733,8735],{"class":3701,"line":8734},94,[3699,8736,4465],{"class":4324},[3699,8738,8740],{"class":3701,"line":8739},95,[3699,8741,8742],{"class":4324},"    Повертає SHA-256 хеш refresh token для зберігання у БД.\n",[3699,8744,8746],{"class":3701,"line":8745},96,[3699,8747,3730],{"emptyLinePlaceholder":3729},[3699,8749,8751],{"class":3701,"line":8750},97,[3699,8752,8753],{"class":4324},"    Чому SHA-256, а не bcrypt?\n",[3699,8755,8757],{"class":3701,"line":8756},98,[3699,8758,8759],{"class":4324},"    - Refresh token вже має 256 біт ентропії (випадковий) —\n",[3699,8761,8763],{"class":3701,"line":8762},99,[3699,8764,8765],{"class":4324},"      rainbow table атаки принципово неможливі без знання токена.\n",[3699,8767,8769],{"class":3701,"line":8768},100,[3699,8770,8771],{"class":4324},"    - bcrypt навмисно повільний (~160 мс) — це зайво для lookup-операцій\n",[3699,8773,8775],{"class":3701,"line":8774},101,[3699,8776,8777],{"class":4324},"      у БД при кожному запиті refresh.\n",[3699,8779,8781],{"class":3701,"line":8780},102,[3699,8782,8783],{"class":4324},"    - SHA-256 тут достатньо: він захищає від витоку хешів з БД,\n",[3699,8785,8787],{"class":3701,"line":8786},103,[3699,8788,8789],{"class":4324},"      оскільки зворотно обчислити випадковий 256-бітний рядок — нереально.\n",[3699,8791,8793],{"class":3701,"line":8792},104,[3699,8794,3730],{"emptyLinePlaceholder":3729},[3699,8796,8798],{"class":3701,"line":8797},105,[3699,8799,4484],{"class":4324},[3699,8801,8803],{"class":3701,"line":8802},106,[3699,8804,8805],{"class":4324},"        token: Оригінальний refresh token у відкритому вигляді.\n",[3699,8807,8809],{"class":3701,"line":8808},107,[3699,8810,3730],{"emptyLinePlaceholder":3729},[3699,8812,8814],{"class":3701,"line":8813},108,[3699,8815,4498],{"class":4324},[3699,8817,8819],{"class":3701,"line":8818},109,[3699,8820,8821],{"class":4324},"        HEX-рядок SHA-256 хешу (64 символи).\n",[3699,8823,8825],{"class":3701,"line":8824},110,[3699,8826,4465],{"class":4324},[3699,8828,8830,8832],{"class":3701,"line":8829},111,[3699,8831,4576],{"class":4418},[3699,8833,8834],{"class":4422}," hashlib.sha256(token.encode()).hexdigest()\n",[3699,8836,8838],{"class":3701,"line":8837},112,[3699,8839,3730],{"emptyLinePlaceholder":3729},[3699,8841,8843],{"class":3701,"line":8842},113,[3699,8844,3730],{"emptyLinePlaceholder":3729},[3699,8846,8848,8850,8853,8855,8858,8860,8862,8864,8867,8869,8871,8873,8875],{"class":3701,"line":8847},114,[3699,8849,4436],{"class":4435},[3699,8851,8852],{"class":4321}," verify_refresh_token",[3699,8854,4442],{"class":4422},[3699,8856,8857],{"class":4445},"plain_token",[3699,8859,4449],{"class":4422},[3699,8861,4244],{"class":4452},[3699,8863,4200],{"class":4422},[3699,8865,8866],{"class":4445},"stored_hash",[3699,8868,4449],{"class":4422},[3699,8870,4244],{"class":4452},[3699,8872,4455],{"class":4422},[3699,8874,4622],{"class":4452},[3699,8876,4460],{"class":4422},[3699,8878,8880],{"class":3701,"line":8879},115,[3699,8881,4465],{"class":4324},[3699,8883,8885],{"class":3701,"line":8884},116,[3699,8886,8887],{"class":4324},"    Перевіряє відповідність refresh token збереженому SHA-256 хешу.\n",[3699,8889,8891],{"class":3701,"line":8890},117,[3699,8892,3730],{"emptyLinePlaceholder":3729},[3699,8894,8896],{"class":3701,"line":8895},118,[3699,8897,8898],{"class":4324},"    Використовує secrets.compare_digest для захисту від timing attacks\n",[3699,8900,8902],{"class":3701,"line":8901},119,[3699,8903,8904],{"class":4324},"    (атака за часом виконання порівняння рядків).\n",[3699,8906,8908],{"class":3701,"line":8907},120,[3699,8909,3730],{"emptyLinePlaceholder":3729},[3699,8911,8913],{"class":3701,"line":8912},121,[3699,8914,4484],{"class":4324},[3699,8916,8918],{"class":3701,"line":8917},122,[3699,8919,8920],{"class":4324},"        plain_token: Токен у відкритому вигляді (від клієнта).\n",[3699,8922,8924],{"class":3701,"line":8923},123,[3699,8925,8926],{"class":4324},"        stored_hash: SHA-256 хеш з бази даних.\n",[3699,8928,8930],{"class":3701,"line":8929},124,[3699,8931,3730],{"emptyLinePlaceholder":3729},[3699,8933,8935],{"class":3701,"line":8934},125,[3699,8936,4498],{"class":4324},[3699,8938,8940],{"class":3701,"line":8939},126,[3699,8941,8942],{"class":4324},"        True якщо токен відповідає хешу, False — інакше.\n",[3699,8944,8946],{"class":3701,"line":8945},127,[3699,8947,4465],{"class":4324},[3699,8949,8951],{"class":3701,"line":8950},128,[3699,8952,8953],{"class":4422},"    expected_hash = hash_refresh_token(plain_token)\n",[3699,8955,8957],{"class":3701,"line":8956},129,[3699,8958,8959],{"class":4412},"    # secrets.compare_digest — постійний час порівняння, незалежно від відповіді\n",[3699,8961,8963,8965],{"class":3701,"line":8962},130,[3699,8964,4576],{"class":4418},[3699,8966,8967],{"class":4422}," secrets.compare_digest(expected_hash, stored_hash)\n",[3556,8969,8970,8971,4400],{},"А ось оновлений ",[3625,8972,8973],{},"app\u002Fcore\u002Fconfig.py",[3690,8975,8977],{"className":4403,"code":8976,"language":4405,"meta":3695,"style":3695},"# app\u002Fcore\u002Fconfig.py\nfrom pydantic_settings import BaseSettings\n\n\nclass Settings(BaseSettings):\n    # JWT (лише для Access Token)\n    SECRET_KEY: str           # мінімум 32 байти, генерується: openssl rand -hex 32\n    ALGORITHM: str = \"HS256\"\n    ACCESS_TOKEN_EXPIRE_MINUTES: int = 15\n\n    # Opaque Refresh Token (зберігається у БД)\n    REFRESH_TOKEN_EXPIRE_DAYS: int = 30\n\n    class Config:\n        env_file = \".env\"\n\n\nsettings = Settings()\n",[3625,8978,8979,8984,8996,9000,9004,9020,9025,9035,9047,9059,9063,9068,9080,9084,9094,9102,9106,9110],{"__ignoreMap":3695},[3699,8980,8981],{"class":3701,"line":3702},[3699,8982,8983],{"class":4412},"# app\u002Fcore\u002Fconfig.py\n",[3699,8985,8986,8988,8991,8993],{"class":3701,"line":3708},[3699,8987,5828],{"class":4418},[3699,8989,8990],{"class":4422}," pydantic_settings ",[3699,8992,4419],{"class":4418},[3699,8994,8995],{"class":4422}," BaseSettings\n",[3699,8997,8998],{"class":3701,"line":3714},[3699,8999,3730],{"emptyLinePlaceholder":3729},[3699,9001,9002],{"class":3701,"line":3720},[3699,9003,3730],{"emptyLinePlaceholder":3729},[3699,9005,9006,9009,9012,9014,9017],{"class":3701,"line":3726},[3699,9007,9008],{"class":4435},"class",[3699,9010,9011],{"class":4452}," Settings",[3699,9013,4442],{"class":4422},[3699,9015,9016],{"class":4452},"BaseSettings",[3699,9018,9019],{"class":4422},"):\n",[3699,9021,9022],{"class":3701,"line":3733},[3699,9023,9024],{"class":4412},"    # JWT (лише для Access Token)\n",[3699,9026,9027,9030,9032],{"class":3701,"line":3739},[3699,9028,9029],{"class":4422},"    SECRET_KEY: ",[3699,9031,4244],{"class":4452},[3699,9033,9034],{"class":4412},"           # мінімум 32 байти, генерується: openssl rand -hex 32\n",[3699,9036,9037,9040,9042,9044],{"class":3701,"line":3745},[3699,9038,9039],{"class":4422},"    ALGORITHM: ",[3699,9041,4244],{"class":4452},[3699,9043,8297],{"class":4422},[3699,9045,9046],{"class":4324},"\"HS256\"\n",[3699,9048,9049,9052,9054,9056],{"class":3701,"line":3751},[3699,9050,9051],{"class":4422},"    ACCESS_TOKEN_EXPIRE_MINUTES: ",[3699,9053,4254],{"class":4452},[3699,9055,8297],{"class":4422},[3699,9057,9058],{"class":4531},"15\n",[3699,9060,9061],{"class":3701,"line":3757},[3699,9062,3730],{"emptyLinePlaceholder":3729},[3699,9064,9065],{"class":3701,"line":3763},[3699,9066,9067],{"class":4412},"    # Opaque Refresh Token (зберігається у БД)\n",[3699,9069,9070,9073,9075,9077],{"class":3701,"line":3769},[3699,9071,9072],{"class":4422},"    REFRESH_TOKEN_EXPIRE_DAYS: ",[3699,9074,4254],{"class":4452},[3699,9076,8297],{"class":4422},[3699,9078,9079],{"class":4531},"30\n",[3699,9081,9082],{"class":3701,"line":3775},[3699,9083,3730],{"emptyLinePlaceholder":3729},[3699,9085,9086,9089,9092],{"class":3701,"line":3781},[3699,9087,9088],{"class":4435},"    class",[3699,9090,9091],{"class":4452}," Config",[3699,9093,4460],{"class":4422},[3699,9095,9096,9099],{"class":3701,"line":3786},[3699,9097,9098],{"class":4422},"        env_file = ",[3699,9100,9101],{"class":4324},"\".env\"\n",[3699,9103,9104],{"class":3701,"line":3792},[3699,9105,3730],{"emptyLinePlaceholder":3729},[3699,9107,9108],{"class":3701,"line":3798},[3699,9109,3730],{"emptyLinePlaceholder":3729},[3699,9111,9112],{"class":3701,"line":3804},[3699,9113,9114],{"class":4422},"settings = Settings()\n",[3556,9116,9117,9118,4733,9121,9124,9125,9128,9129,6428,9132,9135],{},"Зверніть увагу на розділення: ",[3625,9119,9120],{},"SECRET_KEY",[3625,9122,9123],{},"ALGORITHM"," використовуються ",[3560,9126,9127],{},"виключно"," для Access JWT. Refresh token не потребує жодного секретного ключа — він просто зберігається у БД. Це чіткіша архітектура: якщо ви захочете перейти з ",[3625,9130,9131],{},"HS256",[3625,9133,9134],{},"RS256",", це ніяк не вплине на логіку refresh токенів.",[4716,9137,9138,9143],{},[3556,9139,9140],{},[3560,9141,9142],{},"Порівняння з ASP.NET Core:",[3556,9144,9145,9146,9149,9150,9153,9154,9157,9158,9161,9162,9165,9166,4200,9168,9170,9171,3632,9174,9177,9178,9181,9182,4380],{},"У ASP.NET JWT-токени генеруються через ",[3625,9147,9148],{},"JwtSecurityTokenHandler"," та клас ",[3625,9151,9152],{},"SecurityTokenDescriptor",". Конфігурація відбувається у ",[3625,9155,9156],{},"Program.cs"," через ",[3625,9159,9160],{},"AddAuthentication().AddJwtBearer(...)"," з параметрами ",[3625,9163,9164],{},"TokenValidationParameters",". У FastAPI аналогічна конфігурація — це ",[3625,9167,9120],{},[3625,9169,9123],{}," у Settings та явний виклик ",[3625,9172,9173],{},"jwt.encode()",[3625,9175,9176],{},"jwt.decode()",". ASP.NET Identity також підтримує opaque refresh token через ",[3625,9179,9180],{},"IUserTokenProvider"," — концептуально це той самий підхід: випадковий рядок, збережений у таблиці ",[3625,9183,9184],{},"AspNetUserTokens",[3654,9186],{},[3551,9188,9190],{"id":9189},"частина-3-oauth2-у-fastapi","Частина 3. OAuth2 у FastAPI",[3661,9192,9194],{"id":9193},"що-таке-oauth2-у-контексті-fastapi","Що таке OAuth2 у контексті FastAPI",[3556,9196,9197,9198,9201],{},"Коли розробники чують «OAuth2», вони часто думають про «Увійти через Google» або «Увійти через GitHub». Це ",[3560,9199,9200],{},"OAuth2 Authorization Code Flow"," — стандарт для делегування доступу між сторонніми сервісами.",[3556,9203,9204,9205,9208,9209,9212,9213,9216,9217,9220],{},"Але FastAPI використовує OAuth2 у вужчому сенсі: лише ",[3560,9206,9207],{},"схему передачі токена"," у HTTP-запитах. Конкретно — ",[3560,9210,9211],{},"OAuth2 Password Flow"," (він же ",[3625,9214,9215],{},"grant_type=password","), де клієнт відправляє логін і пароль безпосередньо на ваш сервер і отримує токен. Це доречно для ",[3560,9218,9219],{},"власних"," клієнтів (мобайл-застосунок, SPA), де немає потреби у редиректах до стороннього провайдера.",[3614,9222,9223,9224,4380],{},"FastAPI не реалізує повноцінний OAuth2-сервер (Authorization Server). Він лише використовує OAuth2-специфікований формат запиту для отримання токена та Bearer Token schema для його передачі. Якщо вам потрібен повноцінний OAuth2 Authorization Server — розгляньте окремі рішення: Keycloak, Auth0, або бібліотеку ",[3625,9225,9226],{},"authlib",[3661,9228,9230,9233],{"id":9229},"oauth2passwordbearer-схема-безпеки",[3625,9231,9232],{},"OAuth2PasswordBearer"," — схема безпеки",[3556,9235,9236,9240,9241,4380],{},[3560,9237,9238],{},[3625,9239,9232],{}," — це FastAPI-об'єкт, що описує схему автентифікації для Swagger UI та витягує Bearer Token з заголовка ",[3625,9242,9243],{},"Authorization",[4238,9245,9246,9268,9297],{},[4241,9247,9249,9261],{"name":9232,"type":9248},"клас fastapi.security",[3556,9250,9251,9252,9254,9255,9257,9258,4380],{},"Dependency-клас, що реалізує OAuth2 Password Bearer схему. При використанні як ",[3625,9253,3647],{}," — витягує JWT з заголовка ",[3625,9256,6847],{},". Якщо заголовок відсутній — автоматично повертає ",[3625,9259,9260],{},"401 Unauthorized",[3556,9262,9263,4816,9265],{},[3560,9264,4815],{},[3625,9266,9267],{},"from fastapi.security import OAuth2PasswordBearer",[4241,9269,9272,9278],{"name":9270,"type":9271},"tokenUrl","str (обов'язковий)",[3556,9273,9274,9275,9277],{},"URL ендпоінта, на якому можна отримати токен. Використовується ",[3560,9276,9127],{}," для Swagger UI — щоб кнопка «Authorize» знала, куди відправляти логін і пароль. На логіку верифікації токена не впливає.",[3690,9279,9281],{"className":4403,"code":9280,"language":4405,"meta":3695,"style":3695},"oauth2_scheme = OAuth2PasswordBearer(tokenUrl=\"\u002Fapi\u002Fv1\u002Fauth\u002Flogin\")\n",[3625,9282,9283],{"__ignoreMap":3695},[3699,9284,9285,9288,9290,9292,9295],{"class":3701,"line":3702},[3699,9286,9287],{"class":4422},"oauth2_scheme = OAuth2PasswordBearer(",[3699,9289,9270],{"class":4445},[3699,9291,4556],{"class":4422},[3699,9293,9294],{"class":4324},"\"\u002Fapi\u002Fv1\u002Fauth\u002Flogin\"",[3699,9296,4562],{"class":4422},[4241,9298,5738,9301,9304,9305,9307,9308,9311,9312,9314],{"name":9299,"type":9300},"auto_error","bool = True",[3625,9302,9303],{},"True"," (за замовчуванням) — автоматично повертає ",[3625,9306,7989],{}," при відсутності токена. Якщо ",[3625,9309,9310],{},"False"," — повертає ",[3625,9313,4938],{},", дозволяючи обробити відсутність токена вручну (для опційної автентифікації).",[3556,9316,9317,9318,9320,9321,9324,9325,9327,9328,4380],{},"Коли ",[3625,9319,9232],{}," налаштований, Swagger UI автоматично отримує кнопку ",[3560,9322,9323],{},"«Authorize»"," у правому верхньому куті. Користувач вводить логін і пароль, Swagger відправляє їх на ",[3625,9326,9270],{},", отримує токен і додає його до всіх наступних запитів як ",[3625,9329,6847],{},[3661,9331,9333,9336],{"id":9332},"oauth2passwordrequestform-форма-логіну",[3625,9334,9335],{},"OAuth2PasswordRequestForm"," — форма логіну",[3556,9338,9339,9340,9345],{},"OAuth2 Password Flow вимагає, щоб логін і пароль передавались не як JSON, а як ",[3560,9341,9342],{},[3625,9343,9344],{},"application\u002Fx-www-form-urlencoded"," (HTML-форма). FastAPI надає готову dependency для цього.",[4238,9347,9348,9363,9376,9381,9390],{},[4241,9349,9350,9356],{"name":9335,"type":9248},[3556,9351,9352,9353,9355],{},"Dependency-клас, що парсить ",[3625,9354,9344],{}," тіло запиту та повертає об'єкт з полями форми OAuth2.",[3556,9357,9358,4816,9360],{},[3560,9359,4815],{},[3625,9361,9362],{},"from fastapi.security import OAuth2PasswordRequestForm",[4241,9364,9366,9367,9369,9370,9372,9373,9375],{"name":9365,"type":4244},"username","Поле форми ",[3625,9368,9365],{},". За OAuth2 специфікацією ім'я поля — саме ",[3625,9371,9365],{},", навіть якщо ваша система використовує email. У реалізації ви можете шукати користувача за email, прийнявши ",[3625,9374,9365],{}," як email.",[4241,9377,9366,9378,9380],{"name":4243,"type":4244},[3625,9379,4243],{}," — пароль у відкритому вигляді.",[4241,9382,9385,9386,9389],{"name":9383,"type":9384},"grant_type","str | None","Тип запиту. Для Password Flow має бути ",[3625,9387,9388],{},"\"password\"",". FastAPI валідує це автоматично.",[4241,9391,9393,9394,9397],{"name":9392,"type":4823},"scopes","Список OAuth2 scope (прав доступу). Наприклад: ",[3625,9395,9396],{},"[\"read:projects\", \"write:tasks\"]",". Опційно, за замовчуванням — порожній список.",[3661,9399,9401,9402],{"id":9400},"ланцюжок-авторизації-dependsget_current_user","Ланцюжок авторизації: ",[3625,9403,9404],{},"Depends(get_current_user)",[3556,9406,9407,9408,9411,9412,9414,9415,9417,9418,4380],{},"Центральна концепція авторизації у FastAPI — ",[3560,9409,9410],{},"ланцюжок залежностей",". Замість атрибута ",[3625,9413,3627],{}," на контролері (як в ASP.NET), у FastAPI авторизація реалізується через ",[3625,9416,3647],{},". Це робить процес ",[3560,9419,9420],{},"явним, тестованим і гнучким",[3687,9422,9423],{},[3690,9424,9426],{"className":3692,"code":9425,"language":3694,"meta":3695,"style":3695},"@startuml\nskinparam style plain\nskinparam backgroundColor #ffffff\nskinparam defaultFontSize 12\n\ntitle Ланцюжок залежностей авторизації FastAPI\n\nparticipant \"HTTP\\nRequest\" as req\nparticipant \"oauth2_scheme\\n(OAuth2PasswordBearer)\" as scheme\nparticipant \"get_current_user()\" as gcu\nparticipant \"get_current_active_user()\" as gcau\nparticipant \"Route Handler\\n(ваш ендпоінт)\" as handler\ndatabase \"БД\" as db\n\nreq -> scheme : Authorization: Bearer eyJ...\nnote right of scheme: витягує token string\\nз заголовка\n\nscheme -> gcu : token: str\ngcu -> gcu : decode_access_token(token)\\n→ payload dict\ngcu -> db : SELECT * FROM users\\nWHERE id = payload[\"sub\"]\ndb --> gcu : User object\ngcu -> gcau : user: User\n\ngcau -> gcau : if user.is_active == False:\\n  raise 403 Forbidden\n\ngcau -> handler : current_user: User\nhandler --> req : 200 OK + response data\n@enduml\n",[3625,9427,9428,9432,9436,9440,9444,9448,9453,9457,9462,9467,9472,9477,9482,9487,9491,9496,9501,9505,9510,9515,9520,9525,9530,9534,9539,9543,9548,9553],{"__ignoreMap":3695},[3699,9429,9430],{"class":3701,"line":3702},[3699,9431,3705],{},[3699,9433,9434],{"class":3701,"line":3708},[3699,9435,3711],{},[3699,9437,9438],{"class":3701,"line":3714},[3699,9439,3717],{},[3699,9441,9442],{"class":3701,"line":3720},[3699,9443,5065],{},[3699,9445,9446],{"class":3701,"line":3726},[3699,9447,3730],{"emptyLinePlaceholder":3729},[3699,9449,9450],{"class":3701,"line":3733},[3699,9451,9452],{},"title Ланцюжок залежностей авторизації FastAPI\n",[3699,9454,9455],{"class":3701,"line":3739},[3699,9456,3730],{"emptyLinePlaceholder":3729},[3699,9458,9459],{"class":3701,"line":3745},[3699,9460,9461],{},"participant \"HTTP\\nRequest\" as req\n",[3699,9463,9464],{"class":3701,"line":3751},[3699,9465,9466],{},"participant \"oauth2_scheme\\n(OAuth2PasswordBearer)\" as scheme\n",[3699,9468,9469],{"class":3701,"line":3757},[3699,9470,9471],{},"participant \"get_current_user()\" as gcu\n",[3699,9473,9474],{"class":3701,"line":3763},[3699,9475,9476],{},"participant \"get_current_active_user()\" as gcau\n",[3699,9478,9479],{"class":3701,"line":3769},[3699,9480,9481],{},"participant \"Route Handler\\n(ваш ендпоінт)\" as handler\n",[3699,9483,9484],{"class":3701,"line":3775},[3699,9485,9486],{},"database \"БД\" as db\n",[3699,9488,9489],{"class":3701,"line":3781},[3699,9490,3730],{"emptyLinePlaceholder":3729},[3699,9492,9493],{"class":3701,"line":3786},[3699,9494,9495],{},"req -> scheme : Authorization: Bearer eyJ...\n",[3699,9497,9498],{"class":3701,"line":3792},[3699,9499,9500],{},"note right of scheme: витягує token string\\nз заголовка\n",[3699,9502,9503],{"class":3701,"line":3798},[3699,9504,3730],{"emptyLinePlaceholder":3729},[3699,9506,9507],{"class":3701,"line":3804},[3699,9508,9509],{},"scheme -> gcu : token: str\n",[3699,9511,9512],{"class":3701,"line":3810},[3699,9513,9514],{},"gcu -> gcu : decode_access_token(token)\\n→ payload dict\n",[3699,9516,9517],{"class":3701,"line":3816},[3699,9518,9519],{},"gcu -> db : SELECT * FROM users\\nWHERE id = payload[\"sub\"]\n",[3699,9521,9522],{"class":3701,"line":3822},[3699,9523,9524],{},"db --> gcu : User object\n",[3699,9526,9527],{"class":3701,"line":3827},[3699,9528,9529],{},"gcu -> gcau : user: User\n",[3699,9531,9532],{"class":3701,"line":3832},[3699,9533,3730],{"emptyLinePlaceholder":3729},[3699,9535,9536],{"class":3701,"line":3837},[3699,9537,9538],{},"gcau -> gcau : if user.is_active == False:\\n  raise 403 Forbidden\n",[3699,9540,9541],{"class":3701,"line":3843},[3699,9542,3730],{"emptyLinePlaceholder":3729},[3699,9544,9545],{"class":3701,"line":4590},[3699,9546,9547],{},"gcau -> handler : current_user: User\n",[3699,9549,9550],{"class":3701,"line":4595},[3699,9551,9552],{},"handler --> req : 200 OK + response data\n",[3699,9554,9555],{"class":3701,"line":4627},[3699,9556,3846],{},[3556,9558,9559],{},"Ось реалізація цього ланцюжка:",[3690,9561,9563],{"className":4403,"code":9562,"language":4405,"meta":3695,"style":3695},"# app\u002Fapi\u002Fdeps.py\nfrom fastapi import Depends, HTTPException, status\nfrom fastapi.security import OAuth2PasswordBearer\nfrom jose import JWTError, ExpiredSignatureError\nfrom sqlalchemy.ext.asyncio import AsyncSession\n\nfrom app.core.security import decode_access_token\nfrom app.db.session import get_db\nfrom app.models.user import User\nfrom app.repositories.user import UserRepository\n\n# Один екземпляр на рівні модуля — токен береться з цього URL у Swagger\noauth2_scheme = OAuth2PasswordBearer(tokenUrl=\"\u002Fapi\u002Fv1\u002Fauth\u002Flogin\")\n\n\nasync def get_current_user(\n    token: str = Depends(oauth2_scheme),\n    db: AsyncSession = Depends(get_db),\n) -> User:\n    \"\"\"\n    Витягує та верифікує поточного користувача з JWT access token.\n\n    Dependency-функція першого рівня: перевіряє підпис токена\n    та завантажує User з бази даних.\n\n    Args:\n        token: JWT-рядок, автоматично витягнутий OAuth2PasswordBearer\n               з заголовка Authorization: Bearer \u003Ctoken>.\n        db: Сесія бази даних (async SQLAlchemy).\n\n    Returns:\n        Об'єкт User з бази даних.\n\n    Raises:\n        401 Unauthorized: Токен невалідний, протух або користувача не знайдено.\n    \"\"\"\n    credentials_exception = HTTPException(\n        status_code=status.HTTP_401_UNAUTHORIZED,\n        detail=\"Could not validate credentials\",\n        headers={\"WWW-Authenticate\": \"Bearer\"},  # стандарт OAuth2\n    )\n\n    try:\n        payload = decode_access_token(token)\n\n        # Захист від підміни типу токена\n        if payload.get(\"type\") != \"access\":\n            raise credentials_exception\n\n        user_id: str | None = payload.get(\"sub\")\n        if user_id is None:\n            raise credentials_exception\n\n    except ExpiredSignatureError:\n        raise HTTPException(\n            status_code=status.HTTP_401_UNAUTHORIZED,\n            detail=\"Token has expired\",\n            headers={\"WWW-Authenticate\": \"Bearer\"},\n        )\n    except JWTError:\n        raise credentials_exception\n\n    user_repo = UserRepository(db)\n    user = await user_repo.get_by_id(user_id)\n\n    if user is None:\n        raise credentials_exception\n\n    return user\n\n\nasync def get_current_active_user(\n    current_user: User = Depends(get_current_user),\n) -> User:\n    \"\"\"\n    Перевіряє, що поточний користувач є активним (не заблокованим).\n\n    Dependency-функція другого рівня: надбудова над get_current_user.\n    Використовуйте цю залежність для більшості захищених ендпоінтів.\n\n    Args:\n        current_user: Об'єкт User від залежності get_current_user.\n\n    Returns:\n        Той самий User, якщо він активний.\n\n    Raises:\n        403 Forbidden: Акаунт деактивований.\n    \"\"\"\n    if not current_user.is_active:\n        raise HTTPException(\n            status_code=status.HTTP_403_FORBIDDEN,\n            detail=\"Inactive user account\",\n        )\n    return current_user\n",[3625,9564,9565,9570,9582,9594,9605,9617,9621,9633,9645,9657,9669,9673,9678,9690,9694,9698,9711,9723,9731,9736,9740,9745,9749,9754,9759,9763,9767,9772,9777,9782,9786,9790,9795,9799,9803,9808,9812,9817,9825,9837,9859,9863,9867,9874,9879,9883,9888,9906,9914,9918,9936,9951,9957,9961,9968,9976,9983,9995,10011,10016,10022,10028,10032,10037,10048,10052,10065,10071,10075,10082,10086,10090,10101,10109,10113,10117,10122,10126,10131,10136,10140,10144,10149,10153,10157,10162,10166,10170,10175,10179,10189,10195,10202,10213,10217],{"__ignoreMap":3695},[3699,9566,9567],{"class":3701,"line":3702},[3699,9568,9569],{"class":4412},"# app\u002Fapi\u002Fdeps.py\n",[3699,9571,9572,9574,9577,9579],{"class":3701,"line":3708},[3699,9573,5828],{"class":4418},[3699,9575,9576],{"class":4422}," fastapi ",[3699,9578,4419],{"class":4418},[3699,9580,9581],{"class":4422}," Depends, HTTPException, status\n",[3699,9583,9584,9586,9589,9591],{"class":3701,"line":3714},[3699,9585,5828],{"class":4418},[3699,9587,9588],{"class":4422}," fastapi.security ",[3699,9590,4419],{"class":4418},[3699,9592,9593],{"class":4422}," OAuth2PasswordBearer\n",[3699,9595,9596,9598,9600,9602],{"class":3701,"line":3720},[3699,9597,5828],{"class":4418},[3699,9599,7666],{"class":4422},[3699,9601,4419],{"class":4418},[3699,9603,9604],{"class":4422}," JWTError, ExpiredSignatureError\n",[3699,9606,9607,9609,9612,9614],{"class":3701,"line":3726},[3699,9608,5828],{"class":4418},[3699,9610,9611],{"class":4422}," sqlalchemy.ext.asyncio ",[3699,9613,4419],{"class":4418},[3699,9615,9616],{"class":4422}," AsyncSession\n",[3699,9618,9619],{"class":3701,"line":3733},[3699,9620,3730],{"emptyLinePlaceholder":3729},[3699,9622,9623,9625,9628,9630],{"class":3701,"line":3739},[3699,9624,5828],{"class":4418},[3699,9626,9627],{"class":4422}," app.core.security ",[3699,9629,4419],{"class":4418},[3699,9631,9632],{"class":4422}," decode_access_token\n",[3699,9634,9635,9637,9640,9642],{"class":3701,"line":3745},[3699,9636,5828],{"class":4418},[3699,9638,9639],{"class":4422}," app.db.session ",[3699,9641,4419],{"class":4418},[3699,9643,9644],{"class":4422}," get_db\n",[3699,9646,9647,9649,9652,9654],{"class":3701,"line":3751},[3699,9648,5828],{"class":4418},[3699,9650,9651],{"class":4422}," app.models.user ",[3699,9653,4419],{"class":4418},[3699,9655,9656],{"class":4422}," User\n",[3699,9658,9659,9661,9664,9666],{"class":3701,"line":3757},[3699,9660,5828],{"class":4418},[3699,9662,9663],{"class":4422}," app.repositories.user ",[3699,9665,4419],{"class":4418},[3699,9667,9668],{"class":4422}," UserRepository\n",[3699,9670,9671],{"class":3701,"line":3763},[3699,9672,3730],{"emptyLinePlaceholder":3729},[3699,9674,9675],{"class":3701,"line":3769},[3699,9676,9677],{"class":4412},"# Один екземпляр на рівні модуля — токен береться з цього URL у Swagger\n",[3699,9679,9680,9682,9684,9686,9688],{"class":3701,"line":3775},[3699,9681,9287],{"class":4422},[3699,9683,9270],{"class":4445},[3699,9685,4556],{"class":4422},[3699,9687,9294],{"class":4324},[3699,9689,4562],{"class":4422},[3699,9691,9692],{"class":3701,"line":3781},[3699,9693,3730],{"emptyLinePlaceholder":3729},[3699,9695,9696],{"class":3701,"line":3786},[3699,9697,3730],{"emptyLinePlaceholder":3729},[3699,9699,9700,9703,9706,9709],{"class":3701,"line":3792},[3699,9701,9702],{"class":4435},"async",[3699,9704,9705],{"class":4435}," def",[3699,9707,9708],{"class":4321}," get_current_user",[3699,9710,8268],{"class":4422},[3699,9712,9713,9716,9718,9720],{"class":3701,"line":3798},[3699,9714,9715],{"class":4445},"    token",[3699,9717,4449],{"class":4422},[3699,9719,4244],{"class":4452},[3699,9721,9722],{"class":4422}," = Depends(oauth2_scheme),\n",[3699,9724,9725,9728],{"class":3701,"line":3804},[3699,9726,9727],{"class":4445},"    db",[3699,9729,9730],{"class":4422},": AsyncSession = Depends(get_db),\n",[3699,9732,9733],{"class":3701,"line":3810},[3699,9734,9735],{"class":4422},") -> User:\n",[3699,9737,9738],{"class":3701,"line":3816},[3699,9739,4465],{"class":4324},[3699,9741,9742],{"class":3701,"line":3822},[3699,9743,9744],{"class":4324},"    Витягує та верифікує поточного користувача з JWT access token.\n",[3699,9746,9747],{"class":3701,"line":3827},[3699,9748,3730],{"emptyLinePlaceholder":3729},[3699,9750,9751],{"class":3701,"line":3832},[3699,9752,9753],{"class":4324},"    Dependency-функція першого рівня: перевіряє підпис токена\n",[3699,9755,9756],{"class":3701,"line":3837},[3699,9757,9758],{"class":4324},"    та завантажує User з бази даних.\n",[3699,9760,9761],{"class":3701,"line":3843},[3699,9762,3730],{"emptyLinePlaceholder":3729},[3699,9764,9765],{"class":3701,"line":4590},[3699,9766,4484],{"class":4324},[3699,9768,9769],{"class":3701,"line":4595},[3699,9770,9771],{"class":4324},"        token: JWT-рядок, автоматично витягнутий OAuth2PasswordBearer\n",[3699,9773,9774],{"class":3701,"line":4627},[3699,9775,9776],{"class":4324},"               з заголовка Authorization: Bearer \u003Ctoken>.\n",[3699,9778,9779],{"class":3701,"line":4632},[3699,9780,9781],{"class":4324},"        db: Сесія бази даних (async SQLAlchemy).\n",[3699,9783,9784],{"class":3701,"line":4638},[3699,9785,3730],{"emptyLinePlaceholder":3729},[3699,9787,9788],{"class":3701,"line":4643},[3699,9789,4498],{"class":4324},[3699,9791,9792],{"class":3701,"line":4648},[3699,9793,9794],{"class":4324},"        Об'єкт User з бази даних.\n",[3699,9796,9797],{"class":3701,"line":4654},[3699,9798,3730],{"emptyLinePlaceholder":3729},[3699,9800,9801],{"class":3701,"line":4660},[3699,9802,8545],{"class":4324},[3699,9804,9805],{"class":3701,"line":4665},[3699,9806,9807],{"class":4324},"        401 Unauthorized: Токен невалідний, протух або користувача не знайдено.\n",[3699,9809,9810],{"class":3701,"line":4670},[3699,9811,4465],{"class":4324},[3699,9813,9814],{"class":3701,"line":4676},[3699,9815,9816],{"class":4422},"    credentials_exception = HTTPException(\n",[3699,9818,9819,9822],{"class":3701,"line":4681},[3699,9820,9821],{"class":4445},"        status_code",[3699,9823,9824],{"class":4422},"=status.HTTP_401_UNAUTHORIZED,\n",[3699,9826,9827,9830,9832,9835],{"class":3701,"line":4694},[3699,9828,9829],{"class":4445},"        detail",[3699,9831,4556],{"class":4422},[3699,9833,9834],{"class":4324},"\"Could not validate credentials\"",[3699,9836,7077],{"class":4422},[3699,9838,9839,9842,9845,9848,9850,9853,9856],{"class":3701,"line":4704},[3699,9840,9841],{"class":4445},"        headers",[3699,9843,9844],{"class":4422},"={",[3699,9846,9847],{"class":4324},"\"WWW-Authenticate\"",[3699,9849,4449],{"class":4422},[3699,9851,9852],{"class":4324},"\"Bearer\"",[3699,9854,9855],{"class":4422},"},  ",[3699,9857,9858],{"class":4412},"# стандарт OAuth2\n",[3699,9860,9861],{"class":3701,"line":4709},[3699,9862,7951],{"class":4422},[3699,9864,9865],{"class":3701,"line":6342},[3699,9866,3730],{"emptyLinePlaceholder":3729},[3699,9868,9869,9872],{"class":3701,"line":6348},[3699,9870,9871],{"class":4418},"    try",[3699,9873,4460],{"class":4422},[3699,9875,9876],{"class":3701,"line":6354},[3699,9877,9878],{"class":4422},"        payload = decode_access_token(token)\n",[3699,9880,9881],{"class":3701,"line":6359},[3699,9882,3730],{"emptyLinePlaceholder":3729},[3699,9884,9885],{"class":3701,"line":6365},[3699,9886,9887],{"class":4412},"        # Захист від підміни типу токена\n",[3699,9889,9890,9893,9896,9899,9902,9904],{"class":3701,"line":6371},[3699,9891,9892],{"class":4418},"        if",[3699,9894,9895],{"class":4422}," payload.get(",[3699,9897,9898],{"class":4324},"\"type\"",[3699,9900,9901],{"class":4422},") != ",[3699,9903,8422],{"class":4324},[3699,9905,4460],{"class":4422},[3699,9907,9908,9911],{"class":3701,"line":6377},[3699,9909,9910],{"class":4418},"            raise",[3699,9912,9913],{"class":4422}," credentials_exception\n",[3699,9915,9916],{"class":3701,"line":6382},[3699,9917,3730],{"emptyLinePlaceholder":3729},[3699,9919,9920,9923,9925,9927,9929,9932,9934],{"class":3701,"line":6387},[3699,9921,9922],{"class":4422},"        user_id: ",[3699,9924,4244],{"class":4452},[3699,9926,8292],{"class":4422},[3699,9928,4938],{"class":4435},[3699,9930,9931],{"class":4422}," = payload.get(",[3699,9933,7959],{"class":4324},[3699,9935,4562],{"class":4422},[3699,9937,9938,9940,9943,9946,9949],{"class":3701,"line":6392},[3699,9939,9892],{"class":4418},[3699,9941,9942],{"class":4422}," user_id ",[3699,9944,9945],{"class":4435},"is",[3699,9947,9948],{"class":4435}," None",[3699,9950,4460],{"class":4422},[3699,9952,9953,9955],{"class":3701,"line":6398},[3699,9954,9910],{"class":4418},[3699,9956,9913],{"class":4422},[3699,9958,9959],{"class":3701,"line":6404},[3699,9960,3730],{"emptyLinePlaceholder":3729},[3699,9962,9963,9966],{"class":3701,"line":6409},[3699,9964,9965],{"class":4418},"    except",[3699,9967,7968],{"class":4422},[3699,9969,9970,9973],{"class":3701,"line":8467},[3699,9971,9972],{"class":4418},"        raise",[3699,9974,9975],{"class":4422}," HTTPException(\n",[3699,9977,9978,9981],{"class":3701,"line":8472},[3699,9979,9980],{"class":4445},"            status_code",[3699,9982,9824],{"class":4422},[3699,9984,9985,9988,9990,9993],{"class":3701,"line":8494},[3699,9986,9987],{"class":4445},"            detail",[3699,9989,4556],{"class":4422},[3699,9991,9992],{"class":4324},"\"Token has expired\"",[3699,9994,7077],{"class":4422},[3699,9996,9997,10000,10002,10004,10006,10008],{"class":3701,"line":8499},[3699,9998,9999],{"class":4445},"            headers",[3699,10001,9844],{"class":4422},[3699,10003,9847],{"class":4324},[3699,10005,4449],{"class":4422},[3699,10007,9852],{"class":4324},[3699,10009,10010],{"class":4422},"},\n",[3699,10012,10013],{"class":3701,"line":8505},[3699,10014,10015],{"class":4422},"        )\n",[3699,10017,10018,10020],{"class":3701,"line":8510},[3699,10019,9965],{"class":4418},[3699,10021,8008],{"class":4422},[3699,10023,10024,10026],{"class":3701,"line":8515},[3699,10025,9972],{"class":4418},[3699,10027,9913],{"class":4422},[3699,10029,10030],{"class":3701,"line":8521},[3699,10031,3730],{"emptyLinePlaceholder":3729},[3699,10033,10034],{"class":3701,"line":8526},[3699,10035,10036],{"class":4422},"    user_repo = UserRepository(db)\n",[3699,10038,10039,10042,10045],{"class":3701,"line":8531},[3699,10040,10041],{"class":4422},"    user = ",[3699,10043,10044],{"class":4418},"await",[3699,10046,10047],{"class":4422}," user_repo.get_by_id(user_id)\n",[3699,10049,10050],{"class":3701,"line":8537},[3699,10051,3730],{"emptyLinePlaceholder":3729},[3699,10053,10054,10056,10059,10061,10063],{"class":3701,"line":8542},[3699,10055,5988],{"class":4418},[3699,10057,10058],{"class":4422}," user ",[3699,10060,9945],{"class":4435},[3699,10062,9948],{"class":4435},[3699,10064,4460],{"class":4422},[3699,10066,10067,10069],{"class":3701,"line":8548},[3699,10068,9972],{"class":4418},[3699,10070,9913],{"class":4422},[3699,10072,10073],{"class":3701,"line":8554},[3699,10074,3730],{"emptyLinePlaceholder":3729},[3699,10076,10077,10079],{"class":3701,"line":8560},[3699,10078,4576],{"class":4418},[3699,10080,10081],{"class":4422}," user\n",[3699,10083,10084],{"class":3701,"line":8565},[3699,10085,3730],{"emptyLinePlaceholder":3729},[3699,10087,10088],{"class":3701,"line":8573},[3699,10089,3730],{"emptyLinePlaceholder":3729},[3699,10091,10092,10094,10096,10099],{"class":3701,"line":8579},[3699,10093,9702],{"class":4435},[3699,10095,9705],{"class":4435},[3699,10097,10098],{"class":4321}," get_current_active_user",[3699,10100,8268],{"class":4422},[3699,10102,10103,10106],{"class":3701,"line":8585},[3699,10104,10105],{"class":4445},"    current_user",[3699,10107,10108],{"class":4422},": User = Depends(get_current_user),\n",[3699,10110,10111],{"class":3701,"line":8593},[3699,10112,9735],{"class":4422},[3699,10114,10115],{"class":3701,"line":8598},[3699,10116,4465],{"class":4324},[3699,10118,10119],{"class":3701,"line":8603},[3699,10120,10121],{"class":4324},"    Перевіряє, що поточний користувач є активним (не заблокованим).\n",[3699,10123,10124],{"class":3701,"line":8608},[3699,10125,3730],{"emptyLinePlaceholder":3729},[3699,10127,10128],{"class":3701,"line":8614},[3699,10129,10130],{"class":4324},"    Dependency-функція другого рівня: надбудова над get_current_user.\n",[3699,10132,10133],{"class":3701,"line":8629},[3699,10134,10135],{"class":4324},"    Використовуйте цю залежність для більшості захищених ендпоінтів.\n",[3699,10137,10138],{"class":3701,"line":8634},[3699,10139,3730],{"emptyLinePlaceholder":3729},[3699,10141,10142],{"class":3701,"line":8640},[3699,10143,4484],{"class":4324},[3699,10145,10146],{"class":3701,"line":8645},[3699,10147,10148],{"class":4324},"        current_user: Об'єкт User від залежності get_current_user.\n",[3699,10150,10151],{"class":3701,"line":8651},[3699,10152,3730],{"emptyLinePlaceholder":3729},[3699,10154,10155],{"class":3701,"line":8657},[3699,10156,4498],{"class":4324},[3699,10158,10159],{"class":3701,"line":8663},[3699,10160,10161],{"class":4324},"        Той самий User, якщо він активний.\n",[3699,10163,10164],{"class":3701,"line":8668},[3699,10165,3730],{"emptyLinePlaceholder":3729},[3699,10167,10168],{"class":3701,"line":8673},[3699,10169,8545],{"class":4324},[3699,10171,10172],{"class":3701,"line":8679},[3699,10173,10174],{"class":4324},"        403 Forbidden: Акаунт деактивований.\n",[3699,10176,10177],{"class":3701,"line":8685},[3699,10178,4465],{"class":4324},[3699,10180,10181,10183,10186],{"class":3701,"line":8690},[3699,10182,5988],{"class":4418},[3699,10184,10185],{"class":4435}," not",[3699,10187,10188],{"class":4422}," current_user.is_active:\n",[3699,10190,10191,10193],{"class":3701,"line":8702},[3699,10192,9972],{"class":4418},[3699,10194,9975],{"class":4422},[3699,10196,10197,10199],{"class":3701,"line":8707},[3699,10198,9980],{"class":4445},[3699,10200,10201],{"class":4422},"=status.HTTP_403_FORBIDDEN,\n",[3699,10203,10204,10206,10208,10211],{"class":3701,"line":8712},[3699,10205,9987],{"class":4445},[3699,10207,4556],{"class":4422},[3699,10209,10210],{"class":4324},"\"Inactive user account\"",[3699,10212,7077],{"class":4422},[3699,10214,10215],{"class":3701,"line":8734},[3699,10216,10015],{"class":4422},[3699,10218,10219,10221],{"class":3701,"line":8739},[3699,10220,4576],{"class":4418},[3699,10222,10223],{"class":4422}," current_user\n",[3556,10225,10226],{},"Тепер захистити будь-який роут — одна строчка:",[3690,10228,10230],{"className":4403,"code":10229,"language":4405,"meta":3695,"style":3695},"# app\u002Fapi\u002Fv1\u002Fprojects.py\nfrom fastapi import APIRouter, Depends\nfrom app.api.deps import get_current_active_user\nfrom app.models.user import User\n\nrouter = APIRouter()\n\n\n@router.get(\"\u002Fprojects\")\nasync def list_projects(\n    current_user: User = Depends(get_current_active_user),\n    # ...\n):\n    \"\"\"Повертає список проєктів поточного користувача.\"\"\"\n    ...\n\n\n@router.post(\"\u002Fprojects\")\nasync def create_project(\n    current_user: User = Depends(get_current_active_user),\n    # ...\n):\n    ...\n",[3625,10231,10232,10237,10248,10260,10270,10274,10279,10283,10287,10299,10310,10317,10322,10326,10331,10336,10340,10344,10355,10366,10372,10376,10380],{"__ignoreMap":3695},[3699,10233,10234],{"class":3701,"line":3702},[3699,10235,10236],{"class":4412},"# app\u002Fapi\u002Fv1\u002Fprojects.py\n",[3699,10238,10239,10241,10243,10245],{"class":3701,"line":3708},[3699,10240,5828],{"class":4418},[3699,10242,9576],{"class":4422},[3699,10244,4419],{"class":4418},[3699,10246,10247],{"class":4422}," APIRouter, Depends\n",[3699,10249,10250,10252,10255,10257],{"class":3701,"line":3714},[3699,10251,5828],{"class":4418},[3699,10253,10254],{"class":4422}," app.api.deps ",[3699,10256,4419],{"class":4418},[3699,10258,10259],{"class":4422}," get_current_active_user\n",[3699,10261,10262,10264,10266,10268],{"class":3701,"line":3720},[3699,10263,5828],{"class":4418},[3699,10265,9651],{"class":4422},[3699,10267,4419],{"class":4418},[3699,10269,9656],{"class":4422},[3699,10271,10272],{"class":3701,"line":3726},[3699,10273,3730],{"emptyLinePlaceholder":3729},[3699,10275,10276],{"class":3701,"line":3733},[3699,10277,10278],{"class":4422},"router = APIRouter()\n",[3699,10280,10281],{"class":3701,"line":3739},[3699,10282,3730],{"emptyLinePlaceholder":3729},[3699,10284,10285],{"class":3701,"line":3745},[3699,10286,3730],{"emptyLinePlaceholder":3729},[3699,10288,10289,10292,10294,10297],{"class":3701,"line":3751},[3699,10290,10291],{"class":4321},"@router.get",[3699,10293,4442],{"class":4422},[3699,10295,10296],{"class":4324},"\"\u002Fprojects\"",[3699,10298,4562],{"class":4422},[3699,10300,10301,10303,10305,10308],{"class":3701,"line":3757},[3699,10302,9702],{"class":4435},[3699,10304,9705],{"class":4435},[3699,10306,10307],{"class":4321}," list_projects",[3699,10309,8268],{"class":4422},[3699,10311,10312,10314],{"class":3701,"line":3763},[3699,10313,10105],{"class":4445},[3699,10315,10316],{"class":4422},": User = Depends(get_current_active_user),\n",[3699,10318,10319],{"class":3701,"line":3769},[3699,10320,10321],{"class":4412},"    # ...\n",[3699,10323,10324],{"class":3701,"line":3775},[3699,10325,9019],{"class":4422},[3699,10327,10328],{"class":3701,"line":3781},[3699,10329,10330],{"class":4324},"    \"\"\"Повертає список проєктів поточного користувача.\"\"\"\n",[3699,10332,10333],{"class":3701,"line":3786},[3699,10334,10335],{"class":4422},"    ...\n",[3699,10337,10338],{"class":3701,"line":3792},[3699,10339,3730],{"emptyLinePlaceholder":3729},[3699,10341,10342],{"class":3701,"line":3798},[3699,10343,3730],{"emptyLinePlaceholder":3729},[3699,10345,10346,10349,10351,10353],{"class":3701,"line":3804},[3699,10347,10348],{"class":4321},"@router.post",[3699,10350,4442],{"class":4422},[3699,10352,10296],{"class":4324},[3699,10354,4562],{"class":4422},[3699,10356,10357,10359,10361,10364],{"class":3701,"line":3810},[3699,10358,9702],{"class":4435},[3699,10360,9705],{"class":4435},[3699,10362,10363],{"class":4321}," create_project",[3699,10365,8268],{"class":4422},[3699,10367,10368,10370],{"class":3701,"line":3816},[3699,10369,10105],{"class":4445},[3699,10371,10316],{"class":4422},[3699,10373,10374],{"class":3701,"line":3822},[3699,10375,10321],{"class":4412},[3699,10377,10378],{"class":3701,"line":3827},[3699,10379,9019],{"class":4422},[3699,10381,10382],{"class":3701,"line":3832},[3699,10383,10335],{"class":4422},[4716,10385,10386,10390],{},[3556,10387,10388],{},[3560,10389,9142],{},[3556,10391,10392,10393,10395,10396,10398,10399,10402,10403,10406,10407,10410],{},"В ASP.NET авторизація — атрибут: ",[3625,10394,3627],{}," на контролері або методі. Middleware ",[3625,10397,3631],{}," автоматично перевіряє JWT у кожному запиті та заповнює ",[3625,10400,10401],{},"HttpContext.User",". У FastAPI немає глобального middleware для автентифікації — кожен захищений роут ",[3560,10404,10405],{},"явно"," декларує ",[3625,10408,10409],{},"Depends(get_current_active_user)",". Це більше коду, але набагато більша гнучкість: різні роути можуть використовувати різні схеми автентифікації одночасно (JWT, API Key, OAuth2).",[3661,10412,10414],{"id":10413},"ендпоінти-реєстрації-та-логіну","Ендпоінти реєстрації та логіну",[3556,10416,10417,10418,10421],{},"Ось повна реалізація ",[3625,10419,10420],{},"app\u002Fapi\u002Fv1\u002Fauth.py"," з ендпоінтами реєстрації, логіну, оновлення токена та логауту:",[3690,10423,10425],{"className":4403,"code":10424,"language":4405,"meta":3695,"style":3695},"# app\u002Fapi\u002Fv1\u002Fauth.py\nimport secrets\nfrom datetime import datetime, timezone, timedelta\n\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom fastapi.security import OAuth2PasswordRequestForm\nfrom sqlalchemy.ext.asyncio import AsyncSession\n\nfrom app.core.security import (\n    hash_password,\n    verify_password,\n    create_access_token,\n    generate_refresh_token,\n    hash_refresh_token,\n    verify_refresh_token,\n)\nfrom app.core.config import settings\nfrom app.db.session import get_db\nfrom app.models.user import User\nfrom app.models.refresh_token import RefreshToken\nfrom app.repositories.user import UserRepository\nfrom app.repositories.refresh_token import RefreshTokenRepository\nfrom app.schemas.auth import (\n    UserRegisterRequest,\n    TokenResponse,\n    RefreshRequest,\n)\n\nrouter = APIRouter(prefix=\"\u002Fauth\", tags=[\"auth\"])\n\n\n@router.post(\"\u002Fregister\", response_model=TokenResponse, status_code=status.HTTP_201_CREATED)\nasync def register(\n    data: UserRegisterRequest,\n    db: AsyncSession = Depends(get_db),\n) -> TokenResponse:\n    \"\"\"\n    Реєстрація нового користувача.\n\n    Хешує пароль, створює User у БД, одразу видає токени\n    (без окремого кроку логіну після реєстрації).\n    \"\"\"\n    user_repo = UserRepository(db)\n    token_repo = RefreshTokenRepository(db)\n\n    # Перевірка унікальності email\n    if await user_repo.get_by_email(data.email):\n        raise HTTPException(\n            status_code=status.HTTP_409_CONFLICT,\n            detail=\"User with this email already exists\",\n        )\n\n    # Створення користувача\n    user = User(\n        email=data.email,\n        display_name=data.display_name,\n        hashed_password=hash_password(data.password),\n    )\n    await user_repo.create(user)\n\n    # Видача токенів\n    access_token = create_access_token(subject=str(user.id))\n    plain_refresh_token = generate_refresh_token()\n\n    await token_repo.create(RefreshToken(\n        token_hash=hash_refresh_token(plain_refresh_token),\n        user_id=user.id,\n        expires_at=datetime.now(timezone.utc) + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS),\n    ))\n\n    return TokenResponse(\n        access_token=access_token,\n        refresh_token=plain_refresh_token,\n        token_type=\"bearer\",\n    )\n\n\n@router.post(\"\u002Flogin\", response_model=TokenResponse)\nasync def login(\n    form_data: OAuth2PasswordRequestForm = Depends(),\n    db: AsyncSession = Depends(get_db),\n) -> TokenResponse:\n    \"\"\"\n    Логін через OAuth2 Password Flow.\n\n    Приймає form-data (application\u002Fx-www-form-urlencoded),\n    а не JSON — це вимога OAuth2 специфікації.\n    Поле 'username' тут використовується як email.\n    \"\"\"\n    user_repo = UserRepository(db)\n    token_repo = RefreshTokenRepository(db)\n\n    # Знаходимо користувача (form_data.username — це email)\n    user = await user_repo.get_by_email(form_data.username)\n\n    # Перевіряємо credentials. Завжди виконуємо verify_password,\n    # навіть якщо user == None, щоб уникнути timing attack\n    # (різний час відповіді міг би видати факт існування акаунта)\n    if not user or not verify_password(form_data.password, user.hashed_password):\n        raise HTTPException(\n            status_code=status.HTTP_401_UNAUTHORIZED,\n            detail=\"Incorrect email or password\",\n            headers={\"WWW-Authenticate\": \"Bearer\"},\n        )\n\n    if not user.is_active:\n        raise HTTPException(\n            status_code=status.HTTP_403_FORBIDDEN,\n            detail=\"Account is deactivated\",\n        )\n\n    # Видача токенів\n    access_token = create_access_token(subject=str(user.id))\n    plain_refresh_token = generate_refresh_token()\n\n    await token_repo.create(RefreshToken(\n        token_hash=hash_refresh_token(plain_refresh_token),\n        user_id=user.id,\n        expires_at=datetime.now(timezone.utc) + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS),\n    ))\n\n    return TokenResponse(\n        access_token=access_token,\n        refresh_token=plain_refresh_token,\n        token_type=\"bearer\",\n    )\n\n\n@router.post(\"\u002Frefresh\", response_model=TokenResponse)\nasync def refresh_tokens(\n    body: RefreshRequest,\n    db: AsyncSession = Depends(get_db),\n) -> TokenResponse:\n    \"\"\"\n    Оновлення пари токенів через opaque refresh token.\n\n    Реалізує ротацію: старий refresh token знищується,\n    видається новий. Якщо token вже використаний (не знайдений у БД) —\n    це сигнал крадіжки: відкликаємо ВСІ сесії користувача.\n    \"\"\"\n    token_repo = RefreshTokenRepository(db)\n\n    # Шукаємо токен у БД за SHA-256 хешем\n    stored_token = await token_repo.get_by_hash(hash_refresh_token(body.refresh_token))\n\n    if not stored_token or stored_token.expires_at \u003C datetime.now(timezone.utc):\n        # Токен не знайдений або протух\n        # Якщо не знайдений — можлива ротаційна атака.\n        # Для простоти тут повертаємо 401; у production — додаткова логіка.\n        raise HTTPException(\n            status_code=status.HTTP_401_UNAUTHORIZED,\n            detail=\"Invalid or expired refresh token\",\n        )\n\n    user_id = stored_token.user_id\n\n    # Ротація: видаляємо старий токен\n    await token_repo.delete(stored_token.id)\n\n    # Видаємо нові токени\n    access_token = create_access_token(subject=str(user_id))\n    plain_refresh_token = generate_refresh_token()\n\n    await token_repo.create(RefreshToken(\n        token_hash=hash_refresh_token(plain_refresh_token),\n        user_id=user_id,\n        expires_at=datetime.now(timezone.utc) + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS),\n    ))\n\n    return TokenResponse(\n        access_token=access_token,\n        refresh_token=plain_refresh_token,\n        token_type=\"bearer\",\n    )\n\n\n@router.post(\"\u002Flogout\", status_code=status.HTTP_204_NO_CONTENT)\nasync def logout(\n    body: RefreshRequest,\n    db: AsyncSession = Depends(get_db),\n) -> None:\n    \"\"\"\n    Логаут: відкликає refresh token.\n\n    Access token залишається дійсним до закінчення exp (15 хв).\n    Це прийнятний trade-off для stateless JWT.\n    \"\"\"\n    token_repo = RefreshTokenRepository(db)\n    stored_token = await token_repo.get_by_hash(hash_refresh_token(body.refresh_token))\n    if stored_token:\n        await token_repo.delete(stored_token.id)\n",[3625,10426,10427,10432,10438,10449,10453,10464,10475,10485,10489,10499,10504,10509,10514,10519,10524,10529,10533,10544,10554,10564,10576,10586,10598,10609,10614,10619,10624,10628,10632,10658,10662,10666,10688,10699,10707,10713,10718,10722,10727,10731,10736,10741,10745,10749,10754,10758,10763,10773,10779,10786,10797,10801,10805,10810,10815,10823,10831,10839,10843,10850,10854,10859,10874,10879,10883,10890,10898,10906,10920,10925,10929,10936,10944,10952,10964,10968,10972,10976,10992,11003,11011,11017,11021,11025,11030,11034,11039,11044,11049,11053,11057,11061,11065,11070,11079,11083,11088,11093,11098,11114,11120,11126,11137,11151,11155,11159,11168,11174,11180,11191,11195,11199,11203,11215,11219,11223,11229,11235,11241,11251,11255,11259,11265,11271,11277,11287,11291,11295,11299,11314,11324,11333,11340,11345,11350,11356,11361,11367,11373,11379,11384,11389,11394,11400,11411,11416,11431,11437,11443,11449,11456,11463,11475,11480,11485,11491,11496,11502,11510,11515,11521,11535,11540,11545,11552,11559,11567,11578,11583,11588,11595,11602,11609,11620,11625,11630,11635,11652,11664,11671,11678,11687,11692,11698,11703,11709,11715,11720,11725,11734,11742],{"__ignoreMap":3695},[3699,10428,10429],{"class":3701,"line":3702},[3699,10430,10431],{"class":4412},"# app\u002Fapi\u002Fv1\u002Fauth.py\n",[3699,10433,10434,10436],{"class":3701,"line":3708},[3699,10435,4419],{"class":4418},[3699,10437,6972],{"class":4422},[3699,10439,10440,10442,10444,10446],{"class":3701,"line":3714},[3699,10441,5828],{"class":4418},[3699,10443,7678],{"class":4422},[3699,10445,4419],{"class":4418},[3699,10447,10448],{"class":4422}," datetime, timezone, timedelta\n",[3699,10450,10451],{"class":3701,"line":3720},[3699,10452,3730],{"emptyLinePlaceholder":3729},[3699,10454,10455,10457,10459,10461],{"class":3701,"line":3726},[3699,10456,5828],{"class":4418},[3699,10458,9576],{"class":4422},[3699,10460,4419],{"class":4418},[3699,10462,10463],{"class":4422}," APIRouter, Depends, HTTPException, status\n",[3699,10465,10466,10468,10470,10472],{"class":3701,"line":3733},[3699,10467,5828],{"class":4418},[3699,10469,9588],{"class":4422},[3699,10471,4419],{"class":4418},[3699,10473,10474],{"class":4422}," OAuth2PasswordRequestForm\n",[3699,10476,10477,10479,10481,10483],{"class":3701,"line":3739},[3699,10478,5828],{"class":4418},[3699,10480,9611],{"class":4422},[3699,10482,4419],{"class":4418},[3699,10484,9616],{"class":4422},[3699,10486,10487],{"class":3701,"line":3745},[3699,10488,3730],{"emptyLinePlaceholder":3729},[3699,10490,10491,10493,10495,10497],{"class":3701,"line":3751},[3699,10492,5828],{"class":4418},[3699,10494,9627],{"class":4422},[3699,10496,4419],{"class":4418},[3699,10498,7023],{"class":4422},[3699,10500,10501],{"class":3701,"line":3757},[3699,10502,10503],{"class":4422},"    hash_password,\n",[3699,10505,10506],{"class":3701,"line":3763},[3699,10507,10508],{"class":4422},"    verify_password,\n",[3699,10510,10511],{"class":3701,"line":3769},[3699,10512,10513],{"class":4422},"    create_access_token,\n",[3699,10515,10516],{"class":3701,"line":3775},[3699,10517,10518],{"class":4422},"    generate_refresh_token,\n",[3699,10520,10521],{"class":3701,"line":3781},[3699,10522,10523],{"class":4422},"    hash_refresh_token,\n",[3699,10525,10526],{"class":3701,"line":3786},[3699,10527,10528],{"class":4422},"    verify_refresh_token,\n",[3699,10530,10531],{"class":3701,"line":3792},[3699,10532,4562],{"class":4422},[3699,10534,10535,10537,10539,10541],{"class":3701,"line":3798},[3699,10536,5828],{"class":4418},[3699,10538,8121],{"class":4422},[3699,10540,4419],{"class":4418},[3699,10542,10543],{"class":4422}," settings\n",[3699,10545,10546,10548,10550,10552],{"class":3701,"line":3804},[3699,10547,5828],{"class":4418},[3699,10549,9639],{"class":4422},[3699,10551,4419],{"class":4418},[3699,10553,9644],{"class":4422},[3699,10555,10556,10558,10560,10562],{"class":3701,"line":3810},[3699,10557,5828],{"class":4418},[3699,10559,9651],{"class":4422},[3699,10561,4419],{"class":4418},[3699,10563,9656],{"class":4422},[3699,10565,10566,10568,10571,10573],{"class":3701,"line":3816},[3699,10567,5828],{"class":4418},[3699,10569,10570],{"class":4422}," app.models.refresh_token ",[3699,10572,4419],{"class":4418},[3699,10574,10575],{"class":4422}," RefreshToken\n",[3699,10577,10578,10580,10582,10584],{"class":3701,"line":3822},[3699,10579,5828],{"class":4418},[3699,10581,9663],{"class":4422},[3699,10583,4419],{"class":4418},[3699,10585,9668],{"class":4422},[3699,10587,10588,10590,10593,10595],{"class":3701,"line":3827},[3699,10589,5828],{"class":4418},[3699,10591,10592],{"class":4422}," app.repositories.refresh_token ",[3699,10594,4419],{"class":4418},[3699,10596,10597],{"class":4422}," RefreshTokenRepository\n",[3699,10599,10600,10602,10605,10607],{"class":3701,"line":3832},[3699,10601,5828],{"class":4418},[3699,10603,10604],{"class":4422}," app.schemas.auth ",[3699,10606,4419],{"class":4418},[3699,10608,7023],{"class":4422},[3699,10610,10611],{"class":3701,"line":3837},[3699,10612,10613],{"class":4422},"    UserRegisterRequest,\n",[3699,10615,10616],{"class":3701,"line":3843},[3699,10617,10618],{"class":4422},"    TokenResponse,\n",[3699,10620,10621],{"class":3701,"line":4590},[3699,10622,10623],{"class":4422},"    RefreshRequest,\n",[3699,10625,10626],{"class":3701,"line":4595},[3699,10627,4562],{"class":4422},[3699,10629,10630],{"class":3701,"line":4627},[3699,10631,3730],{"emptyLinePlaceholder":3729},[3699,10633,10634,10637,10640,10642,10645,10647,10650,10652,10655],{"class":3701,"line":4632},[3699,10635,10636],{"class":4422},"router = APIRouter(",[3699,10638,10639],{"class":4445},"prefix",[3699,10641,4556],{"class":4422},[3699,10643,10644],{"class":4324},"\"\u002Fauth\"",[3699,10646,4200],{"class":4422},[3699,10648,10649],{"class":4445},"tags",[3699,10651,7938],{"class":4422},[3699,10653,10654],{"class":4324},"\"auth\"",[3699,10656,10657],{"class":4422},"])\n",[3699,10659,10660],{"class":3701,"line":4638},[3699,10661,3730],{"emptyLinePlaceholder":3729},[3699,10663,10664],{"class":3701,"line":4643},[3699,10665,3730],{"emptyLinePlaceholder":3729},[3699,10667,10668,10670,10672,10675,10677,10680,10683,10685],{"class":3701,"line":4648},[3699,10669,10348],{"class":4321},[3699,10671,4442],{"class":4422},[3699,10673,10674],{"class":4324},"\"\u002Fregister\"",[3699,10676,4200],{"class":4422},[3699,10678,10679],{"class":4445},"response_model",[3699,10681,10682],{"class":4422},"=TokenResponse, ",[3699,10684,7984],{"class":4445},[3699,10686,10687],{"class":4422},"=status.HTTP_201_CREATED)\n",[3699,10689,10690,10692,10694,10697],{"class":3701,"line":4654},[3699,10691,9702],{"class":4435},[3699,10693,9705],{"class":4435},[3699,10695,10696],{"class":4321}," register",[3699,10698,8268],{"class":4422},[3699,10700,10701,10704],{"class":3701,"line":4660},[3699,10702,10703],{"class":4445},"    data",[3699,10705,10706],{"class":4422},": UserRegisterRequest,\n",[3699,10708,10709,10711],{"class":3701,"line":4665},[3699,10710,9727],{"class":4445},[3699,10712,9730],{"class":4422},[3699,10714,10715],{"class":3701,"line":4670},[3699,10716,10717],{"class":4422},") -> TokenResponse:\n",[3699,10719,10720],{"class":3701,"line":4676},[3699,10721,4465],{"class":4324},[3699,10723,10724],{"class":3701,"line":4681},[3699,10725,10726],{"class":4324},"    Реєстрація нового користувача.\n",[3699,10728,10729],{"class":3701,"line":4694},[3699,10730,3730],{"emptyLinePlaceholder":3729},[3699,10732,10733],{"class":3701,"line":4704},[3699,10734,10735],{"class":4324},"    Хешує пароль, створює User у БД, одразу видає токени\n",[3699,10737,10738],{"class":3701,"line":4709},[3699,10739,10740],{"class":4324},"    (без окремого кроку логіну після реєстрації).\n",[3699,10742,10743],{"class":3701,"line":6342},[3699,10744,4465],{"class":4324},[3699,10746,10747],{"class":3701,"line":6348},[3699,10748,10036],{"class":4422},[3699,10750,10751],{"class":3701,"line":6354},[3699,10752,10753],{"class":4422},"    token_repo = RefreshTokenRepository(db)\n",[3699,10755,10756],{"class":3701,"line":6359},[3699,10757,3730],{"emptyLinePlaceholder":3729},[3699,10759,10760],{"class":3701,"line":6365},[3699,10761,10762],{"class":4412},"    # Перевірка унікальності email\n",[3699,10764,10765,10767,10770],{"class":3701,"line":6371},[3699,10766,5988],{"class":4418},[3699,10768,10769],{"class":4418}," await",[3699,10771,10772],{"class":4422}," user_repo.get_by_email(data.email):\n",[3699,10774,10775,10777],{"class":3701,"line":6377},[3699,10776,9972],{"class":4418},[3699,10778,9975],{"class":4422},[3699,10780,10781,10783],{"class":3701,"line":6382},[3699,10782,9980],{"class":4445},[3699,10784,10785],{"class":4422},"=status.HTTP_409_CONFLICT,\n",[3699,10787,10788,10790,10792,10795],{"class":3701,"line":6387},[3699,10789,9987],{"class":4445},[3699,10791,4556],{"class":4422},[3699,10793,10794],{"class":4324},"\"User with this email already exists\"",[3699,10796,7077],{"class":4422},[3699,10798,10799],{"class":3701,"line":6392},[3699,10800,10015],{"class":4422},[3699,10802,10803],{"class":3701,"line":6398},[3699,10804,3730],{"emptyLinePlaceholder":3729},[3699,10806,10807],{"class":3701,"line":6404},[3699,10808,10809],{"class":4412},"    # Створення користувача\n",[3699,10811,10812],{"class":3701,"line":6409},[3699,10813,10814],{"class":4422},"    user = User(\n",[3699,10816,10817,10820],{"class":3701,"line":8467},[3699,10818,10819],{"class":4445},"        email",[3699,10821,10822],{"class":4422},"=data.email,\n",[3699,10824,10825,10828],{"class":3701,"line":8472},[3699,10826,10827],{"class":4445},"        display_name",[3699,10829,10830],{"class":4422},"=data.display_name,\n",[3699,10832,10833,10836],{"class":3701,"line":8494},[3699,10834,10835],{"class":4445},"        hashed_password",[3699,10837,10838],{"class":4422},"=hash_password(data.password),\n",[3699,10840,10841],{"class":3701,"line":8499},[3699,10842,7951],{"class":4422},[3699,10844,10845,10847],{"class":3701,"line":8505},[3699,10846,4966],{"class":4418},[3699,10848,10849],{"class":4422}," user_repo.create(user)\n",[3699,10851,10852],{"class":3701,"line":8510},[3699,10853,3730],{"emptyLinePlaceholder":3729},[3699,10855,10856],{"class":3701,"line":8515},[3699,10857,10858],{"class":4412},"    # Видача токенів\n",[3699,10860,10861,10864,10867,10869,10871],{"class":3701,"line":8521},[3699,10862,10863],{"class":4422},"    access_token = create_access_token(",[3699,10865,10866],{"class":4445},"subject",[3699,10868,4556],{"class":4422},[3699,10870,4244],{"class":4452},[3699,10872,10873],{"class":4422},"(user.id))\n",[3699,10875,10876],{"class":3701,"line":8526},[3699,10877,10878],{"class":4422},"    plain_refresh_token = generate_refresh_token()\n",[3699,10880,10881],{"class":3701,"line":8531},[3699,10882,3730],{"emptyLinePlaceholder":3729},[3699,10884,10885,10887],{"class":3701,"line":8537},[3699,10886,4966],{"class":4418},[3699,10888,10889],{"class":4422}," token_repo.create(RefreshToken(\n",[3699,10891,10892,10895],{"class":3701,"line":8542},[3699,10893,10894],{"class":4445},"        token_hash",[3699,10896,10897],{"class":4422},"=hash_refresh_token(plain_refresh_token),\n",[3699,10899,10900,10903],{"class":3701,"line":8548},[3699,10901,10902],{"class":4445},"        user_id",[3699,10904,10905],{"class":4422},"=user.id,\n",[3699,10907,10908,10911,10914,10917],{"class":3701,"line":8554},[3699,10909,10910],{"class":4445},"        expires_at",[3699,10912,10913],{"class":4422},"=datetime.now(timezone.utc) + timedelta(",[3699,10915,10916],{"class":4445},"days",[3699,10918,10919],{"class":4422},"=settings.REFRESH_TOKEN_EXPIRE_DAYS),\n",[3699,10921,10922],{"class":3701,"line":8560},[3699,10923,10924],{"class":4422},"    ))\n",[3699,10926,10927],{"class":3701,"line":8565},[3699,10928,3730],{"emptyLinePlaceholder":3729},[3699,10930,10931,10933],{"class":3701,"line":8573},[3699,10932,4576],{"class":4418},[3699,10934,10935],{"class":4422}," TokenResponse(\n",[3699,10937,10938,10941],{"class":3701,"line":8579},[3699,10939,10940],{"class":4445},"        access_token",[3699,10942,10943],{"class":4422},"=access_token,\n",[3699,10945,10946,10949],{"class":3701,"line":8585},[3699,10947,10948],{"class":4445},"        refresh_token",[3699,10950,10951],{"class":4422},"=plain_refresh_token,\n",[3699,10953,10954,10957,10959,10962],{"class":3701,"line":8593},[3699,10955,10956],{"class":4445},"        token_type",[3699,10958,4556],{"class":4422},[3699,10960,10961],{"class":4324},"\"bearer\"",[3699,10963,7077],{"class":4422},[3699,10965,10966],{"class":3701,"line":8598},[3699,10967,7951],{"class":4422},[3699,10969,10970],{"class":3701,"line":8603},[3699,10971,3730],{"emptyLinePlaceholder":3729},[3699,10973,10974],{"class":3701,"line":8608},[3699,10975,3730],{"emptyLinePlaceholder":3729},[3699,10977,10978,10980,10982,10985,10987,10989],{"class":3701,"line":8614},[3699,10979,10348],{"class":4321},[3699,10981,4442],{"class":4422},[3699,10983,10984],{"class":4324},"\"\u002Flogin\"",[3699,10986,4200],{"class":4422},[3699,10988,10679],{"class":4445},[3699,10990,10991],{"class":4422},"=TokenResponse)\n",[3699,10993,10994,10996,10998,11001],{"class":3701,"line":8629},[3699,10995,9702],{"class":4435},[3699,10997,9705],{"class":4435},[3699,10999,11000],{"class":4321}," login",[3699,11002,8268],{"class":4422},[3699,11004,11005,11008],{"class":3701,"line":8634},[3699,11006,11007],{"class":4445},"    form_data",[3699,11009,11010],{"class":4422},": OAuth2PasswordRequestForm = Depends(),\n",[3699,11012,11013,11015],{"class":3701,"line":8640},[3699,11014,9727],{"class":4445},[3699,11016,9730],{"class":4422},[3699,11018,11019],{"class":3701,"line":8645},[3699,11020,10717],{"class":4422},[3699,11022,11023],{"class":3701,"line":8651},[3699,11024,4465],{"class":4324},[3699,11026,11027],{"class":3701,"line":8657},[3699,11028,11029],{"class":4324},"    Логін через OAuth2 Password Flow.\n",[3699,11031,11032],{"class":3701,"line":8663},[3699,11033,3730],{"emptyLinePlaceholder":3729},[3699,11035,11036],{"class":3701,"line":8668},[3699,11037,11038],{"class":4324},"    Приймає form-data (application\u002Fx-www-form-urlencoded),\n",[3699,11040,11041],{"class":3701,"line":8673},[3699,11042,11043],{"class":4324},"    а не JSON — це вимога OAuth2 специфікації.\n",[3699,11045,11046],{"class":3701,"line":8679},[3699,11047,11048],{"class":4324},"    Поле 'username' тут використовується як email.\n",[3699,11050,11051],{"class":3701,"line":8685},[3699,11052,4465],{"class":4324},[3699,11054,11055],{"class":3701,"line":8690},[3699,11056,10036],{"class":4422},[3699,11058,11059],{"class":3701,"line":8702},[3699,11060,10753],{"class":4422},[3699,11062,11063],{"class":3701,"line":8707},[3699,11064,3730],{"emptyLinePlaceholder":3729},[3699,11066,11067],{"class":3701,"line":8712},[3699,11068,11069],{"class":4412},"    # Знаходимо користувача (form_data.username — це email)\n",[3699,11071,11072,11074,11076],{"class":3701,"line":8734},[3699,11073,10041],{"class":4422},[3699,11075,10044],{"class":4418},[3699,11077,11078],{"class":4422}," user_repo.get_by_email(form_data.username)\n",[3699,11080,11081],{"class":3701,"line":8739},[3699,11082,3730],{"emptyLinePlaceholder":3729},[3699,11084,11085],{"class":3701,"line":8745},[3699,11086,11087],{"class":4412},"    # Перевіряємо credentials. Завжди виконуємо verify_password,\n",[3699,11089,11090],{"class":3701,"line":8750},[3699,11091,11092],{"class":4412},"    # навіть якщо user == None, щоб уникнути timing attack\n",[3699,11094,11095],{"class":3701,"line":8756},[3699,11096,11097],{"class":4412},"    # (різний час відповіді міг би видати факт існування акаунта)\n",[3699,11099,11100,11102,11104,11106,11109,11111],{"class":3701,"line":8762},[3699,11101,5988],{"class":4418},[3699,11103,10185],{"class":4435},[3699,11105,10058],{"class":4422},[3699,11107,11108],{"class":4435},"or",[3699,11110,10185],{"class":4435},[3699,11112,11113],{"class":4422}," verify_password(form_data.password, user.hashed_password):\n",[3699,11115,11116,11118],{"class":3701,"line":8768},[3699,11117,9972],{"class":4418},[3699,11119,9975],{"class":4422},[3699,11121,11122,11124],{"class":3701,"line":8774},[3699,11123,9980],{"class":4445},[3699,11125,9824],{"class":4422},[3699,11127,11128,11130,11132,11135],{"class":3701,"line":8780},[3699,11129,9987],{"class":4445},[3699,11131,4556],{"class":4422},[3699,11133,11134],{"class":4324},"\"Incorrect email or password\"",[3699,11136,7077],{"class":4422},[3699,11138,11139,11141,11143,11145,11147,11149],{"class":3701,"line":8786},[3699,11140,9999],{"class":4445},[3699,11142,9844],{"class":4422},[3699,11144,9847],{"class":4324},[3699,11146,4449],{"class":4422},[3699,11148,9852],{"class":4324},[3699,11150,10010],{"class":4422},[3699,11152,11153],{"class":3701,"line":8792},[3699,11154,10015],{"class":4422},[3699,11156,11157],{"class":3701,"line":8797},[3699,11158,3730],{"emptyLinePlaceholder":3729},[3699,11160,11161,11163,11165],{"class":3701,"line":8802},[3699,11162,5988],{"class":4418},[3699,11164,10185],{"class":4435},[3699,11166,11167],{"class":4422}," user.is_active:\n",[3699,11169,11170,11172],{"class":3701,"line":8808},[3699,11171,9972],{"class":4418},[3699,11173,9975],{"class":4422},[3699,11175,11176,11178],{"class":3701,"line":8813},[3699,11177,9980],{"class":4445},[3699,11179,10201],{"class":4422},[3699,11181,11182,11184,11186,11189],{"class":3701,"line":8818},[3699,11183,9987],{"class":4445},[3699,11185,4556],{"class":4422},[3699,11187,11188],{"class":4324},"\"Account is deactivated\"",[3699,11190,7077],{"class":4422},[3699,11192,11193],{"class":3701,"line":8824},[3699,11194,10015],{"class":4422},[3699,11196,11197],{"class":3701,"line":8829},[3699,11198,3730],{"emptyLinePlaceholder":3729},[3699,11200,11201],{"class":3701,"line":8837},[3699,11202,10858],{"class":4412},[3699,11204,11205,11207,11209,11211,11213],{"class":3701,"line":8842},[3699,11206,10863],{"class":4422},[3699,11208,10866],{"class":4445},[3699,11210,4556],{"class":4422},[3699,11212,4244],{"class":4452},[3699,11214,10873],{"class":4422},[3699,11216,11217],{"class":3701,"line":8847},[3699,11218,10878],{"class":4422},[3699,11220,11221],{"class":3701,"line":8879},[3699,11222,3730],{"emptyLinePlaceholder":3729},[3699,11224,11225,11227],{"class":3701,"line":8884},[3699,11226,4966],{"class":4418},[3699,11228,10889],{"class":4422},[3699,11230,11231,11233],{"class":3701,"line":8890},[3699,11232,10894],{"class":4445},[3699,11234,10897],{"class":4422},[3699,11236,11237,11239],{"class":3701,"line":8895},[3699,11238,10902],{"class":4445},[3699,11240,10905],{"class":4422},[3699,11242,11243,11245,11247,11249],{"class":3701,"line":8901},[3699,11244,10910],{"class":4445},[3699,11246,10913],{"class":4422},[3699,11248,10916],{"class":4445},[3699,11250,10919],{"class":4422},[3699,11252,11253],{"class":3701,"line":8907},[3699,11254,10924],{"class":4422},[3699,11256,11257],{"class":3701,"line":8912},[3699,11258,3730],{"emptyLinePlaceholder":3729},[3699,11260,11261,11263],{"class":3701,"line":8917},[3699,11262,4576],{"class":4418},[3699,11264,10935],{"class":4422},[3699,11266,11267,11269],{"class":3701,"line":8923},[3699,11268,10940],{"class":4445},[3699,11270,10943],{"class":4422},[3699,11272,11273,11275],{"class":3701,"line":8929},[3699,11274,10948],{"class":4445},[3699,11276,10951],{"class":4422},[3699,11278,11279,11281,11283,11285],{"class":3701,"line":8934},[3699,11280,10956],{"class":4445},[3699,11282,4556],{"class":4422},[3699,11284,10961],{"class":4324},[3699,11286,7077],{"class":4422},[3699,11288,11289],{"class":3701,"line":8939},[3699,11290,7951],{"class":4422},[3699,11292,11293],{"class":3701,"line":8945},[3699,11294,3730],{"emptyLinePlaceholder":3729},[3699,11296,11297],{"class":3701,"line":8950},[3699,11298,3730],{"emptyLinePlaceholder":3729},[3699,11300,11301,11303,11305,11308,11310,11312],{"class":3701,"line":8956},[3699,11302,10348],{"class":4321},[3699,11304,4442],{"class":4422},[3699,11306,11307],{"class":4324},"\"\u002Frefresh\"",[3699,11309,4200],{"class":4422},[3699,11311,10679],{"class":4445},[3699,11313,10991],{"class":4422},[3699,11315,11316,11318,11320,11322],{"class":3701,"line":8962},[3699,11317,9702],{"class":4435},[3699,11319,9705],{"class":4435},[3699,11321,7020],{"class":4321},[3699,11323,8268],{"class":4422},[3699,11325,11327,11330],{"class":3701,"line":11326},131,[3699,11328,11329],{"class":4445},"    body",[3699,11331,11332],{"class":4422},": RefreshRequest,\n",[3699,11334,11336,11338],{"class":3701,"line":11335},132,[3699,11337,9727],{"class":4445},[3699,11339,9730],{"class":4422},[3699,11341,11343],{"class":3701,"line":11342},133,[3699,11344,10717],{"class":4422},[3699,11346,11348],{"class":3701,"line":11347},134,[3699,11349,4465],{"class":4324},[3699,11351,11353],{"class":3701,"line":11352},135,[3699,11354,11355],{"class":4324},"    Оновлення пари токенів через opaque refresh token.\n",[3699,11357,11359],{"class":3701,"line":11358},136,[3699,11360,3730],{"emptyLinePlaceholder":3729},[3699,11362,11364],{"class":3701,"line":11363},137,[3699,11365,11366],{"class":4324},"    Реалізує ротацію: старий refresh token знищується,\n",[3699,11368,11370],{"class":3701,"line":11369},138,[3699,11371,11372],{"class":4324},"    видається новий. Якщо token вже використаний (не знайдений у БД) —\n",[3699,11374,11376],{"class":3701,"line":11375},139,[3699,11377,11378],{"class":4324},"    це сигнал крадіжки: відкликаємо ВСІ сесії користувача.\n",[3699,11380,11382],{"class":3701,"line":11381},140,[3699,11383,4465],{"class":4324},[3699,11385,11387],{"class":3701,"line":11386},141,[3699,11388,10753],{"class":4422},[3699,11390,11392],{"class":3701,"line":11391},142,[3699,11393,3730],{"emptyLinePlaceholder":3729},[3699,11395,11397],{"class":3701,"line":11396},143,[3699,11398,11399],{"class":4412},"    # Шукаємо токен у БД за SHA-256 хешем\n",[3699,11401,11403,11406,11408],{"class":3701,"line":11402},144,[3699,11404,11405],{"class":4422},"    stored_token = ",[3699,11407,10044],{"class":4418},[3699,11409,11410],{"class":4422}," token_repo.get_by_hash(hash_refresh_token(body.refresh_token))\n",[3699,11412,11414],{"class":3701,"line":11413},145,[3699,11415,3730],{"emptyLinePlaceholder":3729},[3699,11417,11419,11421,11423,11426,11428],{"class":3701,"line":11418},146,[3699,11420,5988],{"class":4418},[3699,11422,10185],{"class":4435},[3699,11424,11425],{"class":4422}," stored_token ",[3699,11427,11108],{"class":4435},[3699,11429,11430],{"class":4422}," stored_token.expires_at \u003C datetime.now(timezone.utc):\n",[3699,11432,11434],{"class":3701,"line":11433},147,[3699,11435,11436],{"class":4412},"        # Токен не знайдений або протух\n",[3699,11438,11440],{"class":3701,"line":11439},148,[3699,11441,11442],{"class":4412},"        # Якщо не знайдений — можлива ротаційна атака.\n",[3699,11444,11446],{"class":3701,"line":11445},149,[3699,11447,11448],{"class":4412},"        # Для простоти тут повертаємо 401; у production — додаткова логіка.\n",[3699,11450,11452,11454],{"class":3701,"line":11451},150,[3699,11453,9972],{"class":4418},[3699,11455,9975],{"class":4422},[3699,11457,11459,11461],{"class":3701,"line":11458},151,[3699,11460,9980],{"class":4445},[3699,11462,9824],{"class":4422},[3699,11464,11466,11468,11470,11473],{"class":3701,"line":11465},152,[3699,11467,9987],{"class":4445},[3699,11469,4556],{"class":4422},[3699,11471,11472],{"class":4324},"\"Invalid or expired refresh token\"",[3699,11474,7077],{"class":4422},[3699,11476,11478],{"class":3701,"line":11477},153,[3699,11479,10015],{"class":4422},[3699,11481,11483],{"class":3701,"line":11482},154,[3699,11484,3730],{"emptyLinePlaceholder":3729},[3699,11486,11488],{"class":3701,"line":11487},155,[3699,11489,11490],{"class":4422},"    user_id = stored_token.user_id\n",[3699,11492,11494],{"class":3701,"line":11493},156,[3699,11495,3730],{"emptyLinePlaceholder":3729},[3699,11497,11499],{"class":3701,"line":11498},157,[3699,11500,11501],{"class":4412},"    # Ротація: видаляємо старий токен\n",[3699,11503,11505,11507],{"class":3701,"line":11504},158,[3699,11506,4966],{"class":4418},[3699,11508,11509],{"class":4422}," token_repo.delete(stored_token.id)\n",[3699,11511,11513],{"class":3701,"line":11512},159,[3699,11514,3730],{"emptyLinePlaceholder":3729},[3699,11516,11518],{"class":3701,"line":11517},160,[3699,11519,11520],{"class":4412},"    # Видаємо нові токени\n",[3699,11522,11524,11526,11528,11530,11532],{"class":3701,"line":11523},161,[3699,11525,10863],{"class":4422},[3699,11527,10866],{"class":4445},[3699,11529,4556],{"class":4422},[3699,11531,4244],{"class":4452},[3699,11533,11534],{"class":4422},"(user_id))\n",[3699,11536,11538],{"class":3701,"line":11537},162,[3699,11539,10878],{"class":4422},[3699,11541,11543],{"class":3701,"line":11542},163,[3699,11544,3730],{"emptyLinePlaceholder":3729},[3699,11546,11548,11550],{"class":3701,"line":11547},164,[3699,11549,4966],{"class":4418},[3699,11551,10889],{"class":4422},[3699,11553,11555,11557],{"class":3701,"line":11554},165,[3699,11556,10894],{"class":4445},[3699,11558,10897],{"class":4422},[3699,11560,11562,11564],{"class":3701,"line":11561},166,[3699,11563,10902],{"class":4445},[3699,11565,11566],{"class":4422},"=user_id,\n",[3699,11568,11570,11572,11574,11576],{"class":3701,"line":11569},167,[3699,11571,10910],{"class":4445},[3699,11573,10913],{"class":4422},[3699,11575,10916],{"class":4445},[3699,11577,10919],{"class":4422},[3699,11579,11581],{"class":3701,"line":11580},168,[3699,11582,10924],{"class":4422},[3699,11584,11586],{"class":3701,"line":11585},169,[3699,11587,3730],{"emptyLinePlaceholder":3729},[3699,11589,11591,11593],{"class":3701,"line":11590},170,[3699,11592,4576],{"class":4418},[3699,11594,10935],{"class":4422},[3699,11596,11598,11600],{"class":3701,"line":11597},171,[3699,11599,10940],{"class":4445},[3699,11601,10943],{"class":4422},[3699,11603,11605,11607],{"class":3701,"line":11604},172,[3699,11606,10948],{"class":4445},[3699,11608,10951],{"class":4422},[3699,11610,11612,11614,11616,11618],{"class":3701,"line":11611},173,[3699,11613,10956],{"class":4445},[3699,11615,4556],{"class":4422},[3699,11617,10961],{"class":4324},[3699,11619,7077],{"class":4422},[3699,11621,11623],{"class":3701,"line":11622},174,[3699,11624,7951],{"class":4422},[3699,11626,11628],{"class":3701,"line":11627},175,[3699,11629,3730],{"emptyLinePlaceholder":3729},[3699,11631,11633],{"class":3701,"line":11632},176,[3699,11634,3730],{"emptyLinePlaceholder":3729},[3699,11636,11638,11640,11642,11645,11647,11649],{"class":3701,"line":11637},177,[3699,11639,10348],{"class":4321},[3699,11641,4442],{"class":4422},[3699,11643,11644],{"class":4324},"\"\u002Flogout\"",[3699,11646,4200],{"class":4422},[3699,11648,7984],{"class":4445},[3699,11650,11651],{"class":4422},"=status.HTTP_204_NO_CONTENT)\n",[3699,11653,11655,11657,11659,11662],{"class":3701,"line":11654},178,[3699,11656,9702],{"class":4435},[3699,11658,9705],{"class":4435},[3699,11660,11661],{"class":4321}," logout",[3699,11663,8268],{"class":4422},[3699,11665,11667,11669],{"class":3701,"line":11666},179,[3699,11668,11329],{"class":4445},[3699,11670,11332],{"class":4422},[3699,11672,11674,11676],{"class":3701,"line":11673},180,[3699,11675,9727],{"class":4445},[3699,11677,9730],{"class":4422},[3699,11679,11681,11683,11685],{"class":3701,"line":11680},181,[3699,11682,4455],{"class":4422},[3699,11684,4938],{"class":4435},[3699,11686,4460],{"class":4422},[3699,11688,11690],{"class":3701,"line":11689},182,[3699,11691,4465],{"class":4324},[3699,11693,11695],{"class":3701,"line":11694},183,[3699,11696,11697],{"class":4324},"    Логаут: відкликає refresh token.\n",[3699,11699,11701],{"class":3701,"line":11700},184,[3699,11702,3730],{"emptyLinePlaceholder":3729},[3699,11704,11706],{"class":3701,"line":11705},185,[3699,11707,11708],{"class":4324},"    Access token залишається дійсним до закінчення exp (15 хв).\n",[3699,11710,11712],{"class":3701,"line":11711},186,[3699,11713,11714],{"class":4324},"    Це прийнятний trade-off для stateless JWT.\n",[3699,11716,11718],{"class":3701,"line":11717},187,[3699,11719,4465],{"class":4324},[3699,11721,11723],{"class":3701,"line":11722},188,[3699,11724,10753],{"class":4422},[3699,11726,11728,11730,11732],{"class":3701,"line":11727},189,[3699,11729,11405],{"class":4422},[3699,11731,10044],{"class":4418},[3699,11733,11410],{"class":4422},[3699,11735,11737,11739],{"class":3701,"line":11736},190,[3699,11738,5988],{"class":4418},[3699,11740,11741],{"class":4422}," stored_token:\n",[3699,11743,11745,11748],{"class":3701,"line":11744},191,[3699,11746,11747],{"class":4418},"        await",[3699,11749,11509],{"class":4422},[3614,11751,11752,11753,11756,11757,11760,11761,4380],{},"Зверніть увагу на коментар про ",[3560,11754,11755],{},"timing attack"," у ендпоінті ",[3625,11758,11759],{},"\u002Flogin",". Якщо б ми повертали різні помилки для «користувача не знайдено» та «неправильний пароль», зловмисник міг би перебором визначати, які email зареєстровані у системі. Завжди повертайте однакову загальну помилку: ",[3625,11762,11134],{},[3654,11764],{},[3551,11766,11768],{"id":11767},"повна-реалізація-копіюй-та-запускай","Повна реалізація: копіюй та запускай",[3556,11770,11771,11772,11775],{},"Усі попередні приклади були розбиті на шматки для пояснення концепцій. Тут — повна, робоча реалізація всього auth-шару TaskForge. Жодних ",[3625,11773,11774],{},"...",", жодних посилань на невизначені модулі.",[3661,11777,11779],{"id":11778},"структура-файлів","Структура файлів",[3690,11781,11784],{"className":11782,"code":11783,"language":5306},[5304],"taskforge\u002F\n├── .env\n├── create_tables.py\n└── app\u002F\n    ├── main.py\n    ├── core\u002F\n    │   ├── config.py\n    │   └── security.py\n    ├── db\u002F\n    │   └── session.py\n    ├── models\u002F\n    │   ├── base.py\n    │   ├── user.py\n    │   └── refresh_token.py\n    ├── schemas\u002F\n    │   └── auth.py\n    ├── repositories\u002F\n    │   ├── user.py\n    │   └── refresh_token.py\n    └── api\u002F\n        ├── deps.py\n        └── v1\u002F\n            └── auth.py\n",[3625,11785,11783],{"__ignoreMap":3695},[3661,11787,11789],{"id":11788},"встановлення-залежностей","Встановлення залежностей",[4304,11791,11792,11839,11878],{},[4307,11793,11794],{"label":4309},[3690,11795,11797],{"className":4312,"code":11796,"language":4314,"meta":3695,"style":3695},"pip install fastapi \"uvicorn[standard]\" \"sqlalchemy[asyncio]\" asyncpg \\\n            bcrypt \"python-jose[cryptography]\" pydantic-settings \\\n            email-validator\n",[3625,11798,11799,11821,11834],{"__ignoreMap":3695},[3699,11800,11801,11803,11805,11808,11811,11814,11817],{"class":3701,"line":3702},[3699,11802,4309],{"class":4321},[3699,11804,4325],{"class":4324},[3699,11806,11807],{"class":4324}," fastapi",[3699,11809,11810],{"class":4324}," \"uvicorn[standard]\"",[3699,11812,11813],{"class":4324}," \"sqlalchemy[asyncio]\"",[3699,11815,11816],{"class":4324}," asyncpg",[3699,11818,11820],{"class":11819},"sjcCO"," \\\n",[3699,11822,11823,11826,11829,11832],{"class":3701,"line":3708},[3699,11824,11825],{"class":4324},"            bcrypt",[3699,11827,11828],{"class":4324}," \"python-jose[cryptography]\"",[3699,11830,11831],{"class":4324}," pydantic-settings",[3699,11833,11820],{"class":11819},[3699,11835,11836],{"class":3701,"line":3714},[3699,11837,11838],{"class":4324},"            email-validator\n",[4307,11840,11841],{"label":4331},[3690,11842,11844],{"className":4312,"code":11843,"language":4314,"meta":3695,"style":3695},"uv add fastapi \"uvicorn[standard]\" \"sqlalchemy[asyncio]\" asyncpg \\\n       bcrypt \"python-jose[cryptography]\" pydantic-settings \\\n       email-validator\n",[3625,11845,11846,11862,11873],{"__ignoreMap":3695},[3699,11847,11848,11850,11852,11854,11856,11858,11860],{"class":3701,"line":3702},[3699,11849,4331],{"class":4321},[3699,11851,4343],{"class":4324},[3699,11853,11807],{"class":4324},[3699,11855,11810],{"class":4324},[3699,11857,11813],{"class":4324},[3699,11859,11816],{"class":4324},[3699,11861,11820],{"class":11819},[3699,11863,11864,11867,11869,11871],{"class":3701,"line":3708},[3699,11865,11866],{"class":4324},"       bcrypt",[3699,11868,11828],{"class":4324},[3699,11870,11831],{"class":4324},[3699,11872,11820],{"class":11819},[3699,11874,11875],{"class":3701,"line":3714},[3699,11876,11877],{"class":4324},"       email-validator\n",[4307,11879,11880],{"label":4348},[3690,11881,11883],{"className":4312,"code":11882,"language":4314,"meta":3695,"style":3695},"poetry add fastapi \"uvicorn[standard]\" \"sqlalchemy[asyncio]\" asyncpg \\\n           bcrypt \"python-jose[cryptography]\" pydantic-settings \\\n           email-validator\n",[3625,11884,11885,11901,11912],{"__ignoreMap":3695},[3699,11886,11887,11889,11891,11893,11895,11897,11899],{"class":3701,"line":3702},[3699,11888,4348],{"class":4321},[3699,11890,4343],{"class":4324},[3699,11892,11807],{"class":4324},[3699,11894,11810],{"class":4324},[3699,11896,11813],{"class":4324},[3699,11898,11816],{"class":4324},[3699,11900,11820],{"class":11819},[3699,11902,11903,11906,11908,11910],{"class":3701,"line":3708},[3699,11904,11905],{"class":4324},"           bcrypt",[3699,11907,11828],{"class":4324},[3699,11909,11831],{"class":4324},[3699,11911,11820],{"class":11819},[3699,11913,11914],{"class":3701,"line":3714},[3699,11915,11916],{"class":4324},"           email-validator\n",[3661,11918,11920],{"id":11919},"усі-файли","Усі файли",[11922,11923,11924,11970,12108,12760,12939,12988,13295,13667,13845,14105,14458,14906,16086,16247],"code-tree",{},[3690,11925,11930],{"className":11926,"code":11927,"filename":11928,"language":11929,"meta":3695,"style":3695},"language-ini shiki shiki-themes light-plus dark-plus dark-plus","DATABASE_URL=postgresql+asyncpg:\u002F\u002Fpostgres:postgres@localhost:5432\u002Ftaskforge\nSECRET_KEY=09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7\nALGORITHM=HS256\nACCESS_TOKEN_EXPIRE_MINUTES=15\nREFRESH_TOKEN_EXPIRE_DAYS=30\n",".env","ini",[3625,11931,11932,11940,11947,11954,11962],{"__ignoreMap":3695},[3699,11933,11934,11937],{"class":3701,"line":3702},[3699,11935,11936],{"class":4435},"DATABASE_URL",[3699,11938,11939],{"class":4422},"=postgresql+asyncpg:\u002F\u002Fpostgres:postgres@localhost:5432\u002Ftaskforge\n",[3699,11941,11942,11944],{"class":3701,"line":3708},[3699,11943,9120],{"class":4435},[3699,11945,11946],{"class":4422},"=09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7\n",[3699,11948,11949,11951],{"class":3701,"line":3714},[3699,11950,9123],{"class":4435},[3699,11952,11953],{"class":4422},"=HS256\n",[3699,11955,11956,11959],{"class":3701,"line":3720},[3699,11957,11958],{"class":4435},"ACCESS_TOKEN_EXPIRE_MINUTES",[3699,11960,11961],{"class":4422},"=15\n",[3699,11963,11964,11967],{"class":3701,"line":3726},[3699,11965,11966],{"class":4435},"REFRESH_TOKEN_EXPIRE_DAYS",[3699,11968,11969],{"class":4422},"=30\n",[3690,11971,11973],{"className":4403,"code":11972,"filename":8973,"language":4405,"meta":3695,"style":3695},"from pydantic_settings import BaseSettings, SettingsConfigDict\n\n\nclass Settings(BaseSettings):\n    model_config = SettingsConfigDict(env_file=\".env\", env_file_encoding=\"utf-8\")\n\n    DATABASE_URL: str\n\n    # JWT — лише для Access Token\n    SECRET_KEY: str\n    ALGORITHM: str = \"HS256\"\n    ACCESS_TOKEN_EXPIRE_MINUTES: int = 15\n\n    # Opaque Refresh Token — зберігається у БД\n    REFRESH_TOKEN_EXPIRE_DAYS: int = 30\n\n\nsettings = Settings()\n",[3625,11974,11975,11986,11990,11994,12006,12030,12034,12042,12046,12051,12057,12067,12077,12081,12086,12096,12100,12104],{"__ignoreMap":3695},[3699,11976,11977,11979,11981,11983],{"class":3701,"line":3702},[3699,11978,5828],{"class":4418},[3699,11980,8990],{"class":4422},[3699,11982,4419],{"class":4418},[3699,11984,11985],{"class":4422}," BaseSettings, SettingsConfigDict\n",[3699,11987,11988],{"class":3701,"line":3708},[3699,11989,3730],{"emptyLinePlaceholder":3729},[3699,11991,11992],{"class":3701,"line":3714},[3699,11993,3730],{"emptyLinePlaceholder":3729},[3699,11995,11996,11998,12000,12002,12004],{"class":3701,"line":3720},[3699,11997,9008],{"class":4435},[3699,11999,9011],{"class":4452},[3699,12001,4442],{"class":4422},[3699,12003,9016],{"class":4452},[3699,12005,9019],{"class":4422},[3699,12007,12008,12011,12014,12016,12019,12021,12024,12026,12028],{"class":3701,"line":3726},[3699,12009,12010],{"class":4422},"    model_config = SettingsConfigDict(",[3699,12012,12013],{"class":4445},"env_file",[3699,12015,4556],{"class":4422},[3699,12017,12018],{"class":4324},"\".env\"",[3699,12020,4200],{"class":4422},[3699,12022,12023],{"class":4445},"env_file_encoding",[3699,12025,4556],{"class":4422},[3699,12027,4525],{"class":4324},[3699,12029,4562],{"class":4422},[3699,12031,12032],{"class":3701,"line":3733},[3699,12033,3730],{"emptyLinePlaceholder":3729},[3699,12035,12036,12039],{"class":3701,"line":3739},[3699,12037,12038],{"class":4422},"    DATABASE_URL: ",[3699,12040,12041],{"class":4452},"str\n",[3699,12043,12044],{"class":3701,"line":3745},[3699,12045,3730],{"emptyLinePlaceholder":3729},[3699,12047,12048],{"class":3701,"line":3751},[3699,12049,12050],{"class":4412},"    # JWT — лише для Access Token\n",[3699,12052,12053,12055],{"class":3701,"line":3757},[3699,12054,9029],{"class":4422},[3699,12056,12041],{"class":4452},[3699,12058,12059,12061,12063,12065],{"class":3701,"line":3763},[3699,12060,9039],{"class":4422},[3699,12062,4244],{"class":4452},[3699,12064,8297],{"class":4422},[3699,12066,9046],{"class":4324},[3699,12068,12069,12071,12073,12075],{"class":3701,"line":3769},[3699,12070,9051],{"class":4422},[3699,12072,4254],{"class":4452},[3699,12074,8297],{"class":4422},[3699,12076,9058],{"class":4531},[3699,12078,12079],{"class":3701,"line":3775},[3699,12080,3730],{"emptyLinePlaceholder":3729},[3699,12082,12083],{"class":3701,"line":3781},[3699,12084,12085],{"class":4412},"    # Opaque Refresh Token — зберігається у БД\n",[3699,12087,12088,12090,12092,12094],{"class":3701,"line":3786},[3699,12089,9072],{"class":4422},[3699,12091,4254],{"class":4452},[3699,12093,8297],{"class":4422},[3699,12095,9079],{"class":4531},[3699,12097,12098],{"class":3701,"line":3792},[3699,12099,3730],{"emptyLinePlaceholder":3729},[3699,12101,12102],{"class":3701,"line":3798},[3699,12103,3730],{"emptyLinePlaceholder":3729},[3699,12105,12106],{"class":3701,"line":3804},[3699,12107,9114],{"class":4422},[3690,12109,12111],{"className":4403,"code":12110,"filename":4396,"language":4405,"meta":3695,"style":3695},"import bcrypt\nimport hashlib\nimport secrets\nfrom datetime import datetime, timedelta, timezone\nfrom typing import Any\n\nfrom jose import jwt\n\nfrom app.core.config import settings\n\n# ─── Хешування паролів ────────────────────────────────────────────────────────\ndef hash_password(plain_password: str) -> str:\n    \"\"\"Хешує пароль через bcrypt (work factor=12, вбудований salt).\"\"\"\n    pwd_bytes = plain_password.encode(\"utf-8\")[:72]\n    salt = bcrypt.gensalt(rounds=12)\n    return bcrypt.hashpw(pwd_bytes, salt).decode(\"utf-8\")\n\n\ndef verify_password(plain_password: str, hashed_password: str) -> bool:\n    \"\"\"Перевіряє відповідність пароля збереженому bcrypt-хешу.\"\"\"\n    pwd_bytes = plain_password.encode(\"utf-8\")[:72]\n    return bcrypt.checkpw(pwd_bytes, hashed_password.encode(\"utf-8\"))\n\n\n# ─── Access JWT ───────────────────────────────────────────────────────────────\ndef create_access_token(subject: str | Any, extra_claims: dict | None = None) -> str:\n    \"\"\"\n    Створює короткоживучий Access JWT-токен.\n\n    Args:\n        subject: ID користувача (записується у claim 'sub').\n        extra_claims: Додаткові довільні claims (email, role тощо).\n\n    Returns:\n        Підписаний JWT рядок.\n    \"\"\"\n    now = datetime.now(timezone.utc)\n    payload: dict = {\n        \"sub\": str(subject),\n        \"iat\": now,\n        \"exp\": now + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES),\n        \"type\": \"access\",  # захист від підміни типу токена\n    }\n    if extra_claims:\n        payload.update(extra_claims)\n    return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM)\n\n\ndef decode_access_token(token: str) -> dict:\n    \"\"\"\n    Верифікує підпис Access JWT та повертає payload.\n\n    Raises:\n        jose.ExpiredSignatureError: токен протух.\n        jose.JWTError: невалідний підпис або формат.\n    \"\"\"\n    return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])\n\n\n# ─── Opaque Refresh Token ─────────────────────────────────────────────────────\ndef generate_refresh_token() -> str:\n    \"\"\"\n    Генерує криптографічно безпечний opaque refresh token.\n\n    Returns:\n        URL-safe рядок (43 символи = 32 байти в base64url).\n        Він НЕ є JWT — не містить жодних вбудованих даних.\n    \"\"\"\n    return secrets.token_urlsafe(32)\n\n\ndef hash_refresh_token(token: str) -> str:\n    \"\"\"\n    SHA-256 хеш refresh token для зберігання у БД.\n\n    Чому SHA-256 (а не bcrypt):\n    - Token вже є 256-бітним випадковим рядком — rainbow tables неможливі.\n    - bcrypt (~160 мс) зайво повільний для lookup-операцій при кожному refresh.\n    - SHA-256 (~мкс) захищає від витоку хешів з БД без втрати продуктивності.\n\n    Returns:\n        HEX-рядок SHA-256 хешу (64 символи).\n    \"\"\"\n    return hashlib.sha256(token.encode()).hexdigest()\n\n\ndef verify_refresh_token(plain_token: str, stored_hash: str) -> bool:\n    \"\"\"\n    Перевіряє відповідність refresh token збереженому SHA-256 хешу.\n    Використовує secrets.compare_digest для захисту від timing attacks.\n    \"\"\"\n    return secrets.compare_digest(hash_refresh_token(plain_token), stored_hash)\n",[3625,12112,12113,12119,12125,12131,12141,12151,12155,12165,12169,12179,12183,12187,12207,12212,12224,12236,12246,12250,12254,12282,12287,12299,12309,12313,12317,12322,12360,12364,12368,12372,12376,12381,12386,12390,12394,12399,12403,12407,12417,12427,12433,12445,12458,12462,12468,12472,12482,12486,12490,12510,12514,12519,12523,12527,12532,12537,12541,12553,12557,12561,12565,12577,12581,12585,12589,12593,12598,12603,12607,12617,12621,12625,12645,12649,12654,12658,12663,12668,12673,12678,12682,12686,12690,12694,12700,12704,12708,12736,12740,12744,12749,12753],{"__ignoreMap":3695},[3699,12114,12115,12117],{"class":3701,"line":3702},[3699,12116,4419],{"class":4418},[3699,12118,4328],{"class":4422},[3699,12120,12121,12123],{"class":3701,"line":3708},[3699,12122,4419],{"class":4418},[3699,12124,8068],{"class":4422},[3699,12126,12127,12129],{"class":3701,"line":3714},[3699,12128,4419],{"class":4418},[3699,12130,6972],{"class":4422},[3699,12132,12133,12135,12137,12139],{"class":3701,"line":3720},[3699,12134,5828],{"class":4418},[3699,12136,7678],{"class":4422},[3699,12138,4419],{"class":4418},[3699,12140,7683],{"class":4422},[3699,12142,12143,12145,12147,12149],{"class":3701,"line":3726},[3699,12144,5828],{"class":4418},[3699,12146,8091],{"class":4422},[3699,12148,4419],{"class":4418},[3699,12150,8096],{"class":4422},[3699,12152,12153],{"class":3701,"line":3733},[3699,12154,3730],{"emptyLinePlaceholder":3729},[3699,12156,12157,12159,12161,12163],{"class":3701,"line":3739},[3699,12158,5828],{"class":4418},[3699,12160,7666],{"class":4422},[3699,12162,4419],{"class":4418},[3699,12164,7671],{"class":4422},[3699,12166,12167],{"class":3701,"line":3745},[3699,12168,3730],{"emptyLinePlaceholder":3729},[3699,12170,12171,12173,12175,12177],{"class":3701,"line":3751},[3699,12172,5828],{"class":4418},[3699,12174,8121],{"class":4422},[3699,12176,4419],{"class":4418},[3699,12178,10543],{"class":4422},[3699,12180,12181],{"class":3701,"line":3757},[3699,12182,3730],{"emptyLinePlaceholder":3729},[3699,12184,12185],{"class":3701,"line":3763},[3699,12186,8138],{"class":4412},[3699,12188,12189,12191,12193,12195,12197,12199,12201,12203,12205],{"class":3701,"line":3769},[3699,12190,4436],{"class":4435},[3699,12192,4439],{"class":4321},[3699,12194,4442],{"class":4422},[3699,12196,4446],{"class":4445},[3699,12198,4449],{"class":4422},[3699,12200,4244],{"class":4452},[3699,12202,4455],{"class":4422},[3699,12204,4244],{"class":4452},[3699,12206,4460],{"class":4422},[3699,12208,12209],{"class":3701,"line":3775},[3699,12210,12211],{"class":4324},"    \"\"\"Хешує пароль через bcrypt (work factor=12, вбудований salt).\"\"\"\n",[3699,12213,12214,12216,12218,12220,12222],{"class":3701,"line":3781},[3699,12215,4522],{"class":4422},[3699,12217,4525],{"class":4324},[3699,12219,4528],{"class":4422},[3699,12221,4532],{"class":4531},[3699,12223,4535],{"class":4422},[3699,12225,12226,12228,12230,12232,12234],{"class":3701,"line":3786},[3699,12227,4550],{"class":4422},[3699,12229,4553],{"class":4445},[3699,12231,4556],{"class":4422},[3699,12233,4559],{"class":4531},[3699,12235,4562],{"class":4422},[3699,12237,12238,12240,12242,12244],{"class":3701,"line":3792},[3699,12239,4576],{"class":4418},[3699,12241,4579],{"class":4422},[3699,12243,4525],{"class":4324},[3699,12245,4562],{"class":4422},[3699,12247,12248],{"class":3701,"line":3798},[3699,12249,3730],{"emptyLinePlaceholder":3729},[3699,12251,12252],{"class":3701,"line":3804},[3699,12253,3730],{"emptyLinePlaceholder":3729},[3699,12255,12256,12258,12260,12262,12264,12266,12268,12270,12272,12274,12276,12278,12280],{"class":3701,"line":3810},[3699,12257,4436],{"class":4435},[3699,12259,4600],{"class":4321},[3699,12261,4442],{"class":4422},[3699,12263,4446],{"class":4445},[3699,12265,4449],{"class":4422},[3699,12267,4244],{"class":4452},[3699,12269,4200],{"class":4422},[3699,12271,4613],{"class":4445},[3699,12273,4449],{"class":4422},[3699,12275,4244],{"class":4452},[3699,12277,4455],{"class":4422},[3699,12279,4622],{"class":4452},[3699,12281,4460],{"class":4422},[3699,12283,12284],{"class":3701,"line":3816},[3699,12285,12286],{"class":4324},"    \"\"\"Перевіряє відповідність пароля збереженому bcrypt-хешу.\"\"\"\n",[3699,12288,12289,12291,12293,12295,12297],{"class":3701,"line":3822},[3699,12290,4522],{"class":4422},[3699,12292,4525],{"class":4324},[3699,12294,4528],{"class":4422},[3699,12296,4532],{"class":4531},[3699,12298,4535],{"class":4422},[3699,12300,12301,12303,12305,12307],{"class":3701,"line":3827},[3699,12302,4576],{"class":4418},[3699,12304,8240],{"class":4422},[3699,12306,4525],{"class":4324},[3699,12308,8245],{"class":4422},[3699,12310,12311],{"class":3701,"line":3832},[3699,12312,3730],{"emptyLinePlaceholder":3729},[3699,12314,12315],{"class":3701,"line":3837},[3699,12316,3730],{"emptyLinePlaceholder":3729},[3699,12318,12319],{"class":3701,"line":3843},[3699,12320,12321],{"class":4412},"# ─── Access JWT ───────────────────────────────────────────────────────────────\n",[3699,12323,12324,12326,12328,12330,12332,12334,12336,12339,12342,12344,12346,12348,12350,12352,12354,12356,12358],{"class":3701,"line":4590},[3699,12325,4436],{"class":4435},[3699,12327,8265],{"class":4321},[3699,12329,4442],{"class":4422},[3699,12331,10866],{"class":4445},[3699,12333,4449],{"class":4422},[3699,12335,4244],{"class":4452},[3699,12337,12338],{"class":4422}," | Any, ",[3699,12340,12341],{"class":4445},"extra_claims",[3699,12343,4449],{"class":4422},[3699,12345,7621],{"class":4452},[3699,12347,8292],{"class":4422},[3699,12349,4938],{"class":4435},[3699,12351,8297],{"class":4422},[3699,12353,4938],{"class":4435},[3699,12355,4455],{"class":4422},[3699,12357,4244],{"class":4452},[3699,12359,4460],{"class":4422},[3699,12361,12362],{"class":3701,"line":4595},[3699,12363,4465],{"class":4324},[3699,12365,12366],{"class":3701,"line":4627},[3699,12367,8318],{"class":4324},[3699,12369,12370],{"class":3701,"line":4632},[3699,12371,3730],{"emptyLinePlaceholder":3729},[3699,12373,12374],{"class":3701,"line":4638},[3699,12375,4484],{"class":4324},[3699,12377,12378],{"class":3701,"line":4643},[3699,12379,12380],{"class":4324},"        subject: ID користувача (записується у claim 'sub').\n",[3699,12382,12383],{"class":3701,"line":4648},[3699,12384,12385],{"class":4324},"        extra_claims: Додаткові довільні claims (email, role тощо).\n",[3699,12387,12388],{"class":3701,"line":4654},[3699,12389,3730],{"emptyLinePlaceholder":3729},[3699,12391,12392],{"class":3701,"line":4660},[3699,12393,4498],{"class":4324},[3699,12395,12396],{"class":3701,"line":4665},[3699,12397,12398],{"class":4324},"        Підписаний JWT рядок.\n",[3699,12400,12401],{"class":3701,"line":4670},[3699,12402,4465],{"class":4324},[3699,12404,12405],{"class":3701,"line":4676},[3699,12406,8368],{"class":4422},[3699,12408,12409,12412,12414],{"class":3701,"line":4681},[3699,12410,12411],{"class":4422},"    payload: ",[3699,12413,7621],{"class":4452},[3699,12415,12416],{"class":4422}," = {\n",[3699,12418,12419,12421,12423,12425],{"class":3701,"line":4694},[3699,12420,7705],{"class":4324},[3699,12422,4449],{"class":4422},[3699,12424,4244],{"class":4452},[3699,12426,8398],{"class":4422},[3699,12428,12429,12431],{"class":3701,"line":4704},[3699,12430,7748],{"class":4324},[3699,12432,8405],{"class":4422},[3699,12434,12435,12437,12440,12442],{"class":3701,"line":4709},[3699,12436,7729],{"class":4324},[3699,12438,12439],{"class":4422},": now + timedelta(",[3699,12441,7735],{"class":4445},[3699,12443,12444],{"class":4422},"=settings.ACCESS_TOKEN_EXPIRE_MINUTES),\n",[3699,12446,12447,12449,12451,12453,12455],{"class":3701,"line":6342},[3699,12448,8417],{"class":4324},[3699,12450,4449],{"class":4422},[3699,12452,8422],{"class":4324},[3699,12454,7054],{"class":4422},[3699,12456,12457],{"class":4412},"# захист від підміни типу токена\n",[3699,12459,12460],{"class":3701,"line":6348},[3699,12461,8432],{"class":4422},[3699,12463,12464,12466],{"class":3701,"line":6354},[3699,12465,5988],{"class":4418},[3699,12467,8439],{"class":4422},[3699,12469,12470],{"class":3701,"line":6359},[3699,12471,8444],{"class":4422},[3699,12473,12474,12476,12478,12480],{"class":3701,"line":6365},[3699,12475,4576],{"class":4418},[3699,12477,8455],{"class":4422},[3699,12479,7639],{"class":4445},[3699,12481,8460],{"class":4422},[3699,12483,12484],{"class":3701,"line":6371},[3699,12485,3730],{"emptyLinePlaceholder":3729},[3699,12487,12488],{"class":3701,"line":6377},[3699,12489,3730],{"emptyLinePlaceholder":3729},[3699,12491,12492,12494,12496,12498,12500,12502,12504,12506,12508],{"class":3701,"line":6382},[3699,12493,4436],{"class":4435},[3699,12495,8477],{"class":4321},[3699,12497,4442],{"class":4422},[3699,12499,7801],{"class":4445},[3699,12501,4449],{"class":4422},[3699,12503,4244],{"class":4452},[3699,12505,4455],{"class":4422},[3699,12507,7621],{"class":4452},[3699,12509,4460],{"class":4422},[3699,12511,12512],{"class":3701,"line":6387},[3699,12513,4465],{"class":4324},[3699,12515,12516],{"class":3701,"line":6392},[3699,12517,12518],{"class":4324},"    Верифікує підпис Access JWT та повертає payload.\n",[3699,12520,12521],{"class":3701,"line":6398},[3699,12522,3730],{"emptyLinePlaceholder":3729},[3699,12524,12525],{"class":3701,"line":6404},[3699,12526,8545],{"class":4324},[3699,12528,12529],{"class":3701,"line":6409},[3699,12530,12531],{"class":4324},"        jose.ExpiredSignatureError: токен протух.\n",[3699,12533,12534],{"class":3701,"line":8467},[3699,12535,12536],{"class":4324},"        jose.JWTError: невалідний підпис або формат.\n",[3699,12538,12539],{"class":3701,"line":8472},[3699,12540,4465],{"class":4324},[3699,12542,12543,12545,12548,12550],{"class":3701,"line":8494},[3699,12544,4576],{"class":4418},[3699,12546,12547],{"class":4422}," jwt.decode(token, settings.SECRET_KEY, ",[3699,12549,7816],{"class":4445},[3699,12551,12552],{"class":4422},"=[settings.ALGORITHM])\n",[3699,12554,12555],{"class":3701,"line":8499},[3699,12556,3730],{"emptyLinePlaceholder":3729},[3699,12558,12559],{"class":3701,"line":8505},[3699,12560,3730],{"emptyLinePlaceholder":3729},[3699,12562,12563],{"class":3701,"line":8510},[3699,12564,8611],{"class":4412},[3699,12566,12567,12569,12571,12573,12575],{"class":3701,"line":8515},[3699,12568,4436],{"class":4435},[3699,12570,8619],{"class":4321},[3699,12572,8622],{"class":4422},[3699,12574,4244],{"class":4452},[3699,12576,4460],{"class":4422},[3699,12578,12579],{"class":3701,"line":8521},[3699,12580,4465],{"class":4324},[3699,12582,12583],{"class":3701,"line":8526},[3699,12584,8637],{"class":4324},[3699,12586,12587],{"class":3701,"line":8531},[3699,12588,3730],{"emptyLinePlaceholder":3729},[3699,12590,12591],{"class":3701,"line":8537},[3699,12592,4498],{"class":4324},[3699,12594,12595],{"class":3701,"line":8542},[3699,12596,12597],{"class":4324},"        URL-safe рядок (43 символи = 32 байти в base64url).\n",[3699,12599,12600],{"class":3701,"line":8548},[3699,12601,12602],{"class":4324},"        Він НЕ є JWT — не містить жодних вбудованих даних.\n",[3699,12604,12605],{"class":3701,"line":8554},[3699,12606,4465],{"class":4324},[3699,12608,12609,12611,12613,12615],{"class":3701,"line":8560},[3699,12610,4576],{"class":4418},[3699,12612,8695],{"class":4422},[3699,12614,5918],{"class":4531},[3699,12616,4562],{"class":4422},[3699,12618,12619],{"class":3701,"line":8565},[3699,12620,3730],{"emptyLinePlaceholder":3729},[3699,12622,12623],{"class":3701,"line":8573},[3699,12624,3730],{"emptyLinePlaceholder":3729},[3699,12626,12627,12629,12631,12633,12635,12637,12639,12641,12643],{"class":3701,"line":8579},[3699,12628,4436],{"class":4435},[3699,12630,8717],{"class":4321},[3699,12632,4442],{"class":4422},[3699,12634,7801],{"class":4445},[3699,12636,4449],{"class":4422},[3699,12638,4244],{"class":4452},[3699,12640,4455],{"class":4422},[3699,12642,4244],{"class":4452},[3699,12644,4460],{"class":4422},[3699,12646,12647],{"class":3701,"line":8585},[3699,12648,4465],{"class":4324},[3699,12650,12651],{"class":3701,"line":8593},[3699,12652,12653],{"class":4324},"    SHA-256 хеш refresh token для зберігання у БД.\n",[3699,12655,12656],{"class":3701,"line":8598},[3699,12657,3730],{"emptyLinePlaceholder":3729},[3699,12659,12660],{"class":3701,"line":8603},[3699,12661,12662],{"class":4324},"    Чому SHA-256 (а не bcrypt):\n",[3699,12664,12665],{"class":3701,"line":8608},[3699,12666,12667],{"class":4324},"    - Token вже є 256-бітним випадковим рядком — rainbow tables неможливі.\n",[3699,12669,12670],{"class":3701,"line":8614},[3699,12671,12672],{"class":4324},"    - bcrypt (~160 мс) зайво повільний для lookup-операцій при кожному refresh.\n",[3699,12674,12675],{"class":3701,"line":8629},[3699,12676,12677],{"class":4324},"    - SHA-256 (~мкс) захищає від витоку хешів з БД без втрати продуктивності.\n",[3699,12679,12680],{"class":3701,"line":8634},[3699,12681,3730],{"emptyLinePlaceholder":3729},[3699,12683,12684],{"class":3701,"line":8640},[3699,12685,4498],{"class":4324},[3699,12687,12688],{"class":3701,"line":8645},[3699,12689,8821],{"class":4324},[3699,12691,12692],{"class":3701,"line":8651},[3699,12693,4465],{"class":4324},[3699,12695,12696,12698],{"class":3701,"line":8657},[3699,12697,4576],{"class":4418},[3699,12699,8834],{"class":4422},[3699,12701,12702],{"class":3701,"line":8663},[3699,12703,3730],{"emptyLinePlaceholder":3729},[3699,12705,12706],{"class":3701,"line":8668},[3699,12707,3730],{"emptyLinePlaceholder":3729},[3699,12709,12710,12712,12714,12716,12718,12720,12722,12724,12726,12728,12730,12732,12734],{"class":3701,"line":8673},[3699,12711,4436],{"class":4435},[3699,12713,8852],{"class":4321},[3699,12715,4442],{"class":4422},[3699,12717,8857],{"class":4445},[3699,12719,4449],{"class":4422},[3699,12721,4244],{"class":4452},[3699,12723,4200],{"class":4422},[3699,12725,8866],{"class":4445},[3699,12727,4449],{"class":4422},[3699,12729,4244],{"class":4452},[3699,12731,4455],{"class":4422},[3699,12733,4622],{"class":4452},[3699,12735,4460],{"class":4422},[3699,12737,12738],{"class":3701,"line":8679},[3699,12739,4465],{"class":4324},[3699,12741,12742],{"class":3701,"line":8685},[3699,12743,8887],{"class":4324},[3699,12745,12746],{"class":3701,"line":8690},[3699,12747,12748],{"class":4324},"    Використовує secrets.compare_digest для захисту від timing attacks.\n",[3699,12750,12751],{"class":3701,"line":8702},[3699,12752,4465],{"class":4324},[3699,12754,12755,12757],{"class":3701,"line":8707},[3699,12756,4576],{"class":4418},[3699,12758,12759],{"class":4422}," secrets.compare_digest(hash_refresh_token(plain_token), stored_hash)\n",[3690,12761,12764],{"className":4403,"code":12762,"filename":12763,"language":4405,"meta":3695,"style":3695},"from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine\n\nfrom app.core.config import settings\n\nengine = create_async_engine(\n    settings.DATABASE_URL,\n    echo=False,       # встановіть True для дебагу SQL запитів\n    pool_size=10,\n    max_overflow=20,\n)\n\nAsyncSessionLocal = async_sessionmaker(\n    bind=engine,\n    class_=AsyncSession,\n    expire_on_commit=False,  # не перезавантажувати об'єкти після commit\n)\n\n\nasync def get_db() -> AsyncSession:\n    \"\"\"FastAPI dependency: видає async сесію БД на час обробки запиту.\"\"\"\n    async with AsyncSessionLocal() as session:\n        yield session\n","app\u002Fdb\u002Fsession.py",[3625,12765,12766,12777,12781,12791,12795,12800,12805,12819,12830,12842,12846,12850,12855,12863,12871,12885,12889,12893,12897,12909,12914,12931],{"__ignoreMap":3695},[3699,12767,12768,12770,12772,12774],{"class":3701,"line":3702},[3699,12769,5828],{"class":4418},[3699,12771,9611],{"class":4422},[3699,12773,4419],{"class":4418},[3699,12775,12776],{"class":4422}," AsyncSession, async_sessionmaker, create_async_engine\n",[3699,12778,12779],{"class":3701,"line":3708},[3699,12780,3730],{"emptyLinePlaceholder":3729},[3699,12782,12783,12785,12787,12789],{"class":3701,"line":3714},[3699,12784,5828],{"class":4418},[3699,12786,8121],{"class":4422},[3699,12788,4419],{"class":4418},[3699,12790,10543],{"class":4422},[3699,12792,12793],{"class":3701,"line":3720},[3699,12794,3730],{"emptyLinePlaceholder":3729},[3699,12796,12797],{"class":3701,"line":3726},[3699,12798,12799],{"class":4422},"engine = create_async_engine(\n",[3699,12801,12802],{"class":3701,"line":3733},[3699,12803,12804],{"class":4422},"    settings.DATABASE_URL,\n",[3699,12806,12807,12810,12812,12814,12816],{"class":3701,"line":3739},[3699,12808,12809],{"class":4445},"    echo",[3699,12811,4556],{"class":4422},[3699,12813,9310],{"class":4435},[3699,12815,5875],{"class":4422},[3699,12817,12818],{"class":4412},"# встановіть True для дебагу SQL запитів\n",[3699,12820,12821,12824,12826,12828],{"class":3701,"line":3745},[3699,12822,12823],{"class":4445},"    pool_size",[3699,12825,4556],{"class":4422},[3699,12827,5498],{"class":4531},[3699,12829,7077],{"class":4422},[3699,12831,12832,12835,12837,12840],{"class":3701,"line":3751},[3699,12833,12834],{"class":4445},"    max_overflow",[3699,12836,4556],{"class":4422},[3699,12838,12839],{"class":4531},"20",[3699,12841,7077],{"class":4422},[3699,12843,12844],{"class":3701,"line":3757},[3699,12845,4562],{"class":4422},[3699,12847,12848],{"class":3701,"line":3763},[3699,12849,3730],{"emptyLinePlaceholder":3729},[3699,12851,12852],{"class":3701,"line":3769},[3699,12853,12854],{"class":4422},"AsyncSessionLocal = async_sessionmaker(\n",[3699,12856,12857,12860],{"class":3701,"line":3775},[3699,12858,12859],{"class":4445},"    bind",[3699,12861,12862],{"class":4422},"=engine,\n",[3699,12864,12865,12868],{"class":3701,"line":3781},[3699,12866,12867],{"class":4445},"    class_",[3699,12869,12870],{"class":4422},"=AsyncSession,\n",[3699,12872,12873,12876,12878,12880,12882],{"class":3701,"line":3786},[3699,12874,12875],{"class":4445},"    expire_on_commit",[3699,12877,4556],{"class":4422},[3699,12879,9310],{"class":4435},[3699,12881,7054],{"class":4422},[3699,12883,12884],{"class":4412},"# не перезавантажувати об'єкти після commit\n",[3699,12886,12887],{"class":3701,"line":3792},[3699,12888,4562],{"class":4422},[3699,12890,12891],{"class":3701,"line":3798},[3699,12892,3730],{"emptyLinePlaceholder":3729},[3699,12894,12895],{"class":3701,"line":3804},[3699,12896,3730],{"emptyLinePlaceholder":3729},[3699,12898,12899,12901,12903,12906],{"class":3701,"line":3810},[3699,12900,9702],{"class":4435},[3699,12902,9705],{"class":4435},[3699,12904,12905],{"class":4321}," get_db",[3699,12907,12908],{"class":4422},"() -> AsyncSession:\n",[3699,12910,12911],{"class":3701,"line":3816},[3699,12912,12913],{"class":4324},"    \"\"\"FastAPI dependency: видає async сесію БД на час обробки запиту.\"\"\"\n",[3699,12915,12916,12919,12922,12925,12928],{"class":3701,"line":3822},[3699,12917,12918],{"class":4418},"    async",[3699,12920,12921],{"class":4418}," with",[3699,12923,12924],{"class":4422}," AsyncSessionLocal() ",[3699,12926,12927],{"class":4418},"as",[3699,12929,12930],{"class":4422}," session:\n",[3699,12932,12933,12936],{"class":3701,"line":3827},[3699,12934,12935],{"class":4418},"        yield",[3699,12937,12938],{"class":4422}," session\n",[3690,12940,12943],{"className":4403,"code":12941,"filename":12942,"language":4405,"meta":3695,"style":3695},"from sqlalchemy.orm import DeclarativeBase\n\n\nclass Base(DeclarativeBase):\n    \"\"\"Базовий клас для всіх SQLAlchemy моделей.\"\"\"\n    pass\n","app\u002Fmodels\u002Fbase.py",[3625,12944,12945,12957,12961,12965,12979,12984],{"__ignoreMap":3695},[3699,12946,12947,12949,12952,12954],{"class":3701,"line":3702},[3699,12948,5828],{"class":4418},[3699,12950,12951],{"class":4422}," sqlalchemy.orm ",[3699,12953,4419],{"class":4418},[3699,12955,12956],{"class":4422}," DeclarativeBase\n",[3699,12958,12959],{"class":3701,"line":3708},[3699,12960,3730],{"emptyLinePlaceholder":3729},[3699,12962,12963],{"class":3701,"line":3714},[3699,12964,3730],{"emptyLinePlaceholder":3729},[3699,12966,12967,12969,12972,12974,12977],{"class":3701,"line":3720},[3699,12968,9008],{"class":4435},[3699,12970,12971],{"class":4452}," Base",[3699,12973,4442],{"class":4422},[3699,12975,12976],{"class":4452},"DeclarativeBase",[3699,12978,9019],{"class":4422},[3699,12980,12981],{"class":3701,"line":3726},[3699,12982,12983],{"class":4324},"    \"\"\"Базовий клас для всіх SQLAlchemy моделей.\"\"\"\n",[3699,12985,12986],{"class":3701,"line":3733},[3699,12987,6023],{"class":4418},[3690,12989,12992],{"className":4403,"code":12990,"filename":12991,"language":4405,"meta":3695,"style":3695},"import uuid\n\nfrom sqlalchemy import Boolean, String\nfrom sqlalchemy.dialects.postgresql import UUID\nfrom sqlalchemy.orm import Mapped, mapped_column, relationship\n\nfrom app.models.base import Base\n\n\nclass User(Base):\n    __tablename__ = \"users\"\n\n    id: Mapped[uuid.UUID] = mapped_column(\n        UUID(as_uuid=True), primary_key=True, default=uuid.uuid4\n    )\n    email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)\n    display_name: Mapped[str] = mapped_column(String(100), nullable=False)\n    # bcrypt завжди дає рівно 60 символів\n    hashed_password: Mapped[str] = mapped_column(String(60), nullable=False)\n    is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)\n\n    refresh_tokens: Mapped[list[\"RefreshToken\"]] = relationship(  # type: ignore[name-defined]\n        \"RefreshToken\", back_populates=\"user\", cascade=\"all, delete-orphan\"\n    )\n","app\u002Fmodels\u002Fuser.py",[3625,12993,12994,13001,13005,13017,13029,13040,13044,13056,13060,13064,13078,13086,13090,13098,13127,13131,13173,13195,13200,13222,13248,13252,13266,13291],{"__ignoreMap":3695},[3699,12995,12996,12998],{"class":3701,"line":3702},[3699,12997,4419],{"class":4418},[3699,12999,13000],{"class":4422}," uuid\n",[3699,13002,13003],{"class":3701,"line":3708},[3699,13004,3730],{"emptyLinePlaceholder":3729},[3699,13006,13007,13009,13012,13014],{"class":3701,"line":3714},[3699,13008,5828],{"class":4418},[3699,13010,13011],{"class":4422}," sqlalchemy ",[3699,13013,4419],{"class":4418},[3699,13015,13016],{"class":4422}," Boolean, String\n",[3699,13018,13019,13021,13024,13026],{"class":3701,"line":3720},[3699,13020,5828],{"class":4418},[3699,13022,13023],{"class":4422}," sqlalchemy.dialects.postgresql ",[3699,13025,4419],{"class":4418},[3699,13027,13028],{"class":4422}," UUID\n",[3699,13030,13031,13033,13035,13037],{"class":3701,"line":3726},[3699,13032,5828],{"class":4418},[3699,13034,12951],{"class":4422},[3699,13036,4419],{"class":4418},[3699,13038,13039],{"class":4422}," Mapped, mapped_column, relationship\n",[3699,13041,13042],{"class":3701,"line":3733},[3699,13043,3730],{"emptyLinePlaceholder":3729},[3699,13045,13046,13048,13051,13053],{"class":3701,"line":3739},[3699,13047,5828],{"class":4418},[3699,13049,13050],{"class":4422}," app.models.base ",[3699,13052,4419],{"class":4418},[3699,13054,13055],{"class":4422}," Base\n",[3699,13057,13058],{"class":3701,"line":3745},[3699,13059,3730],{"emptyLinePlaceholder":3729},[3699,13061,13062],{"class":3701,"line":3751},[3699,13063,3730],{"emptyLinePlaceholder":3729},[3699,13065,13066,13068,13071,13073,13076],{"class":3701,"line":3757},[3699,13067,9008],{"class":4435},[3699,13069,13070],{"class":4452}," User",[3699,13072,4442],{"class":4422},[3699,13074,13075],{"class":4452},"Base",[3699,13077,9019],{"class":4422},[3699,13079,13080,13083],{"class":3701,"line":3763},[3699,13081,13082],{"class":4422},"    __tablename__ = ",[3699,13084,13085],{"class":4324},"\"users\"\n",[3699,13087,13088],{"class":3701,"line":3769},[3699,13089,3730],{"emptyLinePlaceholder":3729},[3699,13091,13092,13095],{"class":3701,"line":3775},[3699,13093,13094],{"class":4321},"    id",[3699,13096,13097],{"class":4422},": Mapped[uuid.UUID] = mapped_column(\n",[3699,13099,13100,13103,13106,13108,13110,13112,13115,13117,13119,13121,13124],{"class":3701,"line":3781},[3699,13101,13102],{"class":4422},"        UUID(",[3699,13104,13105],{"class":4445},"as_uuid",[3699,13107,4556],{"class":4422},[3699,13109,9303],{"class":4435},[3699,13111,3648],{"class":4422},[3699,13113,13114],{"class":4445},"primary_key",[3699,13116,4556],{"class":4422},[3699,13118,9303],{"class":4435},[3699,13120,4200],{"class":4422},[3699,13122,13123],{"class":4445},"default",[3699,13125,13126],{"class":4422},"=uuid.uuid4\n",[3699,13128,13129],{"class":3701,"line":3786},[3699,13130,7951],{"class":4422},[3699,13132,13133,13136,13138,13141,13144,13146,13149,13151,13153,13155,13158,13160,13162,13164,13167,13169,13171],{"class":3701,"line":3792},[3699,13134,13135],{"class":4422},"    email: Mapped[",[3699,13137,4244],{"class":4452},[3699,13139,13140],{"class":4422},"] = mapped_column(String(",[3699,13142,13143],{"class":4531},"255",[3699,13145,3648],{"class":4422},[3699,13147,13148],{"class":4445},"unique",[3699,13150,4556],{"class":4422},[3699,13152,9303],{"class":4435},[3699,13154,4200],{"class":4422},[3699,13156,13157],{"class":4445},"nullable",[3699,13159,4556],{"class":4422},[3699,13161,9310],{"class":4435},[3699,13163,4200],{"class":4422},[3699,13165,13166],{"class":4445},"index",[3699,13168,4556],{"class":4422},[3699,13170,9303],{"class":4435},[3699,13172,4562],{"class":4422},[3699,13174,13175,13178,13180,13182,13185,13187,13189,13191,13193],{"class":3701,"line":3798},[3699,13176,13177],{"class":4422},"    display_name: Mapped[",[3699,13179,4244],{"class":4452},[3699,13181,13140],{"class":4422},[3699,13183,13184],{"class":4531},"100",[3699,13186,3648],{"class":4422},[3699,13188,13157],{"class":4445},[3699,13190,4556],{"class":4422},[3699,13192,9310],{"class":4435},[3699,13194,4562],{"class":4422},[3699,13196,13197],{"class":3701,"line":3804},[3699,13198,13199],{"class":4412},"    # bcrypt завжди дає рівно 60 символів\n",[3699,13201,13202,13205,13207,13209,13212,13214,13216,13218,13220],{"class":3701,"line":3810},[3699,13203,13204],{"class":4422},"    hashed_password: Mapped[",[3699,13206,4244],{"class":4452},[3699,13208,13140],{"class":4422},[3699,13210,13211],{"class":4531},"60",[3699,13213,3648],{"class":4422},[3699,13215,13157],{"class":4445},[3699,13217,4556],{"class":4422},[3699,13219,9310],{"class":4435},[3699,13221,4562],{"class":4422},[3699,13223,13224,13227,13229,13232,13234,13236,13238,13240,13242,13244,13246],{"class":3701,"line":3816},[3699,13225,13226],{"class":4422},"    is_active: Mapped[",[3699,13228,4622],{"class":4452},[3699,13230,13231],{"class":4422},"] = mapped_column(Boolean, ",[3699,13233,13123],{"class":4445},[3699,13235,4556],{"class":4422},[3699,13237,9303],{"class":4435},[3699,13239,4200],{"class":4422},[3699,13241,13157],{"class":4445},[3699,13243,4556],{"class":4422},[3699,13245,9310],{"class":4435},[3699,13247,4562],{"class":4422},[3699,13249,13250],{"class":3701,"line":3822},[3699,13251,3730],{"emptyLinePlaceholder":3729},[3699,13253,13254,13257,13260,13263],{"class":3701,"line":3827},[3699,13255,13256],{"class":4422},"    refresh_tokens: Mapped[list[",[3699,13258,13259],{"class":4324},"\"RefreshToken\"",[3699,13261,13262],{"class":4422},"]] = relationship(  ",[3699,13264,13265],{"class":4412},"# type: ignore[name-defined]\n",[3699,13267,13268,13271,13273,13276,13278,13281,13283,13286,13288],{"class":3701,"line":3832},[3699,13269,13270],{"class":4324},"        \"RefreshToken\"",[3699,13272,4200],{"class":4422},[3699,13274,13275],{"class":4445},"back_populates",[3699,13277,4556],{"class":4422},[3699,13279,13280],{"class":4324},"\"user\"",[3699,13282,4200],{"class":4422},[3699,13284,13285],{"class":4445},"cascade",[3699,13287,4556],{"class":4422},[3699,13289,13290],{"class":4324},"\"all, delete-orphan\"\n",[3699,13292,13293],{"class":3701,"line":3837},[3699,13294,7951],{"class":4422},[3690,13296,13299],{"className":4403,"code":13297,"filename":13298,"language":4405,"meta":3695,"style":3695},"import uuid\nfrom datetime import datetime\n\nfrom sqlalchemy import DateTime, ForeignKey, String, Text\nfrom sqlalchemy.dialects.postgresql import UUID\nfrom sqlalchemy.orm import Mapped, mapped_column, relationship\n\nfrom app.models.base import Base\n\n\nclass RefreshToken(Base):\n    __tablename__ = \"refresh_tokens\"\n\n    id: Mapped[uuid.UUID] = mapped_column(\n        UUID(as_uuid=True), primary_key=True, default=uuid.uuid4\n    )\n    # SHA-256 хеш opaque token (64 HEX символи)\n    token_hash: Mapped[str] = mapped_column(\n        String(64), unique=True, nullable=False, index=True\n    )\n    user_id: Mapped[uuid.UUID] = mapped_column(\n        UUID(as_uuid=True),\n        ForeignKey(\"users.id\", ondelete=\"CASCADE\"),\n        nullable=False,\n    )\n    expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)\n    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)\n\n    # Метадані для аудиту — показуємо користувачу список активних сесій\n    user_agent: Mapped[str | None] = mapped_column(Text, nullable=True)\n    ip_address: Mapped[str | None] = mapped_column(String(45), nullable=True)\n\n    user: Mapped[\"User\"] = relationship(\"User\", back_populates=\"refresh_tokens\")  # type: ignore[name-defined]\n","app\u002Fmodels\u002Frefresh_token.py",[3625,13300,13301,13307,13318,13322,13333,13343,13353,13357,13367,13371,13375,13388,13395,13399,13405,13429,13433,13438,13448,13481,13485,13490,13502,13522,13533,13537,13559,13580,13584,13589,13611,13637,13641],{"__ignoreMap":3695},[3699,13302,13303,13305],{"class":3701,"line":3702},[3699,13304,4419],{"class":4418},[3699,13306,13000],{"class":4422},[3699,13308,13309,13311,13313,13315],{"class":3701,"line":3708},[3699,13310,5828],{"class":4418},[3699,13312,7678],{"class":4422},[3699,13314,4419],{"class":4418},[3699,13316,13317],{"class":4422}," datetime\n",[3699,13319,13320],{"class":3701,"line":3714},[3699,13321,3730],{"emptyLinePlaceholder":3729},[3699,13323,13324,13326,13328,13330],{"class":3701,"line":3720},[3699,13325,5828],{"class":4418},[3699,13327,13011],{"class":4422},[3699,13329,4419],{"class":4418},[3699,13331,13332],{"class":4422}," DateTime, ForeignKey, String, Text\n",[3699,13334,13335,13337,13339,13341],{"class":3701,"line":3726},[3699,13336,5828],{"class":4418},[3699,13338,13023],{"class":4422},[3699,13340,4419],{"class":4418},[3699,13342,13028],{"class":4422},[3699,13344,13345,13347,13349,13351],{"class":3701,"line":3733},[3699,13346,5828],{"class":4418},[3699,13348,12951],{"class":4422},[3699,13350,4419],{"class":4418},[3699,13352,13039],{"class":4422},[3699,13354,13355],{"class":3701,"line":3739},[3699,13356,3730],{"emptyLinePlaceholder":3729},[3699,13358,13359,13361,13363,13365],{"class":3701,"line":3745},[3699,13360,5828],{"class":4418},[3699,13362,13050],{"class":4422},[3699,13364,4419],{"class":4418},[3699,13366,13055],{"class":4422},[3699,13368,13369],{"class":3701,"line":3751},[3699,13370,3730],{"emptyLinePlaceholder":3729},[3699,13372,13373],{"class":3701,"line":3757},[3699,13374,3730],{"emptyLinePlaceholder":3729},[3699,13376,13377,13379,13382,13384,13386],{"class":3701,"line":3763},[3699,13378,9008],{"class":4435},[3699,13380,13381],{"class":4452}," RefreshToken",[3699,13383,4442],{"class":4422},[3699,13385,13075],{"class":4452},[3699,13387,9019],{"class":4422},[3699,13389,13390,13392],{"class":3701,"line":3769},[3699,13391,13082],{"class":4422},[3699,13393,13394],{"class":4324},"\"refresh_tokens\"\n",[3699,13396,13397],{"class":3701,"line":3775},[3699,13398,3730],{"emptyLinePlaceholder":3729},[3699,13400,13401,13403],{"class":3701,"line":3781},[3699,13402,13094],{"class":4321},[3699,13404,13097],{"class":4422},[3699,13406,13407,13409,13411,13413,13415,13417,13419,13421,13423,13425,13427],{"class":3701,"line":3786},[3699,13408,13102],{"class":4422},[3699,13410,13105],{"class":4445},[3699,13412,4556],{"class":4422},[3699,13414,9303],{"class":4435},[3699,13416,3648],{"class":4422},[3699,13418,13114],{"class":4445},[3699,13420,4556],{"class":4422},[3699,13422,9303],{"class":4435},[3699,13424,4200],{"class":4422},[3699,13426,13123],{"class":4445},[3699,13428,13126],{"class":4422},[3699,13430,13431],{"class":3701,"line":3792},[3699,13432,7951],{"class":4422},[3699,13434,13435],{"class":3701,"line":3798},[3699,13436,13437],{"class":4412},"    # SHA-256 хеш opaque token (64 HEX символи)\n",[3699,13439,13440,13443,13445],{"class":3701,"line":3804},[3699,13441,13442],{"class":4422},"    token_hash: Mapped[",[3699,13444,4244],{"class":4452},[3699,13446,13447],{"class":4422},"] = mapped_column(\n",[3699,13449,13450,13453,13456,13458,13460,13462,13464,13466,13468,13470,13472,13474,13476,13478],{"class":3701,"line":3810},[3699,13451,13452],{"class":4422},"        String(",[3699,13454,13455],{"class":4531},"64",[3699,13457,3648],{"class":4422},[3699,13459,13148],{"class":4445},[3699,13461,4556],{"class":4422},[3699,13463,9303],{"class":4435},[3699,13465,4200],{"class":4422},[3699,13467,13157],{"class":4445},[3699,13469,4556],{"class":4422},[3699,13471,9310],{"class":4435},[3699,13473,4200],{"class":4422},[3699,13475,13166],{"class":4445},[3699,13477,4556],{"class":4422},[3699,13479,13480],{"class":4435},"True\n",[3699,13482,13483],{"class":3701,"line":3816},[3699,13484,7951],{"class":4422},[3699,13486,13487],{"class":3701,"line":3822},[3699,13488,13489],{"class":4422},"    user_id: Mapped[uuid.UUID] = mapped_column(\n",[3699,13491,13492,13494,13496,13498,13500],{"class":3701,"line":3827},[3699,13493,13102],{"class":4422},[3699,13495,13105],{"class":4445},[3699,13497,4556],{"class":4422},[3699,13499,9303],{"class":4435},[3699,13501,7743],{"class":4422},[3699,13503,13504,13507,13510,13512,13515,13517,13520],{"class":3701,"line":3832},[3699,13505,13506],{"class":4422},"        ForeignKey(",[3699,13508,13509],{"class":4324},"\"users.id\"",[3699,13511,4200],{"class":4422},[3699,13513,13514],{"class":4445},"ondelete",[3699,13516,4556],{"class":4422},[3699,13518,13519],{"class":4324},"\"CASCADE\"",[3699,13521,7743],{"class":4422},[3699,13523,13524,13527,13529,13531],{"class":3701,"line":3837},[3699,13525,13526],{"class":4445},"        nullable",[3699,13528,4556],{"class":4422},[3699,13530,9310],{"class":4435},[3699,13532,7077],{"class":4422},[3699,13534,13535],{"class":3701,"line":3843},[3699,13536,7951],{"class":4422},[3699,13538,13539,13542,13545,13547,13549,13551,13553,13555,13557],{"class":3701,"line":4590},[3699,13540,13541],{"class":4422},"    expires_at: Mapped[datetime] = mapped_column(DateTime(",[3699,13543,13544],{"class":4445},"timezone",[3699,13546,4556],{"class":4422},[3699,13548,9303],{"class":4435},[3699,13550,3648],{"class":4422},[3699,13552,13157],{"class":4445},[3699,13554,4556],{"class":4422},[3699,13556,9310],{"class":4435},[3699,13558,4562],{"class":4422},[3699,13560,13561,13564,13566,13568,13570,13572,13574,13576,13578],{"class":3701,"line":4595},[3699,13562,13563],{"class":4422},"    created_at: Mapped[datetime] = mapped_column(DateTime(",[3699,13565,13544],{"class":4445},[3699,13567,4556],{"class":4422},[3699,13569,9303],{"class":4435},[3699,13571,3648],{"class":4422},[3699,13573,13157],{"class":4445},[3699,13575,4556],{"class":4422},[3699,13577,9310],{"class":4435},[3699,13579,4562],{"class":4422},[3699,13581,13582],{"class":3701,"line":4627},[3699,13583,3730],{"emptyLinePlaceholder":3729},[3699,13585,13586],{"class":3701,"line":4632},[3699,13587,13588],{"class":4412},"    # Метадані для аудиту — показуємо користувачу список активних сесій\n",[3699,13590,13591,13594,13596,13598,13600,13603,13605,13607,13609],{"class":3701,"line":4638},[3699,13592,13593],{"class":4422},"    user_agent: Mapped[",[3699,13595,4244],{"class":4452},[3699,13597,8292],{"class":4422},[3699,13599,4938],{"class":4435},[3699,13601,13602],{"class":4422},"] = mapped_column(Text, ",[3699,13604,13157],{"class":4445},[3699,13606,4556],{"class":4422},[3699,13608,9303],{"class":4435},[3699,13610,4562],{"class":4422},[3699,13612,13613,13616,13618,13620,13622,13624,13627,13629,13631,13633,13635],{"class":3701,"line":4643},[3699,13614,13615],{"class":4422},"    ip_address: Mapped[",[3699,13617,4244],{"class":4452},[3699,13619,8292],{"class":4422},[3699,13621,4938],{"class":4435},[3699,13623,13140],{"class":4422},[3699,13625,13626],{"class":4531},"45",[3699,13628,3648],{"class":4422},[3699,13630,13157],{"class":4445},[3699,13632,4556],{"class":4422},[3699,13634,9303],{"class":4435},[3699,13636,4562],{"class":4422},[3699,13638,13639],{"class":3701,"line":4648},[3699,13640,3730],{"emptyLinePlaceholder":3729},[3699,13642,13643,13646,13649,13652,13654,13656,13658,13660,13663,13665],{"class":3701,"line":4654},[3699,13644,13645],{"class":4422},"    user: Mapped[",[3699,13647,13648],{"class":4324},"\"User\"",[3699,13650,13651],{"class":4422},"] = relationship(",[3699,13653,13648],{"class":4324},[3699,13655,4200],{"class":4422},[3699,13657,13275],{"class":4445},[3699,13659,4556],{"class":4422},[3699,13661,13662],{"class":4324},"\"refresh_tokens\"",[3699,13664,5975],{"class":4422},[3699,13666,13265],{"class":4412},[3690,13668,13671],{"className":4403,"code":13669,"filename":13670,"language":4405,"meta":3695,"style":3695},"from pydantic import BaseModel, EmailStr, Field\n\n\nclass UserRegisterRequest(BaseModel):\n    email: EmailStr\n    display_name: str = Field(min_length=2, max_length=100)\n    # max_length=72 — bcrypt обрізає пароль на 72 байтах\n    password: str = Field(min_length=8, max_length=72)\n\n\nclass TokenResponse(BaseModel):\n    access_token: str\n    refresh_token: str\n    token_type: str = \"bearer\"\n\n\nclass RefreshRequest(BaseModel):\n    refresh_token: str\n","app\u002Fschemas\u002Fauth.py",[3625,13672,13673,13685,13689,13693,13707,13712,13740,13745,13771,13775,13779,13792,13799,13806,13818,13822,13826,13839],{"__ignoreMap":3695},[3699,13674,13675,13677,13680,13682],{"class":3701,"line":3702},[3699,13676,5828],{"class":4418},[3699,13678,13679],{"class":4422}," pydantic ",[3699,13681,4419],{"class":4418},[3699,13683,13684],{"class":4422}," BaseModel, EmailStr, Field\n",[3699,13686,13687],{"class":3701,"line":3708},[3699,13688,3730],{"emptyLinePlaceholder":3729},[3699,13690,13691],{"class":3701,"line":3714},[3699,13692,3730],{"emptyLinePlaceholder":3729},[3699,13694,13695,13697,13700,13702,13705],{"class":3701,"line":3720},[3699,13696,9008],{"class":4435},[3699,13698,13699],{"class":4452}," UserRegisterRequest",[3699,13701,4442],{"class":4422},[3699,13703,13704],{"class":4452},"BaseModel",[3699,13706,9019],{"class":4422},[3699,13708,13709],{"class":3701,"line":3726},[3699,13710,13711],{"class":4422},"    email: EmailStr\n",[3699,13713,13714,13717,13719,13722,13725,13727,13729,13731,13734,13736,13738],{"class":3701,"line":3733},[3699,13715,13716],{"class":4422},"    display_name: ",[3699,13718,4244],{"class":4452},[3699,13720,13721],{"class":4422}," = Field(",[3699,13723,13724],{"class":4445},"min_length",[3699,13726,4556],{"class":4422},[3699,13728,5872],{"class":4531},[3699,13730,4200],{"class":4422},[3699,13732,13733],{"class":4445},"max_length",[3699,13735,4556],{"class":4422},[3699,13737,13184],{"class":4531},[3699,13739,4562],{"class":4422},[3699,13741,13742],{"class":3701,"line":3739},[3699,13743,13744],{"class":4412},"    # max_length=72 — bcrypt обрізає пароль на 72 байтах\n",[3699,13746,13747,13750,13752,13754,13756,13758,13761,13763,13765,13767,13769],{"class":3701,"line":3745},[3699,13748,13749],{"class":4422},"    password: ",[3699,13751,4244],{"class":4452},[3699,13753,13721],{"class":4422},[3699,13755,13724],{"class":4445},[3699,13757,4556],{"class":4422},[3699,13759,13760],{"class":4531},"8",[3699,13762,4200],{"class":4422},[3699,13764,13733],{"class":4445},[3699,13766,4556],{"class":4422},[3699,13768,4532],{"class":4531},[3699,13770,4562],{"class":4422},[3699,13772,13773],{"class":3701,"line":3751},[3699,13774,3730],{"emptyLinePlaceholder":3729},[3699,13776,13777],{"class":3701,"line":3757},[3699,13778,3730],{"emptyLinePlaceholder":3729},[3699,13780,13781,13783,13786,13788,13790],{"class":3701,"line":3763},[3699,13782,9008],{"class":4435},[3699,13784,13785],{"class":4452}," TokenResponse",[3699,13787,4442],{"class":4422},[3699,13789,13704],{"class":4452},[3699,13791,9019],{"class":4422},[3699,13793,13794,13797],{"class":3701,"line":3769},[3699,13795,13796],{"class":4422},"    access_token: ",[3699,13798,12041],{"class":4452},[3699,13800,13801,13804],{"class":3701,"line":3775},[3699,13802,13803],{"class":4422},"    refresh_token: ",[3699,13805,12041],{"class":4452},[3699,13807,13808,13811,13813,13815],{"class":3701,"line":3781},[3699,13809,13810],{"class":4422},"    token_type: ",[3699,13812,4244],{"class":4452},[3699,13814,8297],{"class":4422},[3699,13816,13817],{"class":4324},"\"bearer\"\n",[3699,13819,13820],{"class":3701,"line":3786},[3699,13821,3730],{"emptyLinePlaceholder":3729},[3699,13823,13824],{"class":3701,"line":3792},[3699,13825,3730],{"emptyLinePlaceholder":3729},[3699,13827,13828,13830,13833,13835,13837],{"class":3701,"line":3798},[3699,13829,9008],{"class":4435},[3699,13831,13832],{"class":4452}," RefreshRequest",[3699,13834,4442],{"class":4422},[3699,13836,13704],{"class":4452},[3699,13838,9019],{"class":4422},[3699,13840,13841,13843],{"class":3701,"line":3804},[3699,13842,13803],{"class":4422},[3699,13844,12041],{"class":4452},[3690,13846,13849],{"className":4403,"code":13847,"filename":13848,"language":4405,"meta":3695,"style":3695},"import uuid\n\nfrom sqlalchemy import select\nfrom sqlalchemy.ext.asyncio import AsyncSession\n\nfrom app.models.user import User\n\n\nclass UserRepository:\n    def __init__(self, db: AsyncSession) -> None:\n        self._db = db\n\n    async def get_by_id(self, user_id: str | uuid.UUID) -> User | None:\n        result = await self._db.execute(select(User).where(User.id == user_id))\n        return result.scalar_one_or_none()\n\n    async def get_by_email(self, email: str) -> User | None:\n        result = await self._db.execute(select(User).where(User.email == email))\n        return result.scalar_one_or_none()\n\n    async def create(self, user: User) -> User:\n        self._db.add(user)\n        await self._db.commit()\n        await self._db.refresh(user)\n        return user\n","app\u002Frepositories\u002Fuser.py",[3625,13850,13851,13857,13861,13872,13882,13886,13896,13900,13904,13913,13938,13946,13950,13979,13992,14000,14004,14032,14043,14049,14053,14074,14081,14090,14099],{"__ignoreMap":3695},[3699,13852,13853,13855],{"class":3701,"line":3702},[3699,13854,4419],{"class":4418},[3699,13856,13000],{"class":4422},[3699,13858,13859],{"class":3701,"line":3708},[3699,13860,3730],{"emptyLinePlaceholder":3729},[3699,13862,13863,13865,13867,13869],{"class":3701,"line":3714},[3699,13864,5828],{"class":4418},[3699,13866,13011],{"class":4422},[3699,13868,4419],{"class":4418},[3699,13870,13871],{"class":4422}," select\n",[3699,13873,13874,13876,13878,13880],{"class":3701,"line":3720},[3699,13875,5828],{"class":4418},[3699,13877,9611],{"class":4422},[3699,13879,4419],{"class":4418},[3699,13881,9616],{"class":4422},[3699,13883,13884],{"class":3701,"line":3726},[3699,13885,3730],{"emptyLinePlaceholder":3729},[3699,13887,13888,13890,13892,13894],{"class":3701,"line":3733},[3699,13889,5828],{"class":4418},[3699,13891,9651],{"class":4422},[3699,13893,4419],{"class":4418},[3699,13895,9656],{"class":4422},[3699,13897,13898],{"class":3701,"line":3739},[3699,13899,3730],{"emptyLinePlaceholder":3729},[3699,13901,13902],{"class":3701,"line":3745},[3699,13903,3730],{"emptyLinePlaceholder":3729},[3699,13905,13906,13908,13911],{"class":3701,"line":3751},[3699,13907,9008],{"class":4435},[3699,13909,13910],{"class":4452}," UserRepository",[3699,13912,4460],{"class":4422},[3699,13914,13915,13918,13921,13923,13926,13928,13931,13934,13936],{"class":3701,"line":3757},[3699,13916,13917],{"class":4435},"    def",[3699,13919,13920],{"class":4321}," __init__",[3699,13922,4442],{"class":4422},[3699,13924,13925],{"class":4445},"self",[3699,13927,4200],{"class":4422},[3699,13929,13930],{"class":4445},"db",[3699,13932,13933],{"class":4422},": AsyncSession) -> ",[3699,13935,4938],{"class":4435},[3699,13937,4460],{"class":4422},[3699,13939,13940,13943],{"class":3701,"line":3763},[3699,13941,13942],{"class":4435},"        self",[3699,13944,13945],{"class":4422},"._db = db\n",[3699,13947,13948],{"class":3701,"line":3769},[3699,13949,3730],{"emptyLinePlaceholder":3729},[3699,13951,13952,13954,13956,13959,13961,13963,13965,13968,13970,13972,13975,13977],{"class":3701,"line":3775},[3699,13953,12918],{"class":4435},[3699,13955,9705],{"class":4435},[3699,13957,13958],{"class":4321}," get_by_id",[3699,13960,4442],{"class":4422},[3699,13962,13925],{"class":4445},[3699,13964,4200],{"class":4422},[3699,13966,13967],{"class":4445},"user_id",[3699,13969,4449],{"class":4422},[3699,13971,4244],{"class":4452},[3699,13973,13974],{"class":4422}," | uuid.UUID) -> User | ",[3699,13976,4938],{"class":4435},[3699,13978,4460],{"class":4422},[3699,13980,13981,13984,13986,13989],{"class":3701,"line":3781},[3699,13982,13983],{"class":4422},"        result = ",[3699,13985,10044],{"class":4418},[3699,13987,13988],{"class":4435}," self",[3699,13990,13991],{"class":4422},"._db.execute(select(User).where(User.id == user_id))\n",[3699,13993,13994,13997],{"class":3701,"line":3786},[3699,13995,13996],{"class":4418},"        return",[3699,13998,13999],{"class":4422}," result.scalar_one_or_none()\n",[3699,14001,14002],{"class":3701,"line":3792},[3699,14003,3730],{"emptyLinePlaceholder":3729},[3699,14005,14006,14008,14010,14013,14015,14017,14019,14021,14023,14025,14028,14030],{"class":3701,"line":3798},[3699,14007,12918],{"class":4435},[3699,14009,9705],{"class":4435},[3699,14011,14012],{"class":4321}," get_by_email",[3699,14014,4442],{"class":4422},[3699,14016,13925],{"class":4445},[3699,14018,4200],{"class":4422},[3699,14020,6580],{"class":4445},[3699,14022,4449],{"class":4422},[3699,14024,4244],{"class":4452},[3699,14026,14027],{"class":4422},") -> User | ",[3699,14029,4938],{"class":4435},[3699,14031,4460],{"class":4422},[3699,14033,14034,14036,14038,14040],{"class":3701,"line":3804},[3699,14035,13983],{"class":4422},[3699,14037,10044],{"class":4418},[3699,14039,13988],{"class":4435},[3699,14041,14042],{"class":4422},"._db.execute(select(User).where(User.email == email))\n",[3699,14044,14045,14047],{"class":3701,"line":3810},[3699,14046,13996],{"class":4418},[3699,14048,13999],{"class":4422},[3699,14050,14051],{"class":3701,"line":3816},[3699,14052,3730],{"emptyLinePlaceholder":3729},[3699,14054,14055,14057,14059,14062,14064,14066,14068,14071],{"class":3701,"line":3822},[3699,14056,12918],{"class":4435},[3699,14058,9705],{"class":4435},[3699,14060,14061],{"class":4321}," create",[3699,14063,4442],{"class":4422},[3699,14065,13925],{"class":4445},[3699,14067,4200],{"class":4422},[3699,14069,14070],{"class":4445},"user",[3699,14072,14073],{"class":4422},": User) -> User:\n",[3699,14075,14076,14078],{"class":3701,"line":3827},[3699,14077,13942],{"class":4435},[3699,14079,14080],{"class":4422},"._db.add(user)\n",[3699,14082,14083,14085,14087],{"class":3701,"line":3832},[3699,14084,11747],{"class":4418},[3699,14086,13988],{"class":4435},[3699,14088,14089],{"class":4422},"._db.commit()\n",[3699,14091,14092,14094,14096],{"class":3701,"line":3837},[3699,14093,11747],{"class":4418},[3699,14095,13988],{"class":4435},[3699,14097,14098],{"class":4422},"._db.refresh(user)\n",[3699,14100,14101,14103],{"class":3701,"line":3843},[3699,14102,13996],{"class":4418},[3699,14104,10081],{"class":4422},[3690,14106,14109],{"className":4403,"code":14107,"filename":14108,"language":4405,"meta":3695,"style":3695},"import uuid\n\nfrom sqlalchemy import select\nfrom sqlalchemy.ext.asyncio import AsyncSession\n\nfrom app.models.refresh_token import RefreshToken\n\n\nclass RefreshTokenRepository:\n    def __init__(self, db: AsyncSession) -> None:\n        self._db = db\n\n    async def get_by_hash(self, token_hash: str) -> RefreshToken | None:\n        result = await self._db.execute(\n            select(RefreshToken).where(RefreshToken.token_hash == token_hash)\n        )\n        return result.scalar_one_or_none()\n\n    async def create(self, token: RefreshToken) -> RefreshToken:\n        self._db.add(token)\n        await self._db.commit()\n        await self._db.refresh(token)\n        return token\n\n    async def delete(self, token_id: uuid.UUID) -> None:\n        token = await self._db.get(RefreshToken, token_id)\n        if token:\n            await self._db.delete(token)\n            await self._db.commit()\n\n    async def delete_all_for_user(self, user_id: uuid.UUID) -> None:\n        \"\"\"Відкликати всі сесії користувача при виявленні крадіжки токена.\"\"\"\n        result = await self._db.execute(\n            select(RefreshToken).where(RefreshToken.user_id == user_id)\n        )\n        for token in result.scalars().all():\n            await self._db.delete(token)\n        await self._db.commit()\n","app\u002Frepositories\u002Frefresh_token.py",[3625,14110,14111,14117,14121,14131,14141,14145,14155,14159,14163,14172,14192,14198,14202,14231,14242,14247,14251,14257,14261,14280,14287,14295,14304,14311,14315,14340,14352,14359,14369,14377,14381,14404,14409,14419,14424,14428,14442,14450],{"__ignoreMap":3695},[3699,14112,14113,14115],{"class":3701,"line":3702},[3699,14114,4419],{"class":4418},[3699,14116,13000],{"class":4422},[3699,14118,14119],{"class":3701,"line":3708},[3699,14120,3730],{"emptyLinePlaceholder":3729},[3699,14122,14123,14125,14127,14129],{"class":3701,"line":3714},[3699,14124,5828],{"class":4418},[3699,14126,13011],{"class":4422},[3699,14128,4419],{"class":4418},[3699,14130,13871],{"class":4422},[3699,14132,14133,14135,14137,14139],{"class":3701,"line":3720},[3699,14134,5828],{"class":4418},[3699,14136,9611],{"class":4422},[3699,14138,4419],{"class":4418},[3699,14140,9616],{"class":4422},[3699,14142,14143],{"class":3701,"line":3726},[3699,14144,3730],{"emptyLinePlaceholder":3729},[3699,14146,14147,14149,14151,14153],{"class":3701,"line":3733},[3699,14148,5828],{"class":4418},[3699,14150,10570],{"class":4422},[3699,14152,4419],{"class":4418},[3699,14154,10575],{"class":4422},[3699,14156,14157],{"class":3701,"line":3739},[3699,14158,3730],{"emptyLinePlaceholder":3729},[3699,14160,14161],{"class":3701,"line":3745},[3699,14162,3730],{"emptyLinePlaceholder":3729},[3699,14164,14165,14167,14170],{"class":3701,"line":3751},[3699,14166,9008],{"class":4435},[3699,14168,14169],{"class":4452}," RefreshTokenRepository",[3699,14171,4460],{"class":4422},[3699,14173,14174,14176,14178,14180,14182,14184,14186,14188,14190],{"class":3701,"line":3757},[3699,14175,13917],{"class":4435},[3699,14177,13920],{"class":4321},[3699,14179,4442],{"class":4422},[3699,14181,13925],{"class":4445},[3699,14183,4200],{"class":4422},[3699,14185,13930],{"class":4445},[3699,14187,13933],{"class":4422},[3699,14189,4938],{"class":4435},[3699,14191,4460],{"class":4422},[3699,14193,14194,14196],{"class":3701,"line":3763},[3699,14195,13942],{"class":4435},[3699,14197,13945],{"class":4422},[3699,14199,14200],{"class":3701,"line":3769},[3699,14201,3730],{"emptyLinePlaceholder":3729},[3699,14203,14204,14206,14208,14211,14213,14215,14217,14220,14222,14224,14227,14229],{"class":3701,"line":3775},[3699,14205,12918],{"class":4435},[3699,14207,9705],{"class":4435},[3699,14209,14210],{"class":4321}," get_by_hash",[3699,14212,4442],{"class":4422},[3699,14214,13925],{"class":4445},[3699,14216,4200],{"class":4422},[3699,14218,14219],{"class":4445},"token_hash",[3699,14221,4449],{"class":4422},[3699,14223,4244],{"class":4452},[3699,14225,14226],{"class":4422},") -> RefreshToken | ",[3699,14228,4938],{"class":4435},[3699,14230,4460],{"class":4422},[3699,14232,14233,14235,14237,14239],{"class":3701,"line":3781},[3699,14234,13983],{"class":4422},[3699,14236,10044],{"class":4418},[3699,14238,13988],{"class":4435},[3699,14240,14241],{"class":4422},"._db.execute(\n",[3699,14243,14244],{"class":3701,"line":3786},[3699,14245,14246],{"class":4422},"            select(RefreshToken).where(RefreshToken.token_hash == token_hash)\n",[3699,14248,14249],{"class":3701,"line":3792},[3699,14250,10015],{"class":4422},[3699,14252,14253,14255],{"class":3701,"line":3798},[3699,14254,13996],{"class":4418},[3699,14256,13999],{"class":4422},[3699,14258,14259],{"class":3701,"line":3804},[3699,14260,3730],{"emptyLinePlaceholder":3729},[3699,14262,14263,14265,14267,14269,14271,14273,14275,14277],{"class":3701,"line":3810},[3699,14264,12918],{"class":4435},[3699,14266,9705],{"class":4435},[3699,14268,14061],{"class":4321},[3699,14270,4442],{"class":4422},[3699,14272,13925],{"class":4445},[3699,14274,4200],{"class":4422},[3699,14276,7801],{"class":4445},[3699,14278,14279],{"class":4422},": RefreshToken) -> RefreshToken:\n",[3699,14281,14282,14284],{"class":3701,"line":3816},[3699,14283,13942],{"class":4435},[3699,14285,14286],{"class":4422},"._db.add(token)\n",[3699,14288,14289,14291,14293],{"class":3701,"line":3822},[3699,14290,11747],{"class":4418},[3699,14292,13988],{"class":4435},[3699,14294,14089],{"class":4422},[3699,14296,14297,14299,14301],{"class":3701,"line":3827},[3699,14298,11747],{"class":4418},[3699,14300,13988],{"class":4435},[3699,14302,14303],{"class":4422},"._db.refresh(token)\n",[3699,14305,14306,14308],{"class":3701,"line":3832},[3699,14307,13996],{"class":4418},[3699,14309,14310],{"class":4422}," token\n",[3699,14312,14313],{"class":3701,"line":3837},[3699,14314,3730],{"emptyLinePlaceholder":3729},[3699,14316,14317,14319,14321,14324,14326,14328,14330,14333,14336,14338],{"class":3701,"line":3843},[3699,14318,12918],{"class":4435},[3699,14320,9705],{"class":4435},[3699,14322,14323],{"class":4321}," delete",[3699,14325,4442],{"class":4422},[3699,14327,13925],{"class":4445},[3699,14329,4200],{"class":4422},[3699,14331,14332],{"class":4445},"token_id",[3699,14334,14335],{"class":4422},": uuid.UUID) -> ",[3699,14337,4938],{"class":4435},[3699,14339,4460],{"class":4422},[3699,14341,14342,14345,14347,14349],{"class":3701,"line":4590},[3699,14343,14344],{"class":4422},"        token = ",[3699,14346,10044],{"class":4418},[3699,14348,13988],{"class":4435},[3699,14350,14351],{"class":4422},"._db.get(RefreshToken, token_id)\n",[3699,14353,14354,14356],{"class":3701,"line":4595},[3699,14355,9892],{"class":4418},[3699,14357,14358],{"class":4422}," token:\n",[3699,14360,14361,14364,14366],{"class":3701,"line":4627},[3699,14362,14363],{"class":4418},"            await",[3699,14365,13988],{"class":4435},[3699,14367,14368],{"class":4422},"._db.delete(token)\n",[3699,14370,14371,14373,14375],{"class":3701,"line":4632},[3699,14372,14363],{"class":4418},[3699,14374,13988],{"class":4435},[3699,14376,14089],{"class":4422},[3699,14378,14379],{"class":3701,"line":4638},[3699,14380,3730],{"emptyLinePlaceholder":3729},[3699,14382,14383,14385,14387,14390,14392,14394,14396,14398,14400,14402],{"class":3701,"line":4643},[3699,14384,12918],{"class":4435},[3699,14386,9705],{"class":4435},[3699,14388,14389],{"class":4321}," delete_all_for_user",[3699,14391,4442],{"class":4422},[3699,14393,13925],{"class":4445},[3699,14395,4200],{"class":4422},[3699,14397,13967],{"class":4445},[3699,14399,14335],{"class":4422},[3699,14401,4938],{"class":4435},[3699,14403,4460],{"class":4422},[3699,14405,14406],{"class":3701,"line":4648},[3699,14407,14408],{"class":4324},"        \"\"\"Відкликати всі сесії користувача при виявленні крадіжки токена.\"\"\"\n",[3699,14410,14411,14413,14415,14417],{"class":3701,"line":4654},[3699,14412,13983],{"class":4422},[3699,14414,10044],{"class":4418},[3699,14416,13988],{"class":4435},[3699,14418,14241],{"class":4422},[3699,14420,14421],{"class":3701,"line":4660},[3699,14422,14423],{"class":4422},"            select(RefreshToken).where(RefreshToken.user_id == user_id)\n",[3699,14425,14426],{"class":3701,"line":4665},[3699,14427,10015],{"class":4422},[3699,14429,14430,14433,14436,14439],{"class":3701,"line":4670},[3699,14431,14432],{"class":4418},"        for",[3699,14434,14435],{"class":4422}," token ",[3699,14437,14438],{"class":4418},"in",[3699,14440,14441],{"class":4422}," result.scalars().all():\n",[3699,14443,14444,14446,14448],{"class":3701,"line":4676},[3699,14445,14363],{"class":4418},[3699,14447,13988],{"class":4435},[3699,14449,14368],{"class":4422},[3699,14451,14452,14454,14456],{"class":3701,"line":4681},[3699,14453,11747],{"class":4418},[3699,14455,13988],{"class":4435},[3699,14457,14089],{"class":4422},[3690,14459,14462],{"className":4403,"code":14460,"filename":14461,"language":4405,"meta":3695,"style":3695},"from fastapi import Depends, HTTPException, status\nfrom fastapi.security import OAuth2PasswordBearer\nfrom jose import ExpiredSignatureError, JWTError\nfrom sqlalchemy.ext.asyncio import AsyncSession\n\nfrom app.core.security import decode_access_token\nfrom app.db.session import get_db\nfrom app.models.user import User\nfrom app.repositories.user import UserRepository\n\noauth2_scheme = OAuth2PasswordBearer(tokenUrl=\"\u002Fapi\u002Fv1\u002Fauth\u002Flogin\")\n\n\nasync def get_current_user(\n    token: str = Depends(oauth2_scheme),\n    db: AsyncSession = Depends(get_db),\n) -> User:\n    \"\"\"\n    Dependency (рівень 1): витягує поточного User з JWT access token.\n    Перевіряє підпис, термін дії та існування у БД.\n    \"\"\"\n    unauthorized = HTTPException(\n        status_code=status.HTTP_401_UNAUTHORIZED,\n        detail=\"Could not validate credentials\",\n        headers={\"WWW-Authenticate\": \"Bearer\"},\n    )\n    try:\n        payload = decode_access_token(token)\n        if payload.get(\"type\") != \"access\":\n            raise unauthorized\n        user_id: str | None = payload.get(\"sub\")\n        if user_id is None:\n            raise unauthorized\n    except ExpiredSignatureError:\n        raise HTTPException(\n            status_code=status.HTTP_401_UNAUTHORIZED,\n            detail=\"Token has expired\",\n            headers={\"WWW-Authenticate\": \"Bearer\"},\n        )\n    except JWTError:\n        raise unauthorized\n\n    user = await UserRepository(db).get_by_id(user_id)\n    if user is None:\n        raise unauthorized\n    return user\n\n\nasync def get_current_active_user(\n    current_user: User = Depends(get_current_user),\n) -> User:\n    \"\"\"\n    Dependency (рівень 2): перевіряє, що акаунт не заблокований.\n    Використовуйте для більшості захищених ендпоінтів.\n    \"\"\"\n    if not current_user.is_active:\n        raise HTTPException(\n            status_code=status.HTTP_403_FORBIDDEN,\n            detail=\"Inactive user account\",\n        )\n    return current_user\n","app\u002Fapi\u002Fdeps.py",[3625,14463,14464,14474,14484,14495,14505,14509,14519,14529,14539,14549,14553,14565,14569,14573,14583,14593,14599,14603,14607,14612,14617,14621,14626,14632,14642,14656,14660,14666,14670,14684,14691,14707,14719,14725,14731,14737,14743,14753,14767,14771,14777,14783,14787,14796,14808,14814,14820,14824,14828,14838,14844,14848,14852,14857,14862,14866,14874,14880,14886,14896,14900],{"__ignoreMap":3695},[3699,14465,14466,14468,14470,14472],{"class":3701,"line":3702},[3699,14467,5828],{"class":4418},[3699,14469,9576],{"class":4422},[3699,14471,4419],{"class":4418},[3699,14473,9581],{"class":4422},[3699,14475,14476,14478,14480,14482],{"class":3701,"line":3708},[3699,14477,5828],{"class":4418},[3699,14479,9588],{"class":4422},[3699,14481,4419],{"class":4418},[3699,14483,9593],{"class":4422},[3699,14485,14486,14488,14490,14492],{"class":3701,"line":3714},[3699,14487,5828],{"class":4418},[3699,14489,7666],{"class":4422},[3699,14491,4419],{"class":4418},[3699,14493,14494],{"class":4422}," ExpiredSignatureError, JWTError\n",[3699,14496,14497,14499,14501,14503],{"class":3701,"line":3720},[3699,14498,5828],{"class":4418},[3699,14500,9611],{"class":4422},[3699,14502,4419],{"class":4418},[3699,14504,9616],{"class":4422},[3699,14506,14507],{"class":3701,"line":3726},[3699,14508,3730],{"emptyLinePlaceholder":3729},[3699,14510,14511,14513,14515,14517],{"class":3701,"line":3733},[3699,14512,5828],{"class":4418},[3699,14514,9627],{"class":4422},[3699,14516,4419],{"class":4418},[3699,14518,9632],{"class":4422},[3699,14520,14521,14523,14525,14527],{"class":3701,"line":3739},[3699,14522,5828],{"class":4418},[3699,14524,9639],{"class":4422},[3699,14526,4419],{"class":4418},[3699,14528,9644],{"class":4422},[3699,14530,14531,14533,14535,14537],{"class":3701,"line":3745},[3699,14532,5828],{"class":4418},[3699,14534,9651],{"class":4422},[3699,14536,4419],{"class":4418},[3699,14538,9656],{"class":4422},[3699,14540,14541,14543,14545,14547],{"class":3701,"line":3751},[3699,14542,5828],{"class":4418},[3699,14544,9663],{"class":4422},[3699,14546,4419],{"class":4418},[3699,14548,9668],{"class":4422},[3699,14550,14551],{"class":3701,"line":3757},[3699,14552,3730],{"emptyLinePlaceholder":3729},[3699,14554,14555,14557,14559,14561,14563],{"class":3701,"line":3763},[3699,14556,9287],{"class":4422},[3699,14558,9270],{"class":4445},[3699,14560,4556],{"class":4422},[3699,14562,9294],{"class":4324},[3699,14564,4562],{"class":4422},[3699,14566,14567],{"class":3701,"line":3769},[3699,14568,3730],{"emptyLinePlaceholder":3729},[3699,14570,14571],{"class":3701,"line":3775},[3699,14572,3730],{"emptyLinePlaceholder":3729},[3699,14574,14575,14577,14579,14581],{"class":3701,"line":3781},[3699,14576,9702],{"class":4435},[3699,14578,9705],{"class":4435},[3699,14580,9708],{"class":4321},[3699,14582,8268],{"class":4422},[3699,14584,14585,14587,14589,14591],{"class":3701,"line":3786},[3699,14586,9715],{"class":4445},[3699,14588,4449],{"class":4422},[3699,14590,4244],{"class":4452},[3699,14592,9722],{"class":4422},[3699,14594,14595,14597],{"class":3701,"line":3792},[3699,14596,9727],{"class":4445},[3699,14598,9730],{"class":4422},[3699,14600,14601],{"class":3701,"line":3798},[3699,14602,9735],{"class":4422},[3699,14604,14605],{"class":3701,"line":3804},[3699,14606,4465],{"class":4324},[3699,14608,14609],{"class":3701,"line":3810},[3699,14610,14611],{"class":4324},"    Dependency (рівень 1): витягує поточного User з JWT access token.\n",[3699,14613,14614],{"class":3701,"line":3816},[3699,14615,14616],{"class":4324},"    Перевіряє підпис, термін дії та існування у БД.\n",[3699,14618,14619],{"class":3701,"line":3822},[3699,14620,4465],{"class":4324},[3699,14622,14623],{"class":3701,"line":3827},[3699,14624,14625],{"class":4422},"    unauthorized = HTTPException(\n",[3699,14627,14628,14630],{"class":3701,"line":3832},[3699,14629,9821],{"class":4445},[3699,14631,9824],{"class":4422},[3699,14633,14634,14636,14638,14640],{"class":3701,"line":3837},[3699,14635,9829],{"class":4445},[3699,14637,4556],{"class":4422},[3699,14639,9834],{"class":4324},[3699,14641,7077],{"class":4422},[3699,14643,14644,14646,14648,14650,14652,14654],{"class":3701,"line":3843},[3699,14645,9841],{"class":4445},[3699,14647,9844],{"class":4422},[3699,14649,9847],{"class":4324},[3699,14651,4449],{"class":4422},[3699,14653,9852],{"class":4324},[3699,14655,10010],{"class":4422},[3699,14657,14658],{"class":3701,"line":4590},[3699,14659,7951],{"class":4422},[3699,14661,14662,14664],{"class":3701,"line":4595},[3699,14663,9871],{"class":4418},[3699,14665,4460],{"class":4422},[3699,14667,14668],{"class":3701,"line":4627},[3699,14669,9878],{"class":4422},[3699,14671,14672,14674,14676,14678,14680,14682],{"class":3701,"line":4632},[3699,14673,9892],{"class":4418},[3699,14675,9895],{"class":4422},[3699,14677,9898],{"class":4324},[3699,14679,9901],{"class":4422},[3699,14681,8422],{"class":4324},[3699,14683,4460],{"class":4422},[3699,14685,14686,14688],{"class":3701,"line":4638},[3699,14687,9910],{"class":4418},[3699,14689,14690],{"class":4422}," unauthorized\n",[3699,14692,14693,14695,14697,14699,14701,14703,14705],{"class":3701,"line":4643},[3699,14694,9922],{"class":4422},[3699,14696,4244],{"class":4452},[3699,14698,8292],{"class":4422},[3699,14700,4938],{"class":4435},[3699,14702,9931],{"class":4422},[3699,14704,7959],{"class":4324},[3699,14706,4562],{"class":4422},[3699,14708,14709,14711,14713,14715,14717],{"class":3701,"line":4648},[3699,14710,9892],{"class":4418},[3699,14712,9942],{"class":4422},[3699,14714,9945],{"class":4435},[3699,14716,9948],{"class":4435},[3699,14718,4460],{"class":4422},[3699,14720,14721,14723],{"class":3701,"line":4654},[3699,14722,9910],{"class":4418},[3699,14724,14690],{"class":4422},[3699,14726,14727,14729],{"class":3701,"line":4660},[3699,14728,9965],{"class":4418},[3699,14730,7968],{"class":4422},[3699,14732,14733,14735],{"class":3701,"line":4665},[3699,14734,9972],{"class":4418},[3699,14736,9975],{"class":4422},[3699,14738,14739,14741],{"class":3701,"line":4670},[3699,14740,9980],{"class":4445},[3699,14742,9824],{"class":4422},[3699,14744,14745,14747,14749,14751],{"class":3701,"line":4676},[3699,14746,9987],{"class":4445},[3699,14748,4556],{"class":4422},[3699,14750,9992],{"class":4324},[3699,14752,7077],{"class":4422},[3699,14754,14755,14757,14759,14761,14763,14765],{"class":3701,"line":4681},[3699,14756,9999],{"class":4445},[3699,14758,9844],{"class":4422},[3699,14760,9847],{"class":4324},[3699,14762,4449],{"class":4422},[3699,14764,9852],{"class":4324},[3699,14766,10010],{"class":4422},[3699,14768,14769],{"class":3701,"line":4694},[3699,14770,10015],{"class":4422},[3699,14772,14773,14775],{"class":3701,"line":4704},[3699,14774,9965],{"class":4418},[3699,14776,8008],{"class":4422},[3699,14778,14779,14781],{"class":3701,"line":4709},[3699,14780,9972],{"class":4418},[3699,14782,14690],{"class":4422},[3699,14784,14785],{"class":3701,"line":6342},[3699,14786,3730],{"emptyLinePlaceholder":3729},[3699,14788,14789,14791,14793],{"class":3701,"line":6348},[3699,14790,10041],{"class":4422},[3699,14792,10044],{"class":4418},[3699,14794,14795],{"class":4422}," UserRepository(db).get_by_id(user_id)\n",[3699,14797,14798,14800,14802,14804,14806],{"class":3701,"line":6354},[3699,14799,5988],{"class":4418},[3699,14801,10058],{"class":4422},[3699,14803,9945],{"class":4435},[3699,14805,9948],{"class":4435},[3699,14807,4460],{"class":4422},[3699,14809,14810,14812],{"class":3701,"line":6359},[3699,14811,9972],{"class":4418},[3699,14813,14690],{"class":4422},[3699,14815,14816,14818],{"class":3701,"line":6365},[3699,14817,4576],{"class":4418},[3699,14819,10081],{"class":4422},[3699,14821,14822],{"class":3701,"line":6371},[3699,14823,3730],{"emptyLinePlaceholder":3729},[3699,14825,14826],{"class":3701,"line":6377},[3699,14827,3730],{"emptyLinePlaceholder":3729},[3699,14829,14830,14832,14834,14836],{"class":3701,"line":6382},[3699,14831,9702],{"class":4435},[3699,14833,9705],{"class":4435},[3699,14835,10098],{"class":4321},[3699,14837,8268],{"class":4422},[3699,14839,14840,14842],{"class":3701,"line":6387},[3699,14841,10105],{"class":4445},[3699,14843,10108],{"class":4422},[3699,14845,14846],{"class":3701,"line":6392},[3699,14847,9735],{"class":4422},[3699,14849,14850],{"class":3701,"line":6398},[3699,14851,4465],{"class":4324},[3699,14853,14854],{"class":3701,"line":6404},[3699,14855,14856],{"class":4324},"    Dependency (рівень 2): перевіряє, що акаунт не заблокований.\n",[3699,14858,14859],{"class":3701,"line":6409},[3699,14860,14861],{"class":4324},"    Використовуйте для більшості захищених ендпоінтів.\n",[3699,14863,14864],{"class":3701,"line":8467},[3699,14865,4465],{"class":4324},[3699,14867,14868,14870,14872],{"class":3701,"line":8472},[3699,14869,5988],{"class":4418},[3699,14871,10185],{"class":4435},[3699,14873,10188],{"class":4422},[3699,14875,14876,14878],{"class":3701,"line":8494},[3699,14877,9972],{"class":4418},[3699,14879,9975],{"class":4422},[3699,14881,14882,14884],{"class":3701,"line":8499},[3699,14883,9980],{"class":4445},[3699,14885,10201],{"class":4422},[3699,14887,14888,14890,14892,14894],{"class":3701,"line":8505},[3699,14889,9987],{"class":4445},[3699,14891,4556],{"class":4422},[3699,14893,10210],{"class":4324},[3699,14895,7077],{"class":4422},[3699,14897,14898],{"class":3701,"line":8510},[3699,14899,10015],{"class":4422},[3699,14901,14902,14904],{"class":3701,"line":8515},[3699,14903,4576],{"class":4418},[3699,14905,10223],{"class":4422},[3690,14907,14909],{"className":4403,"code":14908,"filename":10420,"language":4405,"meta":3695,"style":3695},"from datetime import datetime, timedelta, timezone\n\nfrom fastapi import APIRouter, Depends, HTTPException, Request, status\nfrom fastapi.security import OAuth2PasswordRequestForm\nfrom sqlalchemy.ext.asyncio import AsyncSession\n\nfrom app.core.config import settings\nfrom app.core.security import (\n    create_access_token,\n    generate_refresh_token,\n    hash_password,\n    hash_refresh_token,\n    verify_password,\n)\nfrom app.db.session import get_db\nfrom app.models.refresh_token import RefreshToken\nfrom app.models.user import User\nfrom app.repositories.refresh_token import RefreshTokenRepository\nfrom app.repositories.user import UserRepository\nfrom app.schemas.auth import RefreshRequest, TokenResponse, UserRegisterRequest\n\nrouter = APIRouter(prefix=\"\u002Fauth\", tags=[\"Authentication\"])\n\n# Час життя refresh token (для зручності — timedelta, а не Settings.days)\n_REFRESH_TTL = timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)\n\n\ndef _build_refresh_token(\n    plain_token: str,\n    user_id,\n    request: Request,\n) -> RefreshToken:\n    \"\"\"Будує об'єкт RefreshToken для запису у БД.\"\"\"\n    now = datetime.now(timezone.utc)\n    return RefreshToken(\n        token_hash=hash_refresh_token(plain_token),\n        user_id=user_id,\n        expires_at=now + _REFRESH_TTL,\n        created_at=now,\n        user_agent=request.headers.get(\"user-agent\"),\n        ip_address=request.client.host if request.client else None,\n    )\n\n\n@router.post(\"\u002Fregister\", response_model=TokenResponse, status_code=status.HTTP_201_CREATED)\nasync def register(\n    data: UserRegisterRequest,\n    request: Request,\n    db: AsyncSession = Depends(get_db),\n) -> TokenResponse:\n    \"\"\"\n    Реєстрація нового користувача.\n    Повертає пару токенів одразу після реєстрації.\n    \"\"\"\n    user_repo = UserRepository(db)\n    token_repo = RefreshTokenRepository(db)\n\n    if await user_repo.get_by_email(data.email):\n        raise HTTPException(\n            status_code=status.HTTP_409_CONFLICT,\n            detail=\"User with this email already exists\",\n        )\n\n    user = User(\n        email=data.email,\n        display_name=data.display_name,\n        hashed_password=hash_password(data.password),\n    )\n    await user_repo.create(user)\n\n    access_token = create_access_token(subject=str(user.id))\n    plain_refresh = generate_refresh_token()\n    await token_repo.create(_build_refresh_token(plain_refresh, user.id, request))\n\n    return TokenResponse(access_token=access_token, refresh_token=plain_refresh)\n\n\n@router.post(\"\u002Flogin\", response_model=TokenResponse)\nasync def login(\n    request: Request,\n    form_data: OAuth2PasswordRequestForm = Depends(),\n    db: AsyncSession = Depends(get_db),\n) -> TokenResponse:\n    \"\"\"\n    Логін через OAuth2 Password Flow (application\u002Fx-www-form-urlencoded).\n    Поле 'username' трактується як email.\n    \"\"\"\n    user_repo = UserRepository(db)\n    token_repo = RefreshTokenRepository(db)\n\n    user = await user_repo.get_by_email(form_data.username)\n\n    # Захист від timing attack: verify_password викликається завжди,\n    # навіть якщо user не знайдений (підставляємо dummy hash).\n    # Без цього різниця у часі відповіді розкриває факт існування акаунта.\n    _DUMMY_HASH = \"$2b$12$WDVwM8qSIHBGlL7m4qnwK.1m6CfU3f\u002FEKq6dBCQ7yx5TYhYj1F\u002F2W\"\n    password_ok = verify_password(\n        form_data.password,\n        user.hashed_password if user else _DUMMY_HASH,\n    )\n    if not user or not password_ok:\n        raise HTTPException(\n            status_code=status.HTTP_401_UNAUTHORIZED,\n            detail=\"Incorrect email or password\",\n            headers={\"WWW-Authenticate\": \"Bearer\"},\n        )\n\n    if not user.is_active:\n        raise HTTPException(\n            status_code=status.HTTP_403_FORBIDDEN,\n            detail=\"Account is deactivated\",\n        )\n\n    access_token = create_access_token(subject=str(user.id))\n    plain_refresh = generate_refresh_token()\n    await token_repo.create(_build_refresh_token(plain_refresh, user.id, request))\n\n    return TokenResponse(access_token=access_token, refresh_token=plain_refresh)\n\n\n@router.post(\"\u002Frefresh\", response_model=TokenResponse)\nasync def refresh_tokens(\n    body: RefreshRequest,\n    request: Request,\n    db: AsyncSession = Depends(get_db),\n) -> TokenResponse:\n    \"\"\"\n    Оновлення пари токенів через opaque refresh token з ротацією.\n    Якщо токен не знайдено (вже ротовано) — можлива крадіжка:\n    у production варто відкликати всі сесії через delete_all_for_user().\n    \"\"\"\n    token_repo = RefreshTokenRepository(db)\n\n    stored = await token_repo.get_by_hash(hash_refresh_token(body.refresh_token))\n\n    if stored is None:\n        raise HTTPException(\n            status_code=status.HTTP_401_UNAUTHORIZED,\n            detail=\"Refresh token not found or already used\",\n        )\n\n    now = datetime.now(timezone.utc)\n    if stored.expires_at.replace(tzinfo=timezone.utc) \u003C now:\n        await token_repo.delete(stored.id)\n        raise HTTPException(\n            status_code=status.HTTP_401_UNAUTHORIZED,\n            detail=\"Refresh token expired\",\n        )\n\n    user_id = stored.user_id\n\n    # Ротація: видаляємо старий перед видачею нового\n    await token_repo.delete(stored.id)\n\n    access_token = create_access_token(subject=str(user_id))\n    plain_refresh = generate_refresh_token()\n    await token_repo.create(_build_refresh_token(plain_refresh, user_id, request))\n\n    return TokenResponse(access_token=access_token, refresh_token=plain_refresh)\n\n\n@router.post(\"\u002Flogout\", status_code=status.HTTP_204_NO_CONTENT)\nasync def logout(\n    body: RefreshRequest,\n    db: AsyncSession = Depends(get_db),\n) -> None:\n    \"\"\"\n    Логаут: відкликає refresh token.\n    Access token залишається дійсним до exp (15 хв) — нормальний trade-off для JWT.\n    \"\"\"\n    token_repo = RefreshTokenRepository(db)\n    stored = await token_repo.get_by_hash(hash_refresh_token(body.refresh_token))\n    if stored:\n        await token_repo.delete(stored.id)\n",[3625,14910,14911,14921,14925,14936,14946,14956,14960,14970,14980,14984,14988,14992,14996,15000,15004,15014,15024,15034,15044,15054,15065,15069,15090,15094,15099,15109,15113,15117,15126,15137,15144,15152,15157,15162,15166,15173,15180,15186,15193,15201,15214,15234,15238,15242,15246,15264,15274,15280,15286,15292,15296,15300,15304,15309,15313,15317,15321,15325,15333,15339,15345,15355,15359,15363,15367,15373,15379,15385,15389,15395,15399,15411,15416,15423,15427,15446,15450,15454,15468,15478,15484,15490,15496,15500,15504,15509,15514,15518,15522,15526,15530,15538,15542,15547,15552,15557,15565,15570,15575,15589,15593,15608,15614,15620,15630,15644,15648,15652,15660,15666,15672,15682,15686,15690,15702,15706,15712,15716,15730,15734,15738,15752,15762,15768,15774,15780,15784,15788,15793,15798,15803,15807,15811,15815,15824,15828,15841,15847,15853,15864,15868,15872,15876,15889,15896,15902,15908,15919,15923,15927,15932,15936,15941,15947,15951,15963,15967,15974,15978,15992,15996,16000,16014,16024,16030,16036,16044,16048,16052,16057,16061,16065,16073,16080],{"__ignoreMap":3695},[3699,14912,14913,14915,14917,14919],{"class":3701,"line":3702},[3699,14914,5828],{"class":4418},[3699,14916,7678],{"class":4422},[3699,14918,4419],{"class":4418},[3699,14920,7683],{"class":4422},[3699,14922,14923],{"class":3701,"line":3708},[3699,14924,3730],{"emptyLinePlaceholder":3729},[3699,14926,14927,14929,14931,14933],{"class":3701,"line":3714},[3699,14928,5828],{"class":4418},[3699,14930,9576],{"class":4422},[3699,14932,4419],{"class":4418},[3699,14934,14935],{"class":4422}," APIRouter, Depends, HTTPException, Request, status\n",[3699,14937,14938,14940,14942,14944],{"class":3701,"line":3720},[3699,14939,5828],{"class":4418},[3699,14941,9588],{"class":4422},[3699,14943,4419],{"class":4418},[3699,14945,10474],{"class":4422},[3699,14947,14948,14950,14952,14954],{"class":3701,"line":3726},[3699,14949,5828],{"class":4418},[3699,14951,9611],{"class":4422},[3699,14953,4419],{"class":4418},[3699,14955,9616],{"class":4422},[3699,14957,14958],{"class":3701,"line":3733},[3699,14959,3730],{"emptyLinePlaceholder":3729},[3699,14961,14962,14964,14966,14968],{"class":3701,"line":3739},[3699,14963,5828],{"class":4418},[3699,14965,8121],{"class":4422},[3699,14967,4419],{"class":4418},[3699,14969,10543],{"class":4422},[3699,14971,14972,14974,14976,14978],{"class":3701,"line":3745},[3699,14973,5828],{"class":4418},[3699,14975,9627],{"class":4422},[3699,14977,4419],{"class":4418},[3699,14979,7023],{"class":4422},[3699,14981,14982],{"class":3701,"line":3751},[3699,14983,10513],{"class":4422},[3699,14985,14986],{"class":3701,"line":3757},[3699,14987,10518],{"class":4422},[3699,14989,14990],{"class":3701,"line":3763},[3699,14991,10503],{"class":4422},[3699,14993,14994],{"class":3701,"line":3769},[3699,14995,10523],{"class":4422},[3699,14997,14998],{"class":3701,"line":3775},[3699,14999,10508],{"class":4422},[3699,15001,15002],{"class":3701,"line":3781},[3699,15003,4562],{"class":4422},[3699,15005,15006,15008,15010,15012],{"class":3701,"line":3786},[3699,15007,5828],{"class":4418},[3699,15009,9639],{"class":4422},[3699,15011,4419],{"class":4418},[3699,15013,9644],{"class":4422},[3699,15015,15016,15018,15020,15022],{"class":3701,"line":3792},[3699,15017,5828],{"class":4418},[3699,15019,10570],{"class":4422},[3699,15021,4419],{"class":4418},[3699,15023,10575],{"class":4422},[3699,15025,15026,15028,15030,15032],{"class":3701,"line":3798},[3699,15027,5828],{"class":4418},[3699,15029,9651],{"class":4422},[3699,15031,4419],{"class":4418},[3699,15033,9656],{"class":4422},[3699,15035,15036,15038,15040,15042],{"class":3701,"line":3804},[3699,15037,5828],{"class":4418},[3699,15039,10592],{"class":4422},[3699,15041,4419],{"class":4418},[3699,15043,10597],{"class":4422},[3699,15045,15046,15048,15050,15052],{"class":3701,"line":3810},[3699,15047,5828],{"class":4418},[3699,15049,9663],{"class":4422},[3699,15051,4419],{"class":4418},[3699,15053,9668],{"class":4422},[3699,15055,15056,15058,15060,15062],{"class":3701,"line":3816},[3699,15057,5828],{"class":4418},[3699,15059,10604],{"class":4422},[3699,15061,4419],{"class":4418},[3699,15063,15064],{"class":4422}," RefreshRequest, TokenResponse, UserRegisterRequest\n",[3699,15066,15067],{"class":3701,"line":3822},[3699,15068,3730],{"emptyLinePlaceholder":3729},[3699,15070,15071,15073,15075,15077,15079,15081,15083,15085,15088],{"class":3701,"line":3827},[3699,15072,10636],{"class":4422},[3699,15074,10639],{"class":4445},[3699,15076,4556],{"class":4422},[3699,15078,10644],{"class":4324},[3699,15080,4200],{"class":4422},[3699,15082,10649],{"class":4445},[3699,15084,7938],{"class":4422},[3699,15086,15087],{"class":4324},"\"Authentication\"",[3699,15089,10657],{"class":4422},[3699,15091,15092],{"class":3701,"line":3832},[3699,15093,3730],{"emptyLinePlaceholder":3729},[3699,15095,15096],{"class":3701,"line":3837},[3699,15097,15098],{"class":4412},"# Час життя refresh token (для зручності — timedelta, а не Settings.days)\n",[3699,15100,15101,15104,15106],{"class":3701,"line":3843},[3699,15102,15103],{"class":4422},"_REFRESH_TTL = timedelta(",[3699,15105,10916],{"class":4445},[3699,15107,15108],{"class":4422},"=settings.REFRESH_TOKEN_EXPIRE_DAYS)\n",[3699,15110,15111],{"class":3701,"line":4590},[3699,15112,3730],{"emptyLinePlaceholder":3729},[3699,15114,15115],{"class":3701,"line":4595},[3699,15116,3730],{"emptyLinePlaceholder":3729},[3699,15118,15119,15121,15124],{"class":3701,"line":4627},[3699,15120,4436],{"class":4435},[3699,15122,15123],{"class":4321}," _build_refresh_token",[3699,15125,8268],{"class":4422},[3699,15127,15128,15131,15133,15135],{"class":3701,"line":4632},[3699,15129,15130],{"class":4445},"    plain_token",[3699,15132,4449],{"class":4422},[3699,15134,4244],{"class":4452},[3699,15136,7077],{"class":4422},[3699,15138,15139,15142],{"class":3701,"line":4638},[3699,15140,15141],{"class":4445},"    user_id",[3699,15143,7077],{"class":4422},[3699,15145,15146,15149],{"class":3701,"line":4643},[3699,15147,15148],{"class":4445},"    request",[3699,15150,15151],{"class":4422},": Request,\n",[3699,15153,15154],{"class":3701,"line":4648},[3699,15155,15156],{"class":4422},") -> RefreshToken:\n",[3699,15158,15159],{"class":3701,"line":4654},[3699,15160,15161],{"class":4324},"    \"\"\"Будує об'єкт RefreshToken для запису у БД.\"\"\"\n",[3699,15163,15164],{"class":3701,"line":4660},[3699,15165,8368],{"class":4422},[3699,15167,15168,15170],{"class":3701,"line":4665},[3699,15169,4576],{"class":4418},[3699,15171,15172],{"class":4422}," RefreshToken(\n",[3699,15174,15175,15177],{"class":3701,"line":4670},[3699,15176,10894],{"class":4445},[3699,15178,15179],{"class":4422},"=hash_refresh_token(plain_token),\n",[3699,15181,15182,15184],{"class":3701,"line":4676},[3699,15183,10902],{"class":4445},[3699,15185,11566],{"class":4422},[3699,15187,15188,15190],{"class":3701,"line":4681},[3699,15189,10910],{"class":4445},[3699,15191,15192],{"class":4422},"=now + _REFRESH_TTL,\n",[3699,15194,15195,15198],{"class":3701,"line":4694},[3699,15196,15197],{"class":4445},"        created_at",[3699,15199,15200],{"class":4422},"=now,\n",[3699,15202,15203,15206,15209,15212],{"class":3701,"line":4704},[3699,15204,15205],{"class":4445},"        user_agent",[3699,15207,15208],{"class":4422},"=request.headers.get(",[3699,15210,15211],{"class":4324},"\"user-agent\"",[3699,15213,7743],{"class":4422},[3699,15215,15216,15219,15222,15224,15227,15230,15232],{"class":3701,"line":4709},[3699,15217,15218],{"class":4445},"        ip_address",[3699,15220,15221],{"class":4422},"=request.client.host ",[3699,15223,4953],{"class":4418},[3699,15225,15226],{"class":4422}," request.client ",[3699,15228,15229],{"class":4418},"else",[3699,15231,9948],{"class":4435},[3699,15233,7077],{"class":4422},[3699,15235,15236],{"class":3701,"line":6342},[3699,15237,7951],{"class":4422},[3699,15239,15240],{"class":3701,"line":6348},[3699,15241,3730],{"emptyLinePlaceholder":3729},[3699,15243,15244],{"class":3701,"line":6354},[3699,15245,3730],{"emptyLinePlaceholder":3729},[3699,15247,15248,15250,15252,15254,15256,15258,15260,15262],{"class":3701,"line":6359},[3699,15249,10348],{"class":4321},[3699,15251,4442],{"class":4422},[3699,15253,10674],{"class":4324},[3699,15255,4200],{"class":4422},[3699,15257,10679],{"class":4445},[3699,15259,10682],{"class":4422},[3699,15261,7984],{"class":4445},[3699,15263,10687],{"class":4422},[3699,15265,15266,15268,15270,15272],{"class":3701,"line":6365},[3699,15267,9702],{"class":4435},[3699,15269,9705],{"class":4435},[3699,15271,10696],{"class":4321},[3699,15273,8268],{"class":4422},[3699,15275,15276,15278],{"class":3701,"line":6371},[3699,15277,10703],{"class":4445},[3699,15279,10706],{"class":4422},[3699,15281,15282,15284],{"class":3701,"line":6377},[3699,15283,15148],{"class":4445},[3699,15285,15151],{"class":4422},[3699,15287,15288,15290],{"class":3701,"line":6382},[3699,15289,9727],{"class":4445},[3699,15291,9730],{"class":4422},[3699,15293,15294],{"class":3701,"line":6387},[3699,15295,10717],{"class":4422},[3699,15297,15298],{"class":3701,"line":6392},[3699,15299,4465],{"class":4324},[3699,15301,15302],{"class":3701,"line":6398},[3699,15303,10726],{"class":4324},[3699,15305,15306],{"class":3701,"line":6404},[3699,15307,15308],{"class":4324},"    Повертає пару токенів одразу після реєстрації.\n",[3699,15310,15311],{"class":3701,"line":6409},[3699,15312,4465],{"class":4324},[3699,15314,15315],{"class":3701,"line":8467},[3699,15316,10036],{"class":4422},[3699,15318,15319],{"class":3701,"line":8472},[3699,15320,10753],{"class":4422},[3699,15322,15323],{"class":3701,"line":8494},[3699,15324,3730],{"emptyLinePlaceholder":3729},[3699,15326,15327,15329,15331],{"class":3701,"line":8499},[3699,15328,5988],{"class":4418},[3699,15330,10769],{"class":4418},[3699,15332,10772],{"class":4422},[3699,15334,15335,15337],{"class":3701,"line":8505},[3699,15336,9972],{"class":4418},[3699,15338,9975],{"class":4422},[3699,15340,15341,15343],{"class":3701,"line":8510},[3699,15342,9980],{"class":4445},[3699,15344,10785],{"class":4422},[3699,15346,15347,15349,15351,15353],{"class":3701,"line":8515},[3699,15348,9987],{"class":4445},[3699,15350,4556],{"class":4422},[3699,15352,10794],{"class":4324},[3699,15354,7077],{"class":4422},[3699,15356,15357],{"class":3701,"line":8521},[3699,15358,10015],{"class":4422},[3699,15360,15361],{"class":3701,"line":8526},[3699,15362,3730],{"emptyLinePlaceholder":3729},[3699,15364,15365],{"class":3701,"line":8531},[3699,15366,10814],{"class":4422},[3699,15368,15369,15371],{"class":3701,"line":8537},[3699,15370,10819],{"class":4445},[3699,15372,10822],{"class":4422},[3699,15374,15375,15377],{"class":3701,"line":8542},[3699,15376,10827],{"class":4445},[3699,15378,10830],{"class":4422},[3699,15380,15381,15383],{"class":3701,"line":8548},[3699,15382,10835],{"class":4445},[3699,15384,10838],{"class":4422},[3699,15386,15387],{"class":3701,"line":8554},[3699,15388,7951],{"class":4422},[3699,15390,15391,15393],{"class":3701,"line":8560},[3699,15392,4966],{"class":4418},[3699,15394,10849],{"class":4422},[3699,15396,15397],{"class":3701,"line":8565},[3699,15398,3730],{"emptyLinePlaceholder":3729},[3699,15400,15401,15403,15405,15407,15409],{"class":3701,"line":8573},[3699,15402,10863],{"class":4422},[3699,15404,10866],{"class":4445},[3699,15406,4556],{"class":4422},[3699,15408,4244],{"class":4452},[3699,15410,10873],{"class":4422},[3699,15412,15413],{"class":3701,"line":8579},[3699,15414,15415],{"class":4422},"    plain_refresh = generate_refresh_token()\n",[3699,15417,15418,15420],{"class":3701,"line":8585},[3699,15419,4966],{"class":4418},[3699,15421,15422],{"class":4422}," token_repo.create(_build_refresh_token(plain_refresh, user.id, request))\n",[3699,15424,15425],{"class":3701,"line":8593},[3699,15426,3730],{"emptyLinePlaceholder":3729},[3699,15428,15429,15431,15434,15437,15440,15443],{"class":3701,"line":8598},[3699,15430,4576],{"class":4418},[3699,15432,15433],{"class":4422}," TokenResponse(",[3699,15435,15436],{"class":4445},"access_token",[3699,15438,15439],{"class":4422},"=access_token, ",[3699,15441,15442],{"class":4445},"refresh_token",[3699,15444,15445],{"class":4422},"=plain_refresh)\n",[3699,15447,15448],{"class":3701,"line":8603},[3699,15449,3730],{"emptyLinePlaceholder":3729},[3699,15451,15452],{"class":3701,"line":8608},[3699,15453,3730],{"emptyLinePlaceholder":3729},[3699,15455,15456,15458,15460,15462,15464,15466],{"class":3701,"line":8614},[3699,15457,10348],{"class":4321},[3699,15459,4442],{"class":4422},[3699,15461,10984],{"class":4324},[3699,15463,4200],{"class":4422},[3699,15465,10679],{"class":4445},[3699,15467,10991],{"class":4422},[3699,15469,15470,15472,15474,15476],{"class":3701,"line":8629},[3699,15471,9702],{"class":4435},[3699,15473,9705],{"class":4435},[3699,15475,11000],{"class":4321},[3699,15477,8268],{"class":4422},[3699,15479,15480,15482],{"class":3701,"line":8634},[3699,15481,15148],{"class":4445},[3699,15483,15151],{"class":4422},[3699,15485,15486,15488],{"class":3701,"line":8640},[3699,15487,11007],{"class":4445},[3699,15489,11010],{"class":4422},[3699,15491,15492,15494],{"class":3701,"line":8645},[3699,15493,9727],{"class":4445},[3699,15495,9730],{"class":4422},[3699,15497,15498],{"class":3701,"line":8651},[3699,15499,10717],{"class":4422},[3699,15501,15502],{"class":3701,"line":8657},[3699,15503,4465],{"class":4324},[3699,15505,15506],{"class":3701,"line":8663},[3699,15507,15508],{"class":4324},"    Логін через OAuth2 Password Flow (application\u002Fx-www-form-urlencoded).\n",[3699,15510,15511],{"class":3701,"line":8668},[3699,15512,15513],{"class":4324},"    Поле 'username' трактується як email.\n",[3699,15515,15516],{"class":3701,"line":8673},[3699,15517,4465],{"class":4324},[3699,15519,15520],{"class":3701,"line":8679},[3699,15521,10036],{"class":4422},[3699,15523,15524],{"class":3701,"line":8685},[3699,15525,10753],{"class":4422},[3699,15527,15528],{"class":3701,"line":8690},[3699,15529,3730],{"emptyLinePlaceholder":3729},[3699,15531,15532,15534,15536],{"class":3701,"line":8702},[3699,15533,10041],{"class":4422},[3699,15535,10044],{"class":4418},[3699,15537,11078],{"class":4422},[3699,15539,15540],{"class":3701,"line":8707},[3699,15541,3730],{"emptyLinePlaceholder":3729},[3699,15543,15544],{"class":3701,"line":8712},[3699,15545,15546],{"class":4412},"    # Захист від timing attack: verify_password викликається завжди,\n",[3699,15548,15549],{"class":3701,"line":8734},[3699,15550,15551],{"class":4412},"    # навіть якщо user не знайдений (підставляємо dummy hash).\n",[3699,15553,15554],{"class":3701,"line":8739},[3699,15555,15556],{"class":4412},"    # Без цього різниця у часі відповіді розкриває факт існування акаунта.\n",[3699,15558,15559,15562],{"class":3701,"line":8745},[3699,15560,15561],{"class":4422},"    _DUMMY_HASH = ",[3699,15563,15564],{"class":4324},"\"$2b$12$WDVwM8qSIHBGlL7m4qnwK.1m6CfU3f\u002FEKq6dBCQ7yx5TYhYj1F\u002F2W\"\n",[3699,15566,15567],{"class":3701,"line":8750},[3699,15568,15569],{"class":4422},"    password_ok = verify_password(\n",[3699,15571,15572],{"class":3701,"line":8756},[3699,15573,15574],{"class":4422},"        form_data.password,\n",[3699,15576,15577,15580,15582,15584,15586],{"class":3701,"line":8762},[3699,15578,15579],{"class":4422},"        user.hashed_password ",[3699,15581,4953],{"class":4418},[3699,15583,10058],{"class":4422},[3699,15585,15229],{"class":4418},[3699,15587,15588],{"class":4422}," _DUMMY_HASH,\n",[3699,15590,15591],{"class":3701,"line":8768},[3699,15592,7951],{"class":4422},[3699,15594,15595,15597,15599,15601,15603,15605],{"class":3701,"line":8774},[3699,15596,5988],{"class":4418},[3699,15598,10185],{"class":4435},[3699,15600,10058],{"class":4422},[3699,15602,11108],{"class":4435},[3699,15604,10185],{"class":4435},[3699,15606,15607],{"class":4422}," password_ok:\n",[3699,15609,15610,15612],{"class":3701,"line":8780},[3699,15611,9972],{"class":4418},[3699,15613,9975],{"class":4422},[3699,15615,15616,15618],{"class":3701,"line":8786},[3699,15617,9980],{"class":4445},[3699,15619,9824],{"class":4422},[3699,15621,15622,15624,15626,15628],{"class":3701,"line":8792},[3699,15623,9987],{"class":4445},[3699,15625,4556],{"class":4422},[3699,15627,11134],{"class":4324},[3699,15629,7077],{"class":4422},[3699,15631,15632,15634,15636,15638,15640,15642],{"class":3701,"line":8797},[3699,15633,9999],{"class":4445},[3699,15635,9844],{"class":4422},[3699,15637,9847],{"class":4324},[3699,15639,4449],{"class":4422},[3699,15641,9852],{"class":4324},[3699,15643,10010],{"class":4422},[3699,15645,15646],{"class":3701,"line":8802},[3699,15647,10015],{"class":4422},[3699,15649,15650],{"class":3701,"line":8808},[3699,15651,3730],{"emptyLinePlaceholder":3729},[3699,15653,15654,15656,15658],{"class":3701,"line":8813},[3699,15655,5988],{"class":4418},[3699,15657,10185],{"class":4435},[3699,15659,11167],{"class":4422},[3699,15661,15662,15664],{"class":3701,"line":8818},[3699,15663,9972],{"class":4418},[3699,15665,9975],{"class":4422},[3699,15667,15668,15670],{"class":3701,"line":8824},[3699,15669,9980],{"class":4445},[3699,15671,10201],{"class":4422},[3699,15673,15674,15676,15678,15680],{"class":3701,"line":8829},[3699,15675,9987],{"class":4445},[3699,15677,4556],{"class":4422},[3699,15679,11188],{"class":4324},[3699,15681,7077],{"class":4422},[3699,15683,15684],{"class":3701,"line":8837},[3699,15685,10015],{"class":4422},[3699,15687,15688],{"class":3701,"line":8842},[3699,15689,3730],{"emptyLinePlaceholder":3729},[3699,15691,15692,15694,15696,15698,15700],{"class":3701,"line":8847},[3699,15693,10863],{"class":4422},[3699,15695,10866],{"class":4445},[3699,15697,4556],{"class":4422},[3699,15699,4244],{"class":4452},[3699,15701,10873],{"class":4422},[3699,15703,15704],{"class":3701,"line":8879},[3699,15705,15415],{"class":4422},[3699,15707,15708,15710],{"class":3701,"line":8884},[3699,15709,4966],{"class":4418},[3699,15711,15422],{"class":4422},[3699,15713,15714],{"class":3701,"line":8890},[3699,15715,3730],{"emptyLinePlaceholder":3729},[3699,15717,15718,15720,15722,15724,15726,15728],{"class":3701,"line":8895},[3699,15719,4576],{"class":4418},[3699,15721,15433],{"class":4422},[3699,15723,15436],{"class":4445},[3699,15725,15439],{"class":4422},[3699,15727,15442],{"class":4445},[3699,15729,15445],{"class":4422},[3699,15731,15732],{"class":3701,"line":8901},[3699,15733,3730],{"emptyLinePlaceholder":3729},[3699,15735,15736],{"class":3701,"line":8907},[3699,15737,3730],{"emptyLinePlaceholder":3729},[3699,15739,15740,15742,15744,15746,15748,15750],{"class":3701,"line":8912},[3699,15741,10348],{"class":4321},[3699,15743,4442],{"class":4422},[3699,15745,11307],{"class":4324},[3699,15747,4200],{"class":4422},[3699,15749,10679],{"class":4445},[3699,15751,10991],{"class":4422},[3699,15753,15754,15756,15758,15760],{"class":3701,"line":8917},[3699,15755,9702],{"class":4435},[3699,15757,9705],{"class":4435},[3699,15759,7020],{"class":4321},[3699,15761,8268],{"class":4422},[3699,15763,15764,15766],{"class":3701,"line":8923},[3699,15765,11329],{"class":4445},[3699,15767,11332],{"class":4422},[3699,15769,15770,15772],{"class":3701,"line":8929},[3699,15771,15148],{"class":4445},[3699,15773,15151],{"class":4422},[3699,15775,15776,15778],{"class":3701,"line":8934},[3699,15777,9727],{"class":4445},[3699,15779,9730],{"class":4422},[3699,15781,15782],{"class":3701,"line":8939},[3699,15783,10717],{"class":4422},[3699,15785,15786],{"class":3701,"line":8945},[3699,15787,4465],{"class":4324},[3699,15789,15790],{"class":3701,"line":8950},[3699,15791,15792],{"class":4324},"    Оновлення пари токенів через opaque refresh token з ротацією.\n",[3699,15794,15795],{"class":3701,"line":8956},[3699,15796,15797],{"class":4324},"    Якщо токен не знайдено (вже ротовано) — можлива крадіжка:\n",[3699,15799,15800],{"class":3701,"line":8962},[3699,15801,15802],{"class":4324},"    у production варто відкликати всі сесії через delete_all_for_user().\n",[3699,15804,15805],{"class":3701,"line":11326},[3699,15806,4465],{"class":4324},[3699,15808,15809],{"class":3701,"line":11335},[3699,15810,10753],{"class":4422},[3699,15812,15813],{"class":3701,"line":11342},[3699,15814,3730],{"emptyLinePlaceholder":3729},[3699,15816,15817,15820,15822],{"class":3701,"line":11347},[3699,15818,15819],{"class":4422},"    stored = ",[3699,15821,10044],{"class":4418},[3699,15823,11410],{"class":4422},[3699,15825,15826],{"class":3701,"line":11352},[3699,15827,3730],{"emptyLinePlaceholder":3729},[3699,15829,15830,15832,15835,15837,15839],{"class":3701,"line":11358},[3699,15831,5988],{"class":4418},[3699,15833,15834],{"class":4422}," stored ",[3699,15836,9945],{"class":4435},[3699,15838,9948],{"class":4435},[3699,15840,4460],{"class":4422},[3699,15842,15843,15845],{"class":3701,"line":11363},[3699,15844,9972],{"class":4418},[3699,15846,9975],{"class":4422},[3699,15848,15849,15851],{"class":3701,"line":11369},[3699,15850,9980],{"class":4445},[3699,15852,9824],{"class":4422},[3699,15854,15855,15857,15859,15862],{"class":3701,"line":11375},[3699,15856,9987],{"class":4445},[3699,15858,4556],{"class":4422},[3699,15860,15861],{"class":4324},"\"Refresh token not found or already used\"",[3699,15863,7077],{"class":4422},[3699,15865,15866],{"class":3701,"line":11381},[3699,15867,10015],{"class":4422},[3699,15869,15870],{"class":3701,"line":11386},[3699,15871,3730],{"emptyLinePlaceholder":3729},[3699,15873,15874],{"class":3701,"line":11391},[3699,15875,8368],{"class":4422},[3699,15877,15878,15880,15883,15886],{"class":3701,"line":11396},[3699,15879,5988],{"class":4418},[3699,15881,15882],{"class":4422}," stored.expires_at.replace(",[3699,15884,15885],{"class":4445},"tzinfo",[3699,15887,15888],{"class":4422},"=timezone.utc) \u003C now:\n",[3699,15890,15891,15893],{"class":3701,"line":11402},[3699,15892,11747],{"class":4418},[3699,15894,15895],{"class":4422}," token_repo.delete(stored.id)\n",[3699,15897,15898,15900],{"class":3701,"line":11413},[3699,15899,9972],{"class":4418},[3699,15901,9975],{"class":4422},[3699,15903,15904,15906],{"class":3701,"line":11418},[3699,15905,9980],{"class":4445},[3699,15907,9824],{"class":4422},[3699,15909,15910,15912,15914,15917],{"class":3701,"line":11433},[3699,15911,9987],{"class":4445},[3699,15913,4556],{"class":4422},[3699,15915,15916],{"class":4324},"\"Refresh token expired\"",[3699,15918,7077],{"class":4422},[3699,15920,15921],{"class":3701,"line":11439},[3699,15922,10015],{"class":4422},[3699,15924,15925],{"class":3701,"line":11445},[3699,15926,3730],{"emptyLinePlaceholder":3729},[3699,15928,15929],{"class":3701,"line":11451},[3699,15930,15931],{"class":4422},"    user_id = stored.user_id\n",[3699,15933,15934],{"class":3701,"line":11458},[3699,15935,3730],{"emptyLinePlaceholder":3729},[3699,15937,15938],{"class":3701,"line":11465},[3699,15939,15940],{"class":4412},"    # Ротація: видаляємо старий перед видачею нового\n",[3699,15942,15943,15945],{"class":3701,"line":11477},[3699,15944,4966],{"class":4418},[3699,15946,15895],{"class":4422},[3699,15948,15949],{"class":3701,"line":11482},[3699,15950,3730],{"emptyLinePlaceholder":3729},[3699,15952,15953,15955,15957,15959,15961],{"class":3701,"line":11487},[3699,15954,10863],{"class":4422},[3699,15956,10866],{"class":4445},[3699,15958,4556],{"class":4422},[3699,15960,4244],{"class":4452},[3699,15962,11534],{"class":4422},[3699,15964,15965],{"class":3701,"line":11493},[3699,15966,15415],{"class":4422},[3699,15968,15969,15971],{"class":3701,"line":11498},[3699,15970,4966],{"class":4418},[3699,15972,15973],{"class":4422}," token_repo.create(_build_refresh_token(plain_refresh, user_id, request))\n",[3699,15975,15976],{"class":3701,"line":11504},[3699,15977,3730],{"emptyLinePlaceholder":3729},[3699,15979,15980,15982,15984,15986,15988,15990],{"class":3701,"line":11512},[3699,15981,4576],{"class":4418},[3699,15983,15433],{"class":4422},[3699,15985,15436],{"class":4445},[3699,15987,15439],{"class":4422},[3699,15989,15442],{"class":4445},[3699,15991,15445],{"class":4422},[3699,15993,15994],{"class":3701,"line":11517},[3699,15995,3730],{"emptyLinePlaceholder":3729},[3699,15997,15998],{"class":3701,"line":11523},[3699,15999,3730],{"emptyLinePlaceholder":3729},[3699,16001,16002,16004,16006,16008,16010,16012],{"class":3701,"line":11537},[3699,16003,10348],{"class":4321},[3699,16005,4442],{"class":4422},[3699,16007,11644],{"class":4324},[3699,16009,4200],{"class":4422},[3699,16011,7984],{"class":4445},[3699,16013,11651],{"class":4422},[3699,16015,16016,16018,16020,16022],{"class":3701,"line":11542},[3699,16017,9702],{"class":4435},[3699,16019,9705],{"class":4435},[3699,16021,11661],{"class":4321},[3699,16023,8268],{"class":4422},[3699,16025,16026,16028],{"class":3701,"line":11547},[3699,16027,11329],{"class":4445},[3699,16029,11332],{"class":4422},[3699,16031,16032,16034],{"class":3701,"line":11554},[3699,16033,9727],{"class":4445},[3699,16035,9730],{"class":4422},[3699,16037,16038,16040,16042],{"class":3701,"line":11561},[3699,16039,4455],{"class":4422},[3699,16041,4938],{"class":4435},[3699,16043,4460],{"class":4422},[3699,16045,16046],{"class":3701,"line":11569},[3699,16047,4465],{"class":4324},[3699,16049,16050],{"class":3701,"line":11580},[3699,16051,11697],{"class":4324},[3699,16053,16054],{"class":3701,"line":11585},[3699,16055,16056],{"class":4324},"    Access token залишається дійсним до exp (15 хв) — нормальний trade-off для JWT.\n",[3699,16058,16059],{"class":3701,"line":11590},[3699,16060,4465],{"class":4324},[3699,16062,16063],{"class":3701,"line":11597},[3699,16064,10753],{"class":4422},[3699,16066,16067,16069,16071],{"class":3701,"line":11604},[3699,16068,15819],{"class":4422},[3699,16070,10044],{"class":4418},[3699,16072,11410],{"class":4422},[3699,16074,16075,16077],{"class":3701,"line":11611},[3699,16076,5988],{"class":4418},[3699,16078,16079],{"class":4422}," stored:\n",[3699,16081,16082,16084],{"class":3701,"line":11622},[3699,16083,11747],{"class":4418},[3699,16085,15895],{"class":4422},[3690,16087,16090],{"className":4403,"code":16088,"filename":16089,"language":4405,"meta":3695,"style":3695},"from fastapi import FastAPI\n\nfrom app.api.v1 import auth\n\napp = FastAPI(\n    title=\"TaskForge API\",\n    description=\"Project management API — JWT + Opaque Refresh Token auth\",\n    version=\"0.1.0\",\n)\n\napp.include_router(auth.router, prefix=\"\u002Fapi\u002Fv1\")\n\n\n@app.get(\"\u002Fhealth\", tags=[\"System\"])\nasync def health_check() -> dict:\n    return {\"status\": \"ok\"}\n","app\u002Fmain.py",[3625,16091,16092,16103,16107,16119,16123,16128,16140,16152,16164,16168,16172,16186,16190,16194,16215,16230],{"__ignoreMap":3695},[3699,16093,16094,16096,16098,16100],{"class":3701,"line":3702},[3699,16095,5828],{"class":4418},[3699,16097,9576],{"class":4422},[3699,16099,4419],{"class":4418},[3699,16101,16102],{"class":4422}," FastAPI\n",[3699,16104,16105],{"class":3701,"line":3708},[3699,16106,3730],{"emptyLinePlaceholder":3729},[3699,16108,16109,16111,16114,16116],{"class":3701,"line":3714},[3699,16110,5828],{"class":4418},[3699,16112,16113],{"class":4422}," app.api.v1 ",[3699,16115,4419],{"class":4418},[3699,16117,16118],{"class":4422}," auth\n",[3699,16120,16121],{"class":3701,"line":3720},[3699,16122,3730],{"emptyLinePlaceholder":3729},[3699,16124,16125],{"class":3701,"line":3726},[3699,16126,16127],{"class":4422},"app = FastAPI(\n",[3699,16129,16130,16133,16135,16138],{"class":3701,"line":3733},[3699,16131,16132],{"class":4445},"    title",[3699,16134,4556],{"class":4422},[3699,16136,16137],{"class":4324},"\"TaskForge API\"",[3699,16139,7077],{"class":4422},[3699,16141,16142,16145,16147,16150],{"class":3701,"line":3739},[3699,16143,16144],{"class":4445},"    description",[3699,16146,4556],{"class":4422},[3699,16148,16149],{"class":4324},"\"Project management API — JWT + Opaque Refresh Token auth\"",[3699,16151,7077],{"class":4422},[3699,16153,16154,16157,16159,16162],{"class":3701,"line":3745},[3699,16155,16156],{"class":4445},"    version",[3699,16158,4556],{"class":4422},[3699,16160,16161],{"class":4324},"\"0.1.0\"",[3699,16163,7077],{"class":4422},[3699,16165,16166],{"class":3701,"line":3751},[3699,16167,4562],{"class":4422},[3699,16169,16170],{"class":3701,"line":3757},[3699,16171,3730],{"emptyLinePlaceholder":3729},[3699,16173,16174,16177,16179,16181,16184],{"class":3701,"line":3763},[3699,16175,16176],{"class":4422},"app.include_router(auth.router, ",[3699,16178,10639],{"class":4445},[3699,16180,4556],{"class":4422},[3699,16182,16183],{"class":4324},"\"\u002Fapi\u002Fv1\"",[3699,16185,4562],{"class":4422},[3699,16187,16188],{"class":3701,"line":3769},[3699,16189,3730],{"emptyLinePlaceholder":3729},[3699,16191,16192],{"class":3701,"line":3775},[3699,16193,3730],{"emptyLinePlaceholder":3729},[3699,16195,16196,16199,16201,16204,16206,16208,16210,16213],{"class":3701,"line":3781},[3699,16197,16198],{"class":4321},"@app.get",[3699,16200,4442],{"class":4422},[3699,16202,16203],{"class":4324},"\"\u002Fhealth\"",[3699,16205,4200],{"class":4422},[3699,16207,10649],{"class":4445},[3699,16209,7938],{"class":4422},[3699,16211,16212],{"class":4324},"\"System\"",[3699,16214,10657],{"class":4422},[3699,16216,16217,16219,16221,16224,16226,16228],{"class":3701,"line":3786},[3699,16218,9702],{"class":4435},[3699,16220,9705],{"class":4435},[3699,16222,16223],{"class":4321}," health_check",[3699,16225,8622],{"class":4422},[3699,16227,7621],{"class":4452},[3699,16229,4460],{"class":4422},[3699,16231,16232,16234,16237,16240,16242,16245],{"class":3701,"line":3792},[3699,16233,4576],{"class":4418},[3699,16235,16236],{"class":4422}," {",[3699,16238,16239],{"class":4324},"\"status\"",[3699,16241,4449],{"class":4422},[3699,16243,16244],{"class":4324},"\"ok\"",[3699,16246,3778],{"class":4422},[3690,16248,16251],{"className":4403,"code":16249,"filename":16250,"language":4405,"meta":3695,"style":3695},"\"\"\"\nШвидке створення таблиць без Alembic (тільки для розробки).\nУ production використовуйте Alembic міграції.\n\"\"\"\nimport asyncio\n\nfrom app.db.session import engine\nfrom app.models.base import Base\nfrom app.models import user, refresh_token  # noqa: F401 — реєструємо моделі\n\n\nasync def main() -> None:\n    async with engine.begin() as conn:\n        await conn.run_sync(Base.metadata.create_all)\n    print(\"Tables created successfully!\")\n    await engine.dispose()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n","create_tables.py",[3625,16252,16253,16258,16263,16268,16272,16279,16283,16294,16304,16319,16323,16327,16342,16356,16363,16375,16382,16386,16390,16405],{"__ignoreMap":3695},[3699,16254,16255],{"class":3701,"line":3702},[3699,16256,16257],{"class":4324},"\"\"\"\n",[3699,16259,16260],{"class":3701,"line":3708},[3699,16261,16262],{"class":4324},"Швидке створення таблиць без Alembic (тільки для розробки).\n",[3699,16264,16265],{"class":3701,"line":3714},[3699,16266,16267],{"class":4324},"У production використовуйте Alembic міграції.\n",[3699,16269,16270],{"class":3701,"line":3720},[3699,16271,16257],{"class":4324},[3699,16273,16274,16276],{"class":3701,"line":3726},[3699,16275,4419],{"class":4418},[3699,16277,16278],{"class":4422}," asyncio\n",[3699,16280,16281],{"class":3701,"line":3733},[3699,16282,3730],{"emptyLinePlaceholder":3729},[3699,16284,16285,16287,16289,16291],{"class":3701,"line":3739},[3699,16286,5828],{"class":4418},[3699,16288,9639],{"class":4422},[3699,16290,4419],{"class":4418},[3699,16292,16293],{"class":4422}," engine\n",[3699,16295,16296,16298,16300,16302],{"class":3701,"line":3745},[3699,16297,5828],{"class":4418},[3699,16299,13050],{"class":4422},[3699,16301,4419],{"class":4418},[3699,16303,13055],{"class":4422},[3699,16305,16306,16308,16311,16313,16316],{"class":3701,"line":3751},[3699,16307,5828],{"class":4418},[3699,16309,16310],{"class":4422}," app.models ",[3699,16312,4419],{"class":4418},[3699,16314,16315],{"class":4422}," user, refresh_token  ",[3699,16317,16318],{"class":4412},"# noqa: F401 — реєструємо моделі\n",[3699,16320,16321],{"class":3701,"line":3757},[3699,16322,3730],{"emptyLinePlaceholder":3729},[3699,16324,16325],{"class":3701,"line":3763},[3699,16326,3730],{"emptyLinePlaceholder":3729},[3699,16328,16329,16331,16333,16336,16338,16340],{"class":3701,"line":3769},[3699,16330,9702],{"class":4435},[3699,16332,9705],{"class":4435},[3699,16334,16335],{"class":4321}," main",[3699,16337,8622],{"class":4422},[3699,16339,4938],{"class":4435},[3699,16341,4460],{"class":4422},[3699,16343,16344,16346,16348,16351,16353],{"class":3701,"line":3775},[3699,16345,12918],{"class":4418},[3699,16347,12921],{"class":4418},[3699,16349,16350],{"class":4422}," engine.begin() ",[3699,16352,12927],{"class":4418},[3699,16354,16355],{"class":4422}," conn:\n",[3699,16357,16358,16360],{"class":3701,"line":3781},[3699,16359,11747],{"class":4418},[3699,16361,16362],{"class":4422}," conn.run_sync(Base.metadata.create_all)\n",[3699,16364,16365,16368,16370,16373],{"class":3701,"line":3786},[3699,16366,16367],{"class":4321},"    print",[3699,16369,4442],{"class":4422},[3699,16371,16372],{"class":4324},"\"Tables created successfully!\"",[3699,16374,4562],{"class":4422},[3699,16376,16377,16379],{"class":3701,"line":3792},[3699,16378,4966],{"class":4418},[3699,16380,16381],{"class":4422}," engine.dispose()\n",[3699,16383,16384],{"class":3701,"line":3798},[3699,16385,3730],{"emptyLinePlaceholder":3729},[3699,16387,16388],{"class":3701,"line":3804},[3699,16389,3730],{"emptyLinePlaceholder":3729},[3699,16391,16392,16394,16397,16400,16403],{"class":3701,"line":3810},[3699,16393,4953],{"class":4418},[3699,16395,16396],{"class":4445}," __name__",[3699,16398,16399],{"class":4422}," == ",[3699,16401,16402],{"class":4324},"\"__main__\"",[3699,16404,4460],{"class":4422},[3699,16406,16407],{"class":3701,"line":3816},[3699,16408,16409],{"class":4422},"    asyncio.run(main())\n",[3661,16411,16413],{"id":16412},"запуск","Запуск",[16415,16416,16417,16421,16495,16499,16516,16520,16542,16546],"steps",{},[3661,16418,16420],{"id":16419},"запустити-postgresql","Запустити PostgreSQL",[3690,16422,16424],{"className":4312,"code":16423,"language":4314,"meta":3695,"style":3695},"docker run --rm -d \\\n  --name taskforge-db \\\n  -e POSTGRES_USER=postgres \\\n  -e POSTGRES_PASSWORD=postgres \\\n  -e POSTGRES_DB=taskforge \\\n  -p 5432:5432 \\\n  postgres:16\n",[3625,16425,16426,16442,16452,16462,16471,16480,16490],{"__ignoreMap":3695},[3699,16427,16428,16431,16434,16437,16440],{"class":3701,"line":3702},[3699,16429,16430],{"class":4321},"docker",[3699,16432,16433],{"class":4324}," run",[3699,16435,16436],{"class":4435}," --rm",[3699,16438,16439],{"class":4435}," -d",[3699,16441,11820],{"class":11819},[3699,16443,16444,16447,16450],{"class":3701,"line":3708},[3699,16445,16446],{"class":4435},"  --name",[3699,16448,16449],{"class":4324}," taskforge-db",[3699,16451,11820],{"class":11819},[3699,16453,16454,16457,16460],{"class":3701,"line":3714},[3699,16455,16456],{"class":4435},"  -e",[3699,16458,16459],{"class":4324}," POSTGRES_USER=postgres",[3699,16461,11820],{"class":11819},[3699,16463,16464,16466,16469],{"class":3701,"line":3720},[3699,16465,16456],{"class":4435},[3699,16467,16468],{"class":4324}," POSTGRES_PASSWORD=postgres",[3699,16470,11820],{"class":11819},[3699,16472,16473,16475,16478],{"class":3701,"line":3726},[3699,16474,16456],{"class":4435},[3699,16476,16477],{"class":4324}," POSTGRES_DB=taskforge",[3699,16479,11820],{"class":11819},[3699,16481,16482,16485,16488],{"class":3701,"line":3733},[3699,16483,16484],{"class":4435},"  -p",[3699,16486,16487],{"class":4324}," 5432:5432",[3699,16489,11820],{"class":11819},[3699,16491,16492],{"class":3701,"line":3739},[3699,16493,16494],{"class":4324},"  postgres:16\n",[3661,16496,16498],{"id":16497},"створити-таблиці","Створити таблиці",[3690,16500,16502],{"className":4312,"code":16501,"language":4314,"meta":3695,"style":3695},"python create_tables.py\n# Tables created successfully!\n",[3625,16503,16504,16511],{"__ignoreMap":3695},[3699,16505,16506,16508],{"class":3701,"line":3702},[3699,16507,4405],{"class":4321},[3699,16509,16510],{"class":4324}," create_tables.py\n",[3699,16512,16513],{"class":3701,"line":3708},[3699,16514,16515],{"class":4412},"# Tables created successfully!\n",[3661,16517,16519],{"id":16518},"запустити-сервер","Запустити сервер",[3690,16521,16523],{"className":4312,"code":16522,"language":4314,"meta":3695,"style":3695},"uvicorn app.main:app --reload --port 8000\n",[3625,16524,16525],{"__ignoreMap":3695},[3699,16526,16527,16530,16533,16536,16539],{"class":3701,"line":3702},[3699,16528,16529],{"class":4321},"uvicorn",[3699,16531,16532],{"class":4324}," app.main:app",[3699,16534,16535],{"class":4435}," --reload",[3699,16537,16538],{"class":4435}," --port",[3699,16540,16541],{"class":4531}," 8000\n",[3661,16543,16545],{"id":16544},"перевірити-через-swagger","Перевірити через Swagger",[3556,16547,16548,16549,16553,16554,16556,16557,4200,16560,4200,16562,4200,16565,4380],{},"Відкрийте ",[5747,16550,16551],{"href":16551,"rel":16552},"http:\u002F\u002Flocalhost:8000\u002Fdocs",[5751],".\nВи побачите кнопку ",[3560,16555,9323],{}," та всі 4 ендпоінти: ",[3625,16558,16559],{},"\u002Fregister",[3625,16561,11759],{},[3625,16563,16564],{},"\u002Frefresh",[3625,16566,16567],{},"\u002Flogout",[3654,16569],{},[3551,16571,16573],{"id":16572},"частина-4-rbac-контроль-доступу-на-основі-ролей","Частина 4. RBAC — Контроль доступу на основі ролей",[3661,16575,16577],{"id":16576},"що-таке-rbac-і-навіщо-він-потрібен","Що таке RBAC і навіщо він потрібен",[3556,16579,16580,16581,16584,16585,16588],{},"Ми вже вміємо відповідати на питання «",[3560,16582,16583],{},"хто"," ти?» (автентифікація). Тепер час відповісти на питання «",[3560,16586,16587],{},"що"," тобі дозволено?» (авторизація).",[3556,16590,16591,16592,16595],{},"Найпоширеніший підхід — ",[3560,16593,16594],{},"RBAC (Role-Based Access Control)",": кожному користувачу присвоюється роль, і кожна роль дає певний набір дозволів. Замість того, щоб призначати права кожному окремо, ми керуємо ролями — це масштабується навіть на мільйони користувачів.",[3576,16597,16598,16622],{},[3579,16599,16602,16608,16619],{"icon":16600,"title":16601},"i-heroicons-user-group","RBAC — Role-Based",[3556,16603,16604,16607],{},[3560,16605,16606],{},"Хто ти за роллю?"," Права визначаються належністю до групи.",[3556,16609,16610,16613,16616],{},[3625,16611,16612],{},"user → власні проєкти та задачі",[3625,16614,16615],{},"manager → проєкти команди + звіти",[3625,16617,16618],{},"admin → повний доступ",[3556,16620,16621],{},"Просто, масштабується, легко реалізується.",[3579,16623,16626,16631,16636],{"icon":16624,"title":16625},"i-heroicons-adjustments-horizontal","ABAC — Attribute-Based",[3556,16627,16628],{},[3560,16629,16630],{},"Які атрибути у суб'єкта, ресурсу та середовища?",[3556,16632,16633],{},[3625,16634,16635],{},"дозволено: user.dept == project.dept AND hour \u003C 18",[3556,16637,16638],{},"Максимально гнучкий, але значно складніший. Підходить для enterprise з нетривіальними правилами.",[3661,16640,16642],{"id":16641},"enum-ролей","Enum ролей",[3556,16644,16645,16646,16649,16650,4200,16653,16655],{},"Перший крок — визначити ролі як Python ",[3625,16647,16648],{},"Enum",". Це строга типізація замість «магічних рядків» ",[3625,16651,16652],{},"\"admin\"",[3625,16654,13280],{},", які легко написати з помилкою.",[3690,16657,16659],{"className":4403,"code":16658,"language":4405,"meta":3695,"style":3695},"# app\u002Fmodels\u002Frole.py\nfrom enum import Enum\n\n\nclass UserRole(str, Enum):\n    \"\"\"\n    Ролі користувачів TaskForge.\n\n    Успадкування від str критично важливе:\n    - SQLAlchemy зберігає рядок (\"user\", \"admin\"), а не \"UserRole.ADMIN\"\n    - Pydantic коректно серіалізує у JSON без додаткових налаштувань\n    - Порівняння UserRole.ADMIN == \"admin\" -> True (завдяки str-наслідуванню)\n    \"\"\"\n    USER = \"user\"         # звичайний користувач: власні проєкти та задачі\n    MANAGER = \"manager\"   # менеджер: управління командою, перегляд звітів\n    ADMIN = \"admin\"       # адміністратор: повний доступ, управління акаунтами\n",[3625,16660,16661,16666,16678,16682,16686,16703,16707,16712,16716,16721,16726,16731,16736,16740,16750,16761],{"__ignoreMap":3695},[3699,16662,16663],{"class":3701,"line":3702},[3699,16664,16665],{"class":4412},"# app\u002Fmodels\u002Frole.py\n",[3699,16667,16668,16670,16673,16675],{"class":3701,"line":3708},[3699,16669,5828],{"class":4418},[3699,16671,16672],{"class":4422}," enum ",[3699,16674,4419],{"class":4418},[3699,16676,16677],{"class":4422}," Enum\n",[3699,16679,16680],{"class":3701,"line":3714},[3699,16681,3730],{"emptyLinePlaceholder":3729},[3699,16683,16684],{"class":3701,"line":3720},[3699,16685,3730],{"emptyLinePlaceholder":3729},[3699,16687,16688,16690,16693,16695,16697,16699,16701],{"class":3701,"line":3726},[3699,16689,9008],{"class":4435},[3699,16691,16692],{"class":4452}," UserRole",[3699,16694,4442],{"class":4422},[3699,16696,4244],{"class":4452},[3699,16698,4200],{"class":4422},[3699,16700,16648],{"class":4452},[3699,16702,9019],{"class":4422},[3699,16704,16705],{"class":3701,"line":3733},[3699,16706,4465],{"class":4324},[3699,16708,16709],{"class":3701,"line":3739},[3699,16710,16711],{"class":4324},"    Ролі користувачів TaskForge.\n",[3699,16713,16714],{"class":3701,"line":3745},[3699,16715,3730],{"emptyLinePlaceholder":3729},[3699,16717,16718],{"class":3701,"line":3751},[3699,16719,16720],{"class":4324},"    Успадкування від str критично важливе:\n",[3699,16722,16723],{"class":3701,"line":3757},[3699,16724,16725],{"class":4324},"    - SQLAlchemy зберігає рядок (\"user\", \"admin\"), а не \"UserRole.ADMIN\"\n",[3699,16727,16728],{"class":3701,"line":3763},[3699,16729,16730],{"class":4324},"    - Pydantic коректно серіалізує у JSON без додаткових налаштувань\n",[3699,16732,16733],{"class":3701,"line":3769},[3699,16734,16735],{"class":4324},"    - Порівняння UserRole.ADMIN == \"admin\" -> True (завдяки str-наслідуванню)\n",[3699,16737,16738],{"class":3701,"line":3775},[3699,16739,4465],{"class":4324},[3699,16741,16742,16745,16747],{"class":3701,"line":3781},[3699,16743,16744],{"class":4422},"    USER = ",[3699,16746,13280],{"class":4324},[3699,16748,16749],{"class":4412},"         # звичайний користувач: власні проєкти та задачі\n",[3699,16751,16752,16755,16758],{"class":3701,"line":3786},[3699,16753,16754],{"class":4422},"    MANAGER = ",[3699,16756,16757],{"class":4324},"\"manager\"",[3699,16759,16760],{"class":4412},"   # менеджер: управління командою, перегляд звітів\n",[3699,16762,16763,16766,16768],{"class":3701,"line":3792},[3699,16764,16765],{"class":4422},"    ADMIN = ",[3699,16767,16652],{"class":4324},[3699,16769,16770],{"class":4412},"       # адміністратор: повний доступ, управління акаунтами\n",[3614,16772,16773,16776,16777,16779,16780,16783,16784,16787,16788,16790,16791,16793,16794,16797],{},[3625,16774,16775],{},"class UserRole(str, Enum)"," — критична деталь. Без наслідування від ",[3625,16778,4244],{}," SQLAlchemy зберігав би ",[3625,16781,16782],{},"UserRole.ADMIN"," як ",[3625,16785,16786],{},"\"UserRole.ADMIN\"",", а не ",[3625,16789,16652],{},". З ",[3625,16792,4244],{},"-наслідуванням значення enum — це звичайний рядок, сумісний з колонкою ",[3625,16795,16796],{},"VARCHAR"," у БД і JSON-серіалізацією Pydantic.",[3661,16799,16801],{"id":16800},"оновлення-моделі-user","Оновлення моделі User",[3556,16803,16804,16805,16807],{},"Додаємо поле ",[3625,16806,6594],{}," до SQLAlchemy-моделі:",[3690,16809,16811],{"className":4403,"code":16810,"language":4405,"meta":3695,"style":3695},"# app\u002Fmodels\u002Fuser.py\nimport uuid\n\nfrom sqlalchemy import Boolean, String\nfrom sqlalchemy.dialects.postgresql import UUID\nfrom sqlalchemy.orm import Mapped, mapped_column, relationship\n\nfrom app.models.base import Base\nfrom app.models.role import UserRole\n\n\nclass User(Base):\n    __tablename__ = \"users\"\n\n    id: Mapped[uuid.UUID] = mapped_column(\n        UUID(as_uuid=True), primary_key=True, default=uuid.uuid4\n    )\n    email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)\n    display_name: Mapped[str] = mapped_column(String(100), nullable=False)\n    hashed_password: Mapped[str] = mapped_column(String(60), nullable=False)\n    is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)\n\n    # Роль зберігається як VARCHAR — str-значення enum (\"user\", \"manager\", \"admin\")\n    role: Mapped[UserRole] = mapped_column(\n        String(20),\n        default=UserRole.USER,\n        nullable=False,\n    )\n\n    refresh_tokens: Mapped[list[\"RefreshToken\"]] = relationship(  # type: ignore[name-defined]\n        \"RefreshToken\", back_populates=\"user\", cascade=\"all, delete-orphan\"\n    )\n",[3625,16812,16813,16818,16824,16828,16838,16848,16858,16862,16872,16884,16888,16892,16904,16910,16914,16920,16944,16948,16984,17004,17024,17048,17052,17057,17062,17070,17078,17088,17092,17096,17106,17126],{"__ignoreMap":3695},[3699,16814,16815],{"class":3701,"line":3702},[3699,16816,16817],{"class":4412},"# app\u002Fmodels\u002Fuser.py\n",[3699,16819,16820,16822],{"class":3701,"line":3708},[3699,16821,4419],{"class":4418},[3699,16823,13000],{"class":4422},[3699,16825,16826],{"class":3701,"line":3714},[3699,16827,3730],{"emptyLinePlaceholder":3729},[3699,16829,16830,16832,16834,16836],{"class":3701,"line":3720},[3699,16831,5828],{"class":4418},[3699,16833,13011],{"class":4422},[3699,16835,4419],{"class":4418},[3699,16837,13016],{"class":4422},[3699,16839,16840,16842,16844,16846],{"class":3701,"line":3726},[3699,16841,5828],{"class":4418},[3699,16843,13023],{"class":4422},[3699,16845,4419],{"class":4418},[3699,16847,13028],{"class":4422},[3699,16849,16850,16852,16854,16856],{"class":3701,"line":3733},[3699,16851,5828],{"class":4418},[3699,16853,12951],{"class":4422},[3699,16855,4419],{"class":4418},[3699,16857,13039],{"class":4422},[3699,16859,16860],{"class":3701,"line":3739},[3699,16861,3730],{"emptyLinePlaceholder":3729},[3699,16863,16864,16866,16868,16870],{"class":3701,"line":3745},[3699,16865,5828],{"class":4418},[3699,16867,13050],{"class":4422},[3699,16869,4419],{"class":4418},[3699,16871,13055],{"class":4422},[3699,16873,16874,16876,16879,16881],{"class":3701,"line":3751},[3699,16875,5828],{"class":4418},[3699,16877,16878],{"class":4422}," app.models.role ",[3699,16880,4419],{"class":4418},[3699,16882,16883],{"class":4422}," UserRole\n",[3699,16885,16886],{"class":3701,"line":3757},[3699,16887,3730],{"emptyLinePlaceholder":3729},[3699,16889,16890],{"class":3701,"line":3763},[3699,16891,3730],{"emptyLinePlaceholder":3729},[3699,16893,16894,16896,16898,16900,16902],{"class":3701,"line":3769},[3699,16895,9008],{"class":4435},[3699,16897,13070],{"class":4452},[3699,16899,4442],{"class":4422},[3699,16901,13075],{"class":4452},[3699,16903,9019],{"class":4422},[3699,16905,16906,16908],{"class":3701,"line":3775},[3699,16907,13082],{"class":4422},[3699,16909,13085],{"class":4324},[3699,16911,16912],{"class":3701,"line":3781},[3699,16913,3730],{"emptyLinePlaceholder":3729},[3699,16915,16916,16918],{"class":3701,"line":3786},[3699,16917,13094],{"class":4321},[3699,16919,13097],{"class":4422},[3699,16921,16922,16924,16926,16928,16930,16932,16934,16936,16938,16940,16942],{"class":3701,"line":3792},[3699,16923,13102],{"class":4422},[3699,16925,13105],{"class":4445},[3699,16927,4556],{"class":4422},[3699,16929,9303],{"class":4435},[3699,16931,3648],{"class":4422},[3699,16933,13114],{"class":4445},[3699,16935,4556],{"class":4422},[3699,16937,9303],{"class":4435},[3699,16939,4200],{"class":4422},[3699,16941,13123],{"class":4445},[3699,16943,13126],{"class":4422},[3699,16945,16946],{"class":3701,"line":3798},[3699,16947,7951],{"class":4422},[3699,16949,16950,16952,16954,16956,16958,16960,16962,16964,16966,16968,16970,16972,16974,16976,16978,16980,16982],{"class":3701,"line":3804},[3699,16951,13135],{"class":4422},[3699,16953,4244],{"class":4452},[3699,16955,13140],{"class":4422},[3699,16957,13143],{"class":4531},[3699,16959,3648],{"class":4422},[3699,16961,13148],{"class":4445},[3699,16963,4556],{"class":4422},[3699,16965,9303],{"class":4435},[3699,16967,4200],{"class":4422},[3699,16969,13157],{"class":4445},[3699,16971,4556],{"class":4422},[3699,16973,9310],{"class":4435},[3699,16975,4200],{"class":4422},[3699,16977,13166],{"class":4445},[3699,16979,4556],{"class":4422},[3699,16981,9303],{"class":4435},[3699,16983,4562],{"class":4422},[3699,16985,16986,16988,16990,16992,16994,16996,16998,17000,17002],{"class":3701,"line":3810},[3699,16987,13177],{"class":4422},[3699,16989,4244],{"class":4452},[3699,16991,13140],{"class":4422},[3699,16993,13184],{"class":4531},[3699,16995,3648],{"class":4422},[3699,16997,13157],{"class":4445},[3699,16999,4556],{"class":4422},[3699,17001,9310],{"class":4435},[3699,17003,4562],{"class":4422},[3699,17005,17006,17008,17010,17012,17014,17016,17018,17020,17022],{"class":3701,"line":3816},[3699,17007,13204],{"class":4422},[3699,17009,4244],{"class":4452},[3699,17011,13140],{"class":4422},[3699,17013,13211],{"class":4531},[3699,17015,3648],{"class":4422},[3699,17017,13157],{"class":4445},[3699,17019,4556],{"class":4422},[3699,17021,9310],{"class":4435},[3699,17023,4562],{"class":4422},[3699,17025,17026,17028,17030,17032,17034,17036,17038,17040,17042,17044,17046],{"class":3701,"line":3822},[3699,17027,13226],{"class":4422},[3699,17029,4622],{"class":4452},[3699,17031,13231],{"class":4422},[3699,17033,13123],{"class":4445},[3699,17035,4556],{"class":4422},[3699,17037,9303],{"class":4435},[3699,17039,4200],{"class":4422},[3699,17041,13157],{"class":4445},[3699,17043,4556],{"class":4422},[3699,17045,9310],{"class":4435},[3699,17047,4562],{"class":4422},[3699,17049,17050],{"class":3701,"line":3827},[3699,17051,3730],{"emptyLinePlaceholder":3729},[3699,17053,17054],{"class":3701,"line":3832},[3699,17055,17056],{"class":4412},"    # Роль зберігається як VARCHAR — str-значення enum (\"user\", \"manager\", \"admin\")\n",[3699,17058,17059],{"class":3701,"line":3837},[3699,17060,17061],{"class":4422},"    role: Mapped[UserRole] = mapped_column(\n",[3699,17063,17064,17066,17068],{"class":3701,"line":3843},[3699,17065,13452],{"class":4422},[3699,17067,12839],{"class":4531},[3699,17069,7743],{"class":4422},[3699,17071,17072,17075],{"class":3701,"line":4590},[3699,17073,17074],{"class":4445},"        default",[3699,17076,17077],{"class":4422},"=UserRole.USER,\n",[3699,17079,17080,17082,17084,17086],{"class":3701,"line":4595},[3699,17081,13526],{"class":4445},[3699,17083,4556],{"class":4422},[3699,17085,9310],{"class":4435},[3699,17087,7077],{"class":4422},[3699,17089,17090],{"class":3701,"line":4627},[3699,17091,7951],{"class":4422},[3699,17093,17094],{"class":3701,"line":4632},[3699,17095,3730],{"emptyLinePlaceholder":3729},[3699,17097,17098,17100,17102,17104],{"class":3701,"line":4638},[3699,17099,13256],{"class":4422},[3699,17101,13259],{"class":4324},[3699,17103,13262],{"class":4422},[3699,17105,13265],{"class":4412},[3699,17107,17108,17110,17112,17114,17116,17118,17120,17122,17124],{"class":3701,"line":4643},[3699,17109,13270],{"class":4324},[3699,17111,4200],{"class":4422},[3699,17113,13275],{"class":4445},[3699,17115,4556],{"class":4422},[3699,17117,13280],{"class":4324},[3699,17119,4200],{"class":4422},[3699,17121,13285],{"class":4445},[3699,17123,4556],{"class":4422},[3699,17125,13290],{"class":4324},[3699,17127,17128],{"class":3701,"line":4648},[3699,17129,7951],{"class":4422},[3661,17131,17133,17134],{"id":17132},"dependency-фабрика-require_roles","Dependency-фабрика ",[3625,17135,17136],{},"require_roles()",[3556,17138,17139,17140,17143,17144,17146],{},"Ключова ідея — ",[3560,17141,17142],{},"Dependency Factory",": функція, що приймає список дозволених ролей і повертає готову ",[3625,17145,9702],{},"-dependency із замиканням над цим списком. FastAPI вбудовує її у граф залежностей автоматично.",[3687,17148,17149],{},[3690,17150,17152],{"className":3692,"code":17151,"language":3694,"meta":3695,"style":3695},"@startuml\nskinparam style plain\nskinparam backgroundColor #ffffff\nskinparam defaultFontSize 12\n\ntitle Dependency Factory: require_roles()\n\nrectangle \"require_roles(ADMIN)\" as call #e8f4ff {\n  note as n1\n    Виконується один раз при старті.\n    Створює closure із:\n    allowed = {UserRole.ADMIN}\n    Повертає role_checker.\n  end note\n}\n\nrectangle \"role_checker (closure)\" as checker #fff3e0 {\n  note as n2\n    Виконується при КОЖНОМУ запиті:\n    1. Depends(get_current_active_user)\n       -> User з БД\n    2. if user.role not in allowed:\n         raise 403 Forbidden\n    3. return user\n  end note\n}\n\ncall --> checker : повертає (один раз)\nchecker --> checker : FastAPI викликає при кожному запиті\n@enduml\n",[3625,17153,17154,17158,17162,17166,17170,17174,17179,17183,17188,17192,17197,17202,17207,17212,17216,17220,17224,17229,17233,17238,17243,17248,17253,17258,17263,17267,17271,17275,17280,17285],{"__ignoreMap":3695},[3699,17155,17156],{"class":3701,"line":3702},[3699,17157,3705],{},[3699,17159,17160],{"class":3701,"line":3708},[3699,17161,3711],{},[3699,17163,17164],{"class":3701,"line":3714},[3699,17165,3717],{},[3699,17167,17168],{"class":3701,"line":3720},[3699,17169,5065],{},[3699,17171,17172],{"class":3701,"line":3726},[3699,17173,3730],{"emptyLinePlaceholder":3729},[3699,17175,17176],{"class":3701,"line":3733},[3699,17177,17178],{},"title Dependency Factory: require_roles()\n",[3699,17180,17181],{"class":3701,"line":3739},[3699,17182,3730],{"emptyLinePlaceholder":3729},[3699,17184,17185],{"class":3701,"line":3745},[3699,17186,17187],{},"rectangle \"require_roles(ADMIN)\" as call #e8f4ff {\n",[3699,17189,17190],{"class":3701,"line":3751},[3699,17191,3742],{},[3699,17193,17194],{"class":3701,"line":3757},[3699,17195,17196],{},"    Виконується один раз при старті.\n",[3699,17198,17199],{"class":3701,"line":3763},[3699,17200,17201],{},"    Створює closure із:\n",[3699,17203,17204],{"class":3701,"line":3769},[3699,17205,17206],{},"    allowed = {UserRole.ADMIN}\n",[3699,17208,17209],{"class":3701,"line":3775},[3699,17210,17211],{},"    Повертає role_checker.\n",[3699,17213,17214],{"class":3701,"line":3781},[3699,17215,3772],{},[3699,17217,17218],{"class":3701,"line":3786},[3699,17219,3778],{},[3699,17221,17222],{"class":3701,"line":3792},[3699,17223,3730],{"emptyLinePlaceholder":3729},[3699,17225,17226],{"class":3701,"line":3798},[3699,17227,17228],{},"rectangle \"role_checker (closure)\" as checker #fff3e0 {\n",[3699,17230,17231],{"class":3701,"line":3804},[3699,17232,3795],{},[3699,17234,17235],{"class":3701,"line":3810},[3699,17236,17237],{},"    Виконується при КОЖНОМУ запиті:\n",[3699,17239,17240],{"class":3701,"line":3816},[3699,17241,17242],{},"    1. Depends(get_current_active_user)\n",[3699,17244,17245],{"class":3701,"line":3822},[3699,17246,17247],{},"       -> User з БД\n",[3699,17249,17250],{"class":3701,"line":3827},[3699,17251,17252],{},"    2. if user.role not in allowed:\n",[3699,17254,17255],{"class":3701,"line":3832},[3699,17256,17257],{},"         raise 403 Forbidden\n",[3699,17259,17260],{"class":3701,"line":3837},[3699,17261,17262],{},"    3. return user\n",[3699,17264,17265],{"class":3701,"line":3843},[3699,17266,3772],{},[3699,17268,17269],{"class":3701,"line":4590},[3699,17270,3778],{},[3699,17272,17273],{"class":3701,"line":4595},[3699,17274,3730],{"emptyLinePlaceholder":3729},[3699,17276,17277],{"class":3701,"line":4627},[3699,17278,17279],{},"call --> checker : повертає (один раз)\n",[3699,17281,17282],{"class":3701,"line":4632},[3699,17283,17284],{},"checker --> checker : FastAPI викликає при кожному запиті\n",[3699,17286,17287],{"class":3701,"line":4638},[3699,17288,3846],{},[3690,17290,17292],{"className":4403,"code":17291,"language":4405,"meta":3695,"style":3695},"# app\u002Fapi\u002Fdeps.py — повна версія з підтримкою ролей\nfrom collections.abc import Callable\n\nfrom fastapi import Depends, HTTPException, status\nfrom fastapi.security import OAuth2PasswordBearer\nfrom jose import ExpiredSignatureError, JWTError\nfrom sqlalchemy.ext.asyncio import AsyncSession\n\nfrom app.core.security import decode_access_token\nfrom app.db.session import get_db\nfrom app.models.role import UserRole\nfrom app.models.user import User\nfrom app.repositories.user import UserRepository\n\noauth2_scheme = OAuth2PasswordBearer(tokenUrl=\"\u002Fapi\u002Fv1\u002Fauth\u002Flogin\")\n\n\nasync def get_current_user(\n    token: str = Depends(oauth2_scheme),\n    db: AsyncSession = Depends(get_db),\n) -> User:\n    \"\"\"Dependency (рівень 1): перевіряє JWT та завантажує User з БД.\"\"\"\n    unauthorized = HTTPException(\n        status_code=status.HTTP_401_UNAUTHORIZED,\n        detail=\"Could not validate credentials\",\n        headers={\"WWW-Authenticate\": \"Bearer\"},\n    )\n    try:\n        payload = decode_access_token(token)\n        if payload.get(\"type\") != \"access\":\n            raise unauthorized\n        user_id: str | None = payload.get(\"sub\")\n        if user_id is None:\n            raise unauthorized\n    except ExpiredSignatureError:\n        raise HTTPException(\n            status_code=status.HTTP_401_UNAUTHORIZED,\n            detail=\"Token has expired\",\n            headers={\"WWW-Authenticate\": \"Bearer\"},\n        )\n    except JWTError:\n        raise unauthorized\n\n    user = await UserRepository(db).get_by_id(user_id)\n    if user is None:\n        raise unauthorized\n    return user\n\n\nasync def get_current_active_user(\n    current_user: User = Depends(get_current_user),\n) -> User:\n    \"\"\"Dependency (рівень 2): перевіряє is_active.\"\"\"\n    if not current_user.is_active:\n        raise HTTPException(\n            status_code=status.HTTP_403_FORBIDDEN,\n            detail=\"Inactive user account\",\n        )\n    return current_user\n\n\ndef require_roles(*roles: UserRole) -> Callable:\n    \"\"\"\n    Dependency-фабрика для перевірки ролей.\n\n    Приймає одну або кілька дозволених ролей та повертає\n    dependency-функцію, яку FastAPI вбудовує у граф залежностей.\n\n    Args:\n        *roles: Перелік дозволених UserRole значень.\n\n    Returns:\n        Async dependency-функція -> User або 403 Forbidden.\n\n    Використання:\n        # Лише адміністратор:\n        Depends(require_roles(UserRole.ADMIN))\n\n        # Адміністратор або менеджер:\n        Depends(require_roles(UserRole.ADMIN, UserRole.MANAGER))\n    \"\"\"\n    allowed = set(roles)\n\n    async def role_checker(\n        current_user: User = Depends(get_current_active_user),\n    ) -> User:\n        if current_user.role not in allowed:\n            raise HTTPException(\n                status_code=status.HTTP_403_FORBIDDEN,\n                detail=f\"Access denied. Required roles: {[r.value for r in allowed]}\",\n            )\n        return current_user\n\n    return role_checker\n",[3625,17293,17294,17299,17311,17315,17325,17335,17345,17355,17359,17369,17379,17389,17399,17409,17413,17425,17429,17433,17443,17453,17459,17463,17468,17472,17478,17488,17502,17506,17512,17516,17530,17536,17552,17564,17570,17576,17582,17588,17598,17612,17616,17622,17628,17632,17640,17652,17658,17664,17668,17672,17682,17688,17692,17697,17705,17711,17717,17727,17731,17737,17741,17745,17761,17765,17770,17774,17779,17784,17788,17792,17797,17801,17805,17810,17814,17819,17824,17829,17833,17838,17843,17847,17858,17862,17873,17880,17885,17901,17907,17914,17952,17957,17963,17967],{"__ignoreMap":3695},[3699,17295,17296],{"class":3701,"line":3702},[3699,17297,17298],{"class":4412},"# app\u002Fapi\u002Fdeps.py — повна версія з підтримкою ролей\n",[3699,17300,17301,17303,17306,17308],{"class":3701,"line":3708},[3699,17302,5828],{"class":4418},[3699,17304,17305],{"class":4422}," collections.abc ",[3699,17307,4419],{"class":4418},[3699,17309,17310],{"class":4422}," Callable\n",[3699,17312,17313],{"class":3701,"line":3714},[3699,17314,3730],{"emptyLinePlaceholder":3729},[3699,17316,17317,17319,17321,17323],{"class":3701,"line":3720},[3699,17318,5828],{"class":4418},[3699,17320,9576],{"class":4422},[3699,17322,4419],{"class":4418},[3699,17324,9581],{"class":4422},[3699,17326,17327,17329,17331,17333],{"class":3701,"line":3726},[3699,17328,5828],{"class":4418},[3699,17330,9588],{"class":4422},[3699,17332,4419],{"class":4418},[3699,17334,9593],{"class":4422},[3699,17336,17337,17339,17341,17343],{"class":3701,"line":3733},[3699,17338,5828],{"class":4418},[3699,17340,7666],{"class":4422},[3699,17342,4419],{"class":4418},[3699,17344,14494],{"class":4422},[3699,17346,17347,17349,17351,17353],{"class":3701,"line":3739},[3699,17348,5828],{"class":4418},[3699,17350,9611],{"class":4422},[3699,17352,4419],{"class":4418},[3699,17354,9616],{"class":4422},[3699,17356,17357],{"class":3701,"line":3745},[3699,17358,3730],{"emptyLinePlaceholder":3729},[3699,17360,17361,17363,17365,17367],{"class":3701,"line":3751},[3699,17362,5828],{"class":4418},[3699,17364,9627],{"class":4422},[3699,17366,4419],{"class":4418},[3699,17368,9632],{"class":4422},[3699,17370,17371,17373,17375,17377],{"class":3701,"line":3757},[3699,17372,5828],{"class":4418},[3699,17374,9639],{"class":4422},[3699,17376,4419],{"class":4418},[3699,17378,9644],{"class":4422},[3699,17380,17381,17383,17385,17387],{"class":3701,"line":3763},[3699,17382,5828],{"class":4418},[3699,17384,16878],{"class":4422},[3699,17386,4419],{"class":4418},[3699,17388,16883],{"class":4422},[3699,17390,17391,17393,17395,17397],{"class":3701,"line":3769},[3699,17392,5828],{"class":4418},[3699,17394,9651],{"class":4422},[3699,17396,4419],{"class":4418},[3699,17398,9656],{"class":4422},[3699,17400,17401,17403,17405,17407],{"class":3701,"line":3775},[3699,17402,5828],{"class":4418},[3699,17404,9663],{"class":4422},[3699,17406,4419],{"class":4418},[3699,17408,9668],{"class":4422},[3699,17410,17411],{"class":3701,"line":3781},[3699,17412,3730],{"emptyLinePlaceholder":3729},[3699,17414,17415,17417,17419,17421,17423],{"class":3701,"line":3786},[3699,17416,9287],{"class":4422},[3699,17418,9270],{"class":4445},[3699,17420,4556],{"class":4422},[3699,17422,9294],{"class":4324},[3699,17424,4562],{"class":4422},[3699,17426,17427],{"class":3701,"line":3792},[3699,17428,3730],{"emptyLinePlaceholder":3729},[3699,17430,17431],{"class":3701,"line":3798},[3699,17432,3730],{"emptyLinePlaceholder":3729},[3699,17434,17435,17437,17439,17441],{"class":3701,"line":3804},[3699,17436,9702],{"class":4435},[3699,17438,9705],{"class":4435},[3699,17440,9708],{"class":4321},[3699,17442,8268],{"class":4422},[3699,17444,17445,17447,17449,17451],{"class":3701,"line":3810},[3699,17446,9715],{"class":4445},[3699,17448,4449],{"class":4422},[3699,17450,4244],{"class":4452},[3699,17452,9722],{"class":4422},[3699,17454,17455,17457],{"class":3701,"line":3816},[3699,17456,9727],{"class":4445},[3699,17458,9730],{"class":4422},[3699,17460,17461],{"class":3701,"line":3822},[3699,17462,9735],{"class":4422},[3699,17464,17465],{"class":3701,"line":3827},[3699,17466,17467],{"class":4324},"    \"\"\"Dependency (рівень 1): перевіряє JWT та завантажує User з БД.\"\"\"\n",[3699,17469,17470],{"class":3701,"line":3832},[3699,17471,14625],{"class":4422},[3699,17473,17474,17476],{"class":3701,"line":3837},[3699,17475,9821],{"class":4445},[3699,17477,9824],{"class":4422},[3699,17479,17480,17482,17484,17486],{"class":3701,"line":3843},[3699,17481,9829],{"class":4445},[3699,17483,4556],{"class":4422},[3699,17485,9834],{"class":4324},[3699,17487,7077],{"class":4422},[3699,17489,17490,17492,17494,17496,17498,17500],{"class":3701,"line":4590},[3699,17491,9841],{"class":4445},[3699,17493,9844],{"class":4422},[3699,17495,9847],{"class":4324},[3699,17497,4449],{"class":4422},[3699,17499,9852],{"class":4324},[3699,17501,10010],{"class":4422},[3699,17503,17504],{"class":3701,"line":4595},[3699,17505,7951],{"class":4422},[3699,17507,17508,17510],{"class":3701,"line":4627},[3699,17509,9871],{"class":4418},[3699,17511,4460],{"class":4422},[3699,17513,17514],{"class":3701,"line":4632},[3699,17515,9878],{"class":4422},[3699,17517,17518,17520,17522,17524,17526,17528],{"class":3701,"line":4638},[3699,17519,9892],{"class":4418},[3699,17521,9895],{"class":4422},[3699,17523,9898],{"class":4324},[3699,17525,9901],{"class":4422},[3699,17527,8422],{"class":4324},[3699,17529,4460],{"class":4422},[3699,17531,17532,17534],{"class":3701,"line":4643},[3699,17533,9910],{"class":4418},[3699,17535,14690],{"class":4422},[3699,17537,17538,17540,17542,17544,17546,17548,17550],{"class":3701,"line":4648},[3699,17539,9922],{"class":4422},[3699,17541,4244],{"class":4452},[3699,17543,8292],{"class":4422},[3699,17545,4938],{"class":4435},[3699,17547,9931],{"class":4422},[3699,17549,7959],{"class":4324},[3699,17551,4562],{"class":4422},[3699,17553,17554,17556,17558,17560,17562],{"class":3701,"line":4654},[3699,17555,9892],{"class":4418},[3699,17557,9942],{"class":4422},[3699,17559,9945],{"class":4435},[3699,17561,9948],{"class":4435},[3699,17563,4460],{"class":4422},[3699,17565,17566,17568],{"class":3701,"line":4660},[3699,17567,9910],{"class":4418},[3699,17569,14690],{"class":4422},[3699,17571,17572,17574],{"class":3701,"line":4665},[3699,17573,9965],{"class":4418},[3699,17575,7968],{"class":4422},[3699,17577,17578,17580],{"class":3701,"line":4670},[3699,17579,9972],{"class":4418},[3699,17581,9975],{"class":4422},[3699,17583,17584,17586],{"class":3701,"line":4676},[3699,17585,9980],{"class":4445},[3699,17587,9824],{"class":4422},[3699,17589,17590,17592,17594,17596],{"class":3701,"line":4681},[3699,17591,9987],{"class":4445},[3699,17593,4556],{"class":4422},[3699,17595,9992],{"class":4324},[3699,17597,7077],{"class":4422},[3699,17599,17600,17602,17604,17606,17608,17610],{"class":3701,"line":4694},[3699,17601,9999],{"class":4445},[3699,17603,9844],{"class":4422},[3699,17605,9847],{"class":4324},[3699,17607,4449],{"class":4422},[3699,17609,9852],{"class":4324},[3699,17611,10010],{"class":4422},[3699,17613,17614],{"class":3701,"line":4704},[3699,17615,10015],{"class":4422},[3699,17617,17618,17620],{"class":3701,"line":4709},[3699,17619,9965],{"class":4418},[3699,17621,8008],{"class":4422},[3699,17623,17624,17626],{"class":3701,"line":6342},[3699,17625,9972],{"class":4418},[3699,17627,14690],{"class":4422},[3699,17629,17630],{"class":3701,"line":6348},[3699,17631,3730],{"emptyLinePlaceholder":3729},[3699,17633,17634,17636,17638],{"class":3701,"line":6354},[3699,17635,10041],{"class":4422},[3699,17637,10044],{"class":4418},[3699,17639,14795],{"class":4422},[3699,17641,17642,17644,17646,17648,17650],{"class":3701,"line":6359},[3699,17643,5988],{"class":4418},[3699,17645,10058],{"class":4422},[3699,17647,9945],{"class":4435},[3699,17649,9948],{"class":4435},[3699,17651,4460],{"class":4422},[3699,17653,17654,17656],{"class":3701,"line":6365},[3699,17655,9972],{"class":4418},[3699,17657,14690],{"class":4422},[3699,17659,17660,17662],{"class":3701,"line":6371},[3699,17661,4576],{"class":4418},[3699,17663,10081],{"class":4422},[3699,17665,17666],{"class":3701,"line":6377},[3699,17667,3730],{"emptyLinePlaceholder":3729},[3699,17669,17670],{"class":3701,"line":6382},[3699,17671,3730],{"emptyLinePlaceholder":3729},[3699,17673,17674,17676,17678,17680],{"class":3701,"line":6387},[3699,17675,9702],{"class":4435},[3699,17677,9705],{"class":4435},[3699,17679,10098],{"class":4321},[3699,17681,8268],{"class":4422},[3699,17683,17684,17686],{"class":3701,"line":6392},[3699,17685,10105],{"class":4445},[3699,17687,10108],{"class":4422},[3699,17689,17690],{"class":3701,"line":6398},[3699,17691,9735],{"class":4422},[3699,17693,17694],{"class":3701,"line":6404},[3699,17695,17696],{"class":4324},"    \"\"\"Dependency (рівень 2): перевіряє is_active.\"\"\"\n",[3699,17698,17699,17701,17703],{"class":3701,"line":6409},[3699,17700,5988],{"class":4418},[3699,17702,10185],{"class":4435},[3699,17704,10188],{"class":4422},[3699,17706,17707,17709],{"class":3701,"line":8467},[3699,17708,9972],{"class":4418},[3699,17710,9975],{"class":4422},[3699,17712,17713,17715],{"class":3701,"line":8472},[3699,17714,9980],{"class":4445},[3699,17716,10201],{"class":4422},[3699,17718,17719,17721,17723,17725],{"class":3701,"line":8494},[3699,17720,9987],{"class":4445},[3699,17722,4556],{"class":4422},[3699,17724,10210],{"class":4324},[3699,17726,7077],{"class":4422},[3699,17728,17729],{"class":3701,"line":8499},[3699,17730,10015],{"class":4422},[3699,17732,17733,17735],{"class":3701,"line":8505},[3699,17734,4576],{"class":4418},[3699,17736,10223],{"class":4422},[3699,17738,17739],{"class":3701,"line":8510},[3699,17740,3730],{"emptyLinePlaceholder":3729},[3699,17742,17743],{"class":3701,"line":8515},[3699,17744,3730],{"emptyLinePlaceholder":3729},[3699,17746,17747,17749,17752,17755,17758],{"class":3701,"line":8521},[3699,17748,4436],{"class":4435},[3699,17750,17751],{"class":4321}," require_roles",[3699,17753,17754],{"class":4422},"(*",[3699,17756,17757],{"class":4445},"roles",[3699,17759,17760],{"class":4422},": UserRole) -> Callable:\n",[3699,17762,17763],{"class":3701,"line":8526},[3699,17764,4465],{"class":4324},[3699,17766,17767],{"class":3701,"line":8531},[3699,17768,17769],{"class":4324},"    Dependency-фабрика для перевірки ролей.\n",[3699,17771,17772],{"class":3701,"line":8537},[3699,17773,3730],{"emptyLinePlaceholder":3729},[3699,17775,17776],{"class":3701,"line":8542},[3699,17777,17778],{"class":4324},"    Приймає одну або кілька дозволених ролей та повертає\n",[3699,17780,17781],{"class":3701,"line":8548},[3699,17782,17783],{"class":4324},"    dependency-функцію, яку FastAPI вбудовує у граф залежностей.\n",[3699,17785,17786],{"class":3701,"line":8554},[3699,17787,3730],{"emptyLinePlaceholder":3729},[3699,17789,17790],{"class":3701,"line":8560},[3699,17791,4484],{"class":4324},[3699,17793,17794],{"class":3701,"line":8565},[3699,17795,17796],{"class":4324},"        *roles: Перелік дозволених UserRole значень.\n",[3699,17798,17799],{"class":3701,"line":8573},[3699,17800,3730],{"emptyLinePlaceholder":3729},[3699,17802,17803],{"class":3701,"line":8579},[3699,17804,4498],{"class":4324},[3699,17806,17807],{"class":3701,"line":8585},[3699,17808,17809],{"class":4324},"        Async dependency-функція -> User або 403 Forbidden.\n",[3699,17811,17812],{"class":3701,"line":8593},[3699,17813,3730],{"emptyLinePlaceholder":3729},[3699,17815,17816],{"class":3701,"line":8598},[3699,17817,17818],{"class":4324},"    Використання:\n",[3699,17820,17821],{"class":3701,"line":8603},[3699,17822,17823],{"class":4324},"        # Лише адміністратор:\n",[3699,17825,17826],{"class":3701,"line":8608},[3699,17827,17828],{"class":4324},"        Depends(require_roles(UserRole.ADMIN))\n",[3699,17830,17831],{"class":3701,"line":8614},[3699,17832,3730],{"emptyLinePlaceholder":3729},[3699,17834,17835],{"class":3701,"line":8629},[3699,17836,17837],{"class":4324},"        # Адміністратор або менеджер:\n",[3699,17839,17840],{"class":3701,"line":8634},[3699,17841,17842],{"class":4324},"        Depends(require_roles(UserRole.ADMIN, UserRole.MANAGER))\n",[3699,17844,17845],{"class":3701,"line":8640},[3699,17846,4465],{"class":4324},[3699,17848,17849,17852,17855],{"class":3701,"line":8645},[3699,17850,17851],{"class":4422},"    allowed = ",[3699,17853,17854],{"class":4452},"set",[3699,17856,17857],{"class":4422},"(roles)\n",[3699,17859,17860],{"class":3701,"line":8651},[3699,17861,3730],{"emptyLinePlaceholder":3729},[3699,17863,17864,17866,17868,17871],{"class":3701,"line":8657},[3699,17865,12918],{"class":4435},[3699,17867,9705],{"class":4435},[3699,17869,17870],{"class":4321}," role_checker",[3699,17872,8268],{"class":4422},[3699,17874,17875,17878],{"class":3701,"line":8663},[3699,17876,17877],{"class":4445},"        current_user",[3699,17879,10316],{"class":4422},[3699,17881,17882],{"class":3701,"line":8668},[3699,17883,17884],{"class":4422},"    ) -> User:\n",[3699,17886,17887,17889,17892,17895,17898],{"class":3701,"line":8673},[3699,17888,9892],{"class":4418},[3699,17890,17891],{"class":4422}," current_user.role ",[3699,17893,17894],{"class":4435},"not",[3699,17896,17897],{"class":4435}," in",[3699,17899,17900],{"class":4422}," allowed:\n",[3699,17902,17903,17905],{"class":3701,"line":8679},[3699,17904,9910],{"class":4418},[3699,17906,9975],{"class":4422},[3699,17908,17909,17912],{"class":3701,"line":8685},[3699,17910,17911],{"class":4445},"                status_code",[3699,17913,10201],{"class":4422},[3699,17915,17916,17919,17921,17924,17927,17930,17933,17936,17939,17941,17944,17947,17950],{"class":3701,"line":8690},[3699,17917,17918],{"class":4445},"                detail",[3699,17920,4556],{"class":4422},[3699,17922,17923],{"class":4435},"f",[3699,17925,17926],{"class":4324},"\"Access denied. Required roles: ",[3699,17928,17929],{"class":4435},"{",[3699,17931,17932],{"class":4422},"[r.value ",[3699,17934,17935],{"class":4418},"for",[3699,17937,17938],{"class":4422}," r ",[3699,17940,14438],{"class":4418},[3699,17942,17943],{"class":4422}," allowed]",[3699,17945,17946],{"class":4435},"}",[3699,17948,17949],{"class":4324},"\"",[3699,17951,7077],{"class":4422},[3699,17953,17954],{"class":3701,"line":8702},[3699,17955,17956],{"class":4422},"            )\n",[3699,17958,17959,17961],{"class":3701,"line":8707},[3699,17960,13996],{"class":4418},[3699,17962,10223],{"class":4422},[3699,17964,17965],{"class":3701,"line":8712},[3699,17966,3730],{"emptyLinePlaceholder":3729},[3699,17968,17969,17971],{"class":3701,"line":8734},[3699,17970,4576],{"class":4418},[3699,17972,17973],{"class":4422}," role_checker\n",[3661,17975,17977],{"id":17976},"ендпоінти-адмін-панелі","Ендпоінти адмін-панелі",[3690,17979,17981],{"className":4403,"code":17980,"language":4405,"meta":3695,"style":3695},"# app\u002Fapi\u002Fv1\u002Fadmin.py\nimport uuid\n\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom pydantic import BaseModel, EmailStr\nfrom sqlalchemy import select\nfrom sqlalchemy.ext.asyncio import AsyncSession\n\nfrom app.api.deps import require_roles\nfrom app.db.session import get_db\nfrom app.models.role import UserRole\nfrom app.models.user import User\n\nrouter = APIRouter(prefix=\"\u002Fadmin\", tags=[\"Administration\"])\n\n# Alias для зручності — щоб не дублювати require_roles(UserRole.ADMIN) скрізь\nAdminOnly = Depends(require_roles(UserRole.ADMIN))\n\n\n# ─── Pydantic схеми ───────────────────────────────────────────────────────────\nclass UserListResponse(BaseModel):\n    id: uuid.UUID\n    email: EmailStr\n    display_name: str\n    role: UserRole\n    is_active: bool\n\n    model_config = {\"from_attributes\": True}\n\n\nclass UserRoleUpdateRequest(BaseModel):\n    role: UserRole\n\n\n# ─── Ендпоінти ────────────────────────────────────────────────────────────────\n@router.get(\"\u002Fusers\", response_model=list[UserListResponse])\nasync def list_all_users(\n    current_user: User = AdminOnly,\n    db: AsyncSession = Depends(get_db),\n) -> list[User]:\n    \"\"\"Повертає список усіх користувачів. Тільки для адміністраторів.\"\"\"\n    result = await db.execute(select(User).order_by(User.email))\n    return list(result.scalars().all())\n\n\n@router.patch(\"\u002Fusers\u002F{user_id}\u002Frole\", response_model=UserListResponse)\nasync def update_user_role(\n    user_id: uuid.UUID,\n    body: UserRoleUpdateRequest,\n    current_user: User = AdminOnly,\n    db: AsyncSession = Depends(get_db),\n) -> User:\n    \"\"\"Змінює роль. Адмін не може змінити власну роль.\"\"\"\n    target = await db.get(User, user_id)\n    if target is None:\n        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"User not found\")\n    if target.id == current_user.id:\n        raise HTTPException(\n            status_code=status.HTTP_400_BAD_REQUEST,\n            detail=\"Cannot change your own role\",\n        )\n    target.role = body.role\n    await db.commit()\n    await db.refresh(target)\n    return target\n\n\n@router.patch(\"\u002Fusers\u002F{user_id}\u002Fdeactivate\", status_code=status.HTTP_204_NO_CONTENT)\nasync def deactivate_user(\n    user_id: uuid.UUID,\n    current_user: User = AdminOnly,\n    db: AsyncSession = Depends(get_db),\n) -> None:\n    \"\"\"Деактивує акаунт. Адмін не може деактивувати себе.\"\"\"\n    target = await db.get(User, user_id)\n    if target is None:\n        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"User not found\")\n    if target.id == current_user.id:\n        raise HTTPException(\n            status_code=status.HTTP_400_BAD_REQUEST,\n            detail=\"Cannot deactivate your own account\",\n        )\n    target.is_active = False\n    await db.commit()\n\n\n@router.get(\"\u002Freports\u002Fsummary\")\nasync def reports_summary(\n    # Менеджери та адміністратори мають доступ до звітів\n    current_user: User = Depends(require_roles(UserRole.ADMIN, UserRole.MANAGER)),\n    db: AsyncSession = Depends(get_db),\n) -> dict:\n    \"\"\"Зведений звіт. Доступний менеджерам та адміністраторам.\"\"\"\n    result = await db.execute(select(User))\n    users = result.scalars().all()\n    return {\n        \"total_users\": len(users),\n        \"active_users\": sum(1 for u in users if u.is_active),\n        \"admins\": sum(1 for u in users if u.role == UserRole.ADMIN),\n        \"managers\": sum(1 for u in users if u.role == UserRole.MANAGER),\n        \"requested_by\": current_user.email,\n    }\n",[3625,17982,17983,17988,17994,17998,18008,18019,18029,18039,18043,18054,18064,18074,18084,18088,18110,18114,18119,18124,18128,18132,18137,18150,18157,18161,18167,18172,18180,18184,18198,18202,18206,18219,18223,18227,18231,18236,18252,18263,18270,18276,18281,18286,18296,18306,18310,18314,18337,18348,18355,18362,18368,18374,18378,18383,18393,18406,18426,18433,18439,18446,18457,18461,18466,18473,18480,18487,18491,18495,18514,18525,18531,18537,18543,18551,18556,18564,18576,18594,18600,18606,18612,18623,18627,18635,18641,18645,18649,18660,18671,18676,18683,18689,18697,18702,18711,18716,18723,18736,18767,18793,18819,18827],{"__ignoreMap":3695},[3699,17984,17985],{"class":3701,"line":3702},[3699,17986,17987],{"class":4412},"# app\u002Fapi\u002Fv1\u002Fadmin.py\n",[3699,17989,17990,17992],{"class":3701,"line":3708},[3699,17991,4419],{"class":4418},[3699,17993,13000],{"class":4422},[3699,17995,17996],{"class":3701,"line":3714},[3699,17997,3730],{"emptyLinePlaceholder":3729},[3699,17999,18000,18002,18004,18006],{"class":3701,"line":3720},[3699,18001,5828],{"class":4418},[3699,18003,9576],{"class":4422},[3699,18005,4419],{"class":4418},[3699,18007,10463],{"class":4422},[3699,18009,18010,18012,18014,18016],{"class":3701,"line":3726},[3699,18011,5828],{"class":4418},[3699,18013,13679],{"class":4422},[3699,18015,4419],{"class":4418},[3699,18017,18018],{"class":4422}," BaseModel, EmailStr\n",[3699,18020,18021,18023,18025,18027],{"class":3701,"line":3733},[3699,18022,5828],{"class":4418},[3699,18024,13011],{"class":4422},[3699,18026,4419],{"class":4418},[3699,18028,13871],{"class":4422},[3699,18030,18031,18033,18035,18037],{"class":3701,"line":3739},[3699,18032,5828],{"class":4418},[3699,18034,9611],{"class":4422},[3699,18036,4419],{"class":4418},[3699,18038,9616],{"class":4422},[3699,18040,18041],{"class":3701,"line":3745},[3699,18042,3730],{"emptyLinePlaceholder":3729},[3699,18044,18045,18047,18049,18051],{"class":3701,"line":3751},[3699,18046,5828],{"class":4418},[3699,18048,10254],{"class":4422},[3699,18050,4419],{"class":4418},[3699,18052,18053],{"class":4422}," require_roles\n",[3699,18055,18056,18058,18060,18062],{"class":3701,"line":3757},[3699,18057,5828],{"class":4418},[3699,18059,9639],{"class":4422},[3699,18061,4419],{"class":4418},[3699,18063,9644],{"class":4422},[3699,18065,18066,18068,18070,18072],{"class":3701,"line":3763},[3699,18067,5828],{"class":4418},[3699,18069,16878],{"class":4422},[3699,18071,4419],{"class":4418},[3699,18073,16883],{"class":4422},[3699,18075,18076,18078,18080,18082],{"class":3701,"line":3769},[3699,18077,5828],{"class":4418},[3699,18079,9651],{"class":4422},[3699,18081,4419],{"class":4418},[3699,18083,9656],{"class":4422},[3699,18085,18086],{"class":3701,"line":3775},[3699,18087,3730],{"emptyLinePlaceholder":3729},[3699,18089,18090,18092,18094,18096,18099,18101,18103,18105,18108],{"class":3701,"line":3781},[3699,18091,10636],{"class":4422},[3699,18093,10639],{"class":4445},[3699,18095,4556],{"class":4422},[3699,18097,18098],{"class":4324},"\"\u002Fadmin\"",[3699,18100,4200],{"class":4422},[3699,18102,10649],{"class":4445},[3699,18104,7938],{"class":4422},[3699,18106,18107],{"class":4324},"\"Administration\"",[3699,18109,10657],{"class":4422},[3699,18111,18112],{"class":3701,"line":3786},[3699,18113,3730],{"emptyLinePlaceholder":3729},[3699,18115,18116],{"class":3701,"line":3792},[3699,18117,18118],{"class":4412},"# Alias для зручності — щоб не дублювати require_roles(UserRole.ADMIN) скрізь\n",[3699,18120,18121],{"class":3701,"line":3798},[3699,18122,18123],{"class":4422},"AdminOnly = Depends(require_roles(UserRole.ADMIN))\n",[3699,18125,18126],{"class":3701,"line":3804},[3699,18127,3730],{"emptyLinePlaceholder":3729},[3699,18129,18130],{"class":3701,"line":3810},[3699,18131,3730],{"emptyLinePlaceholder":3729},[3699,18133,18134],{"class":3701,"line":3816},[3699,18135,18136],{"class":4412},"# ─── Pydantic схеми ───────────────────────────────────────────────────────────\n",[3699,18138,18139,18141,18144,18146,18148],{"class":3701,"line":3822},[3699,18140,9008],{"class":4435},[3699,18142,18143],{"class":4452}," UserListResponse",[3699,18145,4442],{"class":4422},[3699,18147,13704],{"class":4452},[3699,18149,9019],{"class":4422},[3699,18151,18152,18154],{"class":3701,"line":3827},[3699,18153,13094],{"class":4321},[3699,18155,18156],{"class":4422},": uuid.UUID\n",[3699,18158,18159],{"class":3701,"line":3832},[3699,18160,13711],{"class":4422},[3699,18162,18163,18165],{"class":3701,"line":3837},[3699,18164,13716],{"class":4422},[3699,18166,12041],{"class":4452},[3699,18168,18169],{"class":3701,"line":3843},[3699,18170,18171],{"class":4422},"    role: UserRole\n",[3699,18173,18174,18177],{"class":3701,"line":4590},[3699,18175,18176],{"class":4422},"    is_active: ",[3699,18178,18179],{"class":4452},"bool\n",[3699,18181,18182],{"class":3701,"line":4595},[3699,18183,3730],{"emptyLinePlaceholder":3729},[3699,18185,18186,18189,18192,18194,18196],{"class":3701,"line":4627},[3699,18187,18188],{"class":4422},"    model_config = {",[3699,18190,18191],{"class":4324},"\"from_attributes\"",[3699,18193,4449],{"class":4422},[3699,18195,9303],{"class":4435},[3699,18197,3778],{"class":4422},[3699,18199,18200],{"class":3701,"line":4632},[3699,18201,3730],{"emptyLinePlaceholder":3729},[3699,18203,18204],{"class":3701,"line":4638},[3699,18205,3730],{"emptyLinePlaceholder":3729},[3699,18207,18208,18210,18213,18215,18217],{"class":3701,"line":4643},[3699,18209,9008],{"class":4435},[3699,18211,18212],{"class":4452}," UserRoleUpdateRequest",[3699,18214,4442],{"class":4422},[3699,18216,13704],{"class":4452},[3699,18218,9019],{"class":4422},[3699,18220,18221],{"class":3701,"line":4648},[3699,18222,18171],{"class":4422},[3699,18224,18225],{"class":3701,"line":4654},[3699,18226,3730],{"emptyLinePlaceholder":3729},[3699,18228,18229],{"class":3701,"line":4660},[3699,18230,3730],{"emptyLinePlaceholder":3729},[3699,18232,18233],{"class":3701,"line":4665},[3699,18234,18235],{"class":4412},"# ─── Ендпоінти ────────────────────────────────────────────────────────────────\n",[3699,18237,18238,18240,18242,18245,18247,18249],{"class":3701,"line":4670},[3699,18239,10291],{"class":4321},[3699,18241,4442],{"class":4422},[3699,18243,18244],{"class":4324},"\"\u002Fusers\"",[3699,18246,4200],{"class":4422},[3699,18248,10679],{"class":4445},[3699,18250,18251],{"class":4422},"=list[UserListResponse])\n",[3699,18253,18254,18256,18258,18261],{"class":3701,"line":4676},[3699,18255,9702],{"class":4435},[3699,18257,9705],{"class":4435},[3699,18259,18260],{"class":4321}," list_all_users",[3699,18262,8268],{"class":4422},[3699,18264,18265,18267],{"class":3701,"line":4681},[3699,18266,10105],{"class":4445},[3699,18268,18269],{"class":4422},": User = AdminOnly,\n",[3699,18271,18272,18274],{"class":3701,"line":4694},[3699,18273,9727],{"class":4445},[3699,18275,9730],{"class":4422},[3699,18277,18278],{"class":3701,"line":4704},[3699,18279,18280],{"class":4422},") -> list[User]:\n",[3699,18282,18283],{"class":3701,"line":4709},[3699,18284,18285],{"class":4324},"    \"\"\"Повертає список усіх користувачів. Тільки для адміністраторів.\"\"\"\n",[3699,18287,18288,18291,18293],{"class":3701,"line":6342},[3699,18289,18290],{"class":4422},"    result = ",[3699,18292,10044],{"class":4418},[3699,18294,18295],{"class":4422}," db.execute(select(User).order_by(User.email))\n",[3699,18297,18298,18300,18303],{"class":3701,"line":6348},[3699,18299,4576],{"class":4418},[3699,18301,18302],{"class":4452}," list",[3699,18304,18305],{"class":4422},"(result.scalars().all())\n",[3699,18307,18308],{"class":3701,"line":6354},[3699,18309,3730],{"emptyLinePlaceholder":3729},[3699,18311,18312],{"class":3701,"line":6359},[3699,18313,3730],{"emptyLinePlaceholder":3729},[3699,18315,18316,18319,18321,18324,18327,18330,18332,18334],{"class":3701,"line":6365},[3699,18317,18318],{"class":4321},"@router.patch",[3699,18320,4442],{"class":4422},[3699,18322,18323],{"class":4324},"\"\u002Fusers\u002F",[3699,18325,18326],{"class":4435},"{user_id}",[3699,18328,18329],{"class":4324},"\u002Frole\"",[3699,18331,4200],{"class":4422},[3699,18333,10679],{"class":4445},[3699,18335,18336],{"class":4422},"=UserListResponse)\n",[3699,18338,18339,18341,18343,18346],{"class":3701,"line":6371},[3699,18340,9702],{"class":4435},[3699,18342,9705],{"class":4435},[3699,18344,18345],{"class":4321}," update_user_role",[3699,18347,8268],{"class":4422},[3699,18349,18350,18352],{"class":3701,"line":6377},[3699,18351,15141],{"class":4445},[3699,18353,18354],{"class":4422},": uuid.UUID,\n",[3699,18356,18357,18359],{"class":3701,"line":6382},[3699,18358,11329],{"class":4445},[3699,18360,18361],{"class":4422},": UserRoleUpdateRequest,\n",[3699,18363,18364,18366],{"class":3701,"line":6387},[3699,18365,10105],{"class":4445},[3699,18367,18269],{"class":4422},[3699,18369,18370,18372],{"class":3701,"line":6392},[3699,18371,9727],{"class":4445},[3699,18373,9730],{"class":4422},[3699,18375,18376],{"class":3701,"line":6398},[3699,18377,9735],{"class":4422},[3699,18379,18380],{"class":3701,"line":6404},[3699,18381,18382],{"class":4324},"    \"\"\"Змінює роль. Адмін не може змінити власну роль.\"\"\"\n",[3699,18384,18385,18388,18390],{"class":3701,"line":6409},[3699,18386,18387],{"class":4422},"    target = ",[3699,18389,10044],{"class":4418},[3699,18391,18392],{"class":4422}," db.get(User, user_id)\n",[3699,18394,18395,18397,18400,18402,18404],{"class":3701,"line":8467},[3699,18396,5988],{"class":4418},[3699,18398,18399],{"class":4422}," target ",[3699,18401,9945],{"class":4435},[3699,18403,9948],{"class":4435},[3699,18405,4460],{"class":4422},[3699,18407,18408,18410,18412,18414,18417,18419,18421,18424],{"class":3701,"line":8472},[3699,18409,9972],{"class":4418},[3699,18411,7981],{"class":4422},[3699,18413,7984],{"class":4445},[3699,18415,18416],{"class":4422},"=status.HTTP_404_NOT_FOUND, ",[3699,18418,7994],{"class":4445},[3699,18420,4556],{"class":4422},[3699,18422,18423],{"class":4324},"\"User not found\"",[3699,18425,4562],{"class":4422},[3699,18427,18428,18430],{"class":3701,"line":8494},[3699,18429,5988],{"class":4418},[3699,18431,18432],{"class":4422}," target.id == current_user.id:\n",[3699,18434,18435,18437],{"class":3701,"line":8499},[3699,18436,9972],{"class":4418},[3699,18438,9975],{"class":4422},[3699,18440,18441,18443],{"class":3701,"line":8505},[3699,18442,9980],{"class":4445},[3699,18444,18445],{"class":4422},"=status.HTTP_400_BAD_REQUEST,\n",[3699,18447,18448,18450,18452,18455],{"class":3701,"line":8510},[3699,18449,9987],{"class":4445},[3699,18451,4556],{"class":4422},[3699,18453,18454],{"class":4324},"\"Cannot change your own role\"",[3699,18456,7077],{"class":4422},[3699,18458,18459],{"class":3701,"line":8515},[3699,18460,10015],{"class":4422},[3699,18462,18463],{"class":3701,"line":8521},[3699,18464,18465],{"class":4422},"    target.role = body.role\n",[3699,18467,18468,18470],{"class":3701,"line":8526},[3699,18469,4966],{"class":4418},[3699,18471,18472],{"class":4422}," db.commit()\n",[3699,18474,18475,18477],{"class":3701,"line":8531},[3699,18476,4966],{"class":4418},[3699,18478,18479],{"class":4422}," db.refresh(target)\n",[3699,18481,18482,18484],{"class":3701,"line":8537},[3699,18483,4576],{"class":4418},[3699,18485,18486],{"class":4422}," target\n",[3699,18488,18489],{"class":3701,"line":8542},[3699,18490,3730],{"emptyLinePlaceholder":3729},[3699,18492,18493],{"class":3701,"line":8548},[3699,18494,3730],{"emptyLinePlaceholder":3729},[3699,18496,18497,18499,18501,18503,18505,18508,18510,18512],{"class":3701,"line":8554},[3699,18498,18318],{"class":4321},[3699,18500,4442],{"class":4422},[3699,18502,18323],{"class":4324},[3699,18504,18326],{"class":4435},[3699,18506,18507],{"class":4324},"\u002Fdeactivate\"",[3699,18509,4200],{"class":4422},[3699,18511,7984],{"class":4445},[3699,18513,11651],{"class":4422},[3699,18515,18516,18518,18520,18523],{"class":3701,"line":8560},[3699,18517,9702],{"class":4435},[3699,18519,9705],{"class":4435},[3699,18521,18522],{"class":4321}," deactivate_user",[3699,18524,8268],{"class":4422},[3699,18526,18527,18529],{"class":3701,"line":8565},[3699,18528,15141],{"class":4445},[3699,18530,18354],{"class":4422},[3699,18532,18533,18535],{"class":3701,"line":8573},[3699,18534,10105],{"class":4445},[3699,18536,18269],{"class":4422},[3699,18538,18539,18541],{"class":3701,"line":8579},[3699,18540,9727],{"class":4445},[3699,18542,9730],{"class":4422},[3699,18544,18545,18547,18549],{"class":3701,"line":8585},[3699,18546,4455],{"class":4422},[3699,18548,4938],{"class":4435},[3699,18550,4460],{"class":4422},[3699,18552,18553],{"class":3701,"line":8593},[3699,18554,18555],{"class":4324},"    \"\"\"Деактивує акаунт. Адмін не може деактивувати себе.\"\"\"\n",[3699,18557,18558,18560,18562],{"class":3701,"line":8598},[3699,18559,18387],{"class":4422},[3699,18561,10044],{"class":4418},[3699,18563,18392],{"class":4422},[3699,18565,18566,18568,18570,18572,18574],{"class":3701,"line":8603},[3699,18567,5988],{"class":4418},[3699,18569,18399],{"class":4422},[3699,18571,9945],{"class":4435},[3699,18573,9948],{"class":4435},[3699,18575,4460],{"class":4422},[3699,18577,18578,18580,18582,18584,18586,18588,18590,18592],{"class":3701,"line":8608},[3699,18579,9972],{"class":4418},[3699,18581,7981],{"class":4422},[3699,18583,7984],{"class":4445},[3699,18585,18416],{"class":4422},[3699,18587,7994],{"class":4445},[3699,18589,4556],{"class":4422},[3699,18591,18423],{"class":4324},[3699,18593,4562],{"class":4422},[3699,18595,18596,18598],{"class":3701,"line":8614},[3699,18597,5988],{"class":4418},[3699,18599,18432],{"class":4422},[3699,18601,18602,18604],{"class":3701,"line":8629},[3699,18603,9972],{"class":4418},[3699,18605,9975],{"class":4422},[3699,18607,18608,18610],{"class":3701,"line":8634},[3699,18609,9980],{"class":4445},[3699,18611,18445],{"class":4422},[3699,18613,18614,18616,18618,18621],{"class":3701,"line":8640},[3699,18615,9987],{"class":4445},[3699,18617,4556],{"class":4422},[3699,18619,18620],{"class":4324},"\"Cannot deactivate your own account\"",[3699,18622,7077],{"class":4422},[3699,18624,18625],{"class":3701,"line":8645},[3699,18626,10015],{"class":4422},[3699,18628,18629,18632],{"class":3701,"line":8651},[3699,18630,18631],{"class":4422},"    target.is_active = ",[3699,18633,18634],{"class":4435},"False\n",[3699,18636,18637,18639],{"class":3701,"line":8657},[3699,18638,4966],{"class":4418},[3699,18640,18472],{"class":4422},[3699,18642,18643],{"class":3701,"line":8663},[3699,18644,3730],{"emptyLinePlaceholder":3729},[3699,18646,18647],{"class":3701,"line":8668},[3699,18648,3730],{"emptyLinePlaceholder":3729},[3699,18650,18651,18653,18655,18658],{"class":3701,"line":8673},[3699,18652,10291],{"class":4321},[3699,18654,4442],{"class":4422},[3699,18656,18657],{"class":4324},"\"\u002Freports\u002Fsummary\"",[3699,18659,4562],{"class":4422},[3699,18661,18662,18664,18666,18669],{"class":3701,"line":8679},[3699,18663,9702],{"class":4435},[3699,18665,9705],{"class":4435},[3699,18667,18668],{"class":4321}," reports_summary",[3699,18670,8268],{"class":4422},[3699,18672,18673],{"class":3701,"line":8685},[3699,18674,18675],{"class":4412},"    # Менеджери та адміністратори мають доступ до звітів\n",[3699,18677,18678,18680],{"class":3701,"line":8690},[3699,18679,10105],{"class":4445},[3699,18681,18682],{"class":4422},": User = Depends(require_roles(UserRole.ADMIN, UserRole.MANAGER)),\n",[3699,18684,18685,18687],{"class":3701,"line":8702},[3699,18686,9727],{"class":4445},[3699,18688,9730],{"class":4422},[3699,18690,18691,18693,18695],{"class":3701,"line":8707},[3699,18692,4455],{"class":4422},[3699,18694,7621],{"class":4452},[3699,18696,4460],{"class":4422},[3699,18698,18699],{"class":3701,"line":8712},[3699,18700,18701],{"class":4324},"    \"\"\"Зведений звіт. Доступний менеджерам та адміністраторам.\"\"\"\n",[3699,18703,18704,18706,18708],{"class":3701,"line":8734},[3699,18705,18290],{"class":4422},[3699,18707,10044],{"class":4418},[3699,18709,18710],{"class":4422}," db.execute(select(User))\n",[3699,18712,18713],{"class":3701,"line":8739},[3699,18714,18715],{"class":4422},"    users = result.scalars().all()\n",[3699,18717,18718,18720],{"class":3701,"line":8745},[3699,18719,4576],{"class":4418},[3699,18721,18722],{"class":4422}," {\n",[3699,18724,18725,18728,18730,18733],{"class":3701,"line":8750},[3699,18726,18727],{"class":4324},"        \"total_users\"",[3699,18729,4449],{"class":4422},[3699,18731,18732],{"class":4321},"len",[3699,18734,18735],{"class":4422},"(users),\n",[3699,18737,18738,18741,18743,18746,18748,18751,18754,18757,18759,18762,18764],{"class":3701,"line":8756},[3699,18739,18740],{"class":4324},"        \"active_users\"",[3699,18742,4449],{"class":4422},[3699,18744,18745],{"class":4321},"sum",[3699,18747,4442],{"class":4422},[3699,18749,18750],{"class":4531},"1",[3699,18752,18753],{"class":4418}," for",[3699,18755,18756],{"class":4422}," u ",[3699,18758,14438],{"class":4418},[3699,18760,18761],{"class":4422}," users ",[3699,18763,4953],{"class":4418},[3699,18765,18766],{"class":4422}," u.is_active),\n",[3699,18768,18769,18772,18774,18776,18778,18780,18782,18784,18786,18788,18790],{"class":3701,"line":8762},[3699,18770,18771],{"class":4324},"        \"admins\"",[3699,18773,4449],{"class":4422},[3699,18775,18745],{"class":4321},[3699,18777,4442],{"class":4422},[3699,18779,18750],{"class":4531},[3699,18781,18753],{"class":4418},[3699,18783,18756],{"class":4422},[3699,18785,14438],{"class":4418},[3699,18787,18761],{"class":4422},[3699,18789,4953],{"class":4418},[3699,18791,18792],{"class":4422}," u.role == UserRole.ADMIN),\n",[3699,18794,18795,18798,18800,18802,18804,18806,18808,18810,18812,18814,18816],{"class":3701,"line":8768},[3699,18796,18797],{"class":4324},"        \"managers\"",[3699,18799,4449],{"class":4422},[3699,18801,18745],{"class":4321},[3699,18803,4442],{"class":4422},[3699,18805,18750],{"class":4531},[3699,18807,18753],{"class":4418},[3699,18809,18756],{"class":4422},[3699,18811,14438],{"class":4418},[3699,18813,18761],{"class":4422},[3699,18815,4953],{"class":4418},[3699,18817,18818],{"class":4422}," u.role == UserRole.MANAGER),\n",[3699,18820,18821,18824],{"class":3701,"line":8774},[3699,18822,18823],{"class":4324},"        \"requested_by\"",[3699,18825,18826],{"class":4422},": current_user.email,\n",[3699,18828,18829],{"class":3701,"line":8780},[3699,18830,8432],{"class":4422},[3556,18832,18833,18834,4400],{},"Реєструємо роутер у ",[3625,18835,18836],{},"main.py",[3690,18838,18840],{"className":4403,"code":18839,"language":4405,"meta":3695,"style":3695},"# app\u002Fmain.py\nfrom fastapi import FastAPI\n\nfrom app.api.v1 import admin, auth\n\napp = FastAPI(\n    title=\"TaskForge API\",\n    description=\"Project management API — JWT + Opaque Refresh Token + RBAC\",\n    version=\"0.1.0\",\n)\n\napp.include_router(auth.router, prefix=\"\u002Fapi\u002Fv1\")\napp.include_router(admin.router, prefix=\"\u002Fapi\u002Fv1\")\n\n\n@app.get(\"\u002Fhealth\", tags=[\"System\"])\nasync def health_check() -> dict:\n    return {\"status\": \"ok\"}\n",[3625,18841,18842,18847,18857,18861,18872,18876,18880,18890,18901,18911,18915,18919,18931,18944,18948,18952,18970,18984],{"__ignoreMap":3695},[3699,18843,18844],{"class":3701,"line":3702},[3699,18845,18846],{"class":4412},"# app\u002Fmain.py\n",[3699,18848,18849,18851,18853,18855],{"class":3701,"line":3708},[3699,18850,5828],{"class":4418},[3699,18852,9576],{"class":4422},[3699,18854,4419],{"class":4418},[3699,18856,16102],{"class":4422},[3699,18858,18859],{"class":3701,"line":3714},[3699,18860,3730],{"emptyLinePlaceholder":3729},[3699,18862,18863,18865,18867,18869],{"class":3701,"line":3720},[3699,18864,5828],{"class":4418},[3699,18866,16113],{"class":4422},[3699,18868,4419],{"class":4418},[3699,18870,18871],{"class":4422}," admin, auth\n",[3699,18873,18874],{"class":3701,"line":3726},[3699,18875,3730],{"emptyLinePlaceholder":3729},[3699,18877,18878],{"class":3701,"line":3733},[3699,18879,16127],{"class":4422},[3699,18881,18882,18884,18886,18888],{"class":3701,"line":3739},[3699,18883,16132],{"class":4445},[3699,18885,4556],{"class":4422},[3699,18887,16137],{"class":4324},[3699,18889,7077],{"class":4422},[3699,18891,18892,18894,18896,18899],{"class":3701,"line":3745},[3699,18893,16144],{"class":4445},[3699,18895,4556],{"class":4422},[3699,18897,18898],{"class":4324},"\"Project management API — JWT + Opaque Refresh Token + RBAC\"",[3699,18900,7077],{"class":4422},[3699,18902,18903,18905,18907,18909],{"class":3701,"line":3751},[3699,18904,16156],{"class":4445},[3699,18906,4556],{"class":4422},[3699,18908,16161],{"class":4324},[3699,18910,7077],{"class":4422},[3699,18912,18913],{"class":3701,"line":3757},[3699,18914,4562],{"class":4422},[3699,18916,18917],{"class":3701,"line":3763},[3699,18918,3730],{"emptyLinePlaceholder":3729},[3699,18920,18921,18923,18925,18927,18929],{"class":3701,"line":3769},[3699,18922,16176],{"class":4422},[3699,18924,10639],{"class":4445},[3699,18926,4556],{"class":4422},[3699,18928,16183],{"class":4324},[3699,18930,4562],{"class":4422},[3699,18932,18933,18936,18938,18940,18942],{"class":3701,"line":3775},[3699,18934,18935],{"class":4422},"app.include_router(admin.router, ",[3699,18937,10639],{"class":4445},[3699,18939,4556],{"class":4422},[3699,18941,16183],{"class":4324},[3699,18943,4562],{"class":4422},[3699,18945,18946],{"class":3701,"line":3781},[3699,18947,3730],{"emptyLinePlaceholder":3729},[3699,18949,18950],{"class":3701,"line":3786},[3699,18951,3730],{"emptyLinePlaceholder":3729},[3699,18953,18954,18956,18958,18960,18962,18964,18966,18968],{"class":3701,"line":3792},[3699,18955,16198],{"class":4321},[3699,18957,4442],{"class":4422},[3699,18959,16203],{"class":4324},[3699,18961,4200],{"class":4422},[3699,18963,10649],{"class":4445},[3699,18965,7938],{"class":4422},[3699,18967,16212],{"class":4324},[3699,18969,10657],{"class":4422},[3699,18971,18972,18974,18976,18978,18980,18982],{"class":3701,"line":3798},[3699,18973,9702],{"class":4435},[3699,18975,9705],{"class":4435},[3699,18977,16223],{"class":4321},[3699,18979,8622],{"class":4422},[3699,18981,7621],{"class":4452},[3699,18983,4460],{"class":4422},[3699,18985,18986,18988,18990,18992,18994,18996],{"class":3701,"line":3804},[3699,18987,4576],{"class":4418},[3699,18989,16236],{"class":4422},[3699,18991,16239],{"class":4324},[3699,18993,4449],{"class":4422},[3699,18995,16244],{"class":4324},[3699,18997,3778],{"class":4422},[4716,18999,19000,19004],{},[3556,19001,19002],{},[3560,19003,9142],{},[3556,19005,19006,19007,5458,19010,19013,19014,19017,19018,19021,19022,19024,19025,19028],{},"В ASP.NET перевірка ролей — атрибут: ",[3625,19008,19009],{},"[Authorize(Roles = \"Admin\")]",[3625,19011,19012],{},"[Authorize(Roles = \"Admin,Manager\")]",". У FastAPI — ",[3625,19015,19016],{},"Depends(require_roles(UserRole.ADMIN))",". Концептуально ідентично, але FastAPI-підхід явніший: залежність видима прямо у сигнатурі функції, її легко замінити у тестах через ",[3625,19019,19020],{},"app.dependency_overrides",", і вона повністю типізована. ASP.NET ",[3625,19023,3627],{}," — декларативна магія через reflection. FastAPI ",[3625,19026,19027],{},"Depends"," — явна функціональна композиція.",[3661,19030,19032],{"id":19031},"включення-ролі-у-jwt-токен","Включення ролі у JWT-токен",[3556,19034,19035,19036,19039,19040,19043,19044,4733,19047,19050],{},"Оптимізація: замість завантаження User з БД лише для перевірки ролі — вбудовуємо роль прямо у Access JWT. Тоді при перевірці ролі БД не потрібна взагалі. Оновлюємо виклики ",[3625,19037,19038],{},"create_access_token"," у ",[3625,19041,19042],{},"auth.py"," (у функціях ",[3625,19045,19046],{},"register",[3625,19048,19049],{},"login","):",[3690,19052,19054],{"className":4403,"code":19053,"language":4405,"meta":3695,"style":3695},"# У register() та login() — додаємо роль у extra_claims:\naccess_token = create_access_token(\n    subject=str(user.id),\n    extra_claims={\"role\": user.role.value},  # \"user\" \u002F \"manager\" \u002F \"admin\"\n)\n",[3625,19055,19056,19061,19066,19077,19092],{"__ignoreMap":3695},[3699,19057,19058],{"class":3701,"line":3702},[3699,19059,19060],{"class":4412},"# У register() та login() — додаємо роль у extra_claims:\n",[3699,19062,19063],{"class":3701,"line":3708},[3699,19064,19065],{"class":4422},"access_token = create_access_token(\n",[3699,19067,19068,19070,19072,19074],{"class":3701,"line":3714},[3699,19069,8273],{"class":4445},[3699,19071,4556],{"class":4422},[3699,19073,4244],{"class":4452},[3699,19075,19076],{"class":4422},"(user.id),\n",[3699,19078,19079,19081,19083,19086,19089],{"class":3701,"line":3720},[3699,19080,8285],{"class":4445},[3699,19082,9844],{"class":4422},[3699,19084,19085],{"class":4324},"\"role\"",[3699,19087,19088],{"class":4422},": user.role.value},  ",[3699,19090,19091],{"class":4412},"# \"user\" \u002F \"manager\" \u002F \"admin\"\n",[3699,19093,19094],{"class":3701,"line":3726},[3699,19095,4562],{"class":4422},[4194,19097,19098,19103],{},[3556,19099,19100],{},[3560,19101,19102],{},"Роль у токені vs актуальний стан у БД.",[3556,19104,19105,19106,6428,19109,19111,19112,19115,19116,19118,19119,19122,19123,19126],{},"Якщо адміністратор змінив роль користувача з ",[3625,19107,19108],{},"admin",[3625,19110,14070],{},", але той вже має живий access token з ",[3625,19113,19114],{},"\"role\": \"admin\""," — він матиме адмін-права ще до 15 хвилин (до закінчення ",[3625,19117,6521],{},"). Це прийнятний trade-off для коротких токенів. Якщо ваша система потребує ",[3560,19120,19121],{},"миттєвого"," відкликання ролей — завантажуйте User з БД при кожному запиті через ",[3625,19124,19125],{},"get_current_user",", а не читайте роль з токена.",[3661,19128,19130],{"id":19129},"матриця-доступу-taskforge","Матриця доступу TaskForge",[3862,19132,19133,19154],{},[3865,19134,19135],{},[3868,19136,19137,19140,19145,19150],{},[3871,19138,19139],{},"Ресурс \u002F Дія",[3871,19141,19143],{"align":19142},"center",[3625,19144,14070],{},[3871,19146,19147],{"align":19142},[3625,19148,19149],{},"manager",[3871,19151,19152],{"align":19142},[3625,19153,19108],{},[3896,19155,19156,19167,19178,19189,19200,19211,19222],{},[3868,19157,19158,19161,19163,19165],{},[3901,19159,19160],{},"Читати\u002Fредагувати свої проєкти",[3901,19162,4024],{"align":19142},[3901,19164,4024],{"align":19142},[3901,19166,4024],{"align":19142},[3868,19168,19169,19172,19174,19176],{},[3901,19170,19171],{},"Читати проєкти команди",[3901,19173,3920],{"align":19142},[3901,19175,4024],{"align":19142},[3901,19177,4024],{"align":19142},[3868,19179,19180,19183,19185,19187],{},[3901,19181,19182],{},"Управляти задачами команди",[3901,19184,3920],{"align":19142},[3901,19186,4024],{"align":19142},[3901,19188,4024],{"align":19142},[3868,19190,19191,19194,19196,19198],{},[3901,19192,19193],{},"Переглядати звіти",[3901,19195,3920],{"align":19142},[3901,19197,4024],{"align":19142},[3901,19199,4024],{"align":19142},[3868,19201,19202,19205,19207,19209],{},[3901,19203,19204],{},"Переглядати всіх користувачів",[3901,19206,3920],{"align":19142},[3901,19208,3920],{"align":19142},[3901,19210,4024],{"align":19142},[3868,19212,19213,19216,19218,19220],{},[3901,19214,19215],{},"Змінювати ролі користувачів",[3901,19217,3920],{"align":19142},[3901,19219,3920],{"align":19142},[3901,19221,4024],{"align":19142},[3868,19223,19224,19227,19229,19231],{},[3901,19225,19226],{},"Деактивувати акаунти",[3901,19228,3920],{"align":19142},[3901,19230,3920],{"align":19142},[3901,19232,4024],{"align":19142},[3654,19234],{},[3551,19236,19238],{"id":19237},"частина-5-api-key-автентифікація-для-сервіс-до-сервісу","Частина 5. API Key — автентифікація для сервіс-до-сервісу",[3661,19240,19242],{"id":19241},"навіщо-потрібна-api-key-автентифікація","Навіщо потрібна API Key автентифікація",[3556,19244,19245,19246,19249],{},"JWT чудово підходить для автентифікації кінцевих користувачів через браузер чи мобайл. Але що, якщо до вашого API звертається ",[3560,19247,19248],{},"інший сервіс"," — CI\u002FCD пайплайн, скрипт моніторингу, зовнішній партнер? Такі клієнти не «логіняться» через форму — вони мають довгоживучий ідентифікатор, який передають у кожному запиті.",[3556,19251,19252,19255],{},[3560,19253,19254],{},"API Key"," — це довгий випадковий рядок (token), що ідентифікує клієнта. На відміну від JWT, він:",[6909,19257,19258,19261,19264],{},[4759,19259,19260],{},"Не має строку закінчення дії (відкликається явно через БД)",[4759,19262,19263],{},"Не містить вбудованих даних — лише lookup key",[4759,19265,19266,19267,19270],{},"Може мати обмежений scope (наприклад, ",[3625,19268,19269],{},"read:projects"," без права запису)",[3661,19272,19274],{"id":19273},"два-способи-передачі-api-key","Два способи передачі API Key",[4238,19276,19277,19318],{},[4241,19278,19281,19284],{"name":19279,"type":19280},"HTTP Header (рекомендовано)","X-API-Key: \u003Ckey>",[3556,19282,19283],{},"Стандартний підхід. Заголовок не потрапляє у логи сервера (тіло запиту та заголовки часто фільтруются), не кешується браузером і проксі.",[3690,19285,19289],{"className":19286,"code":19287,"language":19288,"meta":3695,"style":3695},"language-http shiki shiki-themes light-plus dark-plus dark-plus","GET \u002Fapi\u002Fv1\u002Fprojects HTTP\u002F1.1\nX-API-Key: tf_live_bX3R9qKm7Vn2pL8sA1jD5cW4uY6oE0tF\n","http",[3625,19290,19291,19307],{"__ignoreMap":3695},[3699,19292,19293,19296,19299,19302,19304],{"class":3701,"line":3702},[3699,19294,19295],{"class":4418},"GET",[3699,19297,19298],{"class":4422}," \u002Fapi\u002Fv1\u002Fprojects ",[3699,19300,19301],{"class":4435},"HTTP",[3699,19303,6427],{"class":4422},[3699,19305,19306],{"class":4531},"1.1\n",[3699,19308,19309,19313,19315],{"class":3701,"line":3708},[3699,19310,19312],{"class":19311},"sKtos","X-API-Key",[3699,19314,4400],{"class":4435},[3699,19316,19317],{"class":4324}," tf_live_bX3R9qKm7Vn2pL8sA1jD5cW4uY6oE0tF\n",[4241,19319,19322,19325],{"name":19320,"type":19321},"Query Parameter (не рекомендовано)","?api_key=\u003Ckey>",[3556,19323,19324],{},"Простіший для тестування у браузері, але небезпечний: URL потрапляє у логи сервера, браузерну історію, Referer-заголовки.",[3690,19326,19328],{"className":19286,"code":19327,"language":19288,"meta":3695,"style":3695},"GET \u002Fapi\u002Fv1\u002Fprojects?api_key=tf_live_bX3R9qKm7Vn2pL8sA1jD5cW4uY6oE0tF\n",[3625,19329,19330],{"__ignoreMap":3695},[3699,19331,19332,19334],{"class":3701,"line":3702},[3699,19333,19295],{"class":4418},[3699,19335,19336],{"class":4422}," \u002Fapi\u002Fv1\u002Fprojects?api_key=tf_live_bX3R9qKm7Vn2pL8sA1jD5cW4uY6oE0tF\n",[3661,19338,19340],{"id":19339},"реалізація-api-key-у-fastapi","Реалізація API Key у FastAPI",[3556,19342,19343],{},"Модель та репозиторій API Key:",[3690,19345,19347],{"className":4403,"code":19346,"language":4405,"meta":3695,"style":3695},"# app\u002Fmodels\u002Fapi_key.py\nimport uuid\nfrom datetime import datetime\n\nfrom sqlalchemy import Boolean, DateTime, ForeignKey, String, Text\nfrom sqlalchemy.dialects.postgresql import UUID\nfrom sqlalchemy.orm import Mapped, mapped_column, relationship\n\nfrom app.models.base import Base\n\n\nclass ApiKey(Base):\n    __tablename__ = \"api_keys\"\n\n    id: Mapped[uuid.UUID] = mapped_column(\n        UUID(as_uuid=True), primary_key=True, default=uuid.uuid4\n    )\n    # SHA-256 хеш API Key — лише для lookup, не зберігаємо відкрито\n    key_hash: Mapped[str] = mapped_column(String(64), unique=True, nullable=False, index=True)\n    # Prefix для ідентифікації у логах (перші 8 символів ключа): \"tf_live_bX3R9qKm...\"\n    key_prefix: Mapped[str] = mapped_column(String(16), nullable=False)\n\n    name: Mapped[str] = mapped_column(String(100), nullable=False)  # опис: \"CI\u002FCD pipeline\"\n    user_id: Mapped[uuid.UUID] = mapped_column(\n        UUID(as_uuid=True), ForeignKey(\"users.id\", ondelete=\"CASCADE\"), nullable=False\n    )\n    is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)\n    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)\n    last_used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)\n\n    user: Mapped[\"User\"] = relationship(\"User\")  # type: ignore[name-defined]\n",[3625,19348,19349,19354,19360,19370,19374,19385,19395,19405,19409,19419,19423,19427,19440,19447,19451,19457,19481,19485,19490,19527,19532,19553,19557,19581,19585,19616,19620,19644,19664,19690,19694],{"__ignoreMap":3695},[3699,19350,19351],{"class":3701,"line":3702},[3699,19352,19353],{"class":4412},"# app\u002Fmodels\u002Fapi_key.py\n",[3699,19355,19356,19358],{"class":3701,"line":3708},[3699,19357,4419],{"class":4418},[3699,19359,13000],{"class":4422},[3699,19361,19362,19364,19366,19368],{"class":3701,"line":3714},[3699,19363,5828],{"class":4418},[3699,19365,7678],{"class":4422},[3699,19367,4419],{"class":4418},[3699,19369,13317],{"class":4422},[3699,19371,19372],{"class":3701,"line":3720},[3699,19373,3730],{"emptyLinePlaceholder":3729},[3699,19375,19376,19378,19380,19382],{"class":3701,"line":3726},[3699,19377,5828],{"class":4418},[3699,19379,13011],{"class":4422},[3699,19381,4419],{"class":4418},[3699,19383,19384],{"class":4422}," Boolean, DateTime, ForeignKey, String, Text\n",[3699,19386,19387,19389,19391,19393],{"class":3701,"line":3733},[3699,19388,5828],{"class":4418},[3699,19390,13023],{"class":4422},[3699,19392,4419],{"class":4418},[3699,19394,13028],{"class":4422},[3699,19396,19397,19399,19401,19403],{"class":3701,"line":3739},[3699,19398,5828],{"class":4418},[3699,19400,12951],{"class":4422},[3699,19402,4419],{"class":4418},[3699,19404,13039],{"class":4422},[3699,19406,19407],{"class":3701,"line":3745},[3699,19408,3730],{"emptyLinePlaceholder":3729},[3699,19410,19411,19413,19415,19417],{"class":3701,"line":3751},[3699,19412,5828],{"class":4418},[3699,19414,13050],{"class":4422},[3699,19416,4419],{"class":4418},[3699,19418,13055],{"class":4422},[3699,19420,19421],{"class":3701,"line":3757},[3699,19422,3730],{"emptyLinePlaceholder":3729},[3699,19424,19425],{"class":3701,"line":3763},[3699,19426,3730],{"emptyLinePlaceholder":3729},[3699,19428,19429,19431,19434,19436,19438],{"class":3701,"line":3769},[3699,19430,9008],{"class":4435},[3699,19432,19433],{"class":4452}," ApiKey",[3699,19435,4442],{"class":4422},[3699,19437,13075],{"class":4452},[3699,19439,9019],{"class":4422},[3699,19441,19442,19444],{"class":3701,"line":3775},[3699,19443,13082],{"class":4422},[3699,19445,19446],{"class":4324},"\"api_keys\"\n",[3699,19448,19449],{"class":3701,"line":3781},[3699,19450,3730],{"emptyLinePlaceholder":3729},[3699,19452,19453,19455],{"class":3701,"line":3786},[3699,19454,13094],{"class":4321},[3699,19456,13097],{"class":4422},[3699,19458,19459,19461,19463,19465,19467,19469,19471,19473,19475,19477,19479],{"class":3701,"line":3792},[3699,19460,13102],{"class":4422},[3699,19462,13105],{"class":4445},[3699,19464,4556],{"class":4422},[3699,19466,9303],{"class":4435},[3699,19468,3648],{"class":4422},[3699,19470,13114],{"class":4445},[3699,19472,4556],{"class":4422},[3699,19474,9303],{"class":4435},[3699,19476,4200],{"class":4422},[3699,19478,13123],{"class":4445},[3699,19480,13126],{"class":4422},[3699,19482,19483],{"class":3701,"line":3798},[3699,19484,7951],{"class":4422},[3699,19486,19487],{"class":3701,"line":3804},[3699,19488,19489],{"class":4412},"    # SHA-256 хеш API Key — лише для lookup, не зберігаємо відкрито\n",[3699,19491,19492,19495,19497,19499,19501,19503,19505,19507,19509,19511,19513,19515,19517,19519,19521,19523,19525],{"class":3701,"line":3810},[3699,19493,19494],{"class":4422},"    key_hash: Mapped[",[3699,19496,4244],{"class":4452},[3699,19498,13140],{"class":4422},[3699,19500,13455],{"class":4531},[3699,19502,3648],{"class":4422},[3699,19504,13148],{"class":4445},[3699,19506,4556],{"class":4422},[3699,19508,9303],{"class":4435},[3699,19510,4200],{"class":4422},[3699,19512,13157],{"class":4445},[3699,19514,4556],{"class":4422},[3699,19516,9310],{"class":4435},[3699,19518,4200],{"class":4422},[3699,19520,13166],{"class":4445},[3699,19522,4556],{"class":4422},[3699,19524,9303],{"class":4435},[3699,19526,4562],{"class":4422},[3699,19528,19529],{"class":3701,"line":3816},[3699,19530,19531],{"class":4412},"    # Prefix для ідентифікації у логах (перші 8 символів ключа): \"tf_live_bX3R9qKm...\"\n",[3699,19533,19534,19537,19539,19541,19543,19545,19547,19549,19551],{"class":3701,"line":3822},[3699,19535,19536],{"class":4422},"    key_prefix: Mapped[",[3699,19538,4244],{"class":4452},[3699,19540,13140],{"class":4422},[3699,19542,5574],{"class":4531},[3699,19544,3648],{"class":4422},[3699,19546,13157],{"class":4445},[3699,19548,4556],{"class":4422},[3699,19550,9310],{"class":4435},[3699,19552,4562],{"class":4422},[3699,19554,19555],{"class":3701,"line":3827},[3699,19556,3730],{"emptyLinePlaceholder":3729},[3699,19558,19559,19562,19564,19566,19568,19570,19572,19574,19576,19578],{"class":3701,"line":3832},[3699,19560,19561],{"class":4422},"    name: Mapped[",[3699,19563,4244],{"class":4452},[3699,19565,13140],{"class":4422},[3699,19567,13184],{"class":4531},[3699,19569,3648],{"class":4422},[3699,19571,13157],{"class":4445},[3699,19573,4556],{"class":4422},[3699,19575,9310],{"class":4435},[3699,19577,5975],{"class":4422},[3699,19579,19580],{"class":4412},"# опис: \"CI\u002FCD pipeline\"\n",[3699,19582,19583],{"class":3701,"line":3837},[3699,19584,13489],{"class":4422},[3699,19586,19587,19589,19591,19593,19595,19598,19600,19602,19604,19606,19608,19610,19612,19614],{"class":3701,"line":3843},[3699,19588,13102],{"class":4422},[3699,19590,13105],{"class":4445},[3699,19592,4556],{"class":4422},[3699,19594,9303],{"class":4435},[3699,19596,19597],{"class":4422},"), ForeignKey(",[3699,19599,13509],{"class":4324},[3699,19601,4200],{"class":4422},[3699,19603,13514],{"class":4445},[3699,19605,4556],{"class":4422},[3699,19607,13519],{"class":4324},[3699,19609,3648],{"class":4422},[3699,19611,13157],{"class":4445},[3699,19613,4556],{"class":4422},[3699,19615,18634],{"class":4435},[3699,19617,19618],{"class":3701,"line":4590},[3699,19619,7951],{"class":4422},[3699,19621,19622,19624,19626,19628,19630,19632,19634,19636,19638,19640,19642],{"class":3701,"line":4595},[3699,19623,13226],{"class":4422},[3699,19625,4622],{"class":4452},[3699,19627,13231],{"class":4422},[3699,19629,13123],{"class":4445},[3699,19631,4556],{"class":4422},[3699,19633,9303],{"class":4435},[3699,19635,4200],{"class":4422},[3699,19637,13157],{"class":4445},[3699,19639,4556],{"class":4422},[3699,19641,9310],{"class":4435},[3699,19643,4562],{"class":4422},[3699,19645,19646,19648,19650,19652,19654,19656,19658,19660,19662],{"class":3701,"line":4627},[3699,19647,13563],{"class":4422},[3699,19649,13544],{"class":4445},[3699,19651,4556],{"class":4422},[3699,19653,9303],{"class":4435},[3699,19655,3648],{"class":4422},[3699,19657,13157],{"class":4445},[3699,19659,4556],{"class":4422},[3699,19661,9310],{"class":4435},[3699,19663,4562],{"class":4422},[3699,19665,19666,19669,19671,19674,19676,19678,19680,19682,19684,19686,19688],{"class":3701,"line":4632},[3699,19667,19668],{"class":4422},"    last_used_at: Mapped[datetime | ",[3699,19670,4938],{"class":4435},[3699,19672,19673],{"class":4422},"] = mapped_column(DateTime(",[3699,19675,13544],{"class":4445},[3699,19677,4556],{"class":4422},[3699,19679,9303],{"class":4435},[3699,19681,3648],{"class":4422},[3699,19683,13157],{"class":4445},[3699,19685,4556],{"class":4422},[3699,19687,9303],{"class":4435},[3699,19689,4562],{"class":4422},[3699,19691,19692],{"class":3701,"line":4638},[3699,19693,3730],{"emptyLinePlaceholder":3729},[3699,19695,19696,19698,19700,19702,19704,19706],{"class":3701,"line":4643},[3699,19697,13645],{"class":4422},[3699,19699,13648],{"class":4324},[3699,19701,13651],{"class":4422},[3699,19703,13648],{"class":4324},[3699,19705,5975],{"class":4422},[3699,19707,13265],{"class":4412},[3690,19709,19711],{"className":4403,"code":19710,"language":4405,"meta":3695,"style":3695},"# app\u002Frepositories\u002Fapi_key.py\nimport hashlib\nimport secrets\nimport uuid\nfrom datetime import datetime, timezone\n\nfrom sqlalchemy import select\nfrom sqlalchemy.ext.asyncio import AsyncSession\n\nfrom app.models.api_key import ApiKey\n\n\ndef generate_api_key(prefix: str = \"tf_live\") -> tuple[str, str]:\n    \"\"\"\n    Генерує API Key та повертає (plain_key, key_hash).\n\n    Формат: {prefix}_{random_urlsafe_32_bytes}\n    Наприклад: tf_live_bX3R9qKm7Vn2pL8sA1jD5cW4uY6oE0tF\n\n    Returns:\n        Кортеж (відкритий ключ для відправки клієнту, SHA-256 хеш для БД).\n    \"\"\"\n    raw = secrets.token_urlsafe(32)\n    plain_key = f\"{prefix}_{raw}\"\n    key_hash = hashlib.sha256(plain_key.encode()).hexdigest()\n    return plain_key, key_hash\n\n\nclass ApiKeyRepository:\n    def __init__(self, db: AsyncSession) -> None:\n        self._db = db\n\n    async def get_by_hash(self, key_hash: str) -> ApiKey | None:\n        result = await self._db.execute(\n            select(ApiKey).where(ApiKey.key_hash == key_hash, ApiKey.is_active == True)\n        )\n        return result.scalar_one_or_none()\n\n    async def create(\n        self,\n        user_id: uuid.UUID,\n        name: str,\n        plain_key: str,\n        key_hash: str,\n    ) -> ApiKey:\n        api_key = ApiKey(\n            key_hash=key_hash,\n            key_prefix=plain_key[:16],\n            name=name,\n            user_id=user_id,\n            is_active=True,\n            created_at=datetime.now(timezone.utc),\n        )\n        self._db.add(api_key)\n        await self._db.commit()\n        await self._db.refresh(api_key)\n        return api_key\n\n    async def update_last_used(self, api_key: ApiKey) -> None:\n        api_key.last_used_at = datetime.now(timezone.utc)\n        await self._db.commit()\n\n    async def revoke(self, api_key_id: uuid.UUID) -> None:\n        key = await self._db.get(ApiKey, api_key_id)\n        if key:\n            key.is_active = False\n            await self._db.commit()\n",[3625,19712,19713,19718,19724,19730,19736,19747,19751,19761,19771,19775,19787,19791,19795,19827,19831,19836,19840,19845,19850,19854,19858,19863,19867,19876,19903,19908,19915,19919,19923,19932,19952,19958,19962,19990,20000,20009,20013,20019,20023,20033,20039,20045,20056,20067,20078,20083,20088,20096,20109,20117,20124,20135,20143,20147,20154,20162,20171,20178,20182,20207,20212,20220,20224,20248,20260,20267,20274],{"__ignoreMap":3695},[3699,19714,19715],{"class":3701,"line":3702},[3699,19716,19717],{"class":4412},"# app\u002Frepositories\u002Fapi_key.py\n",[3699,19719,19720,19722],{"class":3701,"line":3708},[3699,19721,4419],{"class":4418},[3699,19723,8068],{"class":4422},[3699,19725,19726,19728],{"class":3701,"line":3714},[3699,19727,4419],{"class":4418},[3699,19729,6972],{"class":4422},[3699,19731,19732,19734],{"class":3701,"line":3720},[3699,19733,4419],{"class":4418},[3699,19735,13000],{"class":4422},[3699,19737,19738,19740,19742,19744],{"class":3701,"line":3726},[3699,19739,5828],{"class":4418},[3699,19741,7678],{"class":4422},[3699,19743,4419],{"class":4418},[3699,19745,19746],{"class":4422}," datetime, timezone\n",[3699,19748,19749],{"class":3701,"line":3733},[3699,19750,3730],{"emptyLinePlaceholder":3729},[3699,19752,19753,19755,19757,19759],{"class":3701,"line":3739},[3699,19754,5828],{"class":4418},[3699,19756,13011],{"class":4422},[3699,19758,4419],{"class":4418},[3699,19760,13871],{"class":4422},[3699,19762,19763,19765,19767,19769],{"class":3701,"line":3745},[3699,19764,5828],{"class":4418},[3699,19766,9611],{"class":4422},[3699,19768,4419],{"class":4418},[3699,19770,9616],{"class":4422},[3699,19772,19773],{"class":3701,"line":3751},[3699,19774,3730],{"emptyLinePlaceholder":3729},[3699,19776,19777,19779,19782,19784],{"class":3701,"line":3757},[3699,19778,5828],{"class":4418},[3699,19780,19781],{"class":4422}," app.models.api_key ",[3699,19783,4419],{"class":4418},[3699,19785,19786],{"class":4422}," ApiKey\n",[3699,19788,19789],{"class":3701,"line":3763},[3699,19790,3730],{"emptyLinePlaceholder":3729},[3699,19792,19793],{"class":3701,"line":3769},[3699,19794,3730],{"emptyLinePlaceholder":3729},[3699,19796,19797,19799,19802,19804,19806,19808,19810,19812,19815,19818,19820,19822,19824],{"class":3701,"line":3775},[3699,19798,4436],{"class":4435},[3699,19800,19801],{"class":4321}," generate_api_key",[3699,19803,4442],{"class":4422},[3699,19805,10639],{"class":4445},[3699,19807,4449],{"class":4422},[3699,19809,4244],{"class":4452},[3699,19811,8297],{"class":4422},[3699,19813,19814],{"class":4324},"\"tf_live\"",[3699,19816,19817],{"class":4422},") -> tuple[",[3699,19819,4244],{"class":4452},[3699,19821,4200],{"class":4422},[3699,19823,4244],{"class":4452},[3699,19825,19826],{"class":4422},"]:\n",[3699,19828,19829],{"class":3701,"line":3781},[3699,19830,4465],{"class":4324},[3699,19832,19833],{"class":3701,"line":3786},[3699,19834,19835],{"class":4324},"    Генерує API Key та повертає (plain_key, key_hash).\n",[3699,19837,19838],{"class":3701,"line":3792},[3699,19839,3730],{"emptyLinePlaceholder":3729},[3699,19841,19842],{"class":3701,"line":3798},[3699,19843,19844],{"class":4324},"    Формат: {prefix}_{random_urlsafe_32_bytes}\n",[3699,19846,19847],{"class":3701,"line":3804},[3699,19848,19849],{"class":4324},"    Наприклад: tf_live_bX3R9qKm7Vn2pL8sA1jD5cW4uY6oE0tF\n",[3699,19851,19852],{"class":3701,"line":3810},[3699,19853,3730],{"emptyLinePlaceholder":3729},[3699,19855,19856],{"class":3701,"line":3816},[3699,19857,4498],{"class":4324},[3699,19859,19860],{"class":3701,"line":3822},[3699,19861,19862],{"class":4324},"        Кортеж (відкритий ключ для відправки клієнту, SHA-256 хеш для БД).\n",[3699,19864,19865],{"class":3701,"line":3827},[3699,19866,4465],{"class":4324},[3699,19868,19869,19872,19874],{"class":3701,"line":3832},[3699,19870,19871],{"class":4422},"    raw = secrets.token_urlsafe(",[3699,19873,5918],{"class":4531},[3699,19875,4562],{"class":4422},[3699,19877,19878,19881,19883,19885,19887,19889,19891,19893,19895,19898,19900],{"class":3701,"line":3837},[3699,19879,19880],{"class":4422},"    plain_key = ",[3699,19882,17923],{"class":4435},[3699,19884,17949],{"class":4324},[3699,19886,17929],{"class":4435},[3699,19888,10639],{"class":4422},[3699,19890,17946],{"class":4435},[3699,19892,6431],{"class":4324},[3699,19894,17929],{"class":4435},[3699,19896,19897],{"class":4422},"raw",[3699,19899,17946],{"class":4435},[3699,19901,19902],{"class":4324},"\"\n",[3699,19904,19905],{"class":3701,"line":3843},[3699,19906,19907],{"class":4422},"    key_hash = hashlib.sha256(plain_key.encode()).hexdigest()\n",[3699,19909,19910,19912],{"class":3701,"line":4590},[3699,19911,4576],{"class":4418},[3699,19913,19914],{"class":4422}," plain_key, key_hash\n",[3699,19916,19917],{"class":3701,"line":4595},[3699,19918,3730],{"emptyLinePlaceholder":3729},[3699,19920,19921],{"class":3701,"line":4627},[3699,19922,3730],{"emptyLinePlaceholder":3729},[3699,19924,19925,19927,19930],{"class":3701,"line":4632},[3699,19926,9008],{"class":4435},[3699,19928,19929],{"class":4452}," ApiKeyRepository",[3699,19931,4460],{"class":4422},[3699,19933,19934,19936,19938,19940,19942,19944,19946,19948,19950],{"class":3701,"line":4638},[3699,19935,13917],{"class":4435},[3699,19937,13920],{"class":4321},[3699,19939,4442],{"class":4422},[3699,19941,13925],{"class":4445},[3699,19943,4200],{"class":4422},[3699,19945,13930],{"class":4445},[3699,19947,13933],{"class":4422},[3699,19949,4938],{"class":4435},[3699,19951,4460],{"class":4422},[3699,19953,19954,19956],{"class":3701,"line":4643},[3699,19955,13942],{"class":4435},[3699,19957,13945],{"class":4422},[3699,19959,19960],{"class":3701,"line":4648},[3699,19961,3730],{"emptyLinePlaceholder":3729},[3699,19963,19964,19966,19968,19970,19972,19974,19976,19979,19981,19983,19986,19988],{"class":3701,"line":4654},[3699,19965,12918],{"class":4435},[3699,19967,9705],{"class":4435},[3699,19969,14210],{"class":4321},[3699,19971,4442],{"class":4422},[3699,19973,13925],{"class":4445},[3699,19975,4200],{"class":4422},[3699,19977,19978],{"class":4445},"key_hash",[3699,19980,4449],{"class":4422},[3699,19982,4244],{"class":4452},[3699,19984,19985],{"class":4422},") -> ApiKey | ",[3699,19987,4938],{"class":4435},[3699,19989,4460],{"class":4422},[3699,19991,19992,19994,19996,19998],{"class":3701,"line":4660},[3699,19993,13983],{"class":4422},[3699,19995,10044],{"class":4418},[3699,19997,13988],{"class":4435},[3699,19999,14241],{"class":4422},[3699,20001,20002,20005,20007],{"class":3701,"line":4665},[3699,20003,20004],{"class":4422},"            select(ApiKey).where(ApiKey.key_hash == key_hash, ApiKey.is_active == ",[3699,20006,9303],{"class":4435},[3699,20008,4562],{"class":4422},[3699,20010,20011],{"class":3701,"line":4670},[3699,20012,10015],{"class":4422},[3699,20014,20015,20017],{"class":3701,"line":4676},[3699,20016,13996],{"class":4418},[3699,20018,13999],{"class":4422},[3699,20020,20021],{"class":3701,"line":4681},[3699,20022,3730],{"emptyLinePlaceholder":3729},[3699,20024,20025,20027,20029,20031],{"class":3701,"line":4694},[3699,20026,12918],{"class":4435},[3699,20028,9705],{"class":4435},[3699,20030,14061],{"class":4321},[3699,20032,8268],{"class":4422},[3699,20034,20035,20037],{"class":3701,"line":4704},[3699,20036,13942],{"class":4445},[3699,20038,7077],{"class":4422},[3699,20040,20041,20043],{"class":3701,"line":4709},[3699,20042,10902],{"class":4445},[3699,20044,18354],{"class":4422},[3699,20046,20047,20050,20052,20054],{"class":3701,"line":6342},[3699,20048,20049],{"class":4445},"        name",[3699,20051,4449],{"class":4422},[3699,20053,4244],{"class":4452},[3699,20055,7077],{"class":4422},[3699,20057,20058,20061,20063,20065],{"class":3701,"line":6348},[3699,20059,20060],{"class":4445},"        plain_key",[3699,20062,4449],{"class":4422},[3699,20064,4244],{"class":4452},[3699,20066,7077],{"class":4422},[3699,20068,20069,20072,20074,20076],{"class":3701,"line":6354},[3699,20070,20071],{"class":4445},"        key_hash",[3699,20073,4449],{"class":4422},[3699,20075,4244],{"class":4452},[3699,20077,7077],{"class":4422},[3699,20079,20080],{"class":3701,"line":6359},[3699,20081,20082],{"class":4422},"    ) -> ApiKey:\n",[3699,20084,20085],{"class":3701,"line":6365},[3699,20086,20087],{"class":4422},"        api_key = ApiKey(\n",[3699,20089,20090,20093],{"class":3701,"line":6371},[3699,20091,20092],{"class":4445},"            key_hash",[3699,20094,20095],{"class":4422},"=key_hash,\n",[3699,20097,20098,20101,20104,20106],{"class":3701,"line":6377},[3699,20099,20100],{"class":4445},"            key_prefix",[3699,20102,20103],{"class":4422},"=plain_key[:",[3699,20105,5574],{"class":4531},[3699,20107,20108],{"class":4422},"],\n",[3699,20110,20111,20114],{"class":3701,"line":6382},[3699,20112,20113],{"class":4445},"            name",[3699,20115,20116],{"class":4422},"=name,\n",[3699,20118,20119,20122],{"class":3701,"line":6387},[3699,20120,20121],{"class":4445},"            user_id",[3699,20123,11566],{"class":4422},[3699,20125,20126,20129,20131,20133],{"class":3701,"line":6392},[3699,20127,20128],{"class":4445},"            is_active",[3699,20130,4556],{"class":4422},[3699,20132,9303],{"class":4435},[3699,20134,7077],{"class":4422},[3699,20136,20137,20140],{"class":3701,"line":6398},[3699,20138,20139],{"class":4445},"            created_at",[3699,20141,20142],{"class":4422},"=datetime.now(timezone.utc),\n",[3699,20144,20145],{"class":3701,"line":6404},[3699,20146,10015],{"class":4422},[3699,20148,20149,20151],{"class":3701,"line":6409},[3699,20150,13942],{"class":4435},[3699,20152,20153],{"class":4422},"._db.add(api_key)\n",[3699,20155,20156,20158,20160],{"class":3701,"line":8467},[3699,20157,11747],{"class":4418},[3699,20159,13988],{"class":4435},[3699,20161,14089],{"class":4422},[3699,20163,20164,20166,20168],{"class":3701,"line":8472},[3699,20165,11747],{"class":4418},[3699,20167,13988],{"class":4435},[3699,20169,20170],{"class":4422},"._db.refresh(api_key)\n",[3699,20172,20173,20175],{"class":3701,"line":8494},[3699,20174,13996],{"class":4418},[3699,20176,20177],{"class":4422}," api_key\n",[3699,20179,20180],{"class":3701,"line":8499},[3699,20181,3730],{"emptyLinePlaceholder":3729},[3699,20183,20184,20186,20188,20191,20193,20195,20197,20200,20203,20205],{"class":3701,"line":8505},[3699,20185,12918],{"class":4435},[3699,20187,9705],{"class":4435},[3699,20189,20190],{"class":4321}," update_last_used",[3699,20192,4442],{"class":4422},[3699,20194,13925],{"class":4445},[3699,20196,4200],{"class":4422},[3699,20198,20199],{"class":4445},"api_key",[3699,20201,20202],{"class":4422},": ApiKey) -> ",[3699,20204,4938],{"class":4435},[3699,20206,4460],{"class":4422},[3699,20208,20209],{"class":3701,"line":8510},[3699,20210,20211],{"class":4422},"        api_key.last_used_at = datetime.now(timezone.utc)\n",[3699,20213,20214,20216,20218],{"class":3701,"line":8515},[3699,20215,11747],{"class":4418},[3699,20217,13988],{"class":4435},[3699,20219,14089],{"class":4422},[3699,20221,20222],{"class":3701,"line":8521},[3699,20223,3730],{"emptyLinePlaceholder":3729},[3699,20225,20226,20228,20230,20233,20235,20237,20239,20242,20244,20246],{"class":3701,"line":8526},[3699,20227,12918],{"class":4435},[3699,20229,9705],{"class":4435},[3699,20231,20232],{"class":4321}," revoke",[3699,20234,4442],{"class":4422},[3699,20236,13925],{"class":4445},[3699,20238,4200],{"class":4422},[3699,20240,20241],{"class":4445},"api_key_id",[3699,20243,14335],{"class":4422},[3699,20245,4938],{"class":4435},[3699,20247,4460],{"class":4422},[3699,20249,20250,20253,20255,20257],{"class":3701,"line":8531},[3699,20251,20252],{"class":4422},"        key = ",[3699,20254,10044],{"class":4418},[3699,20256,13988],{"class":4435},[3699,20258,20259],{"class":4422},"._db.get(ApiKey, api_key_id)\n",[3699,20261,20262,20264],{"class":3701,"line":8537},[3699,20263,9892],{"class":4418},[3699,20265,20266],{"class":4422}," key:\n",[3699,20268,20269,20272],{"class":3701,"line":8542},[3699,20270,20271],{"class":4422},"            key.is_active = ",[3699,20273,18634],{"class":4435},[3699,20275,20276,20278,20280],{"class":3701,"line":8548},[3699,20277,14363],{"class":4418},[3699,20279,13988],{"class":4435},[3699,20281,14089],{"class":4422},[3556,20283,20284],{},"Dependency для API Key автентифікації:",[3690,20286,20288],{"className":4403,"code":20287,"language":4405,"meta":3695,"style":3695},"# app\u002Fapi\u002Fdeps.py — додаємо нову dependency\nimport hashlib\n\nfrom fastapi import Header, HTTPException, status\n\nfrom app.db.session import get_db\nfrom app.models.user import User\nfrom app.repositories.api_key import ApiKeyRepository\n\n\nasync def get_user_from_api_key(\n    x_api_key: str | None = Header(default=None, alias=\"X-API-Key\"),\n    db: AsyncSession = Depends(get_db),\n) -> User:\n    \"\"\"\n    Dependency: автентифікує через API Key з заголовка X-API-Key.\n\n    Алгоритм:\n    1. Витягує X-API-Key з заголовка запиту.\n    2. Обчислює SHA-256 хеш ключа.\n    3. Шукає ключ у БД за хешем.\n    4. Повертає пов'язаного User або 401.\n    \"\"\"\n    if x_api_key is None:\n        raise HTTPException(\n            status_code=status.HTTP_401_UNAUTHORIZED,\n            detail=\"X-API-Key header is required\",\n        )\n\n    key_hash = hashlib.sha256(x_api_key.encode()).hexdigest()\n    api_key_repo = ApiKeyRepository(db)\n    api_key = await api_key_repo.get_by_hash(key_hash)\n\n    if api_key is None:\n        raise HTTPException(\n            status_code=status.HTTP_401_UNAUTHORIZED,\n            detail=\"Invalid or revoked API key\",\n        )\n\n    # Оновлюємо last_used_at (у фоні, без блокування відповіді)\n    await api_key_repo.update_last_used(api_key)\n\n    return api_key.user\n",[3625,20289,20290,20295,20301,20305,20316,20320,20330,20340,20352,20356,20360,20371,20405,20411,20415,20419,20424,20428,20433,20438,20443,20448,20453,20457,20470,20476,20482,20493,20497,20501,20506,20511,20521,20525,20538,20544,20550,20561,20565,20569,20574,20581,20585],{"__ignoreMap":3695},[3699,20291,20292],{"class":3701,"line":3702},[3699,20293,20294],{"class":4412},"# app\u002Fapi\u002Fdeps.py — додаємо нову dependency\n",[3699,20296,20297,20299],{"class":3701,"line":3708},[3699,20298,4419],{"class":4418},[3699,20300,8068],{"class":4422},[3699,20302,20303],{"class":3701,"line":3714},[3699,20304,3730],{"emptyLinePlaceholder":3729},[3699,20306,20307,20309,20311,20313],{"class":3701,"line":3720},[3699,20308,5828],{"class":4418},[3699,20310,9576],{"class":4422},[3699,20312,4419],{"class":4418},[3699,20314,20315],{"class":4422}," Header, HTTPException, status\n",[3699,20317,20318],{"class":3701,"line":3726},[3699,20319,3730],{"emptyLinePlaceholder":3729},[3699,20321,20322,20324,20326,20328],{"class":3701,"line":3733},[3699,20323,5828],{"class":4418},[3699,20325,9639],{"class":4422},[3699,20327,4419],{"class":4418},[3699,20329,9644],{"class":4422},[3699,20331,20332,20334,20336,20338],{"class":3701,"line":3739},[3699,20333,5828],{"class":4418},[3699,20335,9651],{"class":4422},[3699,20337,4419],{"class":4418},[3699,20339,9656],{"class":4422},[3699,20341,20342,20344,20347,20349],{"class":3701,"line":3745},[3699,20343,5828],{"class":4418},[3699,20345,20346],{"class":4422}," app.repositories.api_key ",[3699,20348,4419],{"class":4418},[3699,20350,20351],{"class":4422}," ApiKeyRepository\n",[3699,20353,20354],{"class":3701,"line":3751},[3699,20355,3730],{"emptyLinePlaceholder":3729},[3699,20357,20358],{"class":3701,"line":3757},[3699,20359,3730],{"emptyLinePlaceholder":3729},[3699,20361,20362,20364,20366,20369],{"class":3701,"line":3763},[3699,20363,9702],{"class":4435},[3699,20365,9705],{"class":4435},[3699,20367,20368],{"class":4321}," get_user_from_api_key",[3699,20370,8268],{"class":4422},[3699,20372,20373,20376,20378,20380,20382,20384,20387,20389,20391,20393,20395,20398,20400,20403],{"class":3701,"line":3769},[3699,20374,20375],{"class":4445},"    x_api_key",[3699,20377,4449],{"class":4422},[3699,20379,4244],{"class":4452},[3699,20381,8292],{"class":4422},[3699,20383,4938],{"class":4435},[3699,20385,20386],{"class":4422}," = Header(",[3699,20388,13123],{"class":4445},[3699,20390,4556],{"class":4422},[3699,20392,4938],{"class":4435},[3699,20394,4200],{"class":4422},[3699,20396,20397],{"class":4445},"alias",[3699,20399,4556],{"class":4422},[3699,20401,20402],{"class":4324},"\"X-API-Key\"",[3699,20404,7743],{"class":4422},[3699,20406,20407,20409],{"class":3701,"line":3775},[3699,20408,9727],{"class":4445},[3699,20410,9730],{"class":4422},[3699,20412,20413],{"class":3701,"line":3781},[3699,20414,9735],{"class":4422},[3699,20416,20417],{"class":3701,"line":3786},[3699,20418,4465],{"class":4324},[3699,20420,20421],{"class":3701,"line":3792},[3699,20422,20423],{"class":4324},"    Dependency: автентифікує через API Key з заголовка X-API-Key.\n",[3699,20425,20426],{"class":3701,"line":3798},[3699,20427,3730],{"emptyLinePlaceholder":3729},[3699,20429,20430],{"class":3701,"line":3804},[3699,20431,20432],{"class":4324},"    Алгоритм:\n",[3699,20434,20435],{"class":3701,"line":3810},[3699,20436,20437],{"class":4324},"    1. Витягує X-API-Key з заголовка запиту.\n",[3699,20439,20440],{"class":3701,"line":3816},[3699,20441,20442],{"class":4324},"    2. Обчислює SHA-256 хеш ключа.\n",[3699,20444,20445],{"class":3701,"line":3822},[3699,20446,20447],{"class":4324},"    3. Шукає ключ у БД за хешем.\n",[3699,20449,20450],{"class":3701,"line":3827},[3699,20451,20452],{"class":4324},"    4. Повертає пов'язаного User або 401.\n",[3699,20454,20455],{"class":3701,"line":3832},[3699,20456,4465],{"class":4324},[3699,20458,20459,20461,20464,20466,20468],{"class":3701,"line":3837},[3699,20460,5988],{"class":4418},[3699,20462,20463],{"class":4422}," x_api_key ",[3699,20465,9945],{"class":4435},[3699,20467,9948],{"class":4435},[3699,20469,4460],{"class":4422},[3699,20471,20472,20474],{"class":3701,"line":3843},[3699,20473,9972],{"class":4418},[3699,20475,9975],{"class":4422},[3699,20477,20478,20480],{"class":3701,"line":4590},[3699,20479,9980],{"class":4445},[3699,20481,9824],{"class":4422},[3699,20483,20484,20486,20488,20491],{"class":3701,"line":4595},[3699,20485,9987],{"class":4445},[3699,20487,4556],{"class":4422},[3699,20489,20490],{"class":4324},"\"X-API-Key header is required\"",[3699,20492,7077],{"class":4422},[3699,20494,20495],{"class":3701,"line":4627},[3699,20496,10015],{"class":4422},[3699,20498,20499],{"class":3701,"line":4632},[3699,20500,3730],{"emptyLinePlaceholder":3729},[3699,20502,20503],{"class":3701,"line":4638},[3699,20504,20505],{"class":4422},"    key_hash = hashlib.sha256(x_api_key.encode()).hexdigest()\n",[3699,20507,20508],{"class":3701,"line":4643},[3699,20509,20510],{"class":4422},"    api_key_repo = ApiKeyRepository(db)\n",[3699,20512,20513,20516,20518],{"class":3701,"line":4648},[3699,20514,20515],{"class":4422},"    api_key = ",[3699,20517,10044],{"class":4418},[3699,20519,20520],{"class":4422}," api_key_repo.get_by_hash(key_hash)\n",[3699,20522,20523],{"class":3701,"line":4654},[3699,20524,3730],{"emptyLinePlaceholder":3729},[3699,20526,20527,20529,20532,20534,20536],{"class":3701,"line":4660},[3699,20528,5988],{"class":4418},[3699,20530,20531],{"class":4422}," api_key ",[3699,20533,9945],{"class":4435},[3699,20535,9948],{"class":4435},[3699,20537,4460],{"class":4422},[3699,20539,20540,20542],{"class":3701,"line":4665},[3699,20541,9972],{"class":4418},[3699,20543,9975],{"class":4422},[3699,20545,20546,20548],{"class":3701,"line":4670},[3699,20547,9980],{"class":4445},[3699,20549,9824],{"class":4422},[3699,20551,20552,20554,20556,20559],{"class":3701,"line":4676},[3699,20553,9987],{"class":4445},[3699,20555,4556],{"class":4422},[3699,20557,20558],{"class":4324},"\"Invalid or revoked API key\"",[3699,20560,7077],{"class":4422},[3699,20562,20563],{"class":3701,"line":4681},[3699,20564,10015],{"class":4422},[3699,20566,20567],{"class":3701,"line":4694},[3699,20568,3730],{"emptyLinePlaceholder":3729},[3699,20570,20571],{"class":3701,"line":4704},[3699,20572,20573],{"class":4412},"    # Оновлюємо last_used_at (у фоні, без блокування відповіді)\n",[3699,20575,20576,20578],{"class":3701,"line":4709},[3699,20577,4966],{"class":4418},[3699,20579,20580],{"class":4422}," api_key_repo.update_last_used(api_key)\n",[3699,20582,20583],{"class":3701,"line":6342},[3699,20584,3730],{"emptyLinePlaceholder":3729},[3699,20586,20587,20589],{"class":3701,"line":6348},[3699,20588,4576],{"class":4418},[3699,20590,20591],{"class":4422}," api_key.user\n",[3556,20593,20594],{},"Ендпоінти управління API Keys:",[3690,20596,20598],{"className":4403,"code":20597,"language":4405,"meta":3695,"style":3695},"# app\u002Fapi\u002Fv1\u002Fapi_keys.py\nimport uuid\n\nfrom fastapi import APIRouter, Depends, HTTPException, status\nfrom pydantic import BaseModel\nfrom sqlalchemy.ext.asyncio import AsyncSession\n\nfrom app.api.deps import get_current_active_user\nfrom app.db.session import get_db\nfrom app.models.user import User\nfrom app.repositories.api_key import ApiKeyRepository, generate_api_key\n\nrouter = APIRouter(prefix=\"\u002Fapi-keys\", tags=[\"API Keys\"])\n\n\nclass ApiKeyCreateRequest(BaseModel):\n    name: str  # Опис: \"GitHub Actions CI\", \"Monitoring script\"\n\n\nclass ApiKeyCreateResponse(BaseModel):\n    id: uuid.UUID\n    name: str\n    key_prefix: str\n    # plain_key показується ОДИН РАЗ — після цього відновити неможливо\n    plain_key: str\n\n\nclass ApiKeyListResponse(BaseModel):\n    id: uuid.UUID\n    name: str\n    key_prefix: str\n    is_active: bool\n\n    model_config = {\"from_attributes\": True}\n\n\n@router.post(\"\u002F\", response_model=ApiKeyCreateResponse, status_code=status.HTTP_201_CREATED)\nasync def create_api_key(\n    body: ApiKeyCreateRequest,\n    current_user: User = Depends(get_current_active_user),\n    db: AsyncSession = Depends(get_db),\n) -> ApiKeyCreateResponse:\n    \"\"\"\n    Створює новий API Key для поточного користувача.\n    ВАЖЛИВО: plain_key показується лише один раз у відповіді.\n    Збережіть його — відновити буде неможливо.\n    \"\"\"\n    plain_key, key_hash = generate_api_key(prefix=\"tf_live\")\n    api_key = await ApiKeyRepository(db).create(\n        user_id=current_user.id,\n        name=body.name,\n        plain_key=plain_key,\n        key_hash=key_hash,\n    )\n    return ApiKeyCreateResponse(\n        id=api_key.id,\n        name=api_key.name,\n        key_prefix=api_key.key_prefix,\n        plain_key=plain_key,  # єдиний раз — у відповіді на створення\n    )\n\n\n@router.delete(\"\u002F{api_key_id}\", status_code=status.HTTP_204_NO_CONTENT)\nasync def revoke_api_key(\n    api_key_id: uuid.UUID,\n    current_user: User = Depends(get_current_active_user),\n    db: AsyncSession = Depends(get_db),\n) -> None:\n    \"\"\"Відкликає (деактивує) API Key.\"\"\"\n    await ApiKeyRepository(db).revoke(api_key_id)\n",[3625,20599,20600,20605,20611,20615,20625,20636,20646,20650,20660,20670,20680,20691,20695,20717,20721,20725,20738,20748,20752,20756,20769,20775,20781,20788,20793,20800,20804,20808,20821,20827,20833,20839,20845,20849,20861,20865,20869,20889,20900,20907,20913,20919,20924,20928,20933,20938,20943,20947,20960,20969,20976,20983,20990,20996,21000,21007,21015,21022,21030,21040,21044,21048,21052,21073,21084,21091,21097,21103,21111,21116],{"__ignoreMap":3695},[3699,20601,20602],{"class":3701,"line":3702},[3699,20603,20604],{"class":4412},"# app\u002Fapi\u002Fv1\u002Fapi_keys.py\n",[3699,20606,20607,20609],{"class":3701,"line":3708},[3699,20608,4419],{"class":4418},[3699,20610,13000],{"class":4422},[3699,20612,20613],{"class":3701,"line":3714},[3699,20614,3730],{"emptyLinePlaceholder":3729},[3699,20616,20617,20619,20621,20623],{"class":3701,"line":3720},[3699,20618,5828],{"class":4418},[3699,20620,9576],{"class":4422},[3699,20622,4419],{"class":4418},[3699,20624,10463],{"class":4422},[3699,20626,20627,20629,20631,20633],{"class":3701,"line":3726},[3699,20628,5828],{"class":4418},[3699,20630,13679],{"class":4422},[3699,20632,4419],{"class":4418},[3699,20634,20635],{"class":4422}," BaseModel\n",[3699,20637,20638,20640,20642,20644],{"class":3701,"line":3733},[3699,20639,5828],{"class":4418},[3699,20641,9611],{"class":4422},[3699,20643,4419],{"class":4418},[3699,20645,9616],{"class":4422},[3699,20647,20648],{"class":3701,"line":3739},[3699,20649,3730],{"emptyLinePlaceholder":3729},[3699,20651,20652,20654,20656,20658],{"class":3701,"line":3745},[3699,20653,5828],{"class":4418},[3699,20655,10254],{"class":4422},[3699,20657,4419],{"class":4418},[3699,20659,10259],{"class":4422},[3699,20661,20662,20664,20666,20668],{"class":3701,"line":3751},[3699,20663,5828],{"class":4418},[3699,20665,9639],{"class":4422},[3699,20667,4419],{"class":4418},[3699,20669,9644],{"class":4422},[3699,20671,20672,20674,20676,20678],{"class":3701,"line":3757},[3699,20673,5828],{"class":4418},[3699,20675,9651],{"class":4422},[3699,20677,4419],{"class":4418},[3699,20679,9656],{"class":4422},[3699,20681,20682,20684,20686,20688],{"class":3701,"line":3763},[3699,20683,5828],{"class":4418},[3699,20685,20346],{"class":4422},[3699,20687,4419],{"class":4418},[3699,20689,20690],{"class":4422}," ApiKeyRepository, generate_api_key\n",[3699,20692,20693],{"class":3701,"line":3769},[3699,20694,3730],{"emptyLinePlaceholder":3729},[3699,20696,20697,20699,20701,20703,20706,20708,20710,20712,20715],{"class":3701,"line":3775},[3699,20698,10636],{"class":4422},[3699,20700,10639],{"class":4445},[3699,20702,4556],{"class":4422},[3699,20704,20705],{"class":4324},"\"\u002Fapi-keys\"",[3699,20707,4200],{"class":4422},[3699,20709,10649],{"class":4445},[3699,20711,7938],{"class":4422},[3699,20713,20714],{"class":4324},"\"API Keys\"",[3699,20716,10657],{"class":4422},[3699,20718,20719],{"class":3701,"line":3781},[3699,20720,3730],{"emptyLinePlaceholder":3729},[3699,20722,20723],{"class":3701,"line":3786},[3699,20724,3730],{"emptyLinePlaceholder":3729},[3699,20726,20727,20729,20732,20734,20736],{"class":3701,"line":3792},[3699,20728,9008],{"class":4435},[3699,20730,20731],{"class":4452}," ApiKeyCreateRequest",[3699,20733,4442],{"class":4422},[3699,20735,13704],{"class":4452},[3699,20737,9019],{"class":4422},[3699,20739,20740,20743,20745],{"class":3701,"line":3798},[3699,20741,20742],{"class":4422},"    name: ",[3699,20744,4244],{"class":4452},[3699,20746,20747],{"class":4412},"  # Опис: \"GitHub Actions CI\", \"Monitoring script\"\n",[3699,20749,20750],{"class":3701,"line":3804},[3699,20751,3730],{"emptyLinePlaceholder":3729},[3699,20753,20754],{"class":3701,"line":3810},[3699,20755,3730],{"emptyLinePlaceholder":3729},[3699,20757,20758,20760,20763,20765,20767],{"class":3701,"line":3816},[3699,20759,9008],{"class":4435},[3699,20761,20762],{"class":4452}," ApiKeyCreateResponse",[3699,20764,4442],{"class":4422},[3699,20766,13704],{"class":4452},[3699,20768,9019],{"class":4422},[3699,20770,20771,20773],{"class":3701,"line":3822},[3699,20772,13094],{"class":4321},[3699,20774,18156],{"class":4422},[3699,20776,20777,20779],{"class":3701,"line":3827},[3699,20778,20742],{"class":4422},[3699,20780,12041],{"class":4452},[3699,20782,20783,20786],{"class":3701,"line":3832},[3699,20784,20785],{"class":4422},"    key_prefix: ",[3699,20787,12041],{"class":4452},[3699,20789,20790],{"class":3701,"line":3837},[3699,20791,20792],{"class":4412},"    # plain_key показується ОДИН РАЗ — після цього відновити неможливо\n",[3699,20794,20795,20798],{"class":3701,"line":3843},[3699,20796,20797],{"class":4422},"    plain_key: ",[3699,20799,12041],{"class":4452},[3699,20801,20802],{"class":3701,"line":4590},[3699,20803,3730],{"emptyLinePlaceholder":3729},[3699,20805,20806],{"class":3701,"line":4595},[3699,20807,3730],{"emptyLinePlaceholder":3729},[3699,20809,20810,20812,20815,20817,20819],{"class":3701,"line":4627},[3699,20811,9008],{"class":4435},[3699,20813,20814],{"class":4452}," ApiKeyListResponse",[3699,20816,4442],{"class":4422},[3699,20818,13704],{"class":4452},[3699,20820,9019],{"class":4422},[3699,20822,20823,20825],{"class":3701,"line":4632},[3699,20824,13094],{"class":4321},[3699,20826,18156],{"class":4422},[3699,20828,20829,20831],{"class":3701,"line":4638},[3699,20830,20742],{"class":4422},[3699,20832,12041],{"class":4452},[3699,20834,20835,20837],{"class":3701,"line":4643},[3699,20836,20785],{"class":4422},[3699,20838,12041],{"class":4452},[3699,20840,20841,20843],{"class":3701,"line":4648},[3699,20842,18176],{"class":4422},[3699,20844,18179],{"class":4452},[3699,20846,20847],{"class":3701,"line":4654},[3699,20848,3730],{"emptyLinePlaceholder":3729},[3699,20850,20851,20853,20855,20857,20859],{"class":3701,"line":4660},[3699,20852,18188],{"class":4422},[3699,20854,18191],{"class":4324},[3699,20856,4449],{"class":4422},[3699,20858,9303],{"class":4435},[3699,20860,3778],{"class":4422},[3699,20862,20863],{"class":3701,"line":4665},[3699,20864,3730],{"emptyLinePlaceholder":3729},[3699,20866,20867],{"class":3701,"line":4670},[3699,20868,3730],{"emptyLinePlaceholder":3729},[3699,20870,20871,20873,20875,20878,20880,20882,20885,20887],{"class":3701,"line":4676},[3699,20872,10348],{"class":4321},[3699,20874,4442],{"class":4422},[3699,20876,20877],{"class":4324},"\"\u002F\"",[3699,20879,4200],{"class":4422},[3699,20881,10679],{"class":4445},[3699,20883,20884],{"class":4422},"=ApiKeyCreateResponse, ",[3699,20886,7984],{"class":4445},[3699,20888,10687],{"class":4422},[3699,20890,20891,20893,20895,20898],{"class":3701,"line":4681},[3699,20892,9702],{"class":4435},[3699,20894,9705],{"class":4435},[3699,20896,20897],{"class":4321}," create_api_key",[3699,20899,8268],{"class":4422},[3699,20901,20902,20904],{"class":3701,"line":4694},[3699,20903,11329],{"class":4445},[3699,20905,20906],{"class":4422},": ApiKeyCreateRequest,\n",[3699,20908,20909,20911],{"class":3701,"line":4704},[3699,20910,10105],{"class":4445},[3699,20912,10316],{"class":4422},[3699,20914,20915,20917],{"class":3701,"line":4709},[3699,20916,9727],{"class":4445},[3699,20918,9730],{"class":4422},[3699,20920,20921],{"class":3701,"line":6342},[3699,20922,20923],{"class":4422},") -> ApiKeyCreateResponse:\n",[3699,20925,20926],{"class":3701,"line":6348},[3699,20927,4465],{"class":4324},[3699,20929,20930],{"class":3701,"line":6354},[3699,20931,20932],{"class":4324},"    Створює новий API Key для поточного користувача.\n",[3699,20934,20935],{"class":3701,"line":6359},[3699,20936,20937],{"class":4324},"    ВАЖЛИВО: plain_key показується лише один раз у відповіді.\n",[3699,20939,20940],{"class":3701,"line":6365},[3699,20941,20942],{"class":4324},"    Збережіть його — відновити буде неможливо.\n",[3699,20944,20945],{"class":3701,"line":6371},[3699,20946,4465],{"class":4324},[3699,20948,20949,20952,20954,20956,20958],{"class":3701,"line":6377},[3699,20950,20951],{"class":4422},"    plain_key, key_hash = generate_api_key(",[3699,20953,10639],{"class":4445},[3699,20955,4556],{"class":4422},[3699,20957,19814],{"class":4324},[3699,20959,4562],{"class":4422},[3699,20961,20962,20964,20966],{"class":3701,"line":6382},[3699,20963,20515],{"class":4422},[3699,20965,10044],{"class":4418},[3699,20967,20968],{"class":4422}," ApiKeyRepository(db).create(\n",[3699,20970,20971,20973],{"class":3701,"line":6387},[3699,20972,10902],{"class":4445},[3699,20974,20975],{"class":4422},"=current_user.id,\n",[3699,20977,20978,20980],{"class":3701,"line":6392},[3699,20979,20049],{"class":4445},[3699,20981,20982],{"class":4422},"=body.name,\n",[3699,20984,20985,20987],{"class":3701,"line":6398},[3699,20986,20060],{"class":4445},[3699,20988,20989],{"class":4422},"=plain_key,\n",[3699,20991,20992,20994],{"class":3701,"line":6404},[3699,20993,20071],{"class":4445},[3699,20995,20095],{"class":4422},[3699,20997,20998],{"class":3701,"line":6409},[3699,20999,7951],{"class":4422},[3699,21001,21002,21004],{"class":3701,"line":8467},[3699,21003,4576],{"class":4418},[3699,21005,21006],{"class":4422}," ApiKeyCreateResponse(\n",[3699,21008,21009,21012],{"class":3701,"line":8472},[3699,21010,21011],{"class":4445},"        id",[3699,21013,21014],{"class":4422},"=api_key.id,\n",[3699,21016,21017,21019],{"class":3701,"line":8494},[3699,21018,20049],{"class":4445},[3699,21020,21021],{"class":4422},"=api_key.name,\n",[3699,21023,21024,21027],{"class":3701,"line":8499},[3699,21025,21026],{"class":4445},"        key_prefix",[3699,21028,21029],{"class":4422},"=api_key.key_prefix,\n",[3699,21031,21032,21034,21037],{"class":3701,"line":8505},[3699,21033,20060],{"class":4445},[3699,21035,21036],{"class":4422},"=plain_key,  ",[3699,21038,21039],{"class":4412},"# єдиний раз — у відповіді на створення\n",[3699,21041,21042],{"class":3701,"line":8510},[3699,21043,7951],{"class":4422},[3699,21045,21046],{"class":3701,"line":8515},[3699,21047,3730],{"emptyLinePlaceholder":3729},[3699,21049,21050],{"class":3701,"line":8521},[3699,21051,3730],{"emptyLinePlaceholder":3729},[3699,21053,21054,21057,21059,21062,21065,21067,21069,21071],{"class":3701,"line":8526},[3699,21055,21056],{"class":4321},"@router.delete",[3699,21058,4442],{"class":4422},[3699,21060,21061],{"class":4324},"\"\u002F",[3699,21063,21064],{"class":4435},"{api_key_id}",[3699,21066,17949],{"class":4324},[3699,21068,4200],{"class":4422},[3699,21070,7984],{"class":4445},[3699,21072,11651],{"class":4422},[3699,21074,21075,21077,21079,21082],{"class":3701,"line":8531},[3699,21076,9702],{"class":4435},[3699,21078,9705],{"class":4435},[3699,21080,21081],{"class":4321}," revoke_api_key",[3699,21083,8268],{"class":4422},[3699,21085,21086,21089],{"class":3701,"line":8537},[3699,21087,21088],{"class":4445},"    api_key_id",[3699,21090,18354],{"class":4422},[3699,21092,21093,21095],{"class":3701,"line":8542},[3699,21094,10105],{"class":4445},[3699,21096,10316],{"class":4422},[3699,21098,21099,21101],{"class":3701,"line":8548},[3699,21100,9727],{"class":4445},[3699,21102,9730],{"class":4422},[3699,21104,21105,21107,21109],{"class":3701,"line":8554},[3699,21106,4455],{"class":4422},[3699,21108,4938],{"class":4435},[3699,21110,4460],{"class":4422},[3699,21112,21113],{"class":3701,"line":8560},[3699,21114,21115],{"class":4324},"    \"\"\"Відкликає (деактивує) API Key.\"\"\"\n",[3699,21117,21118,21120],{"class":3701,"line":8565},[3699,21119,4966],{"class":4418},[3699,21121,21122],{"class":4422}," ApiKeyRepository(db).revoke(api_key_id)\n",[4716,21124,21125,21129],{},[3556,21126,21127],{},[3560,21128,9142],{},[3556,21130,21131,21132,21135,21136,21138,21139,21142],{},"В ASP.NET API Key автентифікація реалізується через кастомний ",[3625,21133,21134],{},"AuthenticationHandler"," або middleware. У FastAPI — через стандартний ",[3625,21137,3647],{}," з функцією ",[3625,21140,21141],{},"get_user_from_api_key",". Підхід однаковий за суттю, але FastAPI-рішення більш яке і не вимагає реєстрації нової схеми автентифікації.",[3654,21144],{},[3551,21146,21148],{"id":21147},"частина-6-security-headers-захист-через-http-заголовки","Частина 6. Security Headers — захист через HTTP-заголовки",[3661,21150,21152],{"id":21151},"що-таке-security-headers-і-навіщо-вони-потрібні","Що таке Security Headers і навіщо вони потрібні",[3556,21154,21155],{},"Навіть ідеально написаний код може бути вразливим, якщо HTTP-відповіді не містять потрібних заголовків безпеки. Браузер — перша лінія захисту між вашим API та кінцевим користувачем. Правильні заголовки інструктують браузер, як обробляти відповідь, і запобігають цілому класу атак.",[3862,21157,21158,21171],{},[3865,21159,21160],{},[3868,21161,21162,21165,21168],{},[3871,21163,21164],{},"Заголовок",[3871,21166,21167],{},"Від чого захищає",[3871,21169,21170],{},"Приклад значення",[3896,21172,21173,21188,21203,21218,21233,21248],{},[3868,21174,21175,21180,21183],{},[3901,21176,21177],{},[3625,21178,21179],{},"Strict-Transport-Security",[3901,21181,21182],{},"Атаки downgrade HTTPS → HTTP",[3901,21184,21185],{},[3625,21186,21187],{},"max-age=31536000; includeSubDomains",[3868,21189,21190,21195,21198],{},[3901,21191,21192],{},[3625,21193,21194],{},"X-Content-Type-Options",[3901,21196,21197],{},"MIME sniffing атаки",[3901,21199,21200],{},[3625,21201,21202],{},"nosniff",[3868,21204,21205,21210,21213],{},[3901,21206,21207],{},[3625,21208,21209],{},"X-Frame-Options",[3901,21211,21212],{},"Clickjacking через iframe",[3901,21214,21215],{},[3625,21216,21217],{},"DENY",[3868,21219,21220,21225,21228],{},[3901,21221,21222],{},[3625,21223,21224],{},"Content-Security-Policy",[3901,21226,21227],{},"XSS, injection атаки",[3901,21229,21230],{},[3625,21231,21232],{},"default-src 'self'",[3868,21234,21235,21240,21243],{},[3901,21236,21237],{},[3625,21238,21239],{},"Referrer-Policy",[3901,21241,21242],{},"Витік URL у Referer",[3901,21244,21245],{},[3625,21246,21247],{},"strict-origin-when-cross-origin",[3868,21249,21250,21255,21258],{},[3901,21251,21252],{},[3625,21253,21254],{},"Permissions-Policy",[3901,21256,21257],{},"Зловживання API браузера (камера, мікрофон)",[3901,21259,21260],{},[3625,21261,21262],{},"camera=(), microphone=()",[3661,21264,21266],{"id":21265},"реалізація-через-starlette-middleware","Реалізація через Starlette Middleware",[3556,21268,21269,21270,4400],{},"FastAPI побудований на Starlette, тому middleware для додавання заголовків — звичайний ",[3625,21271,21272],{},"BaseHTTPMiddleware",[3690,21274,21276],{"className":4403,"code":21275,"language":4405,"meta":3695,"style":3695},"# app\u002Fmiddleware\u002Fsecurity_headers.py\nfrom starlette.middleware.base import BaseHTTPMiddleware\nfrom starlette.requests import Request\nfrom starlette.responses import Response\n\n\nclass SecurityHeadersMiddleware(BaseHTTPMiddleware):\n    \"\"\"\n    Middleware для додавання HTTP Security Headers до кожної відповіді.\n\n    Захищає від:\n    - XSS (Content-Security-Policy)\n    - Clickjacking (X-Frame-Options)\n    - MIME sniffing (X-Content-Type-Options)\n    - Protocol downgrade (Strict-Transport-Security)\n    - Витоку даних через Referer (Referrer-Policy)\n    \"\"\"\n\n    async def dispatch(self, request: Request, call_next) -> Response:\n        response = await call_next(request)\n\n        # Примушує браузер завжди використовувати HTTPS протягом 1 року.\n        # includeSubDomains поширює правило на всі піддомени.\n        # preload — дозволяє включення до HSTS preload list браузерів.\n        response.headers[\"Strict-Transport-Security\"] = (\n            \"max-age=31536000; includeSubDomains; preload\"\n        )\n\n        # Забороняє браузеру «вгадувати» MIME тип файлу.\n        # Без цього браузер міг би виконати JS замаскований под text\u002Fplain.\n        response.headers[\"X-Content-Type-Options\"] = \"nosniff\"\n\n        # Забороняє відображення сторінки в iframe (захист від clickjacking).\n        # SAMEORIGIN дозволяє лише iframe з того ж домену.\n        response.headers[\"X-Frame-Options\"] = \"DENY\"\n\n        # Базова CSP: дозволяє ресурси лише з власного домену.\n        # Для API (без HTML) достатньо мінімальної CSP.\n        response.headers[\"Content-Security-Policy\"] = (\n            \"default-src 'self'; \"\n            \"frame-ancestors 'none'\"  # еквівалент X-Frame-Options DENY для CSP Level 3\n        )\n\n        # Обмежує витік URL у заголовку Referer.\n        # strict-origin-when-cross-origin: надсилає тільки origin (без path) для cross-origin.\n        response.headers[\"Referrer-Policy\"] = \"strict-origin-when-cross-origin\"\n\n        # Вимикає доступ до браузерних API, що можуть бути зловживані.\n        response.headers[\"Permissions-Policy\"] = (\n            \"camera=(), microphone=(), geolocation=(), payment=()\"\n        )\n\n        # Забороняє Internet Explorer переходити в режим сумісності.\n        # Застарілий, але не шкодить.\n        response.headers[\"X-UA-Compatible\"] = \"IE=edge\"\n\n        return response\n",[3625,21277,21278,21283,21295,21307,21319,21323,21327,21340,21344,21349,21353,21358,21363,21368,21373,21378,21383,21387,21391,21418,21428,21432,21437,21442,21447,21458,21463,21467,21471,21476,21481,21494,21498,21503,21508,21520,21524,21529,21534,21543,21548,21556,21560,21564,21569,21574,21586,21590,21595,21604,21609,21613,21617,21622,21627,21639,21643],{"__ignoreMap":3695},[3699,21279,21280],{"class":3701,"line":3702},[3699,21281,21282],{"class":4412},"# app\u002Fmiddleware\u002Fsecurity_headers.py\n",[3699,21284,21285,21287,21290,21292],{"class":3701,"line":3708},[3699,21286,5828],{"class":4418},[3699,21288,21289],{"class":4422}," starlette.middleware.base ",[3699,21291,4419],{"class":4418},[3699,21293,21294],{"class":4422}," BaseHTTPMiddleware\n",[3699,21296,21297,21299,21302,21304],{"class":3701,"line":3714},[3699,21298,5828],{"class":4418},[3699,21300,21301],{"class":4422}," starlette.requests ",[3699,21303,4419],{"class":4418},[3699,21305,21306],{"class":4422}," Request\n",[3699,21308,21309,21311,21314,21316],{"class":3701,"line":3720},[3699,21310,5828],{"class":4418},[3699,21312,21313],{"class":4422}," starlette.responses ",[3699,21315,4419],{"class":4418},[3699,21317,21318],{"class":4422}," Response\n",[3699,21320,21321],{"class":3701,"line":3726},[3699,21322,3730],{"emptyLinePlaceholder":3729},[3699,21324,21325],{"class":3701,"line":3733},[3699,21326,3730],{"emptyLinePlaceholder":3729},[3699,21328,21329,21331,21334,21336,21338],{"class":3701,"line":3739},[3699,21330,9008],{"class":4435},[3699,21332,21333],{"class":4452}," SecurityHeadersMiddleware",[3699,21335,4442],{"class":4422},[3699,21337,21272],{"class":4452},[3699,21339,9019],{"class":4422},[3699,21341,21342],{"class":3701,"line":3745},[3699,21343,4465],{"class":4324},[3699,21345,21346],{"class":3701,"line":3751},[3699,21347,21348],{"class":4324},"    Middleware для додавання HTTP Security Headers до кожної відповіді.\n",[3699,21350,21351],{"class":3701,"line":3757},[3699,21352,3730],{"emptyLinePlaceholder":3729},[3699,21354,21355],{"class":3701,"line":3763},[3699,21356,21357],{"class":4324},"    Захищає від:\n",[3699,21359,21360],{"class":3701,"line":3769},[3699,21361,21362],{"class":4324},"    - XSS (Content-Security-Policy)\n",[3699,21364,21365],{"class":3701,"line":3775},[3699,21366,21367],{"class":4324},"    - Clickjacking (X-Frame-Options)\n",[3699,21369,21370],{"class":3701,"line":3781},[3699,21371,21372],{"class":4324},"    - MIME sniffing (X-Content-Type-Options)\n",[3699,21374,21375],{"class":3701,"line":3786},[3699,21376,21377],{"class":4324},"    - Protocol downgrade (Strict-Transport-Security)\n",[3699,21379,21380],{"class":3701,"line":3792},[3699,21381,21382],{"class":4324},"    - Витоку даних через Referer (Referrer-Policy)\n",[3699,21384,21385],{"class":3701,"line":3798},[3699,21386,4465],{"class":4324},[3699,21388,21389],{"class":3701,"line":3804},[3699,21390,3730],{"emptyLinePlaceholder":3729},[3699,21392,21393,21395,21397,21400,21402,21404,21406,21409,21412,21415],{"class":3701,"line":3810},[3699,21394,12918],{"class":4435},[3699,21396,9705],{"class":4435},[3699,21398,21399],{"class":4321}," dispatch",[3699,21401,4442],{"class":4422},[3699,21403,13925],{"class":4445},[3699,21405,4200],{"class":4422},[3699,21407,21408],{"class":4445},"request",[3699,21410,21411],{"class":4422},": Request, ",[3699,21413,21414],{"class":4445},"call_next",[3699,21416,21417],{"class":4422},") -> Response:\n",[3699,21419,21420,21423,21425],{"class":3701,"line":3816},[3699,21421,21422],{"class":4422},"        response = ",[3699,21424,10044],{"class":4418},[3699,21426,21427],{"class":4422}," call_next(request)\n",[3699,21429,21430],{"class":3701,"line":3822},[3699,21431,3730],{"emptyLinePlaceholder":3729},[3699,21433,21434],{"class":3701,"line":3827},[3699,21435,21436],{"class":4412},"        # Примушує браузер завжди використовувати HTTPS протягом 1 року.\n",[3699,21438,21439],{"class":3701,"line":3832},[3699,21440,21441],{"class":4412},"        # includeSubDomains поширює правило на всі піддомени.\n",[3699,21443,21444],{"class":3701,"line":3837},[3699,21445,21446],{"class":4412},"        # preload — дозволяє включення до HSTS preload list браузерів.\n",[3699,21448,21449,21452,21455],{"class":3701,"line":3843},[3699,21450,21451],{"class":4422},"        response.headers[",[3699,21453,21454],{"class":4324},"\"Strict-Transport-Security\"",[3699,21456,21457],{"class":4422},"] = (\n",[3699,21459,21460],{"class":3701,"line":4590},[3699,21461,21462],{"class":4324},"            \"max-age=31536000; includeSubDomains; preload\"\n",[3699,21464,21465],{"class":3701,"line":4595},[3699,21466,10015],{"class":4422},[3699,21468,21469],{"class":3701,"line":4627},[3699,21470,3730],{"emptyLinePlaceholder":3729},[3699,21472,21473],{"class":3701,"line":4632},[3699,21474,21475],{"class":4412},"        # Забороняє браузеру «вгадувати» MIME тип файлу.\n",[3699,21477,21478],{"class":3701,"line":4638},[3699,21479,21480],{"class":4412},"        # Без цього браузер міг би виконати JS замаскований под text\u002Fplain.\n",[3699,21482,21483,21485,21488,21491],{"class":3701,"line":4643},[3699,21484,21451],{"class":4422},[3699,21486,21487],{"class":4324},"\"X-Content-Type-Options\"",[3699,21489,21490],{"class":4422},"] = ",[3699,21492,21493],{"class":4324},"\"nosniff\"\n",[3699,21495,21496],{"class":3701,"line":4648},[3699,21497,3730],{"emptyLinePlaceholder":3729},[3699,21499,21500],{"class":3701,"line":4654},[3699,21501,21502],{"class":4412},"        # Забороняє відображення сторінки в iframe (захист від clickjacking).\n",[3699,21504,21505],{"class":3701,"line":4660},[3699,21506,21507],{"class":4412},"        # SAMEORIGIN дозволяє лише iframe з того ж домену.\n",[3699,21509,21510,21512,21515,21517],{"class":3701,"line":4665},[3699,21511,21451],{"class":4422},[3699,21513,21514],{"class":4324},"\"X-Frame-Options\"",[3699,21516,21490],{"class":4422},[3699,21518,21519],{"class":4324},"\"DENY\"\n",[3699,21521,21522],{"class":3701,"line":4670},[3699,21523,3730],{"emptyLinePlaceholder":3729},[3699,21525,21526],{"class":3701,"line":4676},[3699,21527,21528],{"class":4412},"        # Базова CSP: дозволяє ресурси лише з власного домену.\n",[3699,21530,21531],{"class":3701,"line":4681},[3699,21532,21533],{"class":4412},"        # Для API (без HTML) достатньо мінімальної CSP.\n",[3699,21535,21536,21538,21541],{"class":3701,"line":4694},[3699,21537,21451],{"class":4422},[3699,21539,21540],{"class":4324},"\"Content-Security-Policy\"",[3699,21542,21457],{"class":4422},[3699,21544,21545],{"class":3701,"line":4704},[3699,21546,21547],{"class":4324},"            \"default-src 'self'; \"\n",[3699,21549,21550,21553],{"class":3701,"line":4709},[3699,21551,21552],{"class":4324},"            \"frame-ancestors 'none'\"",[3699,21554,21555],{"class":4412},"  # еквівалент X-Frame-Options DENY для CSP Level 3\n",[3699,21557,21558],{"class":3701,"line":6342},[3699,21559,10015],{"class":4422},[3699,21561,21562],{"class":3701,"line":6348},[3699,21563,3730],{"emptyLinePlaceholder":3729},[3699,21565,21566],{"class":3701,"line":6354},[3699,21567,21568],{"class":4412},"        # Обмежує витік URL у заголовку Referer.\n",[3699,21570,21571],{"class":3701,"line":6359},[3699,21572,21573],{"class":4412},"        # strict-origin-when-cross-origin: надсилає тільки origin (без path) для cross-origin.\n",[3699,21575,21576,21578,21581,21583],{"class":3701,"line":6365},[3699,21577,21451],{"class":4422},[3699,21579,21580],{"class":4324},"\"Referrer-Policy\"",[3699,21582,21490],{"class":4422},[3699,21584,21585],{"class":4324},"\"strict-origin-when-cross-origin\"\n",[3699,21587,21588],{"class":3701,"line":6371},[3699,21589,3730],{"emptyLinePlaceholder":3729},[3699,21591,21592],{"class":3701,"line":6377},[3699,21593,21594],{"class":4412},"        # Вимикає доступ до браузерних API, що можуть бути зловживані.\n",[3699,21596,21597,21599,21602],{"class":3701,"line":6382},[3699,21598,21451],{"class":4422},[3699,21600,21601],{"class":4324},"\"Permissions-Policy\"",[3699,21603,21457],{"class":4422},[3699,21605,21606],{"class":3701,"line":6387},[3699,21607,21608],{"class":4324},"            \"camera=(), microphone=(), geolocation=(), payment=()\"\n",[3699,21610,21611],{"class":3701,"line":6392},[3699,21612,10015],{"class":4422},[3699,21614,21615],{"class":3701,"line":6398},[3699,21616,3730],{"emptyLinePlaceholder":3729},[3699,21618,21619],{"class":3701,"line":6404},[3699,21620,21621],{"class":4412},"        # Забороняє Internet Explorer переходити в режим сумісності.\n",[3699,21623,21624],{"class":3701,"line":6409},[3699,21625,21626],{"class":4412},"        # Застарілий, але не шкодить.\n",[3699,21628,21629,21631,21634,21636],{"class":3701,"line":8467},[3699,21630,21451],{"class":4422},[3699,21632,21633],{"class":4324},"\"X-UA-Compatible\"",[3699,21635,21490],{"class":4422},[3699,21637,21638],{"class":4324},"\"IE=edge\"\n",[3699,21640,21641],{"class":3701,"line":8472},[3699,21642,3730],{"emptyLinePlaceholder":3729},[3699,21644,21645,21647],{"class":3701,"line":8494},[3699,21646,13996],{"class":4418},[3699,21648,21649],{"class":4422}," response\n",[3661,21651,21653],{"id":21652},"cors-налаштування","CORS налаштування",[3556,21655,21656,21659],{},[3560,21657,21658],{},"CORS (Cross-Origin Resource Sharing)"," — механізм браузера, що обмежує запити до API з інших доменів. FastAPI має вбудований CORS middleware від Starlette:",[3690,21661,21663],{"className":4403,"code":21662,"language":4405,"meta":3695,"style":3695},"# app\u002Fmiddleware\u002Fcors.py\nfrom fastapi.middleware.cors import CORSMiddleware\n\n# Налаштування для додавання у main.py\nCORS_SETTINGS = {\n    \"allow_origins\": [\n        \"https:\u002F\u002Ftaskforge.app\",        # production frontend\n        \"https:\u002F\u002Fstaging.taskforge.app\", # staging\n        \"http:\u002F\u002Flocalhost:3000\",         # локальна розробка\n        \"http:\u002F\u002Flocalhost:5173\",         # Vite dev server\n    ],\n    \"allow_credentials\": True,   # дозволяє передачу cookie та Authorization header\n    \"allow_methods\": [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\", \"OPTIONS\"],\n    \"allow_headers\": [\n        \"Authorization\",\n        \"Content-Type\",\n        \"X-API-Key\",\n        \"X-Request-ID\",\n    ],\n    \"expose_headers\": [\n        \"X-Request-ID\",              # клієнт бачить ID запиту для дебагу\n    ],\n    \"max_age\": 600,                  # браузер кешує preflight відповідь на 10 хвилин\n}\n",[3625,21664,21665,21670,21682,21686,21691,21696,21704,21715,21725,21736,21746,21751,21766,21804,21811,21818,21825,21832,21839,21843,21850,21860,21864,21879],{"__ignoreMap":3695},[3699,21666,21667],{"class":3701,"line":3702},[3699,21668,21669],{"class":4412},"# app\u002Fmiddleware\u002Fcors.py\n",[3699,21671,21672,21674,21677,21679],{"class":3701,"line":3708},[3699,21673,5828],{"class":4418},[3699,21675,21676],{"class":4422}," fastapi.middleware.cors ",[3699,21678,4419],{"class":4418},[3699,21680,21681],{"class":4422}," CORSMiddleware\n",[3699,21683,21684],{"class":3701,"line":3714},[3699,21685,3730],{"emptyLinePlaceholder":3729},[3699,21687,21688],{"class":3701,"line":3720},[3699,21689,21690],{"class":4412},"# Налаштування для додавання у main.py\n",[3699,21692,21693],{"class":3701,"line":3726},[3699,21694,21695],{"class":4422},"CORS_SETTINGS = {\n",[3699,21697,21698,21701],{"class":3701,"line":3733},[3699,21699,21700],{"class":4324},"    \"allow_origins\"",[3699,21702,21703],{"class":4422},": [\n",[3699,21705,21706,21709,21712],{"class":3701,"line":3739},[3699,21707,21708],{"class":4324},"        \"https:\u002F\u002Ftaskforge.app\"",[3699,21710,21711],{"class":4422},",        ",[3699,21713,21714],{"class":4412},"# production frontend\n",[3699,21716,21717,21720,21722],{"class":3701,"line":3745},[3699,21718,21719],{"class":4324},"        \"https:\u002F\u002Fstaging.taskforge.app\"",[3699,21721,4200],{"class":4422},[3699,21723,21724],{"class":4412},"# staging\n",[3699,21726,21727,21730,21733],{"class":3701,"line":3751},[3699,21728,21729],{"class":4324},"        \"http:\u002F\u002Flocalhost:3000\"",[3699,21731,21732],{"class":4422},",         ",[3699,21734,21735],{"class":4412},"# локальна розробка\n",[3699,21737,21738,21741,21743],{"class":3701,"line":3757},[3699,21739,21740],{"class":4324},"        \"http:\u002F\u002Flocalhost:5173\"",[3699,21742,21732],{"class":4422},[3699,21744,21745],{"class":4412},"# Vite dev server\n",[3699,21747,21748],{"class":3701,"line":3763},[3699,21749,21750],{"class":4422},"    ],\n",[3699,21752,21753,21756,21758,21760,21763],{"class":3701,"line":3769},[3699,21754,21755],{"class":4324},"    \"allow_credentials\"",[3699,21757,4449],{"class":4422},[3699,21759,9303],{"class":4435},[3699,21761,21762],{"class":4422},",   ",[3699,21764,21765],{"class":4412},"# дозволяє передачу cookie та Authorization header\n",[3699,21767,21768,21771,21774,21777,21779,21782,21784,21787,21789,21792,21794,21797,21799,21802],{"class":3701,"line":3775},[3699,21769,21770],{"class":4324},"    \"allow_methods\"",[3699,21772,21773],{"class":4422},": [",[3699,21775,21776],{"class":4324},"\"GET\"",[3699,21778,4200],{"class":4422},[3699,21780,21781],{"class":4324},"\"POST\"",[3699,21783,4200],{"class":4422},[3699,21785,21786],{"class":4324},"\"PUT\"",[3699,21788,4200],{"class":4422},[3699,21790,21791],{"class":4324},"\"PATCH\"",[3699,21793,4200],{"class":4422},[3699,21795,21796],{"class":4324},"\"DELETE\"",[3699,21798,4200],{"class":4422},[3699,21800,21801],{"class":4324},"\"OPTIONS\"",[3699,21803,20108],{"class":4422},[3699,21805,21806,21809],{"class":3701,"line":3781},[3699,21807,21808],{"class":4324},"    \"allow_headers\"",[3699,21810,21703],{"class":4422},[3699,21812,21813,21816],{"class":3701,"line":3786},[3699,21814,21815],{"class":4324},"        \"Authorization\"",[3699,21817,7077],{"class":4422},[3699,21819,21820,21823],{"class":3701,"line":3792},[3699,21821,21822],{"class":4324},"        \"Content-Type\"",[3699,21824,7077],{"class":4422},[3699,21826,21827,21830],{"class":3701,"line":3798},[3699,21828,21829],{"class":4324},"        \"X-API-Key\"",[3699,21831,7077],{"class":4422},[3699,21833,21834,21837],{"class":3701,"line":3804},[3699,21835,21836],{"class":4324},"        \"X-Request-ID\"",[3699,21838,7077],{"class":4422},[3699,21840,21841],{"class":3701,"line":3810},[3699,21842,21750],{"class":4422},[3699,21844,21845,21848],{"class":3701,"line":3816},[3699,21846,21847],{"class":4324},"    \"expose_headers\"",[3699,21849,21703],{"class":4422},[3699,21851,21852,21854,21857],{"class":3701,"line":3822},[3699,21853,21836],{"class":4324},[3699,21855,21856],{"class":4422},",              ",[3699,21858,21859],{"class":4412},"# клієнт бачить ID запиту для дебагу\n",[3699,21861,21862],{"class":3701,"line":3827},[3699,21863,21750],{"class":4422},[3699,21865,21866,21869,21871,21874,21876],{"class":3701,"line":3832},[3699,21867,21868],{"class":4324},"    \"max_age\"",[3699,21870,4449],{"class":4422},[3699,21872,21873],{"class":4531},"600",[3699,21875,7129],{"class":4422},[3699,21877,21878],{"class":4412},"# браузер кешує preflight відповідь на 10 хвилин\n",[3699,21880,21881],{"class":3701,"line":3837},[3699,21882,3778],{"class":4422},[4194,21884,21885,21897],{},[3556,21886,21887],{},[3560,21888,21889,21890,21893,21894,4380],{},"Ніколи не ставте ",[3625,21891,21892],{},"allow_origins=[\"*\"]"," разом з ",[3625,21895,21896],{},"allow_credentials=True",[3556,21898,21899],{},"Це комбінація, яку браузер просто відхилить (CORS spec забороняє wildcard при credentials). Крім того, wildcard origins означає, що будь-який сайт у мережі може робити автентифіковані запити від імені вашого користувача — це і є CSRF атака.",[3661,21901,21903],{"id":21902},"rate-limiting-обмеження-кількості-запитів","Rate Limiting — обмеження кількості запитів",[3556,21905,21906,21907,21910,21911,21914],{},"Захист від брутфорсу та DDoS — обмеження кількості запитів з одного IP або для одного ендпоінта. Найпростіше рішення — бібліотека ",[3625,21908,21909],{},"slowapi"," (адаптація ",[3625,21912,21913],{},"flask-limiter"," для FastAPI\u002FStarlette):",[3690,21916,21918],{"className":4403,"code":21917,"language":4405,"meta":3695,"style":3695},"# app\u002Fmiddleware\u002Frate_limit.py\nfrom slowapi import Limiter, _rate_limit_exceeded_handler\nfrom slowapi.errors import RateLimitExceeded\nfrom slowapi.util import get_remote_address\n\n# Використовуємо IP-адресу клієнта як ключ для лімітування\nlimiter = Limiter(key_func=get_remote_address)\n",[3625,21919,21920,21925,21937,21949,21961,21965,21970],{"__ignoreMap":3695},[3699,21921,21922],{"class":3701,"line":3702},[3699,21923,21924],{"class":4412},"# app\u002Fmiddleware\u002Frate_limit.py\n",[3699,21926,21927,21929,21932,21934],{"class":3701,"line":3708},[3699,21928,5828],{"class":4418},[3699,21930,21931],{"class":4422}," slowapi ",[3699,21933,4419],{"class":4418},[3699,21935,21936],{"class":4422}," Limiter, _rate_limit_exceeded_handler\n",[3699,21938,21939,21941,21944,21946],{"class":3701,"line":3714},[3699,21940,5828],{"class":4418},[3699,21942,21943],{"class":4422}," slowapi.errors ",[3699,21945,4419],{"class":4418},[3699,21947,21948],{"class":4422}," RateLimitExceeded\n",[3699,21950,21951,21953,21956,21958],{"class":3701,"line":3720},[3699,21952,5828],{"class":4418},[3699,21954,21955],{"class":4422}," slowapi.util ",[3699,21957,4419],{"class":4418},[3699,21959,21960],{"class":4422}," get_remote_address\n",[3699,21962,21963],{"class":3701,"line":3726},[3699,21964,3730],{"emptyLinePlaceholder":3729},[3699,21966,21967],{"class":3701,"line":3733},[3699,21968,21969],{"class":4412},"# Використовуємо IP-адресу клієнта як ключ для лімітування\n",[3699,21971,21972,21975,21978],{"class":3701,"line":3739},[3699,21973,21974],{"class":4422},"limiter = Limiter(",[3699,21976,21977],{"class":4445},"key_func",[3699,21979,21980],{"class":4422},"=get_remote_address)\n",[3690,21982,21984],{"className":4403,"code":21983,"language":4405,"meta":3695,"style":3695},"# app\u002Fmain.py — повна фінальна версія з усіма middleware\nfrom fastapi import FastAPI\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom slowapi import _rate_limit_exceeded_handler\nfrom slowapi.errors import RateLimitExceeded\n\nfrom app.api.v1 import admin, api_keys, auth\nfrom app.middleware.rate_limit import limiter\nfrom app.middleware.security_headers import SecurityHeadersMiddleware\n\napp = FastAPI(\n    title=\"TaskForge API\",\n    description=\"Project management API — JWT + Opaque Refresh Token + RBAC\",\n    version=\"0.1.0\",\n)\n\n# ─── State (для slowapi) ──────────────────────────────────────────────────────\napp.state.limiter = limiter\napp.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)\n\n# ─── Middleware (порядок важливий: виконуються у зворотному порядку) ──────────\napp.add_middleware(SecurityHeadersMiddleware)\n\napp.add_middleware(\n    CORSMiddleware,\n    allow_origins=[\n        \"https:\u002F\u002Ftaskforge.app\",\n        \"http:\u002F\u002Flocalhost:3000\",\n        \"http:\u002F\u002Flocalhost:5173\",\n    ],\n    allow_credentials=True,\n    allow_methods=[\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\", \"OPTIONS\"],\n    allow_headers=[\"Authorization\", \"Content-Type\", \"X-API-Key\"],\n    max_age=600,\n)\n\n# ─── Роутери ──────────────────────────────────────────────────────────────────\napp.include_router(auth.router, prefix=\"\u002Fapi\u002Fv1\")\napp.include_router(admin.router, prefix=\"\u002Fapi\u002Fv1\")\napp.include_router(api_keys.router, prefix=\"\u002Fapi\u002Fv1\")\n\n\n@app.get(\"\u002Fhealth\", tags=[\"System\"])\nasync def health_check() -> dict:\n    return {\"status\": \"ok\"}\n",[3625,21985,21986,21991,22001,22011,22022,22032,22036,22047,22059,22071,22075,22079,22089,22099,22109,22113,22117,22122,22127,22132,22136,22141,22146,22150,22155,22160,22168,22174,22180,22186,22190,22201,22232,22253,22264,22268,22272,22277,22289,22301,22314,22318,22322,22340,22354],{"__ignoreMap":3695},[3699,21987,21988],{"class":3701,"line":3702},[3699,21989,21990],{"class":4412},"# app\u002Fmain.py — повна фінальна версія з усіма middleware\n",[3699,21992,21993,21995,21997,21999],{"class":3701,"line":3708},[3699,21994,5828],{"class":4418},[3699,21996,9576],{"class":4422},[3699,21998,4419],{"class":4418},[3699,22000,16102],{"class":4422},[3699,22002,22003,22005,22007,22009],{"class":3701,"line":3714},[3699,22004,5828],{"class":4418},[3699,22006,21676],{"class":4422},[3699,22008,4419],{"class":4418},[3699,22010,21681],{"class":4422},[3699,22012,22013,22015,22017,22019],{"class":3701,"line":3720},[3699,22014,5828],{"class":4418},[3699,22016,21931],{"class":4422},[3699,22018,4419],{"class":4418},[3699,22020,22021],{"class":4422}," _rate_limit_exceeded_handler\n",[3699,22023,22024,22026,22028,22030],{"class":3701,"line":3726},[3699,22025,5828],{"class":4418},[3699,22027,21943],{"class":4422},[3699,22029,4419],{"class":4418},[3699,22031,21948],{"class":4422},[3699,22033,22034],{"class":3701,"line":3733},[3699,22035,3730],{"emptyLinePlaceholder":3729},[3699,22037,22038,22040,22042,22044],{"class":3701,"line":3739},[3699,22039,5828],{"class":4418},[3699,22041,16113],{"class":4422},[3699,22043,4419],{"class":4418},[3699,22045,22046],{"class":4422}," admin, api_keys, auth\n",[3699,22048,22049,22051,22054,22056],{"class":3701,"line":3745},[3699,22050,5828],{"class":4418},[3699,22052,22053],{"class":4422}," app.middleware.rate_limit ",[3699,22055,4419],{"class":4418},[3699,22057,22058],{"class":4422}," limiter\n",[3699,22060,22061,22063,22066,22068],{"class":3701,"line":3751},[3699,22062,5828],{"class":4418},[3699,22064,22065],{"class":4422}," app.middleware.security_headers ",[3699,22067,4419],{"class":4418},[3699,22069,22070],{"class":4422}," SecurityHeadersMiddleware\n",[3699,22072,22073],{"class":3701,"line":3757},[3699,22074,3730],{"emptyLinePlaceholder":3729},[3699,22076,22077],{"class":3701,"line":3763},[3699,22078,16127],{"class":4422},[3699,22080,22081,22083,22085,22087],{"class":3701,"line":3769},[3699,22082,16132],{"class":4445},[3699,22084,4556],{"class":4422},[3699,22086,16137],{"class":4324},[3699,22088,7077],{"class":4422},[3699,22090,22091,22093,22095,22097],{"class":3701,"line":3775},[3699,22092,16144],{"class":4445},[3699,22094,4556],{"class":4422},[3699,22096,18898],{"class":4324},[3699,22098,7077],{"class":4422},[3699,22100,22101,22103,22105,22107],{"class":3701,"line":3781},[3699,22102,16156],{"class":4445},[3699,22104,4556],{"class":4422},[3699,22106,16161],{"class":4324},[3699,22108,7077],{"class":4422},[3699,22110,22111],{"class":3701,"line":3786},[3699,22112,4562],{"class":4422},[3699,22114,22115],{"class":3701,"line":3792},[3699,22116,3730],{"emptyLinePlaceholder":3729},[3699,22118,22119],{"class":3701,"line":3798},[3699,22120,22121],{"class":4412},"# ─── State (для slowapi) ──────────────────────────────────────────────────────\n",[3699,22123,22124],{"class":3701,"line":3804},[3699,22125,22126],{"class":4422},"app.state.limiter = limiter\n",[3699,22128,22129],{"class":3701,"line":3810},[3699,22130,22131],{"class":4422},"app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)\n",[3699,22133,22134],{"class":3701,"line":3816},[3699,22135,3730],{"emptyLinePlaceholder":3729},[3699,22137,22138],{"class":3701,"line":3822},[3699,22139,22140],{"class":4412},"# ─── Middleware (порядок важливий: виконуються у зворотному порядку) ──────────\n",[3699,22142,22143],{"class":3701,"line":3827},[3699,22144,22145],{"class":4422},"app.add_middleware(SecurityHeadersMiddleware)\n",[3699,22147,22148],{"class":3701,"line":3832},[3699,22149,3730],{"emptyLinePlaceholder":3729},[3699,22151,22152],{"class":3701,"line":3837},[3699,22153,22154],{"class":4422},"app.add_middleware(\n",[3699,22156,22157],{"class":3701,"line":3843},[3699,22158,22159],{"class":4422},"    CORSMiddleware,\n",[3699,22161,22162,22165],{"class":3701,"line":4590},[3699,22163,22164],{"class":4445},"    allow_origins",[3699,22166,22167],{"class":4422},"=[\n",[3699,22169,22170,22172],{"class":3701,"line":4595},[3699,22171,21708],{"class":4324},[3699,22173,7077],{"class":4422},[3699,22175,22176,22178],{"class":3701,"line":4627},[3699,22177,21729],{"class":4324},[3699,22179,7077],{"class":4422},[3699,22181,22182,22184],{"class":3701,"line":4632},[3699,22183,21740],{"class":4324},[3699,22185,7077],{"class":4422},[3699,22187,22188],{"class":3701,"line":4638},[3699,22189,21750],{"class":4422},[3699,22191,22192,22195,22197,22199],{"class":3701,"line":4643},[3699,22193,22194],{"class":4445},"    allow_credentials",[3699,22196,4556],{"class":4422},[3699,22198,9303],{"class":4435},[3699,22200,7077],{"class":4422},[3699,22202,22203,22206,22208,22210,22212,22214,22216,22218,22220,22222,22224,22226,22228,22230],{"class":3701,"line":4648},[3699,22204,22205],{"class":4445},"    allow_methods",[3699,22207,7938],{"class":4422},[3699,22209,21776],{"class":4324},[3699,22211,4200],{"class":4422},[3699,22213,21781],{"class":4324},[3699,22215,4200],{"class":4422},[3699,22217,21786],{"class":4324},[3699,22219,4200],{"class":4422},[3699,22221,21791],{"class":4324},[3699,22223,4200],{"class":4422},[3699,22225,21796],{"class":4324},[3699,22227,4200],{"class":4422},[3699,22229,21801],{"class":4324},[3699,22231,20108],{"class":4422},[3699,22233,22234,22237,22239,22242,22244,22247,22249,22251],{"class":3701,"line":4654},[3699,22235,22236],{"class":4445},"    allow_headers",[3699,22238,7938],{"class":4422},[3699,22240,22241],{"class":4324},"\"Authorization\"",[3699,22243,4200],{"class":4422},[3699,22245,22246],{"class":4324},"\"Content-Type\"",[3699,22248,4200],{"class":4422},[3699,22250,20402],{"class":4324},[3699,22252,20108],{"class":4422},[3699,22254,22255,22258,22260,22262],{"class":3701,"line":4660},[3699,22256,22257],{"class":4445},"    max_age",[3699,22259,4556],{"class":4422},[3699,22261,21873],{"class":4531},[3699,22263,7077],{"class":4422},[3699,22265,22266],{"class":3701,"line":4665},[3699,22267,4562],{"class":4422},[3699,22269,22270],{"class":3701,"line":4670},[3699,22271,3730],{"emptyLinePlaceholder":3729},[3699,22273,22274],{"class":3701,"line":4676},[3699,22275,22276],{"class":4412},"# ─── Роутери ──────────────────────────────────────────────────────────────────\n",[3699,22278,22279,22281,22283,22285,22287],{"class":3701,"line":4681},[3699,22280,16176],{"class":4422},[3699,22282,10639],{"class":4445},[3699,22284,4556],{"class":4422},[3699,22286,16183],{"class":4324},[3699,22288,4562],{"class":4422},[3699,22290,22291,22293,22295,22297,22299],{"class":3701,"line":4694},[3699,22292,18935],{"class":4422},[3699,22294,10639],{"class":4445},[3699,22296,4556],{"class":4422},[3699,22298,16183],{"class":4324},[3699,22300,4562],{"class":4422},[3699,22302,22303,22306,22308,22310,22312],{"class":3701,"line":4704},[3699,22304,22305],{"class":4422},"app.include_router(api_keys.router, ",[3699,22307,10639],{"class":4445},[3699,22309,4556],{"class":4422},[3699,22311,16183],{"class":4324},[3699,22313,4562],{"class":4422},[3699,22315,22316],{"class":3701,"line":4709},[3699,22317,3730],{"emptyLinePlaceholder":3729},[3699,22319,22320],{"class":3701,"line":6342},[3699,22321,3730],{"emptyLinePlaceholder":3729},[3699,22323,22324,22326,22328,22330,22332,22334,22336,22338],{"class":3701,"line":6348},[3699,22325,16198],{"class":4321},[3699,22327,4442],{"class":4422},[3699,22329,16203],{"class":4324},[3699,22331,4200],{"class":4422},[3699,22333,10649],{"class":4445},[3699,22335,7938],{"class":4422},[3699,22337,16212],{"class":4324},[3699,22339,10657],{"class":4422},[3699,22341,22342,22344,22346,22348,22350,22352],{"class":3701,"line":6354},[3699,22343,9702],{"class":4435},[3699,22345,9705],{"class":4435},[3699,22347,16223],{"class":4321},[3699,22349,8622],{"class":4422},[3699,22351,7621],{"class":4452},[3699,22353,4460],{"class":4422},[3699,22355,22356,22358,22360,22362,22364,22366],{"class":3701,"line":6359},[3699,22357,4576],{"class":4418},[3699,22359,16236],{"class":4422},[3699,22361,16239],{"class":4324},[3699,22363,4449],{"class":4422},[3699,22365,16244],{"class":4324},[3699,22367,3778],{"class":4422},[3556,22369,22370],{},"Тепер застосовуємо rate limiting до чутливих ендпоінтів:",[3690,22372,22374],{"className":4403,"code":22373,"language":4405,"meta":3695,"style":3695},"# app\u002Fapi\u002Fv1\u002Fauth.py — додаємо @limiter.limit до login та register\nfrom app.middleware.rate_limit import limiter\n\n\n@router.post(\"\u002Flogin\", response_model=TokenResponse)\n@limiter.limit(\"5\u002Fminute\")   # не більше 5 спроб логіну з одного IP за хвилину\nasync def login(\n    request: Request,\n    form_data: OAuth2PasswordRequestForm = Depends(),\n    db: AsyncSession = Depends(get_db),\n) -> TokenResponse:\n    # ... (повна реалізація вже у Частині 3) ...\n    pass\n\n\n@router.post(\"\u002Fregister\", response_model=TokenResponse, status_code=status.HTTP_201_CREATED)\n@limiter.limit(\"3\u002Fhour\")     # не більше 3 реєстрацій з одного IP за годину\nasync def register(\n    request: Request,\n    data: UserRegisterRequest,\n    db: AsyncSession = Depends(get_db),\n) -> TokenResponse:\n    # ... (повна реалізація вже у Частині 3) ...\n    pass\n",[3625,22375,22376,22381,22391,22395,22399,22413,22429,22439,22445,22451,22457,22461,22466,22470,22474,22478,22496,22511,22521,22527,22533,22539,22543,22547],{"__ignoreMap":3695},[3699,22377,22378],{"class":3701,"line":3702},[3699,22379,22380],{"class":4412},"# app\u002Fapi\u002Fv1\u002Fauth.py — додаємо @limiter.limit до login та register\n",[3699,22382,22383,22385,22387,22389],{"class":3701,"line":3708},[3699,22384,5828],{"class":4418},[3699,22386,22053],{"class":4422},[3699,22388,4419],{"class":4418},[3699,22390,22058],{"class":4422},[3699,22392,22393],{"class":3701,"line":3714},[3699,22394,3730],{"emptyLinePlaceholder":3729},[3699,22396,22397],{"class":3701,"line":3720},[3699,22398,3730],{"emptyLinePlaceholder":3729},[3699,22400,22401,22403,22405,22407,22409,22411],{"class":3701,"line":3726},[3699,22402,10348],{"class":4321},[3699,22404,4442],{"class":4422},[3699,22406,10984],{"class":4324},[3699,22408,4200],{"class":4422},[3699,22410,10679],{"class":4445},[3699,22412,10991],{"class":4422},[3699,22414,22415,22418,22420,22423,22426],{"class":3701,"line":3733},[3699,22416,22417],{"class":4321},"@limiter.limit",[3699,22419,4442],{"class":4422},[3699,22421,22422],{"class":4324},"\"5\u002Fminute\"",[3699,22424,22425],{"class":4422},")   ",[3699,22427,22428],{"class":4412},"# не більше 5 спроб логіну з одного IP за хвилину\n",[3699,22430,22431,22433,22435,22437],{"class":3701,"line":3739},[3699,22432,9702],{"class":4435},[3699,22434,9705],{"class":4435},[3699,22436,11000],{"class":4321},[3699,22438,8268],{"class":4422},[3699,22440,22441,22443],{"class":3701,"line":3745},[3699,22442,15148],{"class":4445},[3699,22444,15151],{"class":4422},[3699,22446,22447,22449],{"class":3701,"line":3751},[3699,22448,11007],{"class":4445},[3699,22450,11010],{"class":4422},[3699,22452,22453,22455],{"class":3701,"line":3757},[3699,22454,9727],{"class":4445},[3699,22456,9730],{"class":4422},[3699,22458,22459],{"class":3701,"line":3763},[3699,22460,10717],{"class":4422},[3699,22462,22463],{"class":3701,"line":3769},[3699,22464,22465],{"class":4412},"    # ... (повна реалізація вже у Частині 3) ...\n",[3699,22467,22468],{"class":3701,"line":3775},[3699,22469,6023],{"class":4418},[3699,22471,22472],{"class":3701,"line":3781},[3699,22473,3730],{"emptyLinePlaceholder":3729},[3699,22475,22476],{"class":3701,"line":3786},[3699,22477,3730],{"emptyLinePlaceholder":3729},[3699,22479,22480,22482,22484,22486,22488,22490,22492,22494],{"class":3701,"line":3792},[3699,22481,10348],{"class":4321},[3699,22483,4442],{"class":4422},[3699,22485,10674],{"class":4324},[3699,22487,4200],{"class":4422},[3699,22489,10679],{"class":4445},[3699,22491,10682],{"class":4422},[3699,22493,7984],{"class":4445},[3699,22495,10687],{"class":4422},[3699,22497,22498,22500,22502,22505,22508],{"class":3701,"line":3798},[3699,22499,22417],{"class":4321},[3699,22501,4442],{"class":4422},[3699,22503,22504],{"class":4324},"\"3\u002Fhour\"",[3699,22506,22507],{"class":4422},")     ",[3699,22509,22510],{"class":4412},"# не більше 3 реєстрацій з одного IP за годину\n",[3699,22512,22513,22515,22517,22519],{"class":3701,"line":3804},[3699,22514,9702],{"class":4435},[3699,22516,9705],{"class":4435},[3699,22518,10696],{"class":4321},[3699,22520,8268],{"class":4422},[3699,22522,22523,22525],{"class":3701,"line":3810},[3699,22524,15148],{"class":4445},[3699,22526,15151],{"class":4422},[3699,22528,22529,22531],{"class":3701,"line":3816},[3699,22530,10703],{"class":4445},[3699,22532,10706],{"class":4422},[3699,22534,22535,22537],{"class":3701,"line":3822},[3699,22536,9727],{"class":4445},[3699,22538,9730],{"class":4422},[3699,22540,22541],{"class":3701,"line":3827},[3699,22542,10717],{"class":4422},[3699,22544,22545],{"class":3701,"line":3832},[3699,22546,22465],{"class":4412},[3699,22548,22549],{"class":3701,"line":3837},[3699,22550,6023],{"class":4418},[3556,22552,22553,22554,4400],{},"Встановлення ",[3625,22555,21909],{},[4304,22557,22558,22574,22589],{},[4307,22559,22560],{"label":4309},[3690,22561,22563],{"className":4312,"code":22562,"language":4314,"meta":3695,"style":3695},"pip install slowapi\n",[3625,22564,22565],{"__ignoreMap":3695},[3699,22566,22567,22569,22571],{"class":3701,"line":3702},[3699,22568,4309],{"class":4321},[3699,22570,4325],{"class":4324},[3699,22572,22573],{"class":4324}," slowapi\n",[4307,22575,22576],{"label":4331},[3690,22577,22579],{"className":4312,"code":22578,"language":4314,"meta":3695,"style":3695},"uv add slowapi\n",[3625,22580,22581],{"__ignoreMap":3695},[3699,22582,22583,22585,22587],{"class":3701,"line":3702},[3699,22584,4331],{"class":4321},[3699,22586,4343],{"class":4324},[3699,22588,22573],{"class":4324},[4307,22590,22591],{"label":4348},[3690,22592,22594],{"className":4312,"code":22593,"language":4314,"meta":3695,"style":3695},"poetry add slowapi\n",[3625,22595,22596],{"__ignoreMap":3695},[3699,22597,22598,22600,22602],{"class":3701,"line":3702},[3699,22599,4348],{"class":4321},[3699,22601,4343],{"class":4324},[3699,22603,22573],{"class":4324},[3654,22605],{},[3551,22607,22609],{"id":22608},"практичні-завдання","Практичні завдання",[3661,22611,22613],{"id":22612},"рівень-1-реєстрація-та-логін-з-jwt","Рівень 1: Реєстрація та логін з JWT",[3556,22615,22616,22619],{},[3560,22617,22618],{},"Завдання:"," Запустіть повну реалізацію зі секції «Повна реалізація» та протестуйте через Swagger UI.",[3556,22621,22622],{},[3560,22623,22624],{},"Кроки:",[4756,22626,22627,22633,22645,22648,22657],{},[4759,22628,22629,22630,4380],{},"Скопіюйте всі файли з секції ",[3625,22631,22632],{},"::code-tree",[4759,22634,22635,22636,22638,22639,22641,22642,5462],{},"Створіть ",[3625,22637,11928],{}," із власним ",[3625,22640,9120],{}," (згенеруйте через ",[3625,22643,22644],{},"openssl rand -hex 32",[4759,22646,22647],{},"Запустіть PostgreSQL та сервер.",[4759,22649,16548,22650,22653,22654,4380],{},[3625,22651,22652],{},"\u002Fdocs"," → зареєструйтесь через ",[3625,22655,22656],{},"\u002Fapi\u002Fv1\u002Fauth\u002Fregister",[4759,22658,22659,22660,22662,22663,22665,22666,4380],{},"Натисніть ",[3560,22661,9323],{}," → вставте ",[3625,22664,15436],{}," → перевірте ",[3625,22667,22668],{},"\u002Fhealth",[3556,22670,22671,4816,22674,22676,22677,4733,22679,22681,22682,22685,22686,4380],{},[3560,22672,22673],{},"Критерій успіху:",[3625,22675,16559],{}," повертає ",[3625,22678,15436],{},[3625,22680,15442],{},". Запит до захищеного ендпоінта з токеном повертає ",[3625,22683,22684],{},"200 OK",", без токена — ",[3625,22687,7989],{},[3661,22689,22691],{"id":22690},"рівень-2-refresh-token-flow-з-ротацією","Рівень 2: Refresh Token Flow з ротацією",[3556,22693,22694,22696],{},[3560,22695,22618],{}," Перевірте механізм ротації refresh token руками.",[3556,22698,22699],{},[3560,22700,22624],{},[4756,22702,22703,22712,22725,22733,22739],{},[4759,22704,22705,22706,22708,22709,5462],{},"Залогіньтесь → збережіть ",[3625,22707,15442],{}," (назвемо ",[3625,22710,22711],{},"RT1",[4759,22713,22714,22715,22718,22719,22721,22722,4380],{},"Зробіть запит ",[3625,22716,22717],{},"POST \u002Fapi\u002Fv1\u002Fauth\u002Frefresh"," з ",[3625,22720,22711],{}," → отримайте ",[3625,22723,22724],{},"RT2",[4759,22726,22727,22728,22718,22730,22732],{},"Знову зробіть ",[3625,22729,22717],{},[3625,22731,22711],{}," (вже використаним).",[4759,22734,22735,22736,4380],{},"Переконайтесь, що відповідь — ",[3625,22737,22738],{},"401 \"Refresh token not found or already used\"",[4759,22740,22741,22742,22744],{},"Перевірте таблицю ",[3625,22743,7515],{}," у БД:",[3690,22746,22748],{"className":7005,"code":22747,"language":7007,"meta":3695,"style":3695},"SELECT token_hash, user_id, expires_at, created_at FROM refresh_tokens;\n",[3625,22749,22750],{"__ignoreMap":3695},[3699,22751,22752,22755,22758,22761],{"class":3701,"line":3702},[3699,22753,22754],{"class":4435},"SELECT",[3699,22756,22757],{"class":4422}," token_hash, user_id, expires_at, created_at ",[3699,22759,22760],{"class":4435},"FROM",[3699,22762,22763],{"class":4422}," refresh_tokens;\n",[3556,22765,22766,22768,22769,22771,22772,4380],{},[3560,22767,22673],{}," Після ротації стара ",[3625,22770,14219],{}," більше не існує у БД. Повторне використання старого токена — ",[3625,22773,7989],{},[3661,22775,22777],{"id":22776},"рівень-3-rbac-система-з-ролями","Рівень 3: RBAC-система з ролями",[3556,22779,22780,22782,22783,22786],{},[3560,22781,22618],{}," Додайте роль ",[3625,22784,22785],{},"ADMIN"," до свого акаунта та перевірте матрицю доступу.",[3556,22788,22789],{},[3560,22790,22624],{},[4756,22792,22793,22803,22811,22817],{},[4759,22794,22795,22796,22718,22799,22802],{},"Додайте модуль ",[3625,22797,22798],{},"app\u002Fmodels\u002Frole.py",[3625,22800,22801],{},"UserRole"," enum.",[4759,22804,22805,22806,22808,22809,4380],{},"Оновіть ",[3625,22807,12991],{}," — додайте поле ",[3625,22810,6594],{},[4759,22812,22813,22814,5462],{},"Перестворіть таблиці (",[3625,22815,22816],{},"python create_tables.py",[4759,22818,22819],{},"Вручну оновіть роль свого користувача у БД:",[3690,22821,22823],{"className":7005,"code":22822,"language":7007,"meta":3695,"style":3695},"UPDATE users SET role = 'admin' WHERE email = 'your@email.com';\n",[3625,22824,22825],{"__ignoreMap":3695},[3699,22826,22827,22830,22832,22835,22838,22840,22843,22846,22849,22852],{"class":3701,"line":3702},[3699,22828,22829],{"class":4435},"UPDATE",[3699,22831,18761],{"class":4422},[3699,22833,22834],{"class":4435},"SET",[3699,22836,22837],{"class":4435}," role",[3699,22839,8297],{"class":4422},[3699,22841,22842],{"class":4324},"'admin'",[3699,22844,22845],{"class":4435}," WHERE",[3699,22847,22848],{"class":4422}," email = ",[3699,22850,22851],{"class":4324},"'your@email.com'",[3699,22853,22854],{"class":4422},";\n",[4756,22856,22857,22865,22872],{"start":3726},[4759,22858,22859,22860,19039,22863,4380],{},"Зареєструйте ",[3625,22861,22862],{},"admin.router",[3625,22864,18836],{},[4759,22866,22867,22868,22871],{},"Залогіньтесь → перевірте ",[3625,22869,22870],{},"GET \u002Fapi\u002Fv1\u002Fadmin\u002Fusers"," — має повернути список.",[4759,22873,22874,22875,22877,22878,4380],{},"Зареєструйте нового звичайного користувача і спробуйте ",[3625,22876,22870],{}," з його токеном — має бути ",[3625,22879,22880],{},"403",[3556,22882,22883,22885,22886,22889,22890,4380],{},[3560,22884,22673],{}," Endpoint ",[3625,22887,22888],{},"\u002Fadmin\u002Fusers"," доступний лише з токеном адміністратора. З токеном user-а — ",[3625,22891,22892],{},"403 Forbidden",[3654,22894],{},[3551,22896,22898],{"id":22897},"підсумок","Підсумок",[3556,22900,22901],{},"У цій статті ми побудували повноцінну production-ready систему автентифікації та авторизації для FastAPI. Ось що реалізовано:",[3576,22903,22904,22912,22932,22946,22962,22972,22987],{},[3579,22905,22908,22909,22911],{"icon":22906,"title":22907},"i-heroicons-lock-closed","Хешування паролів","Пряме використання бібліотеки ",[3625,22910,4006],{}," з work factor (rounds) 12. Попереднє обрізання до 72 байтів для запобігання ValueError. Порівняння bcrypt \u002F Argon2 \u002F SHA-256 з реальними числами атак.",[3579,22913,22915,22917,22918,4200,22920,4200,22922,4200,22924,22927,22928,22931],{"icon":6078,"title":22914},"JWT Access Token",[3625,22916,7524],{}," + HS256. Claims: ",[3625,22919,6495],{},[3625,22921,6547],{},[3625,22923,6521],{},[3625,22925,22926],{},"type",". Короткий час життя (15 хв). Claim ",[3625,22929,22930],{},"type=access"," — захист від підміни токена.",[3579,22933,22936,22938,22939,4733,22942,22945],{"icon":22934,"title":22935},"i-heroicons-arrow-path","Opaque Refresh Token",[3625,22937,7511],{}," — 256 біт ентропії. SHA-256 хеш у БД. Ротація при кожному використанні. Збереження ",[3625,22940,22941],{},"user_agent",[3625,22943,22944],{},"ip_address"," для аудиту.",[3579,22947,22949,22951,22952,22954,22955,22957,22958,22961],{"icon":3593,"title":22948},"OAuth2 + FastAPI",[3625,22950,9232],{}," + ",[3625,22953,9335],{},". Ланцюжок залежностей: ",[3625,22956,19125],{}," → ",[3625,22959,22960],{},"get_current_active_user",". Swagger UI «Authorize» з одного налаштування.",[3579,22963,22965,22968,22969,22971],{"icon":16600,"title":22964},"RBAC",[3625,22966,22967],{},"UserRole(str, Enum)"," — USER\u002FMANAGER\u002FADMIN. Dependency-фабрика ",[3625,22970,17136],{},". Роль у JWT для оптимізації (з caveat про миттєве відкликання).",[3579,22973,22975,22976,22979,22980,22983,22984,22986],{"icon":22974,"title":19254},"i-heroicons-command-line","Для сервіс-до-сервісу комунікації. ",[3625,22977,22978],{},"tf_live_"," prefix + SHA-256 хеш у БД. Відстеження ",[3625,22981,22982],{},"last_used_at",". ",[3625,22985,19312],{}," header (не query param).",[3579,22988,22991,22994,22995,22998,22999,23001],{"icon":22989,"title":22990},"i-heroicons-shield-exclamation","Security Headers",[3625,22992,22993],{},"SecurityHeadersMiddleware",": HSTS, X-Content-Type-Options, X-Frame-Options, CSP. CORS з явним ",[3625,22996,22997],{},"allow_origins",". Rate limiting через ",[3625,23000,21909],{}," (5\u002Fхв для login).",[3661,23003,23005],{"id":23004},"що-залишилось-за-межами-статті-для-поглибленого-вивчення","Що залишилось за межами статті (для поглибленого вивчення)",[6909,23007,23008,23015,23021,23034,23043],{},[4759,23009,23010,23012,23013,4380],{},[3560,23011,9200],{}," — «Увійти через Google\u002FGitHub». Бібліотека ",[3625,23014,9226],{},[4759,23016,23017,23020],{},[3560,23018,23019],{},"Asymmetric JWT (RS256\u002FES256)"," — RSA\u002FEC ключові пари для мікросервісної архітектури де різні сервіси мають верифікувати токени, але не мають права їх видавати.",[4759,23022,23023,23026,23027,23030,23031,4380],{},[3560,23024,23025],{},"Permission-based авторизація на рівні об'єкта"," — «чи може цей user редагувати саме цей проєкт?» Через ",[3625,23028,23029],{},"sqlalchemy"," + перевірку ",[3625,23032,23033],{},"ProjectMember.role",[4759,23035,23036,5003,23039,23042],{},[3560,23037,23038],{},"Two-Factor Authentication (2FA\u002FTOTP)",[3625,23040,23041],{},"pyotp"," бібліотека, Google Authenticator сумісність.",[4759,23044,23045,5003,23048,23051],{},[3560,23046,23047],{},"Audit Logging",[3625,23049,23050],{},"structlog"," для структурованих логів автентифікаційних подій.",[23053,23054,23055],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s8Opu, html code.shiki .s8Opu{--shiki-light:#795E26;--shiki-default:#DCDCAA;--shiki-dark:#DCDCAA}html pre.shiki code .sbdoH, html code.shiki .sbdoH{--shiki-light:#A31515;--shiki-default:#CE9178;--shiki-dark:#CE9178}html pre.shiki code .spJ8K, html code.shiki .spJ8K{--shiki-light:#008000;--shiki-default:#6A9955;--shiki-dark:#6A9955}html pre.shiki code .s8xlr, html code.shiki .s8xlr{--shiki-light:#AF00DB;--shiki-default:#C586C0;--shiki-dark:#C586C0}html pre.shiki code .sHH4Y, html code.shiki .sHH4Y{--shiki-light:#000000;--shiki-default:#D4D4D4;--shiki-dark:#D4D4D4}html pre.shiki code .su1O8, html code.shiki .su1O8{--shiki-light:#0000FF;--shiki-default:#569CD6;--shiki-dark:#569CD6}html pre.shiki code .siwwj, html code.shiki .siwwj{--shiki-light:#001080;--shiki-default:#9CDCFE;--shiki-dark:#9CDCFE}html pre.shiki code .sN1BT, html code.shiki .sN1BT{--shiki-light:#267F99;--shiki-default:#4EC9B0;--shiki-dark:#4EC9B0}html pre.shiki code .sJj4R, html code.shiki .sJj4R{--shiki-light:#098658;--shiki-default:#B5CEA8;--shiki-dark:#B5CEA8}html pre.shiki code .sjcCO, html code.shiki .sjcCO{--shiki-light:#EE0000;--shiki-default:#D7BA7D;--shiki-dark:#D7BA7D}html pre.shiki code .sKtos, html code.shiki .sKtos{--shiki-light:#800000;--shiki-default:#569CD6;--shiki-dark:#569CD6}",{"title":3695,"searchDepth":3708,"depth":3708,"links":23057},[23058,23059,23071,23080,23090,23100,23110,23115,23121,23126],{"id":3553,"depth":3708,"text":3554},{"id":3658,"depth":3708,"text":3659,"children":23060},[23061,23062,23063,23064,23066,23068,23069],{"id":3663,"depth":3714,"text":3664},{"id":3856,"depth":3714,"text":3857},{"id":4210,"depth":3714,"text":4211},{"id":4264,"depth":3714,"text":23065},"Хешування паролів у Python: перехід на чистий bcrypt",{"id":4729,"depth":3714,"text":23067},"Спадкова альтернатива: використання passlib та CryptContext",{"id":5022,"depth":3714,"text":5023},{"id":5731,"depth":3714,"text":23070},"Бібліотека argon2-cffi — переможець PHC",{"id":6039,"depth":3708,"text":6040,"children":23072},[23073,23074,23075,23076,23077,23078],{"id":6043,"depth":3714,"text":6044},{"id":6117,"depth":3714,"text":6118},{"id":6438,"depth":3714,"text":6439},{"id":6622,"depth":3714,"text":6623},{"id":6871,"depth":3714,"text":6872},{"id":7521,"depth":3714,"text":23079},"Бібліотека python-jose — створення та верифікація JWT",{"id":9189,"depth":3708,"text":9190,"children":23081},[23082,23083,23085,23087,23089],{"id":9193,"depth":3714,"text":9194},{"id":9229,"depth":3714,"text":23084},"OAuth2PasswordBearer — схема безпеки",{"id":9332,"depth":3714,"text":23086},"OAuth2PasswordRequestForm — форма логіну",{"id":9400,"depth":3714,"text":23088},"Ланцюжок авторизації: Depends(get_current_user)",{"id":10413,"depth":3714,"text":10414},{"id":11767,"depth":3708,"text":11768,"children":23091},[23092,23093,23094,23095,23096,23097,23098,23099],{"id":11778,"depth":3714,"text":11779},{"id":11788,"depth":3714,"text":11789},{"id":11919,"depth":3714,"text":11920},{"id":16412,"depth":3714,"text":16413},{"id":16419,"depth":3714,"text":16420},{"id":16497,"depth":3714,"text":16498},{"id":16518,"depth":3714,"text":16519},{"id":16544,"depth":3714,"text":16545},{"id":16572,"depth":3708,"text":16573,"children":23101},[23102,23103,23104,23105,23107,23108,23109],{"id":16576,"depth":3714,"text":16577},{"id":16641,"depth":3714,"text":16642},{"id":16800,"depth":3714,"text":16801},{"id":17132,"depth":3714,"text":23106},"Dependency-фабрика require_roles()",{"id":17976,"depth":3714,"text":17977},{"id":19031,"depth":3714,"text":19032},{"id":19129,"depth":3714,"text":19130},{"id":19237,"depth":3708,"text":19238,"children":23111},[23112,23113,23114],{"id":19241,"depth":3714,"text":19242},{"id":19273,"depth":3714,"text":19274},{"id":19339,"depth":3714,"text":19340},{"id":21147,"depth":3708,"text":21148,"children":23116},[23117,23118,23119,23120],{"id":21151,"depth":3714,"text":21152},{"id":21265,"depth":3714,"text":21266},{"id":21652,"depth":3714,"text":21653},{"id":21902,"depth":3714,"text":21903},{"id":22608,"depth":3708,"text":22609,"children":23122},[23123,23124,23125],{"id":22612,"depth":3714,"text":22613},{"id":22690,"depth":3714,"text":22691},{"id":22776,"depth":3714,"text":22777},{"id":22897,"depth":3708,"text":22898,"children":23127},[23128],{"id":23004,"depth":3714,"text":23005},"Глибоке занурення у безпеку FastAPI — хешування паролів, JSON Web Tokens, OAuth2 flow, RBAC на рівні проєкту та Security Headers. Порівняння з ASP.NET підходом для розробників, що мігрують на Python.","md",null,{},{"title":2654,"description":23129},"yCahLymt-iejLe2vHgAy90HV9hqA9Tb7l5gEjXAOa34",[23136,23138],{"title":2650,"path":2651,"stem":2652,"description":23137,"children":-1},"Принципи побудови масштабованих систем на FastAPI за допомогою чистої архітектури, патерну Репозиторій (Repository) та Unit of Work для ізоляції бізнес-логіки від деталей інфраструктури.",{"title":2658,"path":2659,"stem":2660,"description":23139,"children":-1},"Глибоке занурення у тестування FastAPI застосунків. Перехід від концепцій xUnit до pytest, налаштування середовища, unit-тестування Pydantic-схем та параметризовані тести.",1787230575534]