Skip to content

Commit 9a6c336

Browse files
feat: python jwt api with early token expiry (#97)
* feat: python time api Signed-off-by: kapish <upadhyaykapish@gmail.com> * feat: api rename Signed-off-by: kapish <upadhyaykapish@gmail.com> --------- Signed-off-by: kapish <upadhyaykapish@gmail.com>
1 parent 2c4b2bf commit 9a6c336

4 files changed

Lines changed: 197 additions & 0 deletions

File tree

python-timefreeze/apis.txt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
0. Register
2+
-> curl -X POST -H "Content-Type: application/json" -d '{"username": "gouravkrosx", "password": "gkrosx"}' http://localhost:5000/register
3+
4+
1. Login (obtain JWT token):
5+
-> curl -X POST -H "Content-Type: application/json" -d '{"username": "gouravkrosx", "password": "gkrosx"}' http://localhost:5000/login
6+
7+
2. Add Item
8+
-> curl -X POST -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNjVjMGE0NTM1NmQ3Y2MxNmFkY2UzMDQ3IiwiZXhwIjoxNzA3MTI1NzA4fQ.ikQh8y1398w1UJ9Sb3IlJnMog9lEfm3kTFiK1_orVgU" -H "Content-Type: application/json" -d '{"name": "Item Name", "description": "Description"}' http://localhost:5000/item
9+
10+
3. Get Item
11+
-> curl -X GET -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNjVjMGE0NTM1NmQ3Y2MxNmFkY2UzMDQ3IiwiZXhwIjoxNzA3MTI1NzA4fQ.ikQh8y1398w1UJ9Sb3IlJnMog9lEfm3kTFiK1_orVgU" http://localhost:5000/item/65c0a56056d7cc16adce3049
12+
13+
4. Update Item
14+
-> curl -X PUT -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNjVjMGE0NTM1NmQ3Y2MxNmFkY2UzMDQ3IiwiZXhwIjoxNzA3MTI1NzA4fQ.ikQh8y1398w1UJ9Sb3IlJnMog9lEfm3kTFiK1_orVgU" -H "Content-Type: application/json" -d '{"name": "Updated Name", "description": "Updated Description"}' http://localhost:5000/item/65c0a56056d7cc16adce3049
15+
16+
5. Delete Item
17+
-> curl -X DELETE -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNjVjMGE0NTM1NmQ3Y2MxNmFkY2UzMDQ3IiwiZXhwIjoxNzA3MTI1NzA4fQ.ikQh8y1398w1UJ9Sb3IlJnMog9lEfm3kTFiK1_orVgU" http://localhost:5000/item/<id>
18+
19+
6. Delete User
20+
-> curl -X DELETE \
21+
http://localhost:5000/user/delete/Sarthak16 \
22+
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNjVjMWE1MGFlZDRkYTJhYmZlYTQ1N2JjIiwiZXhwIjoxNzA3MTkwNTQ5fQ.f_3lBu5gOGSd1S7x5Gcg9LGTO8rMoMpjUeeN0kvMu4w'

python-timefreeze/app.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
from flask import Flask, request, jsonify
2+
from werkzeug.security import generate_password_hash, check_password_hash
3+
import jwt
4+
import datetime
5+
from functools import wraps
6+
7+
app = Flask(__name__)
8+
# It's good practice to load this from environment variables in a real application
9+
app.config["SECRET_KEY"] = "unsafe_secret"
10+
11+
# In-memory storage to remove the database dependency for simplicity
12+
users = {}
13+
items = {}
14+
next_item_id = 1
15+
16+
# JWT token decorator to protect routes
17+
def token_required(f):
18+
@wraps(f)
19+
def decorated(*args, **kwargs):
20+
token = None
21+
if 'Authorization' in request.headers:
22+
# Expecting token format: "Bearer <token>"
23+
try:
24+
token = request.headers['Authorization'].split(" ")[1]
25+
except IndexError:
26+
return jsonify({'message': 'Malformed token header!'}), 400
27+
28+
if not token:
29+
return jsonify({'message': 'Token is missing!'}), 401
30+
31+
try:
32+
# Decode the token to validate it and get the user's identity
33+
data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"])
34+
# Check if the user from the token exists in our store
35+
if data['username'] not in users:
36+
return jsonify({'message': 'User from token not found!'}), 401
37+
current_user = data['username']
38+
except jwt.ExpiredSignatureError:
39+
return jsonify({'message': 'Token has expired!'}), 401
40+
except Exception as e:
41+
return jsonify({'message': 'Token is invalid!', 'error': str(e)}), 401
42+
43+
# Pass the current user's username to the decorated function
44+
return f(current_user, *args, **kwargs)
45+
return decorated
46+
47+
@app.route('/login', methods=['POST'])
48+
def login_user():
49+
auth = request.json
50+
if not auth or not auth.get('username') or not auth.get('password'):
51+
return jsonify({'message': 'Could not verify, missing username or password'}), 401
52+
53+
username = auth['username']
54+
user_password_hash = users.get(username)
55+
56+
if not user_password_hash:
57+
return jsonify({'message': 'User not found'}), 401
58+
59+
if check_password_hash(user_password_hash, auth['password']):
60+
# Create a token with a 2-minute expiration time
61+
token = jwt.encode({
62+
'username': username,
63+
'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=2)
64+
}, app.config['SECRET_KEY'], algorithm="HS256")
65+
66+
return jsonify({'token': token})
67+
68+
return jsonify({'message': 'Password is wrong'}), 403
69+
70+
# --- CRUD Operations for a simple "item" resource ---
71+
72+
@app.route('/item', methods=['POST'])
73+
@token_required
74+
def add_item(current_user):
75+
global next_item_id
76+
data = request.json
77+
if not data:
78+
return jsonify({"message": "No input data provided"}), 400
79+
80+
item_id_str = str(next_item_id)
81+
# Store item data associated with the user who created it
82+
items[item_id_str] = {'data': data, 'owner': current_user}
83+
next_item_id += 1
84+
85+
# Return the ID of the newly created item
86+
return jsonify({'message': 'Item added', 'id': item_id_str}), 201
87+
88+
@app.route('/item/<id>', methods=['GET'])
89+
@token_required
90+
def get_item(current_user, id):
91+
item = items.get(id)
92+
if item:
93+
# For simplicity, any authenticated user can view any item.
94+
return jsonify(item['data']), 200
95+
else:
96+
return jsonify({'message': 'Item not found'}), 404
97+
98+
@app.route('/item/<id>', methods=['PUT'])
99+
@token_required
100+
def update_item(current_user, id):
101+
data = request.json
102+
if not data:
103+
return jsonify({"message": "No input data provided"}), 400
104+
105+
if id in items:
106+
# Simple authorization: only the owner can update their own item
107+
if items[id]['owner'] != current_user:
108+
return jsonify({'message': 'Permission denied: you are not the owner'}), 403
109+
items[id]['data'].update(data)
110+
return jsonify({'message': 'Item updated'}), 200
111+
else:
112+
return jsonify({'message': 'Item not found'}), 404
113+
114+
@app.route('/item/<id>', methods=['DELETE'])
115+
@token_required
116+
def delete_item(current_user, id):
117+
if id in items:
118+
# Simple authorization: only the owner can delete their own item
119+
if items[id]['owner'] != current_user:
120+
return jsonify({'message': 'Permission denied: you are not the owner'}), 403
121+
del items[id]
122+
return jsonify({'message': 'Item deleted'}), 200
123+
else:
124+
return jsonify({'message': 'Item not found'}), 404
125+
126+
# --- User Management ---
127+
128+
@app.route('/register', methods=['POST'])
129+
def register_user():
130+
data = request.get_json()
131+
username = data.get('username')
132+
password = data.get('password')
133+
134+
if not username or not password:
135+
return jsonify({'message': 'Missing username or password'}), 400
136+
137+
if username in users:
138+
return jsonify({'message': 'User already exists'}), 409
139+
140+
hashed_password = generate_password_hash(password)
141+
users[username] = hashed_password
142+
143+
return jsonify({'message': 'User registered successfully'}), 201
144+
145+
@app.route('/user/delete/<username>', methods=['DELETE'])
146+
@token_required
147+
def delete_user_by_username(current_user, username):
148+
# Simple authorization: a user can only delete their own account
149+
if current_user != username:
150+
return jsonify({'message': 'Permission denied: you can only delete your own account'}), 403
151+
152+
if username in users:
153+
del users[username]
154+
# Clean up items owned by the deleted user
155+
items_to_delete = [item_id for item_id, item in items.items() if item['owner'] == username]
156+
for item_id in items_to_delete:
157+
del items[item_id]
158+
return jsonify({'message': 'User and their items deleted successfully'}), 200
159+
else:
160+
return jsonify({'message': 'User not found'}), 404
161+
162+
if __name__ == '__main__':
163+
# Binds to all network interfaces, making it accessible for testing
164+
app.run(host='0.0.0.0', port=5000, debug=False)

python-timefreeze/readme.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
## Command to run mongo container
2+
docker run -v jwtMongoData:/data/db -p 27017:27017 -d --name jwtMongo mongo
3+
4+
## Command to build app container
5+
sudo docker run --name pyjwtApp -p5000:5000 --rm --net keploy-network py-jwt
6+
7+
## Native command to run application
8+
python3 app.py

python-timefreeze/requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Flask
2+
werkzeug
3+
PyJWT

0 commit comments

Comments
 (0)