auth/services/login created
parent
21a61f79bc
commit
6b74fc5f9f
|
@ -0,0 +1,10 @@
|
||||||
|
DB_HOST=host
|
||||||
|
DB_USER=user
|
||||||
|
DB_PASSWORD=password
|
||||||
|
DB_DATABASE=dbname
|
||||||
|
DB_PORT=5432
|
||||||
|
JWT_ALGORITHM=HS256
|
||||||
|
JWT_SECRET=any_long_secret_with_minimal_3O_sign
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||||
|
REFRESH_TOKEN_EXPIRE_MINUTES=1440
|
||||||
|
LOGIN_2FA_PENDING_MINUTES=10
|
137
pdm.lock
137
pdm.lock
|
@ -5,7 +5,7 @@
|
||||||
groups = ["default"]
|
groups = ["default"]
|
||||||
strategy = ["cross_platform", "inherit_metadata"]
|
strategy = ["cross_platform", "inherit_metadata"]
|
||||||
lock_version = "4.4.1"
|
lock_version = "4.4.1"
|
||||||
content_hash = "sha256:cfeec8b9d696e1dab835175659cdadb3bd362a54b05f0e40d095310c9b1c7962"
|
content_hash = "sha256:b57525e7baee5eff69181af4f35e3137f3f5fea752042dc53c35886d639f35d6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "annotated-types"
|
name = "annotated-types"
|
||||||
|
@ -33,6 +33,88 @@ files = [
|
||||||
{file = "anyio-4.6.0.tar.gz", hash = "sha256:137b4559cbb034c477165047febb6ff83f390fc3b20bf181c1fc0a728cb8beeb"},
|
{file = "anyio-4.6.0.tar.gz", hash = "sha256:137b4559cbb034c477165047febb6ff83f390fc3b20bf181c1fc0a728cb8beeb"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argon2-cffi"
|
||||||
|
version = "23.1.0"
|
||||||
|
requires_python = ">=3.7"
|
||||||
|
summary = "Argon2 for Python"
|
||||||
|
groups = ["default"]
|
||||||
|
dependencies = [
|
||||||
|
"argon2-cffi-bindings",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"},
|
||||||
|
{file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argon2-cffi-bindings"
|
||||||
|
version = "21.2.0"
|
||||||
|
requires_python = ">=3.6"
|
||||||
|
summary = "Low-level CFFI bindings for Argon2"
|
||||||
|
groups = ["default"]
|
||||||
|
dependencies = [
|
||||||
|
"cffi>=1.0.1",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"},
|
||||||
|
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"},
|
||||||
|
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"},
|
||||||
|
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"},
|
||||||
|
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"},
|
||||||
|
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"},
|
||||||
|
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"},
|
||||||
|
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"},
|
||||||
|
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"},
|
||||||
|
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"},
|
||||||
|
{file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"},
|
||||||
|
{file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194"},
|
||||||
|
{file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f"},
|
||||||
|
{file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5"},
|
||||||
|
{file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"},
|
||||||
|
{file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7"},
|
||||||
|
{file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"},
|
||||||
|
{file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"},
|
||||||
|
{file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"},
|
||||||
|
{file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"},
|
||||||
|
{file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cffi"
|
||||||
|
version = "1.17.1"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = "Foreign Function Interface for Python calling C code."
|
||||||
|
groups = ["default"]
|
||||||
|
dependencies = [
|
||||||
|
"pycparser",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"},
|
||||||
|
{file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"},
|
||||||
|
{file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"},
|
||||||
|
{file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.1.7"
|
version = "8.1.7"
|
||||||
|
@ -59,6 +141,15 @@ files = [
|
||||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dotenv"
|
||||||
|
version = "0.0.5"
|
||||||
|
summary = "Handle .env files"
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "dotenv-0.0.5.tar.gz", hash = "sha256:b58d2ab3f83dbd4f8a362b21158a606bee87317a9444485566b3c8f0af847091"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi"
|
name = "fastapi"
|
||||||
version = "0.115.0"
|
version = "0.115.0"
|
||||||
|
@ -133,6 +224,39 @@ files = [
|
||||||
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
|
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "passlib"
|
||||||
|
version = "1.7.4"
|
||||||
|
summary = "comprehensive password hashing framework supporting over 30 schemes"
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"},
|
||||||
|
{file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psycopg2"
|
||||||
|
version = "2.9.9"
|
||||||
|
requires_python = ">=3.7"
|
||||||
|
summary = "psycopg2 - Python-PostgreSQL Database Adapter"
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "psycopg2-2.9.9-cp312-cp312-win32.whl", hash = "sha256:d735786acc7dd25815e89cc4ad529a43af779db2e25aa7c626de864127e5a024"},
|
||||||
|
{file = "psycopg2-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:a7653d00b732afb6fc597e29c50ad28087dcb4fbfb28e86092277a559ae4e693"},
|
||||||
|
{file = "psycopg2-2.9.9.tar.gz", hash = "sha256:d1454bde93fb1e224166811694d600e746430c006fbb031ea06ecc2ea41bf156"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycparser"
|
||||||
|
version = "2.22"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = "C parser in Python"
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
|
||||||
|
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "2.9.2"
|
version = "2.9.2"
|
||||||
|
@ -218,6 +342,17 @@ files = [
|
||||||
{file = "pydantic_settings-2.5.2.tar.gz", hash = "sha256:f90b139682bee4d2065273d5185d71d37ea46cfe57e1b5ae184fc6a0b2484ca0"},
|
{file = "pydantic_settings-2.5.2.tar.gz", hash = "sha256:f90b139682bee4d2065273d5185d71d37ea46cfe57e1b5ae184fc6a0b2484ca0"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyjwt"
|
||||||
|
version = "2.9.0"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = "JSON Web Token implementation in Python"
|
||||||
|
groups = ["default"]
|
||||||
|
files = [
|
||||||
|
{file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"},
|
||||||
|
{file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
|
|
@ -11,6 +11,11 @@ dependencies = [
|
||||||
"uvicorn>=0.31.0",
|
"uvicorn>=0.31.0",
|
||||||
"pydantic>=2.9.2",
|
"pydantic>=2.9.2",
|
||||||
"pydantic-settings>=2.5.2",
|
"pydantic-settings>=2.5.2",
|
||||||
|
"dotenv>=0.0.5",
|
||||||
|
"pyjwt>=2.9.0",
|
||||||
|
"psycopg2>=2.9.9",
|
||||||
|
"passlib>=1.7.4",
|
||||||
|
"argon2-cffi>=23.1.0",
|
||||||
]
|
]
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@ -23,3 +28,6 @@ build-backend = "pdm.backend"
|
||||||
|
|
||||||
[tool.pdm]
|
[tool.pdm]
|
||||||
distribution = true
|
distribution = true
|
||||||
|
|
||||||
|
[tool.pdm.scripts]
|
||||||
|
start = "uvicorn src.main:app --reload"
|
|
@ -0,0 +1,26 @@
|
||||||
|
from sqlalchemy import Column, String, Integer, ForeignKey, Boolean, DateTime
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from src.base.database import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Auth_2fa_pending(Base):
|
||||||
|
__tablename__ = "auth_2fa_pending"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
|
user_id = Column(Integer, ForeignKey("users.id"))
|
||||||
|
token = Column(String, unique=True, primary_key=True)
|
||||||
|
code = Column(String)
|
||||||
|
expire = Column(DateTime)
|
||||||
|
|
||||||
|
user = relationship("User", back_populates="auth_2fa_pending")
|
||||||
|
|
||||||
|
|
||||||
|
class Auth_token_valid(Base):
|
||||||
|
__tablename__ = "auth_token_valid"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
|
user_id = Column(Integer, ForeignKey("users.id"), index=True)
|
||||||
|
random = Column(String)
|
||||||
|
expire = Column(DateTime)
|
||||||
|
|
||||||
|
user = relationship("User", back_populates="auth_token_valid")
|
|
@ -0,0 +1,11 @@
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
|
||||||
|
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
|
||||||
|
|
||||||
|
|
||||||
|
def password_hash(password: str) -> str:
|
||||||
|
return pwd_context.hash(password)
|
||||||
|
|
||||||
|
|
||||||
|
def password_verify(password_plain: str, password_hashed: str) -> bool:
|
||||||
|
return pwd_context.verify(password_plain, password_hashed)
|
|
@ -0,0 +1,35 @@
|
||||||
|
from fastapi import APIRouter, Depends, status, HTTPException, Response, Request
|
||||||
|
|
||||||
|
from src.auth.schemas import Login_request
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/auth")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/login",
|
||||||
|
status_code=status.HTTP_202_ACCEPTED,
|
||||||
|
summary="Login user",
|
||||||
|
description="Login user with email and password",
|
||||||
|
)
|
||||||
|
def login(login: Login_request, request: Request, response: Response):
|
||||||
|
if not login.email.__contains__("@"):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST, detail="No valid email address"
|
||||||
|
)
|
||||||
|
|
||||||
|
if login.password == "" or input.email == "":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST, detail="Email or password is empty"
|
||||||
|
)
|
||||||
|
|
||||||
|
return "/api/auth/login"
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/logout")
|
||||||
|
def logout():
|
||||||
|
return "/api/auth/logout"
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/register")
|
||||||
|
def register():
|
||||||
|
return "/api/auth/register"
|
|
@ -0,0 +1,12 @@
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Login_request(BaseModel):
|
||||||
|
email: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
class Login_reply(BaseModel):
|
||||||
|
logged_in: bool
|
||||||
|
requested_2FA: bool
|
||||||
|
type_2FA: str
|
|
@ -0,0 +1,107 @@
|
||||||
|
from src.auth.schemas import Login_request, Login_reply
|
||||||
|
from src.user.models import User
|
||||||
|
from src.auth.models import Auth_2fa_pending, Auth_token_valid
|
||||||
|
from src.auth.pwd import password_hash, password_verify
|
||||||
|
from src.auth.static import Types_2FA
|
||||||
|
from src.base.jwt import jwt_encode, jwt_decode
|
||||||
|
from src.base.random_string import random_string
|
||||||
|
from fastapi import Response, status, HTTPException, Request
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
import datetime, os
|
||||||
|
|
||||||
|
|
||||||
|
def __generate_2FA_pending_token(
|
||||||
|
user: User, db: Session, response: Response, code: str = ""
|
||||||
|
) -> bool:
|
||||||
|
"""Create 2FA_token cookie and save validate data to DB (2FA pending)"""
|
||||||
|
data = {}
|
||||||
|
data["sub"] = user.id
|
||||||
|
data["exp"] = (
|
||||||
|
datetime.datetime.now()
|
||||||
|
+ datetime.timedelta(minutes=int(os.getenv("LOGIN_2FA_PENDING_MINUTES")))
|
||||||
|
).timestamp()
|
||||||
|
|
||||||
|
jwt_token = jwt_encode(data=data)
|
||||||
|
|
||||||
|
response.set_cookie(
|
||||||
|
key="2FA_token_temp",
|
||||||
|
value=jwt_token,
|
||||||
|
httponly=True,
|
||||||
|
expires=(int(os.getenv("LOGIN_2FA_PENDING_MINUTES")) * 60),
|
||||||
|
)
|
||||||
|
db.add(
|
||||||
|
Auth_2fa_pending(
|
||||||
|
user=user,
|
||||||
|
token=jwt_token,
|
||||||
|
code=code,
|
||||||
|
expire=datetime.datetime.now()
|
||||||
|
+ datetime.timedelta(minutes=int(os.getenv("LOGIN_2FA_PENDING_MINUTES"))),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def __generate_acces_token(user: User, db: Session, response: Response) -> bool:
|
||||||
|
"""Create acces_token cookie and save validate data to DB (login)"""
|
||||||
|
data = {}
|
||||||
|
data["sub"] = user.id
|
||||||
|
data["exp"] = (
|
||||||
|
datetime.datetime.now()
|
||||||
|
+ datetime.timedelta(minutes=int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES")))
|
||||||
|
).timestamp()
|
||||||
|
data["rand"] = random_string(8)
|
||||||
|
response.set_cookie(
|
||||||
|
key="acces_token",
|
||||||
|
value=jwt_encode(data=data),
|
||||||
|
httponly=True,
|
||||||
|
expires=(int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES")) * 60),
|
||||||
|
)
|
||||||
|
db.add(
|
||||||
|
Auth_token_valid(
|
||||||
|
user=user,
|
||||||
|
random=data["rand"],
|
||||||
|
expire=datetime.datetime.now()
|
||||||
|
+ datetime.timedelta(minutes=int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES"))),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def login(
|
||||||
|
form: Login_request, db: Session, response: Response, request: Request
|
||||||
|
) -> Response:
|
||||||
|
user: User = (
|
||||||
|
db.query(User).filter(User.email == form.email).first()
|
||||||
|
) # search user by email
|
||||||
|
|
||||||
|
if user == None or not password_verify(
|
||||||
|
password_plain=form.password, password_hashed=user.password
|
||||||
|
):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN, detail="Bad email or password"
|
||||||
|
) # if user not exist or received bad password return 403 error
|
||||||
|
|
||||||
|
refresh_token = request.cookies.get("refresh_token", None)
|
||||||
|
if refresh_token:
|
||||||
|
refresh_token_decoded = jwt_decode(refresh_token)
|
||||||
|
if (
|
||||||
|
refresh_token_decoded != None
|
||||||
|
and refresh_token_decoded["sub"] == user.id
|
||||||
|
and datetime.datetime.fromtimestamp(refresh_token_decoded["exp"])
|
||||||
|
> datetime.datetime.now()
|
||||||
|
):
|
||||||
|
__generate_acces_token(user=user, db=db, response=response)
|
||||||
|
return Login_reply(
|
||||||
|
logged_in=True, requested_2FA=False, type_2FA=Types_2FA.ANY
|
||||||
|
)
|
||||||
|
|
||||||
|
# match types of 2FA and run specified tasks
|
||||||
|
match user.type_2fa:
|
||||||
|
case Types_2FA.TOTP:
|
||||||
|
__generate_2FA_pending_token(user=user, db=db, response=response)
|
||||||
|
return Login_reply(
|
||||||
|
logged_in=False, requested_2FA=True, type_2FA=Types_2FA.TOTP
|
||||||
|
)
|
||||||
|
# here add more supported 2FA methods
|
||||||
|
|
||||||
|
return Login_reply(
|
||||||
|
logged_in=False, requested_2FA=False, type_2FA=Types_2FA.ANY
|
||||||
|
) # not logged in
|
|
@ -0,0 +1,7 @@
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class Types_2FA(Enum):
|
||||||
|
ANY = "ANY"
|
||||||
|
TOTP = "TOTP"
|
||||||
|
EMAIL = "EMAIL"
|
|
@ -0,0 +1,24 @@
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker, Session
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Create database engine
|
||||||
|
engine = create_engine(
|
||||||
|
f"postgresql+psycopg2://{os.getenv("DB_USER")}:{os.getenv("DB_PASSWORD")}@{os.getenv("DB_HOST")}:{os.getenv("DB_PORT")}/{os.getenv("DB_DATABASE")}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# factory for creating sessions (autocommit and autoflush disabled)
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
# base class for SQLAlchemy models
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
|
# function for get DB session
|
||||||
|
def get_session() -> Session:
|
||||||
|
db: Session = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db # return session and before use close it
|
||||||
|
finally:
|
||||||
|
db.close()
|
|
@ -0,0 +1,19 @@
|
||||||
|
import jwt, datetime, os
|
||||||
|
|
||||||
|
|
||||||
|
def jwt_encode(data: dict) -> str:
|
||||||
|
return jwt.encode(
|
||||||
|
payload=data, key=os.getenv("JWT_SECRET"), algorithm=os.getenv("JWT_ALGORITHM")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def jwt_decode(token: str) -> dict | None:
|
||||||
|
try:
|
||||||
|
return jwt.decode(
|
||||||
|
jwt=token,
|
||||||
|
key=os.getenv("JWT_SECRET"),
|
||||||
|
algorithm=os.getenv("JWT_ALGORITHM"),
|
||||||
|
verify=True,
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
return None
|
|
@ -0,0 +1,10 @@
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
|
|
||||||
|
def random_string(length=10):
|
||||||
|
"""generating random string of specified lenght"""
|
||||||
|
letters = string.ascii_letters + string.digits # include digits and letters
|
||||||
|
return "".join(
|
||||||
|
random.choice(letters) for _ in range(length)
|
||||||
|
) # renerate random string
|
|
@ -0,0 +1,18 @@
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from src.middleware.baseMiddleware import BaseMiddleware
|
||||||
|
|
||||||
|
from src.base.database import engine, Base
|
||||||
|
from src.user.models import User
|
||||||
|
from src.auth.models import Auth_2fa_pending, Auth_token_valid
|
||||||
|
|
||||||
|
from src.auth.router import router as authRouter
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(root_path="/api")
|
||||||
|
app.add_middleware(BaseMiddleware)
|
||||||
|
|
||||||
|
app.include_router(router=authRouter)
|
|
@ -0,0 +1,49 @@
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
|
from src.base.database import get_session
|
||||||
|
from src.base.jwt import jwt_decode
|
||||||
|
from src.auth.models import Auth_token_valid
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
from sqlalchemy import and_
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class BaseMiddleware(BaseHTTPMiddleware):
|
||||||
|
async def dispatch(self, request, call_next):
|
||||||
|
try:
|
||||||
|
request.state.db = get_session()
|
||||||
|
except:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Cannot connect to database",
|
||||||
|
)
|
||||||
|
|
||||||
|
acces_cookie = request.cookies.get("acces_token", None)
|
||||||
|
if acces_cookie != None:
|
||||||
|
acces_cookie_decoded = jwt_decode(acces_cookie)
|
||||||
|
|
||||||
|
if acces_cookie_decoded != None:
|
||||||
|
token_valid = (
|
||||||
|
request.state.db.query(Auth_token_valid)
|
||||||
|
.filter(
|
||||||
|
and_(
|
||||||
|
Auth_token_valid.random == acces_cookie_decoded["rand"],
|
||||||
|
Auth_token_valid.user_id == acces_cookie_decoded["sub"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if token_valid != None and token_valid.expire > datetime.datetime.now():
|
||||||
|
request.state.user = token_valid.user
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Acces token not valid, please log-in",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Acces token not valid, please log-in",
|
||||||
|
)
|
||||||
|
|
||||||
|
return await call_next(request)
|
|
@ -0,0 +1,23 @@
|
||||||
|
from sqlalchemy import Column, String, Integer, ForeignKey, Boolean, DateTime
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from src.base.database import Base
|
||||||
|
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
|
username = Column(String)
|
||||||
|
first_name = Column(String)
|
||||||
|
last_name = Column(String)
|
||||||
|
email = Column(String, unique=True)
|
||||||
|
email_backup = Column(String)
|
||||||
|
phone_number = Column(String)
|
||||||
|
password = Column(String)
|
||||||
|
profile_image = Column(String)
|
||||||
|
admin = Column(Boolean)
|
||||||
|
type_2fa = Column(String)
|
||||||
|
totp_secret = Column(String)
|
||||||
|
|
||||||
|
auth_2fa_pending = relationship("Auth_2fa_pending", back_populates="user")
|
||||||
|
auth_token_valid = relationship("Auth_token_valid", back_populates="user")
|
|
@ -0,0 +1,5 @@
|
||||||
|
HS256 - HMAC using SHA-256 hash algorithm (default)
|
||||||
|
|
||||||
|
HS384 - HMAC using SHA-384 hash algorithm
|
||||||
|
|
||||||
|
HS512 - HMAC using SHA-512 hash algorithm
|
Loading…
Reference in New Issue