mirror of
https://github.com/dancojocaru2000/foxbank.git
synced 2025-02-22 23:39:36 +02:00
Implement login
This commit is contained in:
parent
a41137cf0b
commit
be0b22cbed
11 changed files with 243 additions and 9 deletions
23
server/.vscode/launch.json
vendored
Normal file
23
server/.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python: Flask",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"module": "flask",
|
||||
"env": {
|
||||
"FLASK_APP": "server.py",
|
||||
"FLASK_ENV": "development"
|
||||
},
|
||||
"args": [
|
||||
"run",
|
||||
"--no-debugger"
|
||||
],
|
||||
"jinja": true
|
||||
}
|
||||
]
|
||||
}
|
|
@ -10,4 +10,4 @@ RUN pipenv install
|
|||
COPY . .
|
||||
|
||||
EXPOSE 5000
|
||||
CMD [ "pipenv", "run", "gunicorn", "-b", "0.0.0.0:5000", "server:app" ]
|
||||
CMD [ "pipenv", "run", "gunicorn", "-b", "0.0.0.0:5000", "--access-logfile", "-", "server:app" ]
|
|
@ -6,6 +6,8 @@ name = "pypi"
|
|||
[packages]
|
||||
flask = "*"
|
||||
gunicorn = "*"
|
||||
pyotp = "*"
|
||||
flask-cors = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
|
|
32
server/Pipfile.lock
generated
32
server/Pipfile.lock
generated
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "8886b8a6c0d31987ddea5b9e25bd02f7891650c967351486fd3cf0fd4d16271e"
|
||||
"sha256": "2ba252d63658abd009170d14705593521c57c99f82b643fcf232eeb51be35d10"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
|
@ -32,6 +32,14 @@
|
|||
"index": "pypi",
|
||||
"version": "==2.0.2"
|
||||
},
|
||||
"flask-cors": {
|
||||
"hashes": [
|
||||
"sha256:74efc975af1194fc7891ff5cd85b0f7478be4f7f59fe158102e91abb72bb4438",
|
||||
"sha256:b60839393f3b84a0f3746f6cdca56c1ad7426aa738b70d6c61375857823181de"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.0.10"
|
||||
},
|
||||
"gunicorn": {
|
||||
"hashes": [
|
||||
"sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e",
|
||||
|
@ -131,13 +139,29 @@
|
|||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2.0.1"
|
||||
},
|
||||
"pyotp": {
|
||||
"hashes": [
|
||||
"sha256:9d144de0f8a601d6869abe1409f4a3f75f097c37b50a36a3bf165810a6e23f28",
|
||||
"sha256:d28ddfd40e0c1b6a6b9da961c7d47a10261fb58f378cb00f05ce88b26df9c432"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.6.0"
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:b4c634615a0cf5b02cf83c7bedffc8da0ca439f00e79452699454da6fbd4153d",
|
||||
"sha256:feb5ff19b354cde9efd2344ef6d5e79880ce4be643037641b49508bbb850d060"
|
||||
"sha256:6d10741ff20b89cd8c6a536ee9dc90d3002dec0226c78fb98605bfb9ef8a7adf",
|
||||
"sha256:d144f85102f999444d06f9c0e8c737fd0194f10f2f7e5fdb77573f6e2fa4fad0"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==59.4.0"
|
||||
"version": "==59.5.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
|
||||
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.16.0"
|
||||
},
|
||||
"werkzeug": {
|
||||
"hashes": [
|
||||
|
|
|
@ -4,7 +4,9 @@ from flask import current_app, g
|
|||
|
||||
DB_FILE = './data/db.sqlite'
|
||||
|
||||
def get():
|
||||
get_return = sqlite3.Connection
|
||||
|
||||
def get() -> get_return:
|
||||
if 'db' not in g:
|
||||
g.db = sqlite3.connect(
|
||||
DB_FILE,
|
||||
|
|
24
server/db_utils.py
Normal file
24
server/db_utils.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
from functools import wraps
|
||||
|
||||
import db
|
||||
import models
|
||||
|
||||
def get_db(fn):
|
||||
@wraps(fn)
|
||||
def wrapper(*args, **kargs):
|
||||
return fn(db.get(), *args, **kargs)
|
||||
return wrapper
|
||||
|
||||
@get_db
|
||||
def get_user(db: db.get_return, username: str|None = None, user_id: int|None = None) -> models.User | None:
|
||||
cur = db.cursor()
|
||||
if username is not None:
|
||||
cur.execute('select * from users where username=?', (username,))
|
||||
elif user_id is not None:
|
||||
cur.execute('select * from users where id=?', (user_id,))
|
||||
else:
|
||||
raise Exception('Neither username or user_id passed')
|
||||
result = cur.fetchone()
|
||||
if result is None:
|
||||
return None
|
||||
return models.User.from_query(result)
|
54
server/login.py
Normal file
54
server/login.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
from functools import wraps
|
||||
from flask import Blueprint, request
|
||||
|
||||
from pyotp import TOTP
|
||||
|
||||
import db_utils
|
||||
import models
|
||||
import ram_db
|
||||
import returns
|
||||
|
||||
login = Blueprint('login', __name__)
|
||||
|
||||
@login.post('/')
|
||||
def make_login():
|
||||
try:
|
||||
username = request.json['username']
|
||||
code = request.json['code']
|
||||
except (TypeError, KeyError):
|
||||
return returns.INVALID_REQUEST
|
||||
|
||||
user: models.User | None = db_utils.get_user(username=username)
|
||||
if user is None:
|
||||
return returns.INVALID_DETAILS
|
||||
|
||||
otp = TOTP(user.otp)
|
||||
if not otp.verify(code, valid_window=1):
|
||||
return returns.INVALID_DETAILS
|
||||
|
||||
token = ram_db.login_user(user.id)
|
||||
return returns.success(token=token)
|
||||
|
||||
def ensure_logged_in(fn):
|
||||
@wraps(fn)
|
||||
def wrapper(*args, **kargs):
|
||||
token = request.headers.get('Authorization', None)
|
||||
if token is None:
|
||||
return returns.NO_AUTHORIZATION
|
||||
if not token.startswith('Bearer '):
|
||||
return returns.INVALID_AUTHORIZATION
|
||||
token = token[7:]
|
||||
user_id = ram_db.get_user(token)
|
||||
if user_id is None:
|
||||
return returns.INVALID_AUTHORIZATION
|
||||
return fn(user_id=user_id, *args, **kargs)
|
||||
return wrapper
|
||||
|
||||
@login.get('/whoami')
|
||||
@ensure_logged_in
|
||||
def whoami(user_id):
|
||||
user: models.User | None = db_utils.get_user(user_id=user_id)
|
||||
if user is not None:
|
||||
user = user.to_json()
|
||||
|
||||
return returns.success(user=user)
|
25
server/models.py
Normal file
25
server/models.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
id: int
|
||||
username: str
|
||||
email: str
|
||||
otp: str
|
||||
fullname: str
|
||||
|
||||
def to_json(self, include_otp=False, include_id=False):
|
||||
result = {
|
||||
'username': self.username,
|
||||
'email': self.email,
|
||||
'fullname': self.fullname,
|
||||
}
|
||||
if include_id:
|
||||
result['id'] = self.id
|
||||
if include_otp:
|
||||
result['otp'] = self.otp
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def from_query(cls, query_result):
|
||||
return cls(*query_result)
|
30
server/ram_db.py
Normal file
30
server/ram_db.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
from datetime import datetime, timedelta
|
||||
from types import TracebackType
|
||||
from uuid import uuid4
|
||||
|
||||
USED_TOKENS = set()
|
||||
LOGGED_IN_USERS: dict[str, (int, datetime)] = {}
|
||||
|
||||
def login_user(user_id: int) -> str:
|
||||
'''
|
||||
Creates token for user
|
||||
'''
|
||||
token = str(uuid4())
|
||||
while token in USED_TOKENS:
|
||||
token = str(uuid4())
|
||||
if len(USED_TOKENS) > 10_000_000:
|
||||
USED_TOKENS.clear()
|
||||
USED_TOKENS.add(token)
|
||||
LOGGED_IN_USERS[token] = user_id, datetime.now()
|
||||
return token
|
||||
|
||||
def get_user(token: str) -> int | None:
|
||||
if token not in LOGGED_IN_USERS:
|
||||
return None
|
||||
|
||||
user_id, login_date = LOGGED_IN_USERS[token]
|
||||
time_since_login: timedelta = datetime.now() - login_date
|
||||
if time_since_login.total_seconds() > (60 * 30): # 30 mins
|
||||
del LOGGED_IN_USERS[token]
|
||||
return None
|
||||
return user_id
|
46
server/returns.py
Normal file
46
server/returns.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
from http import HTTPStatus as _HTTPStatus
|
||||
|
||||
def _make_error(http_status, code: str):
|
||||
try:
|
||||
http_status = http_status[0]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
'status': 'error',
|
||||
'code': code,
|
||||
}, http_status
|
||||
|
||||
# General
|
||||
|
||||
INVALID_REQUEST = _make_error(
|
||||
_HTTPStatus.BAD_REQUEST,
|
||||
'general/invalid_request',
|
||||
)
|
||||
|
||||
# Login
|
||||
|
||||
INVALID_DETAILS = _make_error(
|
||||
_HTTPStatus.UNAUTHORIZED,
|
||||
'login/invalid_details',
|
||||
)
|
||||
|
||||
NO_AUTHORIZATION = _make_error(
|
||||
_HTTPStatus.UNAUTHORIZED,
|
||||
'login/no_authorization',
|
||||
)
|
||||
|
||||
INVALID_AUTHORIZATION = _make_error(
|
||||
_HTTPStatus.UNAUTHORIZED,
|
||||
'login/invalid_authorization',
|
||||
)
|
||||
|
||||
# Success
|
||||
|
||||
def success(http_status=_HTTPStatus.OK, /, **kargs):
|
||||
try:
|
||||
http_status = http_status[0]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return dict(kargs, status='success'), http_status
|
|
@ -1,10 +1,14 @@
|
|||
from flask import Flask
|
||||
from flask_cors import CORS
|
||||
|
||||
import db
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
db.init_app(app)
|
||||
|
||||
@app.get('/')
|
||||
def root():
|
||||
return 'Hello from FoxBank!'
|
||||
from login import login
|
||||
app.register_blueprint(login, url_prefix='/login')
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
||||
|
|
Loading…
Add table
Reference in a new issue