Skip to content

Commit aebf765

Browse files
committed
API implementation
1 parent 3a1d0b5 commit aebf765

11 files changed

+370
-6
lines changed

API.md

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# API Endpoints
2+
3+
4+
## Tokens
5+
6+
### Create new token
7+
8+
```
9+
POST http://localhost:5000/api/tokens
10+
```
11+
12+
Parameters:
13+
```
14+
Basic-auth login & password
15+
```
16+
17+
### Revoke token
18+
19+
```
20+
DELETE http://localhost:5000/api/tokens
21+
```
22+
23+
Parameters:
24+
```
25+
Basic-auth login & password
26+
```
27+
28+
## Users
29+
30+
### Get all users
31+
32+
```
33+
GET http://localhost:5000/api/users
34+
```
35+
36+
Token required
37+
38+
### Get one user
39+
40+
```
41+
GET http://localhost:5000/api/users/1
42+
```
43+
44+
Token required
45+
46+
### Get followers for user
47+
48+
```
49+
GET http://localhost:5000/api/users/1/followers
50+
```
51+
52+
Token required
53+
54+
### Get followed for user
55+
56+
```
57+
GET http://localhost:5000/api/users/1/followed
58+
```
59+
60+
Token required
61+
62+
### Create new user
63+
64+
```
65+
POST http://localhost:5000/api/users
66+
```
67+
68+
Token not required
69+
70+
Parameters:
71+
```
72+
{
73+
"username": "blah",
74+
"password": "blah",
75+
"email": "[email protected]",
76+
"about_me": "cool guy"
77+
}
78+
```
79+
80+
### Update user
81+
82+
```
83+
PUT http://localhost:5000/api/users/1
84+
```
85+
86+
Token required
87+
88+
Parameters:
89+
```
90+
{
91+
"username": "blah",
92+
"password": "blah",
93+
"email": "[email protected]",
94+
"about_me": "cool guy"
95+
}
96+
```

app/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ def create_app(config_class=Config):
5252
from app.auth import bp as auth_bp
5353
app.register_blueprint(auth_bp, url_prefix='/auth')
5454

55+
from app.api import bp as api_bp
56+
app.register_blueprint(api_bp, url_prefix='/api')
57+
5558
from app.main import bp as main_bp
5659
app.register_blueprint(main_bp)
5760

app/api/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from flask import Blueprint
2+
3+
bp = Blueprint('api', __name__)
4+
5+
from app.api import users, errors, tokens

app/api/auth.py

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from flask import g
2+
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
3+
from app.models import User
4+
from app.api.errors import error_response
5+
6+
basic_auth = HTTPBasicAuth()
7+
token_auth = HTTPTokenAuth()
8+
9+
@basic_auth.verify_password
10+
def verify_password(username, password):
11+
user = User.query.filter_by(username=username).first()
12+
if user is None:
13+
return False
14+
g.current_user = user
15+
return user.check_password(password)
16+
17+
@basic_auth.error_handler
18+
def basic_auth_error():
19+
return error_response(401)
20+
21+
@token_auth.verify_token
22+
def verify_token(token):
23+
g.current_user = User.check_token(token) if token else None
24+
return g.current_user is not None
25+
26+
@token_auth.error_handler
27+
def token_auth_error():
28+
return error_response(401)

app/api/errors.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from flask import jsonify
2+
from werkzeug.http import HTTP_STATUS_CODES
3+
4+
def error_response(status_code, message=None):
5+
payload = {'error': HTTP_STATUS_CODES.get(status_code, 'Unknown error')}
6+
if message:
7+
payload['message'] = message
8+
response = jsonify(payload)
9+
response.status_code = status_code
10+
return response
11+
12+
def bad_request(message):
13+
return error_response(400, message)

app/api/tokens.py

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from flask import jsonify, g
2+
from app import db
3+
from app.api import bp
4+
from app.api.auth import basic_auth, token_auth
5+
from app.api.errors import error_response
6+
from flask_httpauth import HTTPTokenAuth
7+
from app.models import User
8+
9+
token_auth = HTTPTokenAuth()
10+
11+
@bp.route('/tokens', methods=['POST'])
12+
@basic_auth.login_required
13+
def get_token():
14+
token = g.current_user.get_token()
15+
db.session.commit()
16+
return jsonify({'token': token})
17+
18+
@token_auth.verify_token
19+
def verify_token(token):
20+
g.current_user = User.check_token(token) if token else None
21+
return g.current_user is not None
22+
23+
@token_auth.error_handler
24+
def token_auth_error():
25+
return error_response(401)
26+
27+
@bp.route('/tokens', methods=['DELETE'])
28+
@token_auth.login_required
29+
def revoke_token():
30+
g.current_user.revoke_token()
31+
db.session.commit()
32+
return '', 204

app/api/users.py

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from app import db
2+
from app.api import bp
3+
from app.api.errors import bad_request
4+
from app.api.auth import token_auth
5+
from flask import jsonify, request, url_for
6+
from app.models import User
7+
8+
@bp.route('/users/<int:id>', methods=['GET'])
9+
@token_auth.login_required
10+
def get_user(id):
11+
return jsonify(User.query.get_or_404(id).to_dict())
12+
13+
@bp.route('/users', methods=['GET'])
14+
@token_auth.login_required
15+
def get_users():
16+
page = request.args.get('page', 1, type=int)
17+
per_page = min(request.args.get('per_page', 10, type=int), 100)
18+
data = User.to_collection_dict(User.query, page, per_page, 'api.get_users')
19+
return jsonify(data)
20+
21+
@bp.route('/users/<int:id>/followers', methods=['GET'])
22+
@token_auth.login_required
23+
def get_followers(id):
24+
user = User.query.get_or_404(id)
25+
page = request.args.get('page', 1, type=int)
26+
per_page = min(request.args.get('per_page', 10, type=int), 100)
27+
data = User.to_collection_dict(user.followers, page, per_page,
28+
'api.get_followers', id=id)
29+
return jsonify(data)
30+
31+
@bp.route('/users/<int:id>/followed', methods=['GET'])
32+
@token_auth.login_required
33+
def get_followed(id):
34+
user = User.query.get_or_404(id)
35+
page = request.args.get('page', 1, type=int)
36+
per_page = min(request.args.get('per_page', 10, type=int), 100)
37+
data = User.to_collection_dict(user.followed, page, per_page,
38+
'api.get_followed', id=id)
39+
return jsonify(data)
40+
41+
@bp.route('/users', methods=['POST'])
42+
def create_user():
43+
data = request.get_json() or {}
44+
if 'username' not in data or 'email' not in data or 'password' not in data:
45+
return bad_request('must include username, email and password fields')
46+
if User.query.filter_by(username=data['username']).first():
47+
return bad_request('please use a different username')
48+
if User.query.filter_by(email=data['email']).first():
49+
return bad_request('please use a different email address')
50+
user = User()
51+
user.from_dict(data, new_user=True)
52+
db.session.add(user)
53+
db.session.commit()
54+
response = jsonify(user.to_dict())
55+
response.status_code = 201
56+
response.headers['Location'] = url_for('api.get_user', id=user.id)
57+
return response
58+
59+
@bp.route('/users/<int:id>', methods=['PUT'])
60+
@token_auth.login_required
61+
def update_user(id):
62+
user = User.query.get_or_404(id)
63+
data = request.get_json() or {}
64+
if 'username' in data and data['username'] != user.username and \
65+
User.query.filter_by(username=data['username']).first():
66+
return bad_request('please use a different username')
67+
if 'email' in data and data['email'] != user.email and \
68+
User.query.filter_by(email=data['email']).first():
69+
return bad_request('please use a different email address')
70+
user.from_dict(data, new_user=False)
71+
db.session.commit()
72+
return jsonify(user.to_dict())

app/errors/handlers.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
1-
from flask import render_template, current_app
1+
from flask import render_template, request
22
from app import db
33
from app.errors import bp
4+
from app.api.errors import error_response as api_error_response
5+
6+
def wants_json_response():
7+
return request.accept_mimetypes['application/json'] >= \
8+
request.accept_mimetypes['text/html']
49

510
@bp.app_errorhandler(404)
611
def not_found_error(error):
7-
return render_template("errors/404.html", error=error), 404
12+
if wants_json_response():
13+
return api_error_response(404)
14+
return render_template('errors/404.html'), 404
815

916
@bp.app_errorhandler(500)
1017
def internal_error(error):
1118
db.session.rollback()
12-
return render_template("errors/500.html", error=error), 500
19+
if wants_json_response():
20+
return api_error_response(500)
21+
return render_template('errors/500.html'), 500

0 commit comments

Comments
 (0)