├── .gitignore ├── README.md ├── account ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── models.py ├── static │ ├── js │ │ ├── dashboard.js │ │ ├── login.js │ │ └── register.js │ └── stylesheets │ │ └── login.css ├── templates │ ├── account │ │ ├── dashboard.html │ │ ├── register.html │ │ └── register_done.html │ ├── base.html │ └── registration │ │ └── login.html ├── tests.py ├── urls.py └── views.py ├── db.sqlite3 ├── django_jwt_tutorial ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── manage.py ├── musics ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── serializers.py ├── tests.py └── views.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.idea 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-jwt-tutorial 2 | 3 | 認識 JWT 以及透過 Django 實戰 📝 4 | 5 | * [Youtube Tutorial Part1 - JWT tutorial](https://youtu.be/p4uWTwkGtZk) 6 | * [Youtube Tutorial Part2 - django-rest-framework-jwt tutorial](https://youtu.be/CJOysCNAf4s) 7 | * [Youtube Tutorial Part3 - django + jwt tutorial](https://youtu.be/I_vXGjf8t88) 8 | 9 | ## 前言 10 | 11 | 在設計 API 時,通常會有授權以及驗證,而現在很多設計又都是前後端分離,所以,讓我們來了解一下什麼是 JWT :smirk: 12 | 13 | 本篇文章會介紹 [djangorestframework-simplejwt](https://github.com/jazzband/djangorestframework-simplejwt) 這個套件,以及說明 JWT 原理,最後是簡單的實戰。 14 | 15 | 在開始介紹之前,先讓我們來了解 authentication 以及 authorization 之間的差異。 16 | 17 | ### authentication VS authorization 18 | 19 | 這兩個有甚麼差別呢 ? 20 | 21 | 先來看看參考 Django 官網對 [auth](https://docs.djangoproject.com/en/2.0/topics/auth/) 的介紹, 22 | 23 | 原文如下 24 | 25 | ```text 26 | The Django authentication system handles both authentication and authorization. 27 | authentication verifies a user is who they claim to be, 28 | authorization determines what an authenticated user is allowed to do. 29 | ``` 30 | 31 | 舉個例子,假設小明今天輸入帳號密碼成功登入一個網站,這個行為就稱為 **authentication**, 32 | 33 | 而登入之後,可能小明擁有管理員的身分進行刪除文章,這個行為就叫做 **authorization**。 34 | 35 | 換個說法, **authentication** 是確認是否真的有這個人,而 **authorization** 則是這個人是否有 36 | 37 | 權限做一些事情。 38 | 39 | ## JWT 介紹 40 | 41 | 詳細的說明非常建議大家閱讀官方的 [https://jwt.io/introduction/](https://jwt.io/introduction/) 文件,我會挑一些重點出來說明。 42 | 43 | ### 什麼是 JWT 44 | 45 | JWT 全名為 JSON Web Token,是一種公開的標準 ( RFC 7519 ),這種標準定義了 compact 以及 self-contained 的方法, 46 | 47 | 使用JSON 的形式在各方之間安全的傳遞。 48 | 49 | 剛剛提到了 compact 以及 self-contained ,這邊說明一下 :relaxed: 50 | 51 | **Compact** : 因為 JWT 他們的 size 比較小,所以可以透過 URL POST 參數的形式或是加在一個 HTTP header 裡, 52 | 53 | 此外,size小代表傳輸越快。 54 | 55 | **Self-contained** : payload 裡面包含了使用者的資訊,也就是說解析後就可以看到,不需要再去 query 你的 database。 56 | 57 | ### 什麼時候應該使用 JWT 58 | 59 | 最普遍的情境就是 Authentication。 60 | 61 | 使用者一次性的登入成功後,後續的每一個 request 都包含了 JWT,允許使用者瀏覽 routess, services, resources 。 62 | 63 | Single Sign On ( SSO ) ,又稱單一登入或單點登入,是目前廣泛使用 JWT 的一項功能,因為它的開銷很小,而且 64 | 65 | 可以很輕鬆的跨 domains :thumbsup: 66 | 67 | ### JWT 結構 68 | 69 | 在 compact form 中,JWT 由三個 `.` 所組成,分別為 `Header`,`Payload`,`Signature`。 70 | 71 | 所以說,一般的 JWT 格式看起來會像是這樣,`xxxxx.yyyyy.zzzzz`。 72 | 73 | `Header` 74 | 75 | 這部分通常包含兩個部分,JWT 以及 hashing algorithm。 76 | 77 | ```json 78 | { 79 | "typ": "JWT", 80 | "alg": "HS256" 81 | } 82 | ``` 83 | 84 | 這個部分是屬於整個 JWT 的第一個部分 ( 經過 Base64Url encoded ) 。 85 | 86 | `Payload` 87 | 88 | 這部分包含了 claims,claims 通常指的是一個實體 ( 一般來說就 user ) 以及一些額外的資訊,有三種 claims, 89 | 90 | 分別為 `Registered claims`,`Public claims`,`Private claims`。 91 | 92 | `Registered claims` 93 | 94 | 它是一個預先定義的 claims,沒有強制性 ( 但建議使用 ),提供一些實用的內容,如下, 95 | 96 | * "iss" (Issuer) Claim 97 | 98 | * "sub" (Subject) Claim 99 | 100 | * "aud" (Audience) Claim 101 | 102 | * "exp" (Expiration Time) Claim 103 | 104 | 當然不只上面所提到的這些, 105 | 106 | 這邊你可能會問,為什麼都只有三個英文字母做代表 :question: 107 | 108 | 還記得前面提到的 compact 嗎 :question: 這就是原因 ( size 能小點就小一點 )。 109 | 110 | `Public claims` 111 | 112 | 這些是由使用 JWT 的人下去定義的,但避免使用到已定義的名稱 ( 造成衝突 ),所以應該被定義在 113 | 114 | [IANA JSON Web Token Registry](https://www.iana.org/assignments/jwt/jwt.xhtml) 中,或是多使用一些額外的名稱避免衝突。 115 | 116 | `Private claims` 117 | 118 | These are the custom claims created to share information between parties that agree on using them and are neither registered or public claims. 119 | 120 | 底下是一個範例的 payload, 121 | 122 | ```json 123 | { 124 | "sub": "1234567890", 125 | "name": "John Doe", 126 | "admin": true 127 | } 128 | ``` 129 | 130 | 這個部分是屬於整個 JWT 的第二個部分 ( 經過 Base64Url encoded ) 。 131 | 132 | ***這邊要注意一點,JWT 裡面的資訊,基本上任何人都可以閱讀 ( 除非你有額外加密 ),否則不要放重要的資訊在你的 JWT 裡面。*** 133 | 134 | `Signature` 135 | 136 | 要創造一個 signature,你需要有 encoded header,encoded payload,a secret ( 以 Django 來說,通常是你在 137 | 138 | `settings.py` 裡面的 `SECRET_KEY` ),the algorithm specified in the header。 139 | 140 | 假如我們使用 HMAC SHA256 algorithm ,這個 signature 創造的方式如下 141 | 142 | ```text 143 | HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret) 144 | ``` 145 | 146 | 這個部分是屬於整個 JWT 的第三個部分 ( 經過 Base64Url encoded ) 。 147 | 148 | 介紹完 JWT 的三個部分之後,我們把在合在一起來看一遍,以下舉例, 149 | 150 | ```token 151 | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6InR3dHJ1YmlrcyIsImV4cCI6MTUyNTY2MjcxNSwiZW1haWwiOiJ0d3RydWJpa3NAZ21haWwuY29tIiwib3JpZ19pYXQiOjE1MjU2NjI0MTV9.jMqH_jKv5nJuxQ7whpOE5kMyekxTqzsDw8iaibS9Cyo 152 | ``` 153 | 154 | 可以把它貼到 [jwt.io Debugger](http://jwt.io/) 中觀看,如下圖, 155 | 156 | ![alt tag](https://i.imgur.com/m41314q.png) 157 | 158 | 這邊特別說明一下紅色框起來的 `your-256-bit-secret` 這個,基本上這個 secret 就是你在 `settings.py` 中的 `SECRET_KEY`。 159 | 160 | ### JWT 如何運作的呢 161 | 162 | 在 authentication 中,當使用者成功的登入後,server 會回傳一個 JWT 給前端,通常這個 JWT 會在前端被儲存起來, 163 | 164 | 一般來說是存在 local storage 中,但也可以存在 cookies 中。 165 | 166 | JWT 的方法和傳統在 server 中創造一個 session 然後回傳 cookie 的方法不同。 167 | 168 | JWT 儲存的方式也各有優缺點,詳細請參考 [Where to Store Tokens](https://auth0.com/docs/security/store-tokens)。 169 | 170 | 每當使用者想要 access 受保護的 route or resource 時,使用者都必須帶上 JWT,一般 Authorization header 是使用 171 | 172 | Bearer schema,header 的內容看起來如下, 173 | 174 | ```header 175 | Authorization: Bearer 176 | ``` 177 | 178 | 這是一個無狀態的 authentication 機制,因為使用者的狀態絕對不會保存在 server 的 memory 中。 179 | 180 | server 保護的 routes 會去檢查 Authorization header 是否為一個有效的 JWT,如果是,將會允許使用者 181 | 182 | access 受保護的 resources。 183 | 184 | 如同前面所說的 self-contained,全部所需要的資訊都在 JWT 中,可以降低需要 query database 的次數。 185 | 186 | 它允許你可以完全的依賴無狀態的 data APIs ,甚至不需要考慮是正在服務哪個 domains 底下的 API,因 187 | 188 | 為它不使用 cookies。 189 | 190 | 文件上有提到 Cross-Origin Resource Sharing ( CORS ) ,但經過討論,認為指是強調不使用 cookies,所以 191 | 192 | 沒有 domains 的問題而已。 ( 所以不要一直執著在 CORS 上面,不然你一定會覺得超怪 :confused: ) 193 | 194 | 如果不了解什麼是 CORS,可參考我之前的文章 [Same-Origin Policy and CORS Tutorial 📝](https://github.com/twtrubiks/CORS-tutorial)。 195 | 196 | 下面來看一張官網的 JWT 瀏覽器以及 server 之間互動的流程圖, 197 | 198 | ![alt tag](https://i.imgur.com/F0ucQwQ.png) 199 | 200 | 圖片來源 [https://jwt.io/introduction/](https://jwt.io/introduction/)。 201 | 202 | 可以簡單的把瀏覽器想成前端,而將 server 想成後端,這樣就適合用在前後端分離的地方了。 203 | 204 | 這邊還是要再次提早大家,因為 JWT 使用者是可以讀內容的 ( 簡單的 base64 編碼),所以記得 205 | 206 | 不要將重要的資訊放在 JWT 中。 207 | 208 | ### Why should we use JSON Web Tokens 209 | 210 | 文件中的最後一部分是介紹 JSON Web Tokens ( JWT ) 和 Simple Web Tokens (SWT) 以及 211 | 212 | Security Assertion Markup Language Tokens (SAML) 比較。 213 | 214 | 這部分我就不翻譯了,簡單來說,就是 JWT 比較優,size 小,可讀性強以及安全性的分析。 215 | 216 | 呼~ 我終於把大部分的重點都翻譯完了 (有些是依照自己的理解加加減減的翻譯 ) :satisfied: 217 | 218 | ### Token 已經發出去了, 該怎麼讓它失效 :question: 219 | 220 | 像是使用者改密碼, 要如何讓舊的 REFRESH TOKEN 失效呢 :question: 221 | 222 | (Access Token 就沒辦法了, 必須讓它時間到自己過期, 所以通常 Access Token 時間都很短) 223 | 224 | 這時候要搭配 [Blacklist app](https://django-rest-framework-simplejwt.readthedocs.io/en/latest/blacklist_app.html#), 225 | 226 | 當使用者改密碼的時候, 把 refresh token 加入 `token.blacklist()` 黑白單中, 227 | 228 | 這樣下一次, 當使用這個 refresh token 時, 發現這個 refresh token 在黑名單中, 229 | 230 | 就可以強制 user 要重新取 token 了, 類似的 response 如下. 231 | 232 | ```json 233 | { 234 | "detail": "Token is blacklisted", 235 | "code": "token_not_valid" 236 | } 237 | ``` 238 | 239 | Server 端, DB, 只保存 Refresh Token. 240 | 241 | Client 端, 會保存 Access Token 和 Refresh Token. 242 | 243 | ### Sliding tokens 244 | 245 | 本篇的例子都是 Access Token, 還有一種是 Sliding tokens, 246 | 247 | 文件可參考 [Token types](https://django-rest-framework-simplejwt.readthedocs.io/en/latest/token_types.html), 248 | 249 | Sliding tokens 主要是讓使用者方便一點, 但必須要犧牲一點安全性, 如果搭配 Blacklist app 效能會比較低. 250 | 251 | ## 把玩 djangorestframework-simplejwt 252 | 253 | 請參考官方文件 [djangorestframework-simplejwt](https://github.com/jazzband/djangorestframework-simplejwt),或是直接看我的影片說明:relaxed: 254 | 255 | * [Youtube Tutorial Part2 - django-rest-framework-jwt tutorial](https://youtu.be/CJOysCNAf4s) 256 | 257 | 由於這邊會使用到 django-rest-framework 的觀念,所以說你如果不熟悉,可參考我之前的文章 258 | 259 | * [Django-REST-framework 基本教學 - 從無到有 DRF-Beginners-Guide](https://github.com/twtrubiks/django-rest-framework-tutorial) 260 | 261 | 同場加映 [djoser](https://github.com/sunscrapers/djoser),這個套件整合 django 的 authentication system,提供一系列的 262 | 263 | Django Rest Framework ( DRF ) view 去 handle 基本的 registration, login, logout, 264 | 265 | password reset and account activation 等等。 266 | 267 | 在實戰中,我只有使用到 djoser 的 create user 的功能,因為 JWT 本身是沒有建立新的 user 的功能。 268 | 269 | ## 實戰 270 | 271 | 既然都說明那麼多理論了,當然要來簡單實戰一下,我們就用 Django 模擬前後端分離 272 | 273 | ( 為什麼說模擬,因為我實在沒學前端的框架 )。 274 | 275 | 請直接瀏覽 [http://127.0.0.1:8000/account/](http://127.0.0.1:8000/account/)。 276 | 277 | 簡單的畫面登入,如果未登入 ( 無法取得資料 ), 278 | 279 | ![alt tag](https://i.imgur.com/8WWJpZ9.png) 280 | 281 | 紅色的部分是我去 call 一個受保護 API 的資源,因為 header 沒帶上 JWT,所以無法取得資料。 282 | 283 | 可以自行註冊一個帳號,會是使用我的 twtrubiks/password123, 284 | 285 | 成功登入後,取的 JWT,我會將它存在 localStorage 中, 286 | 287 | ![alt tag](https://i.imgur.com/zHpI5O0.png) 288 | 289 | 接著自然可以取得受保護 API 的資源 ( 因 header 有帶上 JWT,且這個 JWT 是有效的 ) 290 | 291 | ![alt tag](https://i.imgur.com/vbot6Ve.png) 292 | 293 | 每當進入 [http://127.0.0.1:8000/account/](http://127.0.0.1:8000/account/) 時,我都會先發一個 `/api/token/refresh/` 去 refresh token, 294 | 295 | 並且將這個 token 存在 localStorage 中 ( 覆蓋掉既有存在 localStore 中的 token ),而這個 token 時效 296 | 297 | 只有 5 分鐘,也就是說假如你持續 5 分鐘沒在網頁上操作 ( 這邊就只能重新整理頁面模擬 ),你的 298 | 299 | token 將會過期,並且看到下面這個畫面,因為你的 token 已經失效了。 300 | 301 | ![alt tag](https://i.imgur.com/8WWJpZ9.png) 302 | 303 | ## 其他 304 | 305 | 符合 RFC 規範, 可參考 [PR2](https://github.com/twtrubiks/django_jwt_tutorial/pull/2) - 感謝 NatLee 306 | 307 | 為了維持教學文的簡單性, 暫時不 merge, 有興趣的可以到連結內觀看. 308 | 309 | ## 執行環境 310 | 311 | * Python 3.8 312 | 313 | ## Reference 314 | 315 | * [djangorestframework-simplejwt](https://github.com/jazzband/djangorestframework-simplejwt) 316 | * [djoser](https://github.com/sunscrapers/djoser) 317 | 318 | ## Donation 319 | 320 | 文章都是我自己研究內化後原創,如果有幫助到您,也想鼓勵我的話,歡迎請我喝一杯咖啡:laughing: 321 | 322 | ![alt tag](https://i.imgur.com/LRct9xa.png) 323 | 324 | [贊助者付款](https://payment.opay.tw/Broadcaster/Donate/9E47FDEF85ABE383A0F5FC6A218606F8) 325 | 326 | ## License 327 | 328 | MIT licens 329 | -------------------------------------------------------------------------------- /account/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twtrubiks/django_jwt_tutorial/acb6baefdd4ae7197a623ef79e7a42af55807b6d/account/__init__.py -------------------------------------------------------------------------------- /account/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /account/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'account' 7 | -------------------------------------------------------------------------------- /account/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.models import User 3 | 4 | 5 | class UserRegistrationForm(forms.ModelForm): 6 | password = forms.CharField(label='Password', 7 | widget=forms.PasswordInput) 8 | 9 | class Meta: 10 | model = User 11 | fields = ('username',) 12 | -------------------------------------------------------------------------------- /account/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /account/static/js/dashboard.js: -------------------------------------------------------------------------------- 1 | var jwt_token = localStorage.getItem('jwt_token'); 2 | var jwt_token_refresh = localStorage.getItem('jwt_token_refresh'); 3 | data = { 4 | "refresh": jwt_token_refresh 5 | }; 6 | $.ajax({ 7 | type: "POST", 8 | url: "http://127.0.0.1:8000/api/token/refresh/", 9 | data: data, 10 | success: function (data) { 11 | localStorage.setItem('jwt_token', data.access); 12 | $.ajax({ 13 | type: "GET", 14 | url: "http://127.0.0.1:8000/api/musics/", 15 | headers: { 16 | "Authorization": "Bearer" + " " + localStorage.getItem('jwt_token') 17 | }, 18 | success: function (data) { 19 | var result = ""; 20 | for (var i in data) { 21 | result += data[i].id + ", " + data[i].song + " , " + data[i].singer + "-----"; 22 | } 23 | $("#result").text(result).css('color', 'blue'); 24 | }, 25 | error: function (data) { 26 | var result = "please login " + data.responseText; 27 | $("#result").text(result).css('color', 'red'); 28 | } 29 | }); 30 | }, 31 | error: function (data) { 32 | var result = "please login " + data.responseText; 33 | $("#result").text(result).css('color', 'red'); 34 | } 35 | }); -------------------------------------------------------------------------------- /account/static/js/login.js: -------------------------------------------------------------------------------- 1 | $("form").on("submit", function (event) { 2 | event.preventDefault(); 3 | $.ajax({ 4 | type: "POST", 5 | url: "http://127.0.0.1:8000/api/token/", 6 | data: $(this).serialize(), 7 | success: function (data) { 8 | localStorage.setItem('jwt_token', data.access); 9 | localStorage.setItem('jwt_token_refresh', data.refresh); 10 | window.location.href = "http://127.0.0.1:8000/account/"; 11 | } 12 | }); 13 | }); -------------------------------------------------------------------------------- /account/static/js/register.js: -------------------------------------------------------------------------------- 1 | $("form").on("submit", function (event) { 2 | event.preventDefault(); 3 | $.ajax({ 4 | type: "POST", 5 | url: "http://127.0.0.1:8000/auth/users/", 6 | data: $(this).serialize(), 7 | success: function () { 8 | window.location.href = "http://127.0.0.1:8000/account/login/"; 9 | }, 10 | error: function (data) { 11 | console.log(data.responseText) 12 | } 13 | }); 14 | }); -------------------------------------------------------------------------------- /account/static/stylesheets/login.css: -------------------------------------------------------------------------------- 1 | a:hover, a:focus, a:active, a:visited, a:link { 2 | text-decoration: none; 3 | } 4 | 5 | /* 6 | @hiercelik - hiercelik.net 7 | @koalapix - koalapix.com 8 | */ 9 | 10 | @import url(//cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.1.1/css/fontawesome.min.css); 11 | @import url(http://fonts.googleapis.com/css?family=Titillium+Web&subset=latin,latin-ext); 12 | @media (min-width: 768px) { 13 | .kpx_row-sm-offset-3 div:first-child[class*="col-"] { 14 | margin-left: 25%; 15 | } 16 | } 17 | 18 | body { 19 | font-family: 'Titillium Web', sans-serif; 20 | } 21 | 22 | a { 23 | color: #ff5400; 24 | } 25 | 26 | a:hover { 27 | opacity: 0.8; 28 | color: #ff5400; 29 | text-decoration: none; 30 | } 31 | 32 | .kpx_login .kpx_authTitle { 33 | text-align: center; 34 | line-height: 300%; 35 | } 36 | 37 | .kpx_login .kpx_socialButtons a { 38 | color: white; 39 | / / In yourUse @body-bg opacity: 0.9; 40 | } 41 | 42 | .kpx_login .kpx_socialButtons a:hover { 43 | color: white; 44 | opacity: 1; 45 | } 46 | 47 | .kpx_login .kpx_socialButtons .kpx_btn-facebook { 48 | background: #3b5998; 49 | -webkit-transition: all 0.5s ease-in-out; 50 | -moz-transition: all 0.5s ease-in-out; 51 | -o-transition: all 0.5s ease-in-out; 52 | transition: all 0.5s ease-in-out; 53 | } 54 | 55 | .kpx_login .kpx_socialButtons .kpx_btn-facebook:hover { 56 | background: #172d5e 57 | } 58 | 59 | .kpx_login .kpx_socialButtons .kpx_btn-facebook:focus { 60 | background: #fff; 61 | color: #3b5998; 62 | border-color: #3b5998; 63 | } 64 | 65 | .kpx_login .kpx_socialButtons .kpx_btn-twitter { 66 | background: #00aced; 67 | -webkit-transition: all 0.5s ease-in-out; 68 | -moz-transition: all 0.5s ease-in-out; 69 | -o-transition: all 0.5s ease-in-out; 70 | transition: all 0.5s ease-in-out; 71 | } 72 | 73 | .kpx_login .kpx_socialButtons .kpx_btn-twitter:hover { 74 | background: #043d52 75 | } 76 | 77 | .kpx_login .kpx_socialButtons .kpx_btn-twitter:focus { 78 | background: #fff; 79 | color: #00aced; 80 | border-color: #00aced; 81 | } 82 | 83 | .kpx_login .kpx_socialButtons .kpx_btn-google-plus { 84 | background: #c32f10; 85 | -webkit-transition: all 0.5s ease-in-out; 86 | -moz-transition: all 0.5s ease-in-out; 87 | -o-transition: all 0.5s ease-in-out; 88 | transition: all 0.5s ease-in-out; 89 | } 90 | 91 | .kpx_login .kpx_socialButtons .kpx_btn-google-plus:hover { 92 | background: #6b1301 93 | } 94 | 95 | .kpx_login .kpx_socialButtons .kpx_btn-google-plus:focus { 96 | background: #fff; 97 | color: #c32f10; 98 | border-color: #c32f10 99 | } 100 | 101 | .kpx_login .kpx_socialButtons .kpx_btn-soundcloud { 102 | background: #ff8800; 103 | -webkit-transition: all 0.5s ease-in-out; 104 | -moz-transition: all 0.5s ease-in-out; 105 | -o-transition: all 0.5s ease-in-out; 106 | transition: all 0.5s ease-in-out; 107 | } 108 | 109 | .kpx_login .kpx_socialButtons .kpx_btn-soundcloud:hover { 110 | background: #c73e04 111 | } 112 | 113 | .kpx_login .kpx_socialButtons .kpx_btn-soundcloud:focus { 114 | background: #fff; 115 | color: #ff8800; 116 | border-color: #ff8800 117 | } 118 | 119 | .kpx_login .kpx_socialButtons .kpx_btn-github { 120 | background: #666666; 121 | -webkit-transition: all 0.5s ease-in-out; 122 | -moz-transition: all 0.5s ease-in-out; 123 | -o-transition: all 0.5s ease-in-out; 124 | transition: all 0.5s ease-in-out; 125 | } 126 | 127 | .kpx_login .kpx_socialButtons .kpx_btn-github:hover { 128 | background: #333333 129 | } 130 | 131 | .kpx_login .kpx_socialButtons .kpx_btn-github:focus { 132 | background: #fff; 133 | color: #666666; 134 | border-color: #666666 135 | } 136 | 137 | .kpx_login .kpx_socialButtons .kpx_btn-steam { 138 | background: #878787; 139 | -webkit-transition: all 0.5s ease-in-out; 140 | -moz-transition: all 0.5s ease-in-out; 141 | -o-transition: all 0.5s ease-in-out; 142 | transition: all 0.5s ease-in-out; 143 | } 144 | 145 | .kpx_login .kpx_socialButtons .kpx_btn-steam:hover { 146 | background: #292929 147 | } 148 | 149 | .kpx_login .kpx_socialButtons .kpx_btn-steam:focus { 150 | background: #fff; 151 | color: #878787; 152 | border-color: #878787 153 | } 154 | 155 | .kpx_login .kpx_socialButtons .kpx_btn-pinterest { 156 | background: #cc2127; 157 | -webkit-transition: all 0.5s ease-in-out; 158 | -moz-transition: all 0.5s ease-in-out; 159 | -o-transition: all 0.5s ease-in-out; 160 | transition: all 0.5s ease-in-out; 161 | } 162 | 163 | .kpx_login .kpx_socialButtons .kpx_btn-pinterest:hover { 164 | background: #780004 165 | } 166 | 167 | .kpx_login .kpx_socialButtons .kpx_btn-pinterest:focus { 168 | background: #fff; 169 | color: #cc2127; 170 | border-color: #cc2127 171 | } 172 | 173 | .kpx_login .kpx_socialButtons .kpx_btn-vimeo { 174 | background: #1ab7ea; 175 | -webkit-transition: all 0.5s ease-in-out; 176 | -moz-transition: all 0.5s ease-in-out; 177 | -o-transition: all 0.5s ease-in-out; 178 | transition: all 0.5s ease-in-out; 179 | } 180 | 181 | .kpx_login .kpx_socialButtons .kpx_btn-vimeo:hover { 182 | background: #162221 183 | } 184 | 185 | .kpx_login .kpx_socialButtons .kpx_btn-vimeo:focus { 186 | background: #fff; 187 | color: #1ab7ea; 188 | border-color: #1ab7ea 189 | } 190 | 191 | .kpx_login .kpx_socialButtons .kpx_btn-lastfm { 192 | background: #c3000d; 193 | -webkit-transition: all 0.5s ease-in-out; 194 | -moz-transition: all 0.5s ease-in-out; 195 | -o-transition: all 0.5s ease-in-out; 196 | transition: all 0.5s ease-in-out; 197 | } 198 | 199 | .kpx_login .kpx_socialButtons .kpx_btn-lastfm:hover { 200 | background: #5e0208 201 | } 202 | 203 | .kpx_login .kpx_socialButtons .kpx_btn-lastfm:focus { 204 | background: #fff; 205 | color: #c3000d; 206 | border-color: #c3000d 207 | } 208 | 209 | .kpx_login .kpx_socialButtons .kpx_btn-yahoo { 210 | background: #400191; 211 | -webkit-transition: all 0.5s ease-in-out; 212 | -moz-transition: all 0.5s ease-in-out; 213 | -o-transition: all 0.5s ease-in-out; 214 | transition: all 0.5s ease-in-out; 215 | } 216 | 217 | .kpx_login .kpx_socialButtons .kpx_btn-yahoo:hover { 218 | background: #230052 219 | } 220 | 221 | .kpx_login .kpx_socialButtons .kpx_btn-yahoo:focus { 222 | background: #fff; 223 | color: #400191; 224 | border-color: #400191 225 | } 226 | 227 | .kpx_login .kpx_socialButtons .kpx_btn-vk { 228 | background: #45668e; 229 | -webkit-transition: all 0.5s ease-in-out; 230 | -moz-transition: all 0.5s ease-in-out; 231 | -o-transition: all 0.5s ease-in-out; 232 | transition: all 0.5s ease-in-out; 233 | } 234 | 235 | .kpx_login .kpx_socialButtons .kpx_btn-vk:hover { 236 | background: #1a3352 237 | } 238 | 239 | .kpx_login .kpx_socialButtons .kpx_btn-vk:focus { 240 | background: #fff; 241 | color: #45668e; 242 | border-color: #45668e 243 | } 244 | 245 | .kpx_login .kpx_socialButtons .kpx_btn-spotify { 246 | background: #7ab800; 247 | -webkit-transition: all 0.5s ease-in-out; 248 | -moz-transition: all 0.5s ease-in-out; 249 | -o-transition: all 0.5s ease-in-out; 250 | transition: all 0.5s ease-in-out; 251 | } 252 | 253 | .kpx_login .kpx_socialButtons .kpx_btn-spotify:hover { 254 | background: #3a5700 255 | } 256 | 257 | .kpx_login .kpx_socialButtons .kpx_btn-spotify:focus { 258 | background: #fff; 259 | color: #7ab800; 260 | border-color: #7ab800 261 | } 262 | 263 | .kpx_login .kpx_socialButtons .kpx_btn-linkedin { 264 | background: #0976b4; 265 | -webkit-transition: all 0.5s ease-in-out; 266 | -moz-transition: all 0.5s ease-in-out; 267 | -o-transition: all 0.5s ease-in-out; 268 | transition: all 0.5s ease-in-out; 269 | } 270 | 271 | .kpx_login .kpx_socialButtons .kpx_btn-linkedin:hover { 272 | background: #004269 273 | } 274 | 275 | .kpx_login .kpx_socialButtons .kpx_btn-linkedin:focus { 276 | background: #fff; 277 | color: #0976b4; 278 | border-color: #0976b4 279 | } 280 | 281 | .kpx_login .kpx_socialButtons .kpx_btn-stumbleupon { 282 | background: #eb4924; 283 | -webkit-transition: all 0.5s ease-in-out; 284 | -moz-transition: all 0.5s ease-in-out; 285 | -o-transition: all 0.5s ease-in-out; 286 | transition: all 0.5s ease-in-out; 287 | } 288 | 289 | .kpx_login .kpx_socialButtons .kpx_btn-stumbleupon:hover { 290 | background: #943019 291 | } 292 | 293 | .kpx_login .kpx_socialButtons .kpx_btn-stumbleupon:focus { 294 | background: #fff; 295 | color: #eb4924; 296 | border-color: #eb4924 297 | } 298 | 299 | .kpx_login .kpx_socialButtons .kpx_btn-tumblr { 300 | background: #35465c; 301 | -webkit-transition: all 0.5s ease-in-out; 302 | -moz-transition: all 0.5s ease-in-out; 303 | -o-transition: all 0.5s ease-in-out; 304 | transition: all 0.5s ease-in-out; 305 | } 306 | 307 | .kpx_login .kpx_socialButtons .kpx_btn-tumblr:hover { 308 | background: #142030 309 | } 310 | 311 | .kpx_login .kpx_socialButtons .kpx_btn-tumblr:focus { 312 | background: #fff; 313 | color: #35465c; 314 | border-color: #35465c 315 | } 316 | 317 | .kpx_login .kpx_loginOr { 318 | position: relative; 319 | font-size: 1.5em; 320 | color: #aaa; 321 | margin-top: 1em; 322 | margin-bottom: 1em; 323 | padding-top: 0.5em; 324 | padding-bottom: 0.5em; 325 | } 326 | 327 | .kpx_login .kpx_loginOr .kpx_hrOr { 328 | background-color: #cdcdcd; 329 | height: 1px; 330 | margin-top: 0px !important; 331 | margin-bottom: 0px !important; 332 | } 333 | 334 | .kpx_login .kpx_loginOr .kpx_spanOr { 335 | display: block; 336 | position: absolute; 337 | left: 50%; 338 | top: -0.6em; 339 | margin-left: -1.5em; 340 | background-color: white; 341 | width: 3em; 342 | text-align: center; 343 | } 344 | 345 | .kpx_login .kpx_loginForm .input-group.i { 346 | width: 2em; 347 | } 348 | 349 | .kpx_login .kpx_loginForm .help-block { 350 | color: red; 351 | } 352 | 353 | @media (min-width: 768px) { 354 | .kpx_login .kpx_forgotPwd { 355 | text-align: right; 356 | margin-top: 10px; 357 | } 358 | } -------------------------------------------------------------------------------- /account/templates/account/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Dashboard{% endblock %} 3 | {% load static %} 4 | {% block content %} 5 |
6 |

Dashboard

7 |

Welcome to your dashboard.

8 |
9 | 10 |
11 |

12 |
13 | 14 | 15 | 16 | 17 | {% endblock %} -------------------------------------------------------------------------------- /account/templates/account/register.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load bootstrap3 %} 3 | {% load static %} 4 | {% block title %}Create an account{% endblock %} 5 | {% block content %} 6 |
7 |

Sign up for Free

8 |

create an account

9 |
10 | 11 |
12 | {% bootstrap_form user_form %} 13 | {% buttons %} 14 | 17 | {% endbuttons %} 18 |
19 | 20 | 21 | 22 | {% endblock %} 23 | 24 | 25 | -------------------------------------------------------------------------------- /account/templates/account/register_done.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Welcome{% endblock %} 3 | {% block content %} 4 |

Welcome {{ new_user.first_name }}!

5 |

Your account has been successfully created. Now you can log in.

6 | {% endblock %} -------------------------------------------------------------------------------- /account/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load bootstrap3 %} 3 | 4 | 5 | 6 | 7 | {% block title %}{% endblock %} 8 | 9 | 10 | 11 | {% bootstrap_css %} 12 | {% bootstrap_javascript %} 13 | 14 | 15 | 16 | 17 | 34 | 35 | 36 |
37 | {% block content %} 38 | {% endblock %} 39 |
40 | 41 | 42 | -------------------------------------------------------------------------------- /account/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | {% load bootstrap3 %} 4 | {% block title %}Login{% endblock %} 5 | {% block content %} 6 |
7 |

Login or Sign up

8 |
9 |
10 |
11 | {% csrf_token %} 12 | {% bootstrap_form form %} 13 | 14 |
15 | {% buttons %} 16 | 20 | {% endbuttons %} 21 |
22 |
23 |
24 |
25 | 26 | 27 | 28 | {% endblock %} 29 | 30 | 31 | -------------------------------------------------------------------------------- /account/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /account/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth import views as auth_views 2 | from django.urls import path 3 | 4 | from account import views 5 | 6 | urlpatterns = [ 7 | path('', views.dashboard, name='dashboard'), 8 | path('register/', views.register, name='register'), 9 | 10 | # login logout 11 | path('login/', auth_views.LoginView.as_view(), name='login'), 12 | ] 13 | -------------------------------------------------------------------------------- /account/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from .forms import UserRegistrationForm 3 | 4 | 5 | def dashboard(request): 6 | return render(request, 'account/dashboard.html', {}) 7 | 8 | 9 | def register(request): 10 | user_form = UserRegistrationForm() 11 | return render(request, 12 | 'account/register.html', 13 | {'user_form': user_form}) 14 | -------------------------------------------------------------------------------- /db.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twtrubiks/django_jwt_tutorial/acb6baefdd4ae7197a623ef79e7a42af55807b6d/db.sqlite3 -------------------------------------------------------------------------------- /django_jwt_tutorial/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twtrubiks/django_jwt_tutorial/acb6baefdd4ae7197a623ef79e7a42af55807b6d/django_jwt_tutorial/__init__.py -------------------------------------------------------------------------------- /django_jwt_tutorial/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for django_jwt_tutorial project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_jwt_tutorial.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /django_jwt_tutorial/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_jwt_tutorial project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.0.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.0/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | from datetime import timedelta 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve().parent.parent 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = 'django-insecure-8f$v#u5w#g9twa_cg3^7!eqd1n(8k7t4iy9t@7d44$5(!q4^kj' 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = [] 30 | 31 | REST_FRAMEWORK = { 32 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 33 | 'rest_framework_simplejwt.authentication.JWTAuthentication', 34 | ) 35 | 36 | } 37 | 38 | SIMPLE_JWT = { 39 | 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), 40 | 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), 41 | 'ROTATE_REFRESH_TOKENS': False, 42 | 'BLACKLIST_AFTER_ROTATION': False, 43 | 'UPDATE_LAST_LOGIN': False, 44 | 45 | 'ALGORITHM': 'HS256', 46 | 'SIGNING_KEY': SECRET_KEY, 47 | 'VERIFYING_KEY': None, 48 | 'AUDIENCE': None, 49 | 'ISSUER': None, 50 | 'JWK_URL': None, 51 | 'LEEWAY': 0, 52 | 53 | 'AUTH_HEADER_TYPES': ('Bearer',), 54 | 'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION', 55 | 'USER_ID_FIELD': 'id', 56 | 'USER_ID_CLAIM': 'user_id', 57 | 'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule', 58 | 59 | 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), 60 | 'TOKEN_TYPE_CLAIM': 'token_type', 61 | 'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser', 62 | 63 | 'JTI_CLAIM': 'jti', 64 | 65 | 'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp', 66 | 'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5), 67 | 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1), 68 | } 69 | 70 | # Application definition 71 | 72 | INSTALLED_APPS = [ 73 | 'musics.apps.MusicsConfig', 74 | 'account.apps.AccountConfig', 75 | 'django.contrib.admin', 76 | 'django.contrib.auth', 77 | 'django.contrib.contenttypes', 78 | 'django.contrib.sessions', 79 | 'django.contrib.messages', 80 | 'django.contrib.staticfiles', 81 | 'rest_framework', 82 | 'rest_framework_simplejwt', 83 | 'bootstrap3', 84 | 'djoser', 85 | 86 | ] 87 | 88 | MIDDLEWARE = [ 89 | 'django.middleware.security.SecurityMiddleware', 90 | 'django.contrib.sessions.middleware.SessionMiddleware', 91 | 'django.middleware.common.CommonMiddleware', 92 | 'django.middleware.csrf.CsrfViewMiddleware', 93 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 94 | 'django.contrib.messages.middleware.MessageMiddleware', 95 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 96 | ] 97 | 98 | ROOT_URLCONF = 'django_jwt_tutorial.urls' 99 | 100 | TEMPLATES = [ 101 | { 102 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 103 | 'DIRS': [], 104 | 'APP_DIRS': True, 105 | 'OPTIONS': { 106 | 'context_processors': [ 107 | 'django.template.context_processors.debug', 108 | 'django.template.context_processors.request', 109 | 'django.contrib.auth.context_processors.auth', 110 | 'django.contrib.messages.context_processors.messages', 111 | ], 112 | }, 113 | }, 114 | ] 115 | 116 | WSGI_APPLICATION = 'django_jwt_tutorial.wsgi.application' 117 | 118 | 119 | # Database 120 | # https://docs.djangoproject.com/en/4.0/ref/settings/#databases 121 | 122 | DATABASES = { 123 | 'default': { 124 | 'ENGINE': 'django.db.backends.sqlite3', 125 | 'NAME': BASE_DIR / 'db.sqlite3', 126 | } 127 | } 128 | 129 | 130 | # Password validation 131 | # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators 132 | 133 | AUTH_PASSWORD_VALIDATORS = [ 134 | { 135 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 136 | }, 137 | { 138 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 139 | }, 140 | { 141 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 142 | }, 143 | { 144 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 145 | }, 146 | ] 147 | 148 | 149 | # Internationalization 150 | # https://docs.djangoproject.com/en/4.0/topics/i18n/ 151 | 152 | LANGUAGE_CODE = 'en-us' 153 | 154 | TIME_ZONE = 'UTC' 155 | 156 | USE_I18N = True 157 | 158 | USE_TZ = True 159 | 160 | 161 | # Static files (CSS, JavaScript, Images) 162 | # https://docs.djangoproject.com/en/4.0/howto/static-files/ 163 | 164 | STATIC_URL = 'static/' 165 | 166 | # Default primary key field type 167 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field 168 | 169 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 170 | -------------------------------------------------------------------------------- /django_jwt_tutorial/urls.py: -------------------------------------------------------------------------------- 1 | """django_jwt_tutorial URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/4.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | from rest_framework_simplejwt.views import ( 19 | TokenObtainPairView, 20 | TokenRefreshView, 21 | ) 22 | 23 | from rest_framework.routers import DefaultRouter 24 | from musics.views import MusicViewSet 25 | 26 | router = DefaultRouter() 27 | router.register('musics', MusicViewSet) 28 | 29 | urlpatterns = [ 30 | path('admin/', admin.site.urls), 31 | path('auth/', include('djoser.urls')), 32 | # for rest_framework 33 | path('api/', include(router.urls)), 34 | path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), 35 | path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), 36 | path('account/', include('account.urls')), 37 | ] 38 | 39 | 40 | -------------------------------------------------------------------------------- /django_jwt_tutorial/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_jwt_tutorial project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_jwt_tutorial.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_jwt_tutorial.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /musics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twtrubiks/django_jwt_tutorial/acb6baefdd4ae7197a623ef79e7a42af55807b6d/musics/__init__.py -------------------------------------------------------------------------------- /musics/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /musics/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MusicsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'musics' 7 | -------------------------------------------------------------------------------- /musics/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.5 on 2018-05-07 06:01 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Music', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('song', models.TextField()), 19 | ('singer', models.TextField()), 20 | ('created', models.DateTimeField(auto_now_add=True)), 21 | ], 22 | options={ 23 | 'db_table': 'music', 24 | }, 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /musics/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twtrubiks/django_jwt_tutorial/acb6baefdd4ae7197a623ef79e7a42af55807b6d/musics/migrations/__init__.py -------------------------------------------------------------------------------- /musics/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | class Music(models.Model): 5 | song = models.TextField() 6 | singer = models.TextField() 7 | created = models.DateTimeField(auto_now_add=True) 8 | 9 | class Meta: 10 | db_table = "music" -------------------------------------------------------------------------------- /musics/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from musics.models import Music 3 | 4 | 5 | class MusicSerializer(serializers.ModelSerializer): 6 | created = serializers.DateTimeField(format='%Y/%m/%d %I:%M:%S') 7 | 8 | class Meta: 9 | model = Music 10 | fields = '__all__' 11 | -------------------------------------------------------------------------------- /musics/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /musics/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | from rest_framework import viewsets 3 | from rest_framework.parsers import JSONParser 4 | from rest_framework.permissions import IsAuthenticated 5 | 6 | from musics.models import Music 7 | from musics.serializers import MusicSerializer 8 | 9 | 10 | # Create your views here. 11 | class MusicViewSet(viewsets.ModelViewSet): 12 | queryset = Music.objects.all() 13 | serializer_class = MusicSerializer 14 | permission_classes = (IsAuthenticated,) 15 | parser_classes = (JSONParser,) 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==4.0.5 2 | django-bootstrap3==21.2 3 | djangorestframework==3.13.1 4 | djangorestframework-simplejwt==5.2.0 5 | djoser==2.0.5 --------------------------------------------------------------------------------