├── .env.example ├── .gitignore ├── README.md ├── accounts ├── __init__.py ├── admin.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20211206_1558.py │ └── __init__.py ├── models.py ├── serializers.py ├── tests.py ├── urls.py ├── utils.py └── views.py ├── main ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py ├── views.py └── wsgi.py ├── manage.py ├── migrate_and_seed.sh ├── requirements.txt ├── seed └── 0001_users.json ├── services ├── authService.py ├── customPermission.py ├── emailService.py └── rederers.py └── social_auth ├── __init__.py ├── admin.py ├── apps.py ├── facebook.py ├── google.py ├── migrations └── __init__.py ├── models.py ├── register.py ├── serializers.py ├── tests.py ├── urls.py └── views.py /.env.example: -------------------------------------------------------------------------------- 1 | SECRET_KEY=abc 2 | DEBUG=1 3 | ALLOWED_HOSTS=127.0.0.1 4 | 5 | # DB CONFIG 6 | DB_NAME=boilerplateDB 7 | DB_USER=root 8 | DB_PASSWORD= 9 | DB_HOST=localhost 10 | DB_PORT=3306 11 | 12 | # Email CONFIG 13 | EMAIL_HOST=SMTP 14 | EMAIL_USE_TLS=True 15 | EMAIL_HOST_USER="test@test.com" 16 | EMAIL_HOST_PASSWORD= 17 | EMAIL_PORT=2525 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/database,python,django,pycharm,visualstudio 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=database,python,django,pycharm,visualstudio 4 | 5 | ### Database ### 6 | *.accdb 7 | *.db 8 | *.dbf 9 | *.mdb 10 | *.pdb 11 | *.sqlite3 12 | 13 | ### Django ### 14 | *.log 15 | *.pot 16 | *.pyc 17 | __pycache__/ 18 | local_settings.py 19 | db.sqlite3 20 | db.sqlite3-journal 21 | media 22 | 23 | # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ 24 | # in your Git repository. Update and uncomment the following line accordingly. 25 | # /staticfiles/ 26 | 27 | ### Django.Python Stack ### 28 | # Byte-compiled / optimized / DLL files 29 | *.py[cod] 30 | *$py.class 31 | 32 | # C extensions 33 | *.so 34 | 35 | # Distribution / packaging 36 | .Python 37 | build/ 38 | develop-eggs/ 39 | dist/ 40 | downloads/ 41 | eggs/ 42 | .eggs/ 43 | lib/ 44 | lib64/ 45 | parts/ 46 | sdist/ 47 | var/ 48 | wheels/ 49 | pip-wheel-metadata/ 50 | share/python-wheels/ 51 | *.egg-info/ 52 | .installed.cfg 53 | *.egg 54 | MANIFEST 55 | 56 | # PyInstaller 57 | # Usually these files are written by a python script from a template 58 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 59 | *.manifest 60 | *.spec 61 | 62 | # Installer logs 63 | pip-log.txt 64 | pip-delete-this-directory.txt 65 | 66 | # Unit test / coverage reports 67 | htmlcov/ 68 | .tox/ 69 | .nox/ 70 | .coverage 71 | .coverage.* 72 | .cache 73 | nosetests.xml 74 | coverage.xml 75 | *.cover 76 | *.py,cover 77 | .hypothesis/ 78 | .pytest_cache/ 79 | 80 | # Translations 81 | *.mo 82 | 83 | # Django stuff: 84 | 85 | # Flask stuff: 86 | instance/ 87 | .webassets-cache 88 | 89 | # Scrapy stuff: 90 | .scrapy 91 | 92 | # Sphinx documentation 93 | docs/_build/ 94 | 95 | # PyBuilder 96 | target/ 97 | 98 | # Jupyter Notebook 99 | .ipynb_checkpoints 100 | 101 | # IPython 102 | profile_default/ 103 | ipython_config.py 104 | 105 | # pyenv 106 | .python-version 107 | 108 | # pipenv 109 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 110 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 111 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 112 | # install all needed dependencies. 113 | #Pipfile.lock 114 | 115 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 116 | __pypackages__/ 117 | 118 | # Celery stuff 119 | celerybeat-schedule 120 | celerybeat.pid 121 | 122 | # SageMath parsed files 123 | *.sage.py 124 | 125 | # Environments 126 | .env 127 | .venv 128 | env/ 129 | venv/ 130 | ENV/ 131 | env.bak/ 132 | venv.bak/ 133 | 134 | # Spyder project settings 135 | .spyderproject 136 | .spyproject 137 | 138 | # Rope project settings 139 | .ropeproject 140 | 141 | # mkdocs documentation 142 | /site 143 | 144 | # mypy 145 | .mypy_cache/ 146 | .dmypy.json 147 | dmypy.json 148 | 149 | # Pyre type checker 150 | .pyre/ 151 | 152 | # pytype static type analyzer 153 | .pytype/ 154 | 155 | ### PyCharm ### 156 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 157 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 158 | 159 | # User-specific stuff 160 | .idea/**/workspace.xml 161 | .idea/**/tasks.xml 162 | .idea/**/usage.statistics.xml 163 | .idea/**/dictionaries 164 | .idea/**/shelf 165 | 166 | # Generated files 167 | .idea/**/contentModel.xml 168 | 169 | # Sensitive or high-churn files 170 | .idea/**/dataSources/ 171 | .idea/**/dataSources.ids 172 | .idea/**/dataSources.local.xml 173 | .idea/**/sqlDataSources.xml 174 | .idea/**/dynamic.xml 175 | .idea/**/uiDesigner.xml 176 | .idea/**/dbnavigator.xml 177 | 178 | # Gradle 179 | .idea/**/gradle.xml 180 | .idea/**/libraries 181 | 182 | # Gradle and Maven with auto-import 183 | # When using Gradle or Maven with auto-import, you should exclude module files, 184 | # since they will be recreated, and may cause churn. Uncomment if using 185 | # auto-import. 186 | # .idea/artifacts 187 | # .idea/compiler.xml 188 | # .idea/jarRepositories.xml 189 | # .idea/modules.xml 190 | # .idea/*.iml 191 | # .idea/modules 192 | # *.iml 193 | # *.ipr 194 | 195 | # CMake 196 | cmake-build-*/ 197 | 198 | # Mongo Explorer plugin 199 | .idea/**/mongoSettings.xml 200 | 201 | # File-based project format 202 | *.iws 203 | 204 | # IntelliJ 205 | out/ 206 | 207 | # mpeltonen/sbt-idea plugin 208 | .idea_modules/ 209 | 210 | # JIRA plugin 211 | atlassian-ide-plugin.xml 212 | 213 | # Cursive Clojure plugin 214 | .idea/replstate.xml 215 | 216 | # Crashlytics plugin (for Android Studio and IntelliJ) 217 | com_crashlytics_export_strings.xml 218 | crashlytics.properties 219 | crashlytics-build.properties 220 | fabric.properties 221 | 222 | # Editor-based Rest Client 223 | .idea/httpRequests 224 | 225 | # Android studio 3.1+ serialized cache file 226 | .idea/caches/build_file_checksums.ser 227 | 228 | ### PyCharm Patch ### 229 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 230 | 231 | # *.iml 232 | # modules.xml 233 | # .idea/misc.xml 234 | # *.ipr 235 | 236 | # Sonarlint plugin 237 | .idea/**/sonarlint/ 238 | 239 | # SonarQube Plugin 240 | .idea/**/sonarIssues.xml 241 | 242 | # Markdown Navigator plugin 243 | .idea/**/markdown-navigator.xml 244 | .idea/**/markdown-navigator-enh.xml 245 | .idea/**/markdown-navigator/ 246 | 247 | # Cache file creation bug 248 | # See https://youtrack.jetbrains.com/issue/JBR-2257 249 | .idea/$CACHE_FILE$ 250 | 251 | ### Python ### 252 | # Byte-compiled / optimized / DLL files 253 | 254 | # C extensions 255 | 256 | # Distribution / packaging 257 | 258 | # PyInstaller 259 | # Usually these files are written by a python script from a template 260 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 261 | 262 | # Installer logs 263 | 264 | # Unit test / coverage reports 265 | 266 | # Translations 267 | 268 | # Django stuff: 269 | 270 | # Flask stuff: 271 | 272 | # Scrapy stuff: 273 | 274 | # Sphinx documentation 275 | 276 | # PyBuilder 277 | 278 | # Jupyter Notebook 279 | 280 | # IPython 281 | 282 | # pyenv 283 | 284 | # pipenv 285 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 286 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 287 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 288 | # install all needed dependencies. 289 | 290 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 291 | 292 | # Celery stuff 293 | 294 | # SageMath parsed files 295 | 296 | # Environments 297 | 298 | # Spyder project settings 299 | 300 | # Rope project settings 301 | 302 | # mkdocs documentation 303 | 304 | # mypy 305 | 306 | # Pyre type checker 307 | 308 | # pytype static type analyzer 309 | 310 | ### VisualStudio ### 311 | ## Ignore Visual Studio temporary files, build results, and 312 | ## files generated by popular Visual Studio add-ons. 313 | ## 314 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 315 | 316 | # User-specific files 317 | *.rsuser 318 | *.suo 319 | *.user 320 | *.userosscache 321 | *.sln.docstates 322 | 323 | # User-specific files (MonoDevelop/Xamarin Studio) 324 | *.userprefs 325 | 326 | # Mono auto generated files 327 | mono_crash.* 328 | 329 | # Build results 330 | [Dd]ebug/ 331 | [Dd]ebugPublic/ 332 | [Rr]elease/ 333 | [Rr]eleases/ 334 | x64/ 335 | x86/ 336 | [Aa][Rr][Mm]/ 337 | [Aa][Rr][Mm]64/ 338 | bld/ 339 | [Bb]in/ 340 | [Oo]bj/ 341 | [Ll]og/ 342 | [Ll]ogs/ 343 | 344 | # Visual Studio 2015/2017 cache/options directory 345 | .vs/ 346 | # Uncomment if you have tasks that create the project's static files in wwwroot 347 | #wwwroot/ 348 | 349 | # Visual Studio 2017 auto generated files 350 | Generated\ Files/ 351 | 352 | # MSTest test Results 353 | [Tt]est[Rr]esult*/ 354 | [Bb]uild[Ll]og.* 355 | 356 | # NUnit 357 | *.VisualState.xml 358 | TestResult.xml 359 | nunit-*.xml 360 | 361 | # Build Results of an ATL Project 362 | [Dd]ebugPS/ 363 | [Rr]eleasePS/ 364 | dlldata.c 365 | 366 | # Benchmark Results 367 | BenchmarkDotNet.Artifacts/ 368 | 369 | # .NET Core 370 | project.lock.json 371 | project.fragment.lock.json 372 | artifacts/ 373 | 374 | # StyleCop 375 | StyleCopReport.xml 376 | 377 | # Files built by Visual Studio 378 | *_i.c 379 | *_p.c 380 | *_h.h 381 | *.ilk 382 | *.meta 383 | *.obj 384 | *.iobj 385 | *.pch 386 | *.ipdb 387 | *.pgc 388 | *.pgd 389 | *.rsp 390 | *.sbr 391 | *.tlb 392 | *.tli 393 | *.tlh 394 | *.tmp 395 | *.tmp_proj 396 | *_wpftmp.csproj 397 | *.vspscc 398 | *.vssscc 399 | .builds 400 | *.pidb 401 | *.svclog 402 | *.scc 403 | 404 | # Chutzpah Test files 405 | _Chutzpah* 406 | 407 | # Visual C++ cache files 408 | ipch/ 409 | *.aps 410 | *.ncb 411 | *.opendb 412 | *.opensdf 413 | *.sdf 414 | *.cachefile 415 | *.VC.db 416 | *.VC.VC.opendb 417 | 418 | # Visual Studio profiler 419 | *.psess 420 | *.vsp 421 | *.vspx 422 | *.sap 423 | 424 | # Visual Studio Trace Files 425 | *.e2e 426 | 427 | # TFS 2012 Local Workspace 428 | $tf/ 429 | 430 | # Guidance Automation Toolkit 431 | *.gpState 432 | 433 | # ReSharper is a .NET coding add-in 434 | _ReSharper*/ 435 | *.[Rr]e[Ss]harper 436 | *.DotSettings.user 437 | 438 | # TeamCity is a build add-in 439 | _TeamCity* 440 | 441 | # DotCover is a Code Coverage Tool 442 | *.dotCover 443 | 444 | # AxoCover is a Code Coverage Tool 445 | .axoCover/* 446 | !.axoCover/settings.json 447 | 448 | # Coverlet is a free, cross platform Code Coverage Tool 449 | coverage*[.json, .xml, .info] 450 | 451 | # Visual Studio code coverage results 452 | *.coverage 453 | *.coveragexml 454 | 455 | # NCrunch 456 | _NCrunch_* 457 | .*crunch*.local.xml 458 | nCrunchTemp_* 459 | 460 | # MightyMoose 461 | *.mm.* 462 | AutoTest.Net/ 463 | 464 | # Web workbench (sass) 465 | .sass-cache/ 466 | 467 | # Installshield output folder 468 | [Ee]xpress/ 469 | 470 | # DocProject is a documentation generator add-in 471 | DocProject/buildhelp/ 472 | DocProject/Help/*.HxT 473 | DocProject/Help/*.HxC 474 | DocProject/Help/*.hhc 475 | DocProject/Help/*.hhk 476 | DocProject/Help/*.hhp 477 | DocProject/Help/Html2 478 | DocProject/Help/html 479 | 480 | # Click-Once directory 481 | publish/ 482 | 483 | # Publish Web Output 484 | *.[Pp]ublish.xml 485 | *.azurePubxml 486 | # Note: Comment the next line if you want to checkin your web deploy settings, 487 | # but database connection strings (with potential passwords) will be unencrypted 488 | *.pubxml 489 | *.publishproj 490 | 491 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 492 | # checkin your Azure Web App publish settings, but sensitive information contained 493 | # in these scripts will be unencrypted 494 | PublishScripts/ 495 | 496 | # NuGet Packages 497 | *.nupkg 498 | # NuGet Symbol Packages 499 | *.snupkg 500 | # The packages folder can be ignored because of Package Restore 501 | **/[Pp]ackages/* 502 | # except build/, which is used as an MSBuild target. 503 | !**/[Pp]ackages/build/ 504 | # Uncomment if necessary however generally it will be regenerated when needed 505 | #!**/[Pp]ackages/repositories.config 506 | # NuGet v3's project.json files produces more ignorable files 507 | *.nuget.props 508 | *.nuget.targets 509 | 510 | # Microsoft Azure Build Output 511 | csx/ 512 | *.build.csdef 513 | 514 | # Microsoft Azure Emulator 515 | ecf/ 516 | rcf/ 517 | 518 | # Windows Store app package directories and files 519 | AppPackages/ 520 | BundleArtifacts/ 521 | Package.StoreAssociation.xml 522 | _pkginfo.txt 523 | *.appx 524 | *.appxbundle 525 | *.appxupload 526 | 527 | # Visual Studio cache files 528 | # files ending in .cache can be ignored 529 | *.[Cc]ache 530 | # but keep track of directories ending in .cache 531 | !?*.[Cc]ache/ 532 | 533 | # Others 534 | ClientBin/ 535 | ~$* 536 | *~ 537 | *.dbmdl 538 | *.dbproj.schemaview 539 | *.jfm 540 | *.pfx 541 | *.publishsettings 542 | orleans.codegen.cs 543 | 544 | # Including strong name files can present a security risk 545 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 546 | #*.snk 547 | 548 | # Since there are multiple workflows, uncomment next line to ignore bower_components 549 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 550 | #bower_components/ 551 | 552 | # RIA/Silverlight projects 553 | Generated_Code/ 554 | 555 | # Backup & report files from converting an old project file 556 | # to a newer Visual Studio version. Backup files are not needed, 557 | # because we have git ;-) 558 | _UpgradeReport_Files/ 559 | Backup*/ 560 | UpgradeLog*.XML 561 | UpgradeLog*.htm 562 | ServiceFabricBackup/ 563 | *.rptproj.bak 564 | 565 | # SQL Server files 566 | *.mdf 567 | *.ldf 568 | *.ndf 569 | 570 | # Business Intelligence projects 571 | *.rdl.data 572 | *.bim.layout 573 | *.bim_*.settings 574 | *.rptproj.rsuser 575 | *- [Bb]ackup.rdl 576 | *- [Bb]ackup ([0-9]).rdl 577 | *- [Bb]ackup ([0-9][0-9]).rdl 578 | 579 | # Microsoft Fakes 580 | FakesAssemblies/ 581 | 582 | # GhostDoc plugin setting file 583 | *.GhostDoc.xml 584 | 585 | # Node.js Tools for Visual Studio 586 | .ntvs_analysis.dat 587 | node_modules/ 588 | 589 | # Visual Studio 6 build log 590 | *.plg 591 | 592 | # Visual Studio 6 workspace options file 593 | *.opt 594 | 595 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 596 | *.vbw 597 | 598 | # Visual Studio LightSwitch build output 599 | **/*.HTMLClient/GeneratedArtifacts 600 | **/*.DesktopClient/GeneratedArtifacts 601 | **/*.DesktopClient/ModelManifest.xml 602 | **/*.Server/GeneratedArtifacts 603 | **/*.Server/ModelManifest.xml 604 | _Pvt_Extensions 605 | 606 | # Paket dependency manager 607 | .paket/paket.exe 608 | paket-files/ 609 | 610 | # FAKE - F# Make 611 | .fake/ 612 | 613 | # CodeRush personal settings 614 | .cr/personal 615 | 616 | # Python Tools for Visual Studio (PTVS) 617 | 618 | # Cake - Uncomment if you are using it 619 | # tools/** 620 | # !tools/packages.config 621 | 622 | # Tabs Studio 623 | *.tss 624 | 625 | # Telerik's JustMock configuration file 626 | *.jmconfig 627 | 628 | # BizTalk build output 629 | *.btp.cs 630 | *.btm.cs 631 | *.odx.cs 632 | *.xsd.cs 633 | 634 | # OpenCover UI analysis results 635 | OpenCover/ 636 | 637 | # Azure Stream Analytics local run output 638 | ASALocalRun/ 639 | 640 | # MSBuild Binary and Structured Log 641 | *.binlog 642 | 643 | # NVidia Nsight GPU debugger configuration file 644 | *.nvuser 645 | 646 | # MFractors (Xamarin productivity tool) working folder 647 | .mfractor/ 648 | 649 | # Local History for Visual Studio 650 | .localhistory/ 651 | 652 | # BeatPulse healthcheck temp database 653 | healthchecksdb 654 | 655 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 656 | MigrationBackup/ 657 | 658 | # Ionide (cross platform F# VS Code tools) working folder 659 | .ionide/ 660 | 661 | # End of https://www.toptal.com/developers/gitignore/api/database,python,django,pycharm,visualstudio 662 | .idea/* 663 | # Visual code studio 664 | .vscode/* 665 | 666 | # Static file 667 | static/* 668 | user_images/* 669 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Rest API Boilerplate 2 | 3 | ## Technology used 4 | 1. Python3.8 5 | 2. Django 3 6 | 3. Django Rest Framework 7 | 4. MySql 8 | 5. Docker 9 | 10 | ## Features 11 | * User SignIn/SignUp 12 | * Forget Password 13 | * Email Verification 14 | * Login with Social Accounts (Facebook/Google) 15 | * OAuth 2.0 (Authentication with Access & Refresh Token) 16 | * Swagger Documentation 17 | * Testing - [**Coming Soon**] 18 | * Docker Config - [**Coming Soon**] 19 | 20 | ## Setup in your local machine 21 | 1. Clone project 22 | ``` 23 | git clone https://github.com/morshedmasud/django-rest-framework-mysql-boilerplate 24 | ``` 25 | 2. Go to project root path and create virtualenv 26 | ``` 27 | virtualenv -p python3 venv 28 | ``` 29 | 3. Active virtualenv 30 | ``` 31 | source venv/bin/activate 32 | ``` 33 | 4. Install all dependency 34 | ```shell script 35 | pip3 install -r requirements.txt 36 | ``` 37 | 5. Don't forget to create **.env** file as like **.env.example** and put necessary values like **DB Info, Email Info** 38 | 6. Database migrations 39 | ```shell script 40 | python3 manage.py makemigrations 41 | ``` 42 | 7. Database Migrate 43 | ```shell script 44 | python3 manage.py migrate 45 | ``` 46 | 8. Database Migrate and Seeder 47 | ```shell script 48 | bash migrate_and_seed.sh 49 | ``` 50 | 9. Finally, run the project by 51 | ```shell script 52 | python3 manage.py runserver 53 | ``` 54 | 10. Generated staticfiles 55 | ```shell script 56 | python3 manage.py collectstatic 57 | ``` 58 | 59 | #### Open the following url for view swagger documentation 60 | ## (http://localhost:8000/swagger/) 61 | -------------------------------------------------------------------------------- /accounts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morshedmasud/django-rest-framework-starter/fd426eca0b80e1326b04e3a01d3787fa38bb9241/accounts/__init__.py -------------------------------------------------------------------------------- /accounts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.models import Group 3 | from django.contrib.auth import get_user_model 4 | from . import models 5 | 6 | # Register your models here. 7 | user = get_user_model() 8 | 9 | 10 | class AccessTokensAdmin(admin.ModelAdmin): 11 | list_display = ['id', 'user_id', 'access_token', 'created_at', 'expired_at'] 12 | 13 | 14 | class RefreshTokensAdmin(admin.ModelAdmin): 15 | list_display = ['id', 'refresh_token', 'access_token_id', 'created_at', 'expired_at'] 16 | 17 | 18 | # admin.site.unregister(Group) 19 | admin.site.register(user) -------------------------------------------------------------------------------- /accounts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountsConfig(AppConfig): 5 | name = 'accounts' 6 | -------------------------------------------------------------------------------- /accounts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0 on 2021-11-25 10:51 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='User', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('password', models.CharField(max_length=128, verbose_name='password')), 19 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 20 | ('email', models.EmailField(blank=True, max_length=200, null=True, unique=True, verbose_name='email')), 21 | ('full_name', models.CharField(blank=True, max_length=150, null=True, verbose_name='Full Name')), 22 | ('phone', models.CharField(blank=True, max_length=50, null=True, unique=True)), 23 | ('photo', models.ImageField(blank=True, null=True, upload_to='user_images/')), 24 | ('address', models.TextField(blank=True, null=True)), 25 | ('verify_code', models.TextField(blank=True, null=True)), 26 | ('is_verified', models.BooleanField(default=False)), 27 | ('updated_at', models.DateTimeField(auto_now_add=True)), 28 | ('created_at', models.DateTimeField(auto_now_add=True)), 29 | ('is_active', models.BooleanField(default=True)), 30 | ('is_admin', models.BooleanField(default=False)), 31 | ('is_staff', models.BooleanField(default=False)), 32 | ('social_id', models.TextField(blank=True, null=True)), 33 | ('provider', models.TextField(blank=True, null=True)), 34 | ('status', models.CharField(choices=[('active', 'Active user'), ('inactive', 'User Inactive'), ('deleted', 'Soft Delete user')], default='active', max_length=32)), 35 | ], 36 | options={ 37 | 'verbose_name_plural': 'users', 38 | }, 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /accounts/migrations/0002_auto_20211206_1558.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0 on 2021-12-06 09:58 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('accounts', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RenameField( 14 | model_name='user', 15 | old_name='provider', 16 | new_name='auth_id', 17 | ), 18 | migrations.RemoveField( 19 | model_name='user', 20 | name='social_id', 21 | ), 22 | migrations.AddField( 23 | model_name='user', 24 | name='auth_provider', 25 | field=models.CharField(default='email', max_length=255), 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /accounts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morshedmasud/django-rest-framework-starter/fd426eca0b80e1326b04e3a01d3787fa38bb9241/accounts/migrations/__init__.py -------------------------------------------------------------------------------- /accounts/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import AbstractBaseUser, BaseUserManager 3 | from rest_framework_simplejwt.tokens import RefreshToken 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | 7 | class UserManager(BaseUserManager): 8 | 9 | def create_user(self, email, full_name, password=None): 10 | if not email: 11 | raise ValueError('User must have email') 12 | 13 | email = email.lower() 14 | full_name = full_name.title() 15 | 16 | user = self.model( 17 | email=self.normalize_email(email), 18 | full_name=full_name, 19 | ) 20 | 21 | user.set_password(password) 22 | user.save(using=self._db) 23 | 24 | return user 25 | 26 | def create_user_social(self, email=None, full_name=None, provider=None, social_id=None): 27 | if not provider and social_id: 28 | raise ValueError('Provider and social id is missing') 29 | 30 | email = email.lower() 31 | full_name = full_name.title() 32 | 33 | user = self.model( 34 | email=self.normalize_email(email), 35 | full_name=full_name, 36 | auth_provider=provider, 37 | auth_id=social_id 38 | ) 39 | user.is_verified = True 40 | 41 | user.save(using=self._db) 42 | return user 43 | 44 | def create_superuser(self, email, full_name=None, password=None): 45 | user = self.create_user( 46 | email=email, 47 | full_name=full_name if full_name else email, 48 | password=password 49 | ) 50 | 51 | user.is_admin = True 52 | user.is_staff = True 53 | user.save(using=self._db) 54 | 55 | return user 56 | 57 | 58 | AUTH_PROVIDERS = {'facebook': 'facebook', 'google': 'google', 'twitter': 'twitter', 'email': 'email'} 59 | 60 | 61 | class User(AbstractBaseUser): 62 | 63 | ACTIVE = 'active' 64 | INACTIVE = 'inactive' 65 | DELETED = 'deleted' 66 | STATUS = [ 67 | (ACTIVE, _('Active user')), 68 | (INACTIVE, _('User Inactive')), 69 | (DELETED, _('Soft Delete user')), 70 | ] 71 | 72 | email = models.EmailField(max_length=200, unique=True, null=True, blank=True, verbose_name='email') 73 | full_name = models.CharField(max_length=150, null=True, blank=True, verbose_name='Full Name') 74 | phone = models.CharField(max_length=50, unique=True, null=True, blank=True) 75 | photo = models.ImageField(upload_to='user_images/', null=True, blank=True) 76 | address = models.TextField(null=True, blank=True) 77 | verify_code = models.TextField(null=True, blank=True) 78 | is_verified = models.BooleanField(default=False) 79 | updated_at = models.DateTimeField(auto_now_add=True) 80 | created_at = models.DateTimeField(auto_now_add=True) 81 | is_active = models.BooleanField(default=True) 82 | is_admin = models.BooleanField(default=False) 83 | is_staff = models.BooleanField(default=False) 84 | 85 | auth_id = models.TextField(null=True, blank=True) 86 | auth_provider = models.CharField(max_length=255, blank=False, null=False, default=AUTH_PROVIDERS.get('email')) 87 | 88 | status = models.CharField(max_length=32, choices=STATUS, default=ACTIVE) 89 | 90 | USERNAME_FIELD = 'email' 91 | REQUIRED_FIELDS = [] 92 | 93 | objects = UserManager() 94 | 95 | def __str__(self): 96 | return self.email 97 | 98 | def get_short_name(self): 99 | return self.email 100 | 101 | def has_perm(self, perm, obj=None): 102 | return self.is_admin 103 | 104 | def has_module_perms(self, app_label): 105 | return self.is_admin 106 | 107 | class Meta: 108 | verbose_name_plural = 'users' 109 | 110 | def get_user_info(self): 111 | return { 112 | 'user_id': self.id, 113 | 'email': self.email, 114 | 'full_name': self.full_name 115 | } 116 | 117 | def tokens(self): 118 | refresh = RefreshToken.for_user(self) 119 | return { 120 | 'refresh': str(refresh), 121 | 'access': str(refresh.access_token) 122 | } 123 | -------------------------------------------------------------------------------- /accounts/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import User 3 | from django.contrib import auth 4 | from rest_framework.exceptions import AuthenticationFailed, NotAcceptable 5 | from django.contrib.auth.tokens import PasswordResetTokenGenerator 6 | from django.utils.encoding import smart_str, force_str, smart_bytes, DjangoUnicodeDecodeError 7 | from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode 8 | from django.contrib.sites.shortcuts import get_current_site 9 | from django.urls import reverse 10 | from .utils import Util 11 | import requests 12 | 13 | 14 | class RegisterSerializer(serializers.ModelSerializer): 15 | password = serializers.CharField(max_length=60, min_length=6, write_only=True) 16 | 17 | class Meta: 18 | model = User 19 | fields = ['email', 'full_name', 'password', 'id'] 20 | 21 | def validate(self, attrs): 22 | return attrs 23 | 24 | def create(self, validated_data): 25 | return User.objects.create_user(**validated_data) 26 | 27 | 28 | class EmailVerificationSerializer(serializers.ModelSerializer): 29 | token = serializers.CharField(max_length=555) 30 | 31 | class Meta: 32 | model = User 33 | fields = ['token'] 34 | 35 | 36 | class LoginSerializer(serializers.ModelSerializer): 37 | email = serializers.EmailField(max_length=255, min_length=3) 38 | password = serializers.CharField(max_length=58, min_length=6, write_only=True) 39 | full_name = serializers.CharField(max_length=255, read_only=True) 40 | tokens = serializers.DictField(read_only=True) 41 | 42 | class Meta: 43 | model = User 44 | fields = ['id', 'email', 'full_name', 'password', 'tokens'] 45 | 46 | def validate(self, attrs): 47 | email = attrs.get('email', '') 48 | password = attrs.get('password', '') 49 | 50 | user = auth.authenticate(email=email, password=password) 51 | if not user: 52 | raise NotAcceptable("Email or Password Incorrect, try again") 53 | if not user.is_active: 54 | raise NotAcceptable("Account Disabled, contact admin") 55 | if not user.is_verified: 56 | raise NotAcceptable("Email is not verified.") 57 | return { 58 | 'id': user.id, 59 | 'email': user.email, 60 | 'full_name': user.full_name, 61 | 'tokens': user.tokens() 62 | } 63 | 64 | 65 | class ResetPasswordWithEmailSerializer(serializers.Serializer): 66 | email = serializers.EmailField(min_length=2) 67 | 68 | class Meta: 69 | fields = ['email'] 70 | 71 | 72 | class PasswordTokenCheckSerializer(serializers.Serializer): 73 | token = serializers.CharField(min_length=1, write_only=True) 74 | uidb64 = serializers.CharField(min_length=1, write_only=True) 75 | 76 | class Meta: 77 | fields = ['token', 'token'] 78 | 79 | 80 | class SetNewPasswordSerializer(serializers.Serializer): 81 | password = serializers.CharField(min_length=8, write_only=True) 82 | token = serializers.CharField(min_length=1, write_only=True) 83 | uidb64 = serializers.CharField(min_length=1, write_only=True) 84 | 85 | class Meta: 86 | fields = ['password', 'token', 'uidb64'] 87 | 88 | def validate(self, attrs): 89 | try: 90 | password = attrs.get('password') 91 | token = attrs.get('token') 92 | uidb64 = attrs.get('uidb64') 93 | 94 | id = force_str(urlsafe_base64_decode(uidb64)) 95 | user = User.objects.get(id=id) 96 | 97 | if not PasswordResetTokenGenerator().check_token(user, token): 98 | raise AuthenticationFailed('The reset link is invalid', 401) 99 | 100 | user.set_password(password) 101 | user.save() 102 | except Exception as err: 103 | raise AuthenticationFailed('The reset link is invalid', 401) 104 | return super().validate(attrs) 105 | 106 | 107 | class UsersSerializer(serializers.ModelSerializer): 108 | 109 | class Meta: 110 | model = User 111 | fields = ['id', 'full_name', 'email', 'phone', 'photo', 'address', 'status', 'created_at'] 112 | 113 | 114 | class MeAPIViewSerializer(serializers.ModelSerializer): 115 | role = serializers.StringRelatedField() 116 | 117 | class Meta: 118 | model = User 119 | fields = ['id', 'full_name', 'email', 'phone', 'photo', 'address', 'created_at'] 120 | -------------------------------------------------------------------------------- /accounts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /accounts/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | from rest_framework_simplejwt.views import ( 4 | TokenRefreshView, 5 | ) 6 | 7 | urlpatterns = [ 8 | path('register', views.RegisterView.as_view(), name='register'), 9 | path('verify-email', views.VerifyEmail.as_view(), name='verify-email'), 10 | path('login', views.LoginApiView.as_view(), name='login'), 11 | 12 | path('token/refresh', TokenRefreshView.as_view(), name='token_refresh'), 13 | 14 | path('request-reset-password', views.ResetPasswordWithEmail.as_view(), name='request-reset-password'), 15 | path('password-reset//', views.PasswordTokenCheckAPI.as_view(), name='password-reset-confirm'), 16 | path('password-reset-complete', views.SetNewPasswordAPIView.as_view(), name='password-reset-complete'), 17 | 18 | path('me', views.MeAPIView.as_view(), name='me'), 19 | path('users', views.UserListAPIView.as_view(), name='all-users'), 20 | path('', views.UserDetailsAPIView.as_view(), name="single-user"), 21 | ] 22 | -------------------------------------------------------------------------------- /accounts/utils.py: -------------------------------------------------------------------------------- 1 | from django.core.mail import EmailMessage 2 | 3 | 4 | class Util: 5 | @staticmethod 6 | def send_email(data): 7 | try: 8 | email = EmailMessage(to=[data['to_email']], subject=data['email_subject'], body=data['email_body']) 9 | 10 | email.send() 11 | except Exception as err: 12 | print(f"raised error while sending email: {err}") 13 | 14 | 15 | -------------------------------------------------------------------------------- /accounts/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.views import APIView 2 | from rest_framework_simplejwt.tokens import RefreshToken 3 | 4 | from rest_framework.response import Response 5 | from rest_framework import status, generics, views 6 | from main import settings 7 | from django.contrib.auth.tokens import PasswordResetTokenGenerator 8 | from django.utils.encoding import smart_str, force_str, smart_bytes, DjangoUnicodeDecodeError 9 | from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode 10 | 11 | 12 | from .models import User 13 | from .serializers import RegisterSerializer, EmailVerificationSerializer, LoginSerializer, \ 14 | ResetPasswordWithEmailSerializer, SetNewPasswordSerializer, PasswordTokenCheckSerializer, MeAPIViewSerializer, \ 15 | UsersSerializer 16 | from .utils import Util 17 | from django.contrib.sites.shortcuts import get_current_site 18 | from django.urls import reverse 19 | import jwt 20 | from django.conf import settings 21 | from drf_yasg.utils import swagger_auto_schema 22 | from drf_yasg import openapi 23 | from rest_framework import permissions 24 | from services.rederers import Renderer 25 | from services import customPermission 26 | 27 | 28 | class RegisterView(generics.GenericAPIView): 29 | serializer_class = RegisterSerializer 30 | renderer_classes = (Renderer,) 31 | 32 | def post(self, request): 33 | user = request.data 34 | serializer = self.serializer_class(data=user) 35 | serializer.is_valid(raise_exception=True) 36 | serializer.save() 37 | 38 | user_data = serializer.data 39 | 40 | user = User.objects.get(email=user_data['email']) 41 | token = RefreshToken.for_user(user).access_token 42 | 43 | relative_link = reverse('verify-email') 44 | current_site = get_current_site(request).domain 45 | 46 | abs_url = f"http://{current_site}{relative_link}?token={token}" 47 | 48 | email_body = 'Hi {} Use below link to verify your email \n {}'.format(user.full_name, abs_url) 49 | data = {'to_email': user.email, 'email_body': email_body, 'email_subject': 'Verify your email'} 50 | Util.send_email(data) 51 | 52 | return Response(user_data, status=status.HTTP_201_CREATED) 53 | 54 | 55 | class VerifyEmail(APIView): 56 | serializer_class = EmailVerificationSerializer 57 | renderer_classes = (Renderer,) 58 | 59 | token_param_config = openapi.Parameter('token', in_=openapi.IN_QUERY, description='Description', type=openapi.TYPE_STRING) 60 | 61 | @swagger_auto_schema(manual_parameters=[token_param_config]) 62 | def get(self, request): 63 | token = request.GET.get('token') 64 | try: 65 | payload = jwt.decode(token, settings.SECRET_KEY) 66 | user = User.objects.get(id=payload['user_id']) 67 | if not user.is_verified: 68 | user.is_verified = True 69 | user.save() 70 | return Response({'email': 'Successfully Verified'}, status=status.HTTP_201_CREATED) 71 | except jwt.exceptions.DecodeError as err: 72 | return Response({'email': 'Invalid Token'}, status=status.HTTP_400_BAD_REQUEST) 73 | 74 | 75 | class LoginApiView(generics.GenericAPIView): 76 | serializer_class = LoginSerializer 77 | renderer_classes = (Renderer,) 78 | 79 | def post(self, request): 80 | serializer = self.serializer_class(data=request.data) 81 | serializer.is_valid(raise_exception=True) 82 | return Response(serializer.data, status=status.HTTP_201_CREATED) 83 | 84 | 85 | class ResetPasswordWithEmail(generics.GenericAPIView): 86 | serializer_class = ResetPasswordWithEmailSerializer 87 | renderer_classes = (Renderer,) 88 | 89 | def post(self, request): 90 | data = {'request': request, 'data': request.data} 91 | serializer = self.serializer_class(data=data) 92 | email = request.data['email'] 93 | 94 | if User.objects.filter(email=email).exists(): 95 | user = User.objects.get(email=email) 96 | 97 | uidb64 = urlsafe_base64_encode(smart_bytes(user.id)) 98 | token = PasswordResetTokenGenerator().make_token(user) 99 | current_site = get_current_site(request=request).domain 100 | 101 | relative_link = reverse('password-reset-confirm', kwargs={'uidb64': uidb64, 'token': token}) 102 | abs_url = 'http://' + current_site + relative_link 103 | 104 | email_body = 'Hi, \n Use below link to reset your password \n {}'.format(abs_url) 105 | data = {'to_email': user.email, 'email_body': email_body, 'email_subject': 'Reset your password'} 106 | Util.send_email(data) 107 | 108 | return Response({'success': 'We have send you a link to reset your password'}, status=status.HTTP_200_OK) 109 | else: 110 | return Response({'error': 'not found email'}, status=status.HTTP_406_NOT_ACCEPTABLE) 111 | 112 | 113 | class PasswordTokenCheckAPI(generics.GenericAPIView): 114 | serializer_class = PasswordTokenCheckSerializer 115 | renderer_classes = (Renderer,) 116 | 117 | def get(self, request, uidb64, token): 118 | try: 119 | id = smart_str(urlsafe_base64_decode(uidb64)) 120 | user = User.objects.get(id=id) 121 | 122 | if not PasswordResetTokenGenerator().check_token(user, token): 123 | return Response({'error': 'Token is not valid, please request a new one'}, status.HTTP_401_UNAUTHORIZED) 124 | 125 | return Response({'success': True, 'message': 'Credential valid', 'uidb64': uidb64, 'token': token}, status=status.HTTP_200_OK) 126 | except DjangoUnicodeDecodeError as err: 127 | return Response({'error': 'Token is not valid, please request a new one'}, status.HTTP_401_UNAUTHORIZED) 128 | 129 | 130 | class SetNewPasswordAPIView(generics.GenericAPIView): 131 | serializer_class = SetNewPasswordSerializer 132 | renderer_classes = (Renderer,) 133 | 134 | def patch(self, request): 135 | serializer = self.serializer_class(data=request.data) 136 | serializer.is_valid(raise_exception=True) 137 | return Response({'success': True, 'message': 'Password reset success'}, status=status.HTTP_200_OK) 138 | 139 | 140 | class UserListAPIView(generics.ListCreateAPIView): 141 | serializer_class = UsersSerializer 142 | queryset = User.objects.all() 143 | renderer_classes = (Renderer,) 144 | permission_classes = (permissions.IsAuthenticated,) 145 | 146 | def perform_create(self, serializer): 147 | return serializer.save() 148 | 149 | def get_queryset(self): 150 | return self.queryset 151 | 152 | 153 | class UserDetailsAPIView(generics.RetrieveUpdateDestroyAPIView): 154 | serializer_class = UsersSerializer 155 | queryset = User.objects.all() 156 | renderer_classes = (Renderer,) 157 | permission_classes = (permissions.IsAuthenticated, customPermission.IsUsersPermission) 158 | lookup_field = 'id' 159 | 160 | def get_queryset(self): 161 | return self.queryset 162 | 163 | 164 | class MeAPIView(views.APIView): 165 | serializer_class = MeAPIViewSerializer 166 | renderer_classes = (Renderer,) 167 | permission_classes = (permissions.IsAuthenticated, ) 168 | 169 | def get(self, request): 170 | user_info = User.objects.get(id=request.user.id) 171 | serializer = self.serializer_class(user_info) 172 | 173 | return Response(serializer.data, status=status.HTTP_200_OK) 174 | -------------------------------------------------------------------------------- /main/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morshedmasud/django-rest-framework-starter/fd426eca0b80e1326b04e3a01d3787fa38bb9241/main/__init__.py -------------------------------------------------------------------------------- /main/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for main 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/3.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', 'main.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /main/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for main project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.0. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | import datetime 15 | 16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = os.environ.get('SECRET_KEY', '%8lx6^a%z&a%2oz&@o@(wtin3kw32$ggtpuhd**)8kcph9eq') 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = str(os.environ.get('DEBUG')) == "1" # 1 == True 28 | 29 | ALLOWED_HOSTS = [] 30 | if not DEBUG: 31 | ALLOWED_HOSTS += [os.environ.get('ALLOWED_HOSTS')] 32 | 33 | 34 | # Application definition 35 | 36 | INSTALLED_APPS = [ 37 | 'django.contrib.admin', 38 | 'django.contrib.auth', 39 | 'django.contrib.contenttypes', 40 | 'django.contrib.sessions', 41 | 'django.contrib.messages', 42 | 'django.contrib.staticfiles', 43 | # Third party 44 | 'drf_yasg', 45 | 'corsheaders', 46 | 'django_seed', 47 | 48 | 'accounts', 49 | 'social_auth' 50 | ] 51 | 52 | AUTH_USER_MODEL = 'accounts.User' 53 | 54 | SWAGGER_SETTINGS = { 55 | 'SECURITY_DEFINITIONS': { 56 | 'Bearer': { 57 | 'type': 'apiKey', 58 | 'name': 'Authorization', 59 | 'in': 'header' 60 | } 61 | } 62 | } 63 | 64 | MIDDLEWARE = [ 65 | 'django.middleware.security.SecurityMiddleware', 66 | 'django.contrib.sessions.middleware.SessionMiddleware', 67 | 'django.middleware.common.CommonMiddleware', 68 | 'django.middleware.csrf.CsrfViewMiddleware', 69 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 70 | 'django.contrib.messages.middleware.MessageMiddleware', 71 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 72 | ] 73 | 74 | ROOT_URLCONF = 'main.urls' 75 | 76 | TEMPLATES = [ 77 | { 78 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 79 | 'DIRS': [], 80 | 'APP_DIRS': True, 81 | 'OPTIONS': { 82 | 'context_processors': [ 83 | 'django.template.context_processors.debug', 84 | 'django.template.context_processors.request', 85 | 'django.contrib.auth.context_processors.auth', 86 | 'django.contrib.messages.context_processors.messages', 87 | ], 88 | }, 89 | }, 90 | ] 91 | 92 | WSGI_APPLICATION = 'main.wsgi.application' 93 | 94 | 95 | # Database 96 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 97 | 98 | DATABASES = { 99 | 'default': { 100 | 'ENGINE': 'django.db.backends.mysql', 101 | 'NAME': os.environ.get('DB_NAME'), 102 | 'USER': os.environ.get('DB_USER'), 103 | 'PASSWORD': os.environ.get('DB_PASSWORD'), 104 | 'HOST': os.environ.get('DB_HOST'), 105 | 'PORT': os.environ.get('DB_PORT') 106 | } 107 | } 108 | 109 | # Config simple jwt as default authentication and pagination 110 | REST_FRAMEWORK = { 111 | 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 112 | 'PAGE_SIZE': 10, 113 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 114 | 'rest_framework_simplejwt.authentication.JWTAuthentication', 115 | ) 116 | } 117 | # Setup JWT expired date 118 | SIMPLE_JWT = { 119 | 'ACCESS_TOKEN_LIFETIME': datetime.timedelta(days=7), 120 | 'REFRESH_TOKEN_LIFETIME': datetime.timedelta(days=14), 121 | } 122 | 123 | 124 | # Password validation 125 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 126 | 127 | AUTH_PASSWORD_VALIDATORS = [ 128 | # { 129 | # 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 130 | # }, 131 | { 132 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 133 | }, 134 | # { 135 | # 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 136 | # }, 137 | # { 138 | # 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 139 | # }, 140 | ] 141 | 142 | 143 | # Internationalization 144 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 145 | 146 | LANGUAGE_CODE = 'en-us' 147 | 148 | TIME_ZONE = 'Asia/Dhaka' 149 | 150 | USE_I18N = True 151 | 152 | USE_L10N = True 153 | 154 | USE_TZ = True 155 | 156 | 157 | # Static files (CSS, JavaScript, Images) 158 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 159 | 160 | STATIC_URL = '/static/' 161 | 162 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 163 | 164 | 165 | # email confirmation 166 | EMAIL_HOST = os.environ.get('EMAIL_HOST') 167 | EMAIL_USE_TLS = True 168 | EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER') 169 | EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD') 170 | EMAIL_PORT = os.environ.get('EMAIL_PORT') 171 | 172 | # CORS 173 | CORS_ORIGIN_ALLOW_ALL = True # If this is used then `CORS_ORIGIN_WHITELIST` will not have any effect 174 | CORS_ALLOW_CREDENTIALS = True 175 | # CORS_ORIGIN_WHITELIST = [ 176 | # 'http://localhost:3000', 177 | # ] # If this is used, then not need to use `CORS_ORIGIN_ALLOW_ALL = True` 178 | # CORS_ORIGIN_REGEX_WHITELIST = [ 179 | # 'http://localhost:3000', 180 | # ] 181 | -------------------------------------------------------------------------------- /main/urls.py: -------------------------------------------------------------------------------- 1 | """base_project URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.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 | 19 | from rest_framework import permissions 20 | from drf_yasg.views import get_schema_view 21 | from drf_yasg import openapi 22 | from django.conf.urls.static import static 23 | from main import settings 24 | from . import views 25 | 26 | 27 | schema_view = get_schema_view( 28 | openapi.Info( 29 | title="Django-Rest API", 30 | default_version='v1', 31 | description="Test description", 32 | terms_of_service="https://www.google.com/policies/terms/", 33 | contact=openapi.Contact(email="contact@snippets.local"), 34 | license=openapi.License(name="BSD License"), 35 | ), 36 | public=True, 37 | permission_classes=(permissions.AllowAny,), 38 | ) 39 | 40 | 41 | urlpatterns = [ 42 | path('', views.index), 43 | # path('swagger(?P\.json|\.yaml)', schema_view.without_ui(cache_timeout=0), name='schema-json'), 44 | path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), 45 | path('redoc', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), 46 | path('admin/', admin.site.urls), 47 | path('user/', include('accounts.urls')), 48 | path('social_auth/', include(('social_auth.urls', 'social_auth'), namespace='social_auth')) 49 | ] 50 | -------------------------------------------------------------------------------- /main/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | 3 | 4 | def index(request): 5 | return HttpResponse("Welcome To Django Rest API v_0.0.1") 6 | 7 | -------------------------------------------------------------------------------- /main/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for main 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/3.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', 'main.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 | import dotenv 6 | 7 | 8 | def main(): 9 | dotenv.read_dotenv() 10 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'main.settings') 11 | try: 12 | from django.core.management import execute_from_command_line 13 | except ImportError as exc: 14 | raise ImportError( 15 | "Couldn't import Django. Are you sure it's installed and " 16 | "available on your PYTHONPATH environment variable? Did you " 17 | "forget to activate a virtual environment?" 18 | ) from exc 19 | execute_from_command_line(sys.argv) 20 | 21 | 22 | if __name__ == '__main__': 23 | main() 24 | -------------------------------------------------------------------------------- /migrate_and_seed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | python3 manage.py migrate 3 | fixtures=$(ls seed/) 4 | while IFS= read -r fixture; do 5 | echo -n "Seeding " 6 | echo $fixture 7 | python3 manage.py loaddata seed/$fixture 8 | done <<< "$fixtures" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.3 2 | asgiref==3.2.10 3 | bcrypt==3.1.7 4 | CacheControl==0.12.6 5 | cachetools==4.2.4 6 | certifi==2023.7.22 7 | cffi==1.14.0 8 | chardet==3.0.4 9 | colorama==0.4.3 10 | contextlib2==0.6.0 11 | coreapi==2.3.3 12 | coreschema==0.0.4 13 | cryptography>=3.3.2 14 | defusedxml==0.7.0rc1 15 | distlib==0.3.0 16 | distro==1.4.0 17 | Django==3.1.14 18 | django-cors-headers==3.5.0 19 | django-dotenv==1.4.2 20 | django-seed==0.2.2 21 | django-sslserver==0.22 22 | djangorestframework==3.11.0 23 | djangorestframework-simplejwt==4.6.0 24 | drf-yasg==1.17.1 25 | facebook-sdk==3.1.0 26 | Faker==4.17.1 27 | google-api-core==2.2.2 28 | google-api-python-client==2.32.0 29 | google-auth==2.3.3 30 | google-auth-httplib2==0.1.0 31 | googleapis-common-protos==1.53.0 32 | html5lib==1.0.1 33 | httplib2==0.20.2 34 | idna==3.7 35 | inflection==0.5.0 36 | ipaddr==2.2.0 37 | itypes==1.2.0 38 | jinja2>=2.11.3 39 | lockfile==0.12.2 40 | MarkupSafe>=2.0 41 | msgpack==0.6.2 42 | mysqlclient==1.4.6 43 | oauthlib==3.1.0 44 | openapi-codec==1.3.2 45 | packaging==20.4 46 | pep517==0.8.2 47 | Pillow>=9.0.1 48 | progress==1.5 49 | protobuf==4.21.6 50 | pyasn1==0.4.8 51 | pyasn1-modules==0.2.8 52 | pycparser==2.20 53 | pyjwt==2.4.0 54 | pyparsing==2.4.7 55 | python-dateutil==2.8.1 56 | python3-openid==3.2.0 57 | pytoml==0.1.21 58 | pytz==2020.1 59 | requests>=2.24.0 60 | requests-oauthlib==1.3.0 61 | rest-social-auth==4.2.0 62 | retrying==1.3.3 63 | rsa==4.8 64 | ruamel.yaml==0.16.10 65 | ruamel.yaml.clib==0.2.0 66 | simplejson==3.17.0 67 | six==1.15.0 68 | social-auth-app-django==3.4.0 69 | social-auth-core==3.3.3 70 | sqlparse==0.5.0 71 | text-unidecode==1.3 72 | toml==0.10.2 73 | uritemplate==3.0.1 74 | urllib3>=1.26.5 75 | webencodings==0.5.1 76 | -------------------------------------------------------------------------------- /seed/0001_users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "accounts.user", 4 | "pk": 1, 5 | "fields": { 6 | "password": "pbkdf2_sha256$180000$w1m8kQqiyypN$k8x7Xkp5fOWr4ImfYySpn/WnKUOJRVN8fLBHJWLJbIs=", 7 | "last_login": null, 8 | "email": "morshed.dev@gmail.com", 9 | "full_name": "Masud Morshed", 10 | "phone": "01670397894", 11 | "photo": "", 12 | "address": "Cumilla", 13 | "verify_code": null, 14 | "is_verified": true, 15 | "updated_at": "2021-08-06T18:04:08.058Z", 16 | "created_at": "2021-08-06T18:04:08.058Z", 17 | "is_active": true, 18 | "is_admin": true, 19 | "is_staff": false, 20 | "auth_id": null, 21 | "auth_provider": "email", 22 | "status": "active" 23 | } 24 | }, 25 | { 26 | "model": "accounts.user", 27 | "pk": 2, 28 | "fields": { 29 | "password": "Democratic around property past better. Be far weight including. Speak easy measure dog.", 30 | "last_login": "1985-08-29T14:05:55Z", 31 | "email": "test@test.com", 32 | "full_name": "Morshed", 33 | "phone": "0111111111", 34 | "photo": "population.jpeg", 35 | "address": "490 Thompson River Suite 905\nNew Randyborough, IA 38597", 36 | "verify_code": "Sister necessary weight yet source indicate. Sense sport word choice thank chair break. Product because recognize almost eat. Statement raise beyond give above increase.", 37 | "is_verified": true, 38 | "updated_at": "2015-10-01T01:35:55Z", 39 | "created_at": "1975-12-19T05:34:33Z", 40 | "is_active": true, 41 | "is_admin": false, 42 | "is_staff": true, 43 | "auth_id": null, 44 | "auth_provider": "email", 45 | "status": "active" 46 | } 47 | }, 48 | { 49 | "model": "accounts.user", 50 | "pk": 3, 51 | "fields": { 52 | "password": "You major nation something. As foreign cause impact party.\nBack follow finish activity must put.", 53 | "last_login": "1981-07-02T10:34:17Z", 54 | "email": "erica62@richmond-johnson.net", 55 | "full_name": "Test User", 56 | "phone": "203.989.7142x519", 57 | "photo": "near.bmp", 58 | "address": "85159 Mark Street Apt. 323\nLaurieton, NY 03852", 59 | "verify_code": "Four four few six positive however. Hair walk election up time. Baby mind woman model resource. Age reveal see cut news only.\nEnvironmental story former inside themselves budget line.", 60 | "is_verified": true, 61 | "updated_at": "2003-06-13T13:42:29Z", 62 | "created_at": "1995-07-25T10:31:32Z", 63 | "is_active": true, 64 | "is_admin": false, 65 | "is_staff": true, 66 | "auth_id": null, 67 | "auth_provider": "email", 68 | "status": "active" 69 | } 70 | }, 71 | { 72 | "model": "accounts.user", 73 | "pk": 4, 74 | "fields": { 75 | "password": "Add hear strategy her that argue.\nTelevision include section letter. Could name food nor mean. Lay notice a scientist alone be.", 76 | "last_login": "2001-08-14T19:17:53Z", 77 | "email": "dthompson@hotmail.com", 78 | "full_name": "Test User2", 79 | "phone": "536.725.5384x01186", 80 | "photo": "kitchen.js", 81 | "address": "657 Thomas Villages Apt. 438\nFostermouth, RI 62493", 82 | "verify_code": "South model whom behavior forget. Particularly prove understand generation finish fund city. Culture significant pattern small test buy least.", 83 | "is_verified": false, 84 | "updated_at": "1978-12-24T00:15:44Z", 85 | "created_at": "1984-01-06T07:01:13Z", 86 | "is_active": true, 87 | "is_admin": true, 88 | "is_staff": false, 89 | "auth_id": null, 90 | "auth_provider": "email", 91 | "status": "active" 92 | } 93 | }, 94 | { 95 | "model": "accounts.user", 96 | "pk": 5, 97 | "fields": { 98 | "password": "Lot everything author. Cause create different among defense discuss matter.", 99 | "last_login": "2018-04-26T00:49:18Z", 100 | "email": "wilkinsonstefanie@garcia-ramirez.com", 101 | "full_name": "Test User 3", 102 | "phone": "(830)727-9879x85409", 103 | "photo": "could.jpg", 104 | "address": "778 Douglas Isle Suite 716\nNew Sharon, PA 92355", 105 | "verify_code": "Career onto information keep prepare. Rock energy reveal world sound simply.\nAgreement career two hold. Quickly reach town clear. Pay once local pay sit.", 106 | "is_verified": true, 107 | "updated_at": "1996-04-30T03:13:10Z", 108 | "created_at": "1994-03-24T05:59:41Z", 109 | "is_active": true, 110 | "is_admin": true, 111 | "is_staff": true, 112 | "auth_id": null, 113 | "auth_provider": "email", 114 | "status": "active" 115 | } 116 | }, 117 | { 118 | "model": "accounts.user", 119 | "pk": 12, 120 | "fields": { 121 | "password": "pbkdf2_sha256$180000$rvfPb7Z6XuMH$mRDR6H0VvGzgNNNzLZQWin4zd68hIctcR4DbjYi8D8g=", 122 | "last_login": "2021-08-07T07:44:24.682Z", 123 | "email": "admin@admin.com", 124 | "full_name": "admin", 125 | "phone": null, 126 | "photo": "", 127 | "address": null, 128 | "verify_code": null, 129 | "is_verified": false, 130 | "updated_at": "2021-08-07T07:42:46.733Z", 131 | "created_at": "2021-08-07T07:42:46.734Z", 132 | "is_active": true, 133 | "is_admin": true, 134 | "is_staff": true, 135 | "auth_id": null, 136 | "auth_provider": "email", 137 | "status": "active" 138 | } 139 | } 140 | ] 141 | -------------------------------------------------------------------------------- /services/authService.py: -------------------------------------------------------------------------------- 1 | from bcrypt import hashpw, gensalt 2 | from datetime import datetime, timedelta 3 | from rest_framework.response import Response 4 | from rest_framework import status 5 | 6 | from accounts import models 7 | from accounts.serializers import AccessTokensSerializer, RefreshTokensSerializer 8 | 9 | 10 | class AUTH: 11 | @staticmethod 12 | def generate_tokens(user_id, request): 13 | try: 14 | data = { 15 | "access_token": { 16 | 'token': (hashpw(str('accessToken').encode('UTF_8') + str(datetime.now()).encode('UTF_8') + str(user_id).encode('UTF_8'), gensalt())).decode('ascii'), 17 | 'created_at': datetime.today(), 18 | 'expired_at': datetime.today() + timedelta(minutes=720) 19 | }, 20 | 'refresh_token': { 21 | 'token': (hashpw(str('refreshToken').encode('UTF_8') + str(datetime.now()).encode('UTF_8') + str(user_id).encode('UTF_8'), gensalt())).decode('ascii'), 22 | 'created_at': datetime.today(), 23 | 'expired_at': datetime.today() + timedelta(minutes=4320) 24 | } 25 | 26 | } 27 | 28 | if request.META.get('HTTP_AUTHORIZATION'): 29 | # print(request.META.get('HTTP_AUTHORIZATION').split(' ')) 30 | # prev_token = models.AccessTokens.objects.filter(user_id=user_id) 31 | # print('prev token', prev_token) 32 | # # if prev_token: 33 | # # models.AccessTokens.objects.get(id=prev_token.access_token_id.id).delete() 34 | 35 | access_data = { 36 | 'access_token': data['access_token']['token'], 37 | 'user_id': user_id, 38 | 'expired_at': data['access_token']['expired_at'], 39 | 'created_at': data['access_token']['created_at'], 40 | } 41 | access_serializer = AccessTokensSerializer(data=access_data) 42 | if access_serializer.is_valid(): 43 | access_serializer.save() 44 | 45 | if access_serializer.data: 46 | refresh_data = { 47 | 'access_token_id': access_serializer.data['id'], 48 | 'refresh_token': data['refresh_token']['token'], 49 | 'user_id': user_id, 50 | 'expired_at': data['refresh_token']['expired_at'], 51 | 'created_at': data['refresh_token']['created_at'] 52 | } 53 | refresh_serializer = RefreshTokensSerializer(data=refresh_data) 54 | if refresh_serializer.is_valid(): 55 | refresh_serializer.save() 56 | return data 57 | else: 58 | return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) 59 | return Response(status=status.HTTP_401_UNAUTHORIZED) 60 | except Exception as err: 61 | return Response(status=status.HTTP_401_UNAUTHORIZED) 62 | 63 | @staticmethod 64 | def sign_out(request): 65 | try: 66 | print(request.META.get('HTTP_AUTHORIZATION')) 67 | token = request.META.get('HTTP_AUTHORIZATION') 68 | find_token = models.AccessTokens.objects.filter(access_token=token) 69 | if find_token: 70 | find_token.delete() 71 | 72 | except Exception as err: 73 | return Response(status=status.HTTP_401_UNAUTHORIZED) 74 | -------------------------------------------------------------------------------- /services/customPermission.py: -------------------------------------------------------------------------------- 1 | from rest_framework import permissions 2 | 3 | 4 | class IsUser(permissions.BasePermission): 5 | 6 | def has_object_permission(self, request, view, obj): 7 | return obj.user == request.user 8 | 9 | 10 | class IsAdmin(permissions.BasePermission): 11 | 12 | def has_permission(self, request, view): 13 | return request.user.role.id == 1 14 | 15 | 16 | class IsBoardMember(permissions.BasePermission): 17 | 18 | def has_permission(self, request, view): 19 | return request.user.role.id == 2 20 | 21 | 22 | class IsEditor(permissions.BasePermission): 23 | 24 | def has_permission(self, request, view): 25 | return request.user.role.id == 3 26 | 27 | 28 | class IsMember(permissions.BasePermission): 29 | 30 | def has_permission(self, request, view): 31 | return request.user.role.id == 4 32 | 33 | 34 | class IsExpensePermission(permissions.BasePermission): 35 | 36 | def has_permission(self, request, view): 37 | method = ['PATCH', 'POST', 'PUT', 'DELETE'] 38 | if request.method in permissions.SAFE_METHODS: 39 | return True 40 | elif request.method in method: 41 | return request.user.role.id < 4 42 | 43 | 44 | class IsDonationPermission(permissions.BasePermission): 45 | 46 | def has_permission(self, request, view): 47 | method = ['PATCH', 'POST', 'PUT', 'DELETE'] 48 | if request.method in permissions.SAFE_METHODS: 49 | return True 50 | elif request.method in method: 51 | return request.user.role.id < 4 52 | 53 | 54 | class IsCampaignPermission(permissions.BasePermission): 55 | 56 | def has_permission(self, request, view): 57 | method = ['PATCH', 'POST', 'PUT', 'DELETE'] 58 | if request.method in permissions.SAFE_METHODS: 59 | return True 60 | elif request.method in method: 61 | return request.user.role.id < 4 62 | 63 | 64 | class IsUsersPermission(permissions.BasePermission): 65 | 66 | def has_permission(self, request, view): 67 | method = ['PATCH', 'POST', 'PUT'] 68 | if request.method in permissions.SAFE_METHODS: 69 | return True 70 | elif request.method in method: 71 | return request.user.role.id <= 4 -------------------------------------------------------------------------------- /services/emailService.py: -------------------------------------------------------------------------------- 1 | from django.core.mail import send_mail 2 | from manobseba import settings 3 | 4 | 5 | class EmailServices: 6 | 7 | @staticmethod 8 | def email_verification(user_info): 9 | try: 10 | message_content = { 11 | 'subject': 'Email Verification', 12 | 'body': f"Hello {user_info['full_name']} Welcome to Manobseba Foundation. {user_info['verify_code']} please use the code to verify your email and keep your account secure. Thanks for helping us.The Manobseba Team", 13 | 'from': settings.EMAIL_HOST_USER, 14 | 'to': [user_info['email']] 15 | } 16 | 17 | send_mail( 18 | message_content['subject'], 19 | message_content['body'], 20 | message_content['from'], 21 | message_content['to'], 22 | fail_silently=False 23 | ) 24 | return True 25 | except SystemError as err: 26 | return False 27 | -------------------------------------------------------------------------------- /services/rederers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import renderers 2 | import json 3 | 4 | 5 | class Renderer(renderers.JSONRenderer): 6 | charset = 'utf-8' 7 | 8 | def render(self, data, accepted_media_type=None, renderer_context=None): 9 | response = '' 10 | if 'ErrorDetail' in str(data): 11 | response = json.dumps({'errors': data}) 12 | else: 13 | response = json.dumps({'data': data}) 14 | return response 15 | -------------------------------------------------------------------------------- /social_auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morshedmasud/django-rest-framework-starter/fd426eca0b80e1326b04e3a01d3787fa38bb9241/social_auth/__init__.py -------------------------------------------------------------------------------- /social_auth/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | # from .models import User 5 | 6 | 7 | # admin.site.register(User) 8 | -------------------------------------------------------------------------------- /social_auth/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SocialAuthConfig(AppConfig): 5 | name = 'social_auth' 6 | -------------------------------------------------------------------------------- /social_auth/facebook.py: -------------------------------------------------------------------------------- 1 | 2 | import facebook 3 | 4 | 5 | class Facebook: 6 | """ 7 | Facebook class to fetch the user info and return it 8 | """ 9 | 10 | @staticmethod 11 | def validate(auth_token): 12 | """ 13 | validate method Queries the facebook GraphAPI to fetch the user info 14 | """ 15 | try: 16 | graph = facebook.GraphAPI(access_token=auth_token) 17 | profile = graph.request('/me?fields=name,email') 18 | return profile 19 | except Exception as err: 20 | print(err) 21 | return "The token is invalid or expired." 22 | -------------------------------------------------------------------------------- /social_auth/google.py: -------------------------------------------------------------------------------- 1 | from google.auth.transport import requests 2 | from google.oauth2 import id_token 3 | 4 | 5 | class Google: 6 | """Google class to fetch the user info and return it""" 7 | 8 | @staticmethod 9 | def validate(auth_token): 10 | """ 11 | validate method Queries the Google oAUTH2 api to fetch the user info 12 | """ 13 | try: 14 | id_info = id_token.verify_oauth2_token( 15 | auth_token, requests.Request()) 16 | 17 | if 'accounts.google.com' in id_info['iss']: 18 | return id_info 19 | 20 | except Exception as err: 21 | print(err) 22 | return "The token is either invalid or has expired" 23 | -------------------------------------------------------------------------------- /social_auth/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morshedmasud/django-rest-framework-starter/fd426eca0b80e1326b04e3a01d3787fa38bb9241/social_auth/migrations/__init__.py -------------------------------------------------------------------------------- /social_auth/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /social_auth/register.py: -------------------------------------------------------------------------------- 1 | 2 | from django.contrib.auth import authenticate 3 | from accounts.models import User 4 | import os 5 | import random 6 | from rest_framework.exceptions import AuthenticationFailed 7 | 8 | 9 | def generate_username(name): 10 | 11 | username = "".join(name.split(' ')).lower() 12 | if not User.objects.filter(username=username).exists(): 13 | return username 14 | else: 15 | random_username = username + str(random.randint(0, 1000)) 16 | return generate_username(random_username) 17 | 18 | 19 | def register_social_user(provider, user_id, email, name): 20 | filtered_user_by_email = User.objects.filter(email=email) 21 | 22 | if filtered_user_by_email.exists(): 23 | if provider == filtered_user_by_email[0].auth_provider: 24 | 25 | registered_user = authenticate( 26 | email=email, password=os.environ.get('SOCIAL_SECRET')) 27 | 28 | return { 29 | 'full_name': registered_user.full_name, 30 | 'email': registered_user.email, 31 | 'tokens': registered_user.tokens()} 32 | 33 | else: 34 | raise AuthenticationFailed( 35 | detail='Please continue your login using ' + filtered_user_by_email[0].auth_provider) 36 | else: 37 | user = { 38 | 'full_name': name, 39 | 'email': email, 40 | 'password': os.environ.get('SOCIAL_SECRET')} 41 | user = User.objects.create_user(**user) 42 | user.is_verified = True 43 | user.auth_provider = provider 44 | user.save() 45 | 46 | new_user = authenticate( 47 | email=email, password=os.environ.get('SOCIAL_SECRET')) 48 | return { 49 | 'email': new_user.email, 50 | 'full_name': new_user.full_name, 51 | 'tokens': new_user.tokens() 52 | } 53 | -------------------------------------------------------------------------------- /social_auth/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from . import google, facebook 3 | from .register import register_social_user 4 | import os 5 | from rest_framework.exceptions import AuthenticationFailed 6 | 7 | 8 | class FacebookSocialAuthSerializer(serializers.Serializer): 9 | """Handles serialization of facebook related data""" 10 | auth_token = serializers.CharField() 11 | 12 | def validate_auth_token(self, auth_token): 13 | user_data = facebook.Facebook.validate(auth_token) 14 | 15 | try: 16 | user_id = user_data['id'] 17 | email = user_data['email'] 18 | name = user_data['name'] 19 | provider = 'facebook' 20 | return register_social_user( 21 | provider=provider, 22 | user_id=user_id, 23 | email=email, 24 | name=name 25 | ) 26 | except Exception as err: 27 | print(err) 28 | raise serializers.ValidationError('The token is invalid or expired. Please login again.') 29 | 30 | 31 | class GoogleSocialAuthSerializer(serializers.Serializer): 32 | auth_token = serializers.CharField() 33 | 34 | def validate_auth_token(self, auth_token): 35 | user_data = google.Google.validate(auth_token) 36 | try: 37 | user_data['sub'] 38 | except: 39 | raise serializers.ValidationError( 40 | 'The token is invalid or expired. Please login again.' 41 | ) 42 | 43 | if user_data['aud'] != os.environ.get('GOOGLE_CLIENT_ID'): 44 | raise AuthenticationFailed('oops, who are you?') 45 | 46 | user_id = user_data['sub'] 47 | email = user_data['email'] 48 | name = user_data['name'] 49 | provider = 'google' 50 | 51 | return register_social_user(provider=provider, user_id=user_id, email=email, name=name) 52 | -------------------------------------------------------------------------------- /social_auth/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /social_auth/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import GoogleSocialAuthView, FacebookSocialAuthView 4 | 5 | urlpatterns = [ 6 | path('google/', GoogleSocialAuthView.as_view()), 7 | path('facebook/', FacebookSocialAuthView.as_view()), 8 | 9 | ] 10 | -------------------------------------------------------------------------------- /social_auth/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import status 2 | from rest_framework.response import Response 3 | from rest_framework.generics import GenericAPIView 4 | 5 | from services.rederers import Renderer 6 | from .serializers import GoogleSocialAuthSerializer, FacebookSocialAuthSerializer 7 | 8 | 9 | class GoogleSocialAuthView(GenericAPIView): 10 | 11 | serializer_class = GoogleSocialAuthSerializer 12 | renderer_classes = (Renderer,) 13 | 14 | def post(self, request): 15 | """ 16 | POST with "auth_token" 17 | 18 | Send an idtoken as from google to get user information 19 | """ 20 | 21 | serializer = self.serializer_class(data=request.data) 22 | serializer.is_valid(raise_exception=True) 23 | data = (serializer.validated_data['auth_token']) 24 | return Response(data, status=status.HTTP_200_OK) 25 | 26 | 27 | class FacebookSocialAuthView(GenericAPIView): 28 | 29 | serializer_class = FacebookSocialAuthSerializer 30 | renderer_classes = (Renderer,) 31 | 32 | def post(self, request): 33 | """ 34 | POST with "auth_token" 35 | 36 | Send an access token as from facebook to get user information 37 | """ 38 | 39 | serializer = self.serializer_class(data=request.data) 40 | serializer.is_valid(raise_exception=True) 41 | data = (serializer.validated_data['auth_token']) 42 | return Response(data, status=status.HTTP_200_OK) 43 | --------------------------------------------------------------------------------