8 Commits

Author SHA1 Message Date
hasslesstech 731b79c865 clean up debugging context 2024-12-29 18:25:30 +02:00
hasslesstech f78fa33397 secure all required endpoints with JWT 2024-12-29 18:21:37 +02:00
hasslesstech 729498db11 add dependencies for JWT support 2024-12-29 18:20:18 +02:00
hasslesstech 589700e4d3 improve test performance 2024-12-29 11:45:05 +02:00
hasslesstech bba801f2d2 add balance entity
Add respecting schema, model and adapt all endpoints to utilize it.
Add two new endpoints for increasing and checking the balance.
2024-12-28 18:50:46 +02:00
hasslesstech 32fd20b3ae migrate user, category and record API to db 2024-12-28 15:10:29 +02:00
hasslesstech b96ba3eb34 add automated testing script for user table 2024-12-28 12:50:32 +02:00
hasslesstech c2104fd9cc finish user API migration to DB 2024-12-28 12:49:54 +02:00
4 changed files with 328 additions and 91 deletions
+267 -91
View File
@@ -1,20 +1,28 @@
from flask import Flask, request, jsonify from flask import Flask, request, jsonify
from flask_jwt_extended import create_access_token, get_jwt_identity, jwt_required, JWTManager
import time import time
import json import json
import uuid import uuid
import datetime
import os
from hashlib import sha256
from marshmallow import Schema, fields from marshmallow import Schema, fields
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__) app = Flask(__name__)
app.config.from_pyfile('config.py', silent=True) app.config.from_pyfile('config.py', silent=True)
app.config["JWT_SECRET_KEY"] = os.getenv("JWT_SECRET_KEY")
db = SQLAlchemy(app) db = SQLAlchemy(app)
jwt = JWTManager(app)
class UserModel(db.Model): class UserModel(db.Model):
__tablename__ = "user" __tablename__ = "user"
uuid = db.Column(db.String(32), unique=True, primary_key=True, nullable=False) uuid = db.Column(db.String(32), unique=True, primary_key=True, nullable=False)
name = db.Column(db.String(64), nullable=False) name = db.Column(db.String(64), nullable=False)
password = db.Column(db.String(64), nullable=False)
bal_uuid = db.Column(db.String(32), db.ForeignKey('balance.uuid'))
class CategoryModel(db.Model): class CategoryModel(db.Model):
__tablename__ = "category" __tablename__ = "category"
@@ -29,9 +37,17 @@ class RecordModel(db.Model):
date = db.Column(db.Date) date = db.Column(db.Date)
amount = db.Column(db.Integer) amount = db.Column(db.Integer)
class BalanceModel(db.Model):
__tablename__ = "balance"
uuid = db.Column(db.String(32), primary_key=True, nullable=False)
value = db.Column(db.Integer, nullable=False)
class UserSchema(Schema): class UserSchema(Schema):
uuid = fields.Str() uuid = fields.Str()
name = fields.Str() name = fields.Str()
password = fields.Str()
bal_uuid = fields.Str()
class CategorySchema(Schema): class CategorySchema(Schema):
uuid = fields.Str() uuid = fields.Str()
@@ -41,9 +57,13 @@ class RecordSchema(Schema):
uuid = fields.Str() uuid = fields.Str()
user_uuid = fields.Str() user_uuid = fields.Str()
cat_uuid = fields.Str() cat_uuid = fields.Str()
date = fields.Date() date = fields.Date('iso')
amount = fields.Integer() amount = fields.Integer()
class BalanceSchema(Schema):
uuid = fields.Str()
value = fields.Integer()
user_schema = UserSchema() user_schema = UserSchema()
users_schema = UserSchema(many = True) users_schema = UserSchema(many = True)
@@ -53,6 +73,8 @@ categories_schema = CategorySchema(many = True)
record_schema = RecordSchema() record_schema = RecordSchema()
records_schema = RecordSchema(many = True) records_schema = RecordSchema(many = True)
balance_schema = BalanceSchema()
# "migration" # "migration"
with app.app_context(): with app.app_context():
db.create_all() db.create_all()
@@ -66,24 +88,103 @@ def ep_healthcheck():
@app.route("/reset_users_because_postman_is_dumb_like_that") @app.route("/reset_users_because_postman_is_dumb_like_that")
def ep_reset(): def ep_reset():
return {} db.session.query(RecordModel).delete()
db.session.query(CategoryModel).delete()
db.session.query(UserModel).delete()
db.session.query(BalanceModel).delete()
db.session.commit()
return {}, 200
@app.route("/users", methods = ["GET"]) @app.route("/users", methods = ["GET"])
@jwt_required()
def ep_users_get(): def ep_users_get():
result = db.session.query(UserModel).all() result = db.session.query(UserModel).all()
return users_schema.dumps(result) return users_schema.dumps(result)
@app.route("/user/<user_id>", methods = ["GET"])
def ep_user_get(user_id):
user = ldb.get_user(user_id)
if 'uuid' in user:
return user
else:
return user, 404
@app.route("/user", methods = ["POST"]) @app.route("/user", methods = ["POST"])
def ep_user_post(): def ep_user_post():
name = request.json.get('name', None)
password = request.json.get('password', None)
pass_hashed = sha256(password.encode("UTF-8")).digest().hex()
b = BalanceModel(uuid=uuid.uuid4().hex, value=0)
u_uuid = uuid.uuid4().hex
bal_uuid = b.uuid
try:
_ = user_schema.load({'uuid': u_uuid, 'name': name,
'password': pass_hashed, 'bal_uuid': bal_uuid})
except Exception as e:
return {}, 403
u = UserModel(uuid=u_uuid, name=name,
password=pass_hashed, bal_uuid=bal_uuid)
at = create_access_token(identity = json.dumps({'uuid': u.uuid, 'bal_uuid': u.bal_uuid}))
try:
db.session.add(b)
db.session.add(u)
db.session.commit()
except Exception as e:
db.session.rollback()
return {}, 403
return jsonify(access_token = at), 200
@app.route("/user", methods = ["GET"])
def ep_user_get():
name = request.json.get('user', None)
password = request.json.get('password', None)
pass_hashed = sha256(password.encode("UTF-8")).digest().hex()
u = db.session.query(UserModel).filter(UserModel.name == name).one_or_none()
if not u:
return {}, 404
if u.password != pass_hashed:
return {"message": "Wrong password."}, 401
at = create_access_token(identity = json.dumps({'uuid': u.uuid, 'bal_uuid': u.bal_uuid}))
return jsonify(access_token = at), 200
@app.route("/user", methods = ["DELETE"])
@jwt_required()
def ep_user_delete():
current_user = json.loads(get_jwt_identity())
try:
db.session.query(UserModel).filter(UserModel.uuid == current_user['uuid']).delete()
db.session.query(BalanceModel).filter(BalanceModel.uuid == current_user['bal_uuid']).delete()
db.session.commit()
except Exception as e:
db.session.rollback()
return {}, 403
return {"message": "Success."}, 200
@app.route("/category", methods = ["GET"])
@jwt_required()
def ep_category_get():
body = request.json
if 'uuid' in body:
result = db.session.query(CategoryModel).filter(CategoryModel.uuid == body['uuid']).all()
if len(result) == 1:
return user_schema.dumps(result[0]), 200
else:
return {}, 404
else:
return {}, 403
@app.route("/category", methods = ["POST"])
@jwt_required()
def ep_category_post():
body = request.json body = request.json
if not body: if not body:
@@ -95,123 +196,198 @@ def ep_user_post():
body.update({'uuid': uuid.uuid4().hex}) body.update({'uuid': uuid.uuid4().hex})
try: try:
_ = user_schema.load(body) _ = category_schema.load(body)
except ValidationError as e: except ValidationError as e:
return {}, 403 return {}, 403
u = UserModel(**body) c = CategoryModel(**body)
try: try:
db.session.add(u) db.session.add(c)
db.session.commit() db.session.commit()
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
return {}, 403 return {}, 403
return jsonify(user_schema.load(body)), 200 return jsonify(category_schema.load(body)), 200
@app.route("/user/<user_id>", methods = ["DELETE"])
def ep_user_delete(user_id):
r = ldb.del_user(user_id)
if 'uuid' in r:
return r
else:
return r, 403
@app.route("/category", methods = ["GET"])
def ep_category_get():
body = request.json
if 'uuid' in body:
category = ldb.get_category(body['uuid'])
if 'uuid' in category:
return category
else:
return category, 404
else:
return {}, 403
@app.route("/category", methods = ["POST"])
def ep_category_post():
body = request.json
if 'name' in body:
r = ldb.add_category(body['name'])
if 'uuid' in r:
return r
else:
return r, 403
else:
return {}, 403
@app.route("/category", methods = ["DELETE"]) @app.route("/category", methods = ["DELETE"])
@jwt_required()
def ep_category_delete(): def ep_category_delete():
body = request.json body = request.json
if 'uuid' in body: if 'uuid' not in body:
category = ldb.del_category(body['uuid']) return {}, 403
if 'uuid' in category: cat_id = body['uuid']
return category
else: try:
return category, 404 result = db.session.query(CategoryModel).filter(CategoryModel.uuid == cat_id).all()
except Exception as e:
return {}, 403
if len(result) == 0:
return {}, 404
try:
db.session.query(CategoryModel).filter(CategoryModel.uuid == cat_id).delete()
db.session.commit()
except Exception as e:
return {}, 403
return category_schema.dumps(result[0]), 200
@app.route("/record/<record_id>", methods = ["GET"])
@jwt_required()
def ep_record_get(record_id):
current_user = json.loads(get_jwt_identity())
result = db.session.query(RecordModel).filter(RecordModel.uuid == record_id).one_or_none()
if result and result.user_uuid == current_user['uuid']:
return user_schema.dumps(result), 200
else: else:
return {}, 403 return {}, 403
@app.route("/record/<record_id>", methods = ["GET"])
def ep_record_get(record_id):
r = ldb.get_record(record_id)
if 'uuid' in r:
return r
else:
return r, 404
@app.route("/record", methods = ["GET"]) @app.route("/record", methods = ["GET"])
@jwt_required()
def ep_record_get_filtered(): def ep_record_get_filtered():
options = {} r = db.session.query(RecordModel)
filtered = False
if 'user_id' in request.json: if 'user_id' in request.json:
options['user_id'] = request.json['user_id'] r = r.filter(RecordModel.user_uuid == request.json['user_id'])
filtered = True
elif 'user_uuid' in request.json:
r = r.filter(RecordModel.user_uuid == request.json['user_uuid'])
filtered = True
if 'cat_id' in request.json: if 'cat_id' in request.json:
options['cat_id'] = request.json['cat_id'] r = r.filter(RecordModel.cat_uuid == request.json['cat_id'])
filtered = True
if 'cat_uuid' in request.json:
r = r.filter(RecordModel.cat_uuid == request.json['cat_uuid'])
filtered = True
if len(list(options.keys())) == 0: if filtered:
return [], 400 return records_schema.dumps(r.all())
else:
r = ldb.filter_records(options) return [], 403
return json.dumps(r)
@app.route("/record/<record_id>", methods = ["DELETE"]) @app.route("/record/<record_id>", methods = ["DELETE"])
@jwt_required()
def ep_record_del(record_id): def ep_record_del(record_id):
r = ldb.del_record(record_id) current_user = json.loads(get_jwt_identity())
if 'uuid' in r: try:
return r result = db.session.query(RecordModel).filter(RecordModel.uuid == record_id).one_or_none()
except Exception as e:
return {}, 403
if result and result.user_uuid == current_user['uuid']:
db.session.query(RecordModel).filter(RecordModel.uuid == record_id).delete()
db.session.commit()
else: else:
return r, 404 return {}, 401
return record_schema.dumps(result), 200
@app.route("/record", methods = ["POST"]) @app.route("/record", methods = ["POST"])
@jwt_required()
def ep_record_post(): def ep_record_post():
body = request.json current_user = json.loads(get_jwt_identity())
if 'user_id' not in body: amount = request.json.get('amount', None)
category = request.json.get('category', None)
if not all([amount, category]):
return {}, 403
u_uuid = uuid.uuid4().hex
r_time = time.strftime("%Y-%m-%d")
try:
_ = record_schema.load({'amount': amount, "cat_uuid": category, "uuid": u_uuid,
'user_uuid': current_user['uuid'], 'date': r_time})
except Exception as e:
return {}, 400 return {}, 400
if 'cat_id' not in body: r = RecordModel(amount=amount, cat_uuid=category, uuid=u_uuid, user_uuid=current_user['uuid'], date=r_time)
return {}, 400
if 'amount' not in body: v = db.session \
return {}, 400 .query(BalanceModel) \
.filter(BalanceModel.uuid == current_user['bal_uuid']) \
.one_or_none() \
.value
r = ldb.add_record(body['user_id'], body['cat_id'], body['amount']) BalanceModel \
.metadata.tables.get("balance") \
.update() \
.where(BalanceModel.metadata.tables.get("balance").c.uuid == current_user['bal_uuid']) \
.values(value = v - amount)
if 'uuid' in r: try:
return r db.session.add(r)
else: db.session.commit()
return r, 403 except Exception as e:
db.session.rollback()
return {}, 403
return {'uuid': r.uuid}, 200
@app.route("/balance_up", methods = ["POST"])
@jwt_required()
def ep_balance_up():
current_user = json.loads(get_jwt_identity())
amount = request.json.get('amount', None)
try:
v = db.session \
.query(BalanceModel) \
.filter(BalanceModel.uuid == current_user['bal_uuid']) \
.one_or_none() \
.value
BalanceModel.metadata.tables.get("balance").update().where(BalanceModel.metadata.tables.get("balance").c.uuid == current_user['bal_uuid']).values(value = v + amount)
except Exception as e:
return {}, 407
return {}, 200
@app.route("/balance", methods = ["GET"])
@jwt_required()
def ep_balance_get():
current_user = json.loads(get_jwt_identity())
result = db.session.query(BalanceModel).filter(BalanceModel.uuid == current_user['bal_uuid']).one_or_none()
return balance_schema.dumps(result), 200
@jwt.expired_token_loader
def expired_token_callback(jwt_header, jwt_payload):
return (
jsonify({"message": "The token has expired.", "error": "token_expired"}),
401,
)
@jwt.invalid_token_loader
def invalid_token_callback(error):
return (
jsonify(
{"message": "Signature verification failed.", "error": "invalid_token"}
),
401,
)
@jwt.unauthorized_loader
def missing_token_callback(error):
return (
jsonify(
{
"description": "Request does not contain an access token.",
"error": "authorization_required",
}
),
401,
)
+1
View File
@@ -14,6 +14,7 @@ services:
- ./app:/app/app - ./app:/app/app
env_file: env_file:
- db.env - db.env
- app.env
depends_on: depends_on:
- db - db
+2
View File
@@ -3,6 +3,7 @@ apispec==6.8.0
blinker==1.8.2 blinker==1.8.2
click==8.1.7 click==8.1.7
Flask==3.0.3 Flask==3.0.3
Flask-JWT-Extended==4.7.1
Flask-Migrate==4.0.7 Flask-Migrate==4.0.7
flask-smorest==0.45.0 flask-smorest==0.45.0
Flask-SQLAlchemy==3.1.1 Flask-SQLAlchemy==3.1.1
@@ -14,6 +15,7 @@ MarkupSafe==2.1.5
marshmallow==3.23.2 marshmallow==3.23.2
packaging==24.2 packaging==24.2
psycopg2-binary==2.9.10 psycopg2-binary==2.9.10
PyJWT==2.10.1
SQLAlchemy==2.0.36 SQLAlchemy==2.0.36
typing_extensions==4.12.2 typing_extensions==4.12.2
webargs==8.6.0 webargs==8.6.0
+58
View File
@@ -0,0 +1,58 @@
#!/bin/sh
HOST=$1
if [ -z HOST ]; then
echo "Host not specified, exiting"
exit 1;
fi;
echo -n 'Resetting DB...'
curl -X GET -f -s \
http://$HOST:12402/reset_users_because_postman_is_dumb_like_that > /dev/null
if [ $? -ne 0 ]; then
echo 'Exiting due to previous error'
exit 1
fi
echo ' Done.'
echo -n 'Creating users...'
for i in $(seq 1); do
curl -X POST -f -s \
--data "{\"name\": \"hi$i\"}" \
--header "Content-Type: application/json" \
http://$HOST:12402/user > /dev/null;
if [ $? -ne 0 ]; then
echo 'Exiting due to previous error'
exit 1
fi
done
echo ' Done.'
echo -n 'Getting user UUID to delete...'
DELETION_UUID=$(curl -X GET -f -s \
http://$HOST:12402/users \
| jq .[0].uuid);
if [ $? -ne 0 ]; then
echo 'Exiting due to previous error'
exit 1
fi
DELETION_UUID=${DELETION_UUID#\"}
DELETION_UUID=${DELETION_UUID%\"}
echo " $DELETION_UUID."
echo -n "Deleting user $DELETION_UUID..."
curl -X DELETE -f -s \
http://$HOST:12402/user/$DELETION_UUID > /dev/null
if [ $? -ne 0 ]; then
echo 'Exiting due to previous error'
exit 1
fi
echo ' Done.'