X Tutup
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
.envs
docker-compose.yml
postgres_data
postgres-data
4 changes: 2 additions & 2 deletions auth-ms/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

from dotenv import load_dotenv # type: ignore

load_dotenv(join(dirname(__file__), "../.envs/auth-ms"))
load_dotenv(join(dirname(__file__), "../.envs/postgres"))
load_dotenv(join(dirname(__file__), "../.envs/.local/.auth-ms"))
load_dotenv(join(dirname(__file__), "../.envs/.local/.postgres"))


class Configuration:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Rename password to password_hash

Revision ID: 685b9f987143
Revises: 477f954a5666
Create Date: 2023-03-25 20:15:35.179707

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '685b9f987143'
down_revision = '477f954a5666'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('password_hash', sa.String(), nullable=False))
batch_op.drop_column('password')

# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('user', schema=None) as batch_op:
batch_op.add_column(sa.Column('password', sa.VARCHAR(), autoincrement=False, nullable=False))
batch_op.drop_column('password_hash')

# ### end Alembic commands ###
18 changes: 17 additions & 1 deletion auth-ms/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import uuid

from sqlalchemy.dialects.postgresql import UUID # type: ignore
from werkzeug.security import check_password_hash, generate_password_hash

from app import db

Expand All @@ -10,7 +11,22 @@ class User(db.Model):
username = db.Column(db.String, unique=True, nullable=False)
nickname = db.Column(db.String, nullable=False)
refresh_token = db.Column(db.String)
password = db.Column(db.String, nullable=False)
password_hash = db.Column(db.String, nullable=False)

def __init__(self, *args, **kwargs):
super(User, self).__init__(*args, **kwargs)
self.username = self.username.lower()

@property
def password(self):
raise AttributeError('password is not a readable attribute')

@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)

def verify_password(self, password):
return check_password_hash(self.password_hash, password)

def __repr__(self):
return f"<User: {self.username}>"
110 changes: 82 additions & 28 deletions auth-ms/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,80 @@
import jwt # type: ignore
from flask import jsonify, request

from app import app
from app import app, db
from models import User


class JwtPayload(TypedDict):
class JwtAccessPayload(TypedDict):
id: str
username: str
exp: datetime
token_type: Literal["access", "refresh"]
token_type: Literal["access"]


# Example user database. { username: {password: "val", refresh_token: "val"} }
users = {
"user1": {"password": "abc"},
"user2": {"password": "abc"},
}
class JwtRefreshPayload(TypedDict):
id: str
exp: datetime
token_type: Literal["refresh"]


def get_user_by_username(username) -> User:
return User.query.filter(User.username == username.lower()).first()


def get_user_by_id(id) -> User:
return User.query.filter(User.id == id).first()


def update_refresh_token(id, refresh_token) -> bool:
try:
user = get_user_by_id(id)
user.refresh_token = refresh_token
db.session.add(user)
db.session.commit()
return True
except Exception as e:
print("<failed to update refresh_token in db>", e)
return False


def remove_refresh_token(id) -> tuple[dict, int]:
try:
user = get_user_by_id(id)
if user:
user.refresh_token = None
db.session.add(user)
db.session.commit()
return {"message": "Refresh token was deleted"}, 200
except Exception as e:
print("<failed to update update entry in db>", e)
return {"message": "failed to update update entry in db"}, 500


def create_user(user: User) -> tuple[dict, int]:
"""Returns jwt-tokens, or mistakes"""
try:
is_user_exists = db.session.query(
db.exists().where(User.username == user.username)
).scalar()
if is_user_exists:
return {"message": "This username is already taken"}, 409

db.session.add(user)
db.session.commit()

json_answer, status_code = generate_tokens(user)
return json_answer, status_code

def get_user(username):
# Find user in database
return users.get(username)
except Exception as e:
print("<failed to send a create-query to the database:>", e)
return {"message": "failed to write the user to the db"}, 500


def _generate_access_token(username) -> str:
payload: JwtPayload = {
"username": username,
def _generate_access_token(user: User) -> str:
payload: JwtAccessPayload = {
"id": str(user.id),
"username": user.username,
"exp": datetime.utcnow() + app.config["JWT_ACCESS_EXPIRATION_DELTA"],
"token_type": "access",
}
Expand All @@ -39,9 +89,9 @@ def _generate_access_token(username) -> str:
)


def _generate_refresh_token(username) -> str:
payload: JwtPayload = {
"username": username,
def _generate_refresh_token(user: User) -> str:
payload: JwtRefreshPayload = {
"id": str(user.id),
"exp": datetime.utcnow() + app.config["JWT_REFRESH_EXPIRATION_DELTA"],
"token_type": "refresh",
}
Expand All @@ -52,14 +102,18 @@ def _generate_refresh_token(username) -> str:
)


def generate_tokens(username) -> tuple[str, str]:
access_token = _generate_access_token(username)
refresh_token = _generate_refresh_token(username)
def generate_tokens(user: User) -> tuple[dict, int]:
access_token = _generate_access_token(user)
refresh_token = _generate_refresh_token(user)

# Save refresh in db for single use
users[username]["refresh_token"] = refresh_token
is_result_success = update_refresh_token(user.id, refresh_token)

return access_token, refresh_token
if is_result_success:
return {
"access_token": access_token,
"refresh_token": refresh_token,
}, 200
return {"message": "Can't update refresh_token in db"}, 500


def refresh_tokens(refresh_token) -> tuple[dict, int]:
Expand All @@ -70,19 +124,19 @@ def refresh_tokens(refresh_token) -> tuple[dict, int]:
payload = jwt.decode(
refresh_token, app.config["SECRET_KEY"], algorithms=["HS256"]
)
user = get_user_by_id(payload.get("id"))
if (
payload.get("token_type") != "refresh"
or users[payload.get("username")].get("refresh_token")
!= refresh_token
or user.refresh_token != refresh_token
):
return {"message": "Refresh token is invalid!"}, 401
except jwt.ExpiredSignatureError:
return {"message": "Refresh token has expired!"}, 401
except jwt.InvalidTokenError:
return {"message": "Refresh token is invalid!"}, 401

access_token, refresh_token = generate_tokens(payload.get("username"))
return {"access_token": access_token, "refresh_token": refresh_token}, 200
json_answer, status_code = generate_tokens(user)
return json_answer, status_code


def token_required(func):
Expand All @@ -103,6 +157,6 @@ def wrapper(*args, **kwargs):
except jwt.InvalidTokenError:
return jsonify({"message": "Access token is invalid!"}), 401

return func(payload.get("username"), *args, **kwargs)
return func(payload.get("id"), *args, **kwargs)

return wrapper
51 changes: 41 additions & 10 deletions auth-ms/views.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,44 @@
from flask import jsonify, request

from app import app
from services import generate_tokens, get_user, refresh_tokens, token_required
from models import User
from services import (
create_user,
generate_tokens,
get_user_by_id,
get_user_by_username,
remove_refresh_token,
refresh_tokens,
token_required,
)


@app.route("/login", methods=["POST"])
def login():
# Find user in database
username = request.form.get("username")
password = request.form.get("password")
user = get_user(username)
user = get_user_by_username(username)

if user and user.get("password") == password:
access_token, refresh_token = generate_tokens(username)
tokens = {"access_token": access_token, "refresh_token": refresh_token}
return jsonify(tokens), 200
if user and user.verify_password(password):
json_answer, status_code = generate_tokens(user)
return jsonify(json_answer), status_code
else:
return jsonify({"message": "Invalid username or password"}), 401


@app.route("/signup", methods=["POST"])
def signup():
user = User(
username=request.form.get("username"),
nickname=request.form.get("nickname"),
password=request.form.get("password"),
)

json_answer, status_code = create_user(user)
return jsonify(json_answer), status_code


@app.route("/refresh")
def refresh():
refresh_token = request.headers.get("Authorization")
Expand All @@ -28,15 +48,26 @@ def refresh():

@app.route("/verify")
@token_required
def verify(username):
def verify(user_id):
"""Verifies if the token is valid"""

return jsonify({"message": "Access token is valid"}), 200


@app.route("/get_credentials")
@app.route("/get-credentials")
@token_required
def get_credentials(username):
def get_credentials(user_id):
"""decode token and return content"""

return jsonify({"username": username}), 200
user = get_user_by_id(user_id)

return jsonify({"username": user.username, "nickname": user.nickname}), 200


@app.route("/logout")
@token_required
def logout(user_id):
"""decode token and return content"""

json_answer, status_code = remove_refresh_token(user_id)
return jsonify(json_answer), status_code
6 changes: 4 additions & 2 deletions docker-compose.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ DEV=$(
env_file:
- ./.envs/.local/.postgres
volumes:
- ./postgres_data:/var/lib/postgresql/data/
- ./postgres-data:/var/lib/postgresql/data/

auth-ms:
container_name: 'auth-ms'
Expand Down Expand Up @@ -46,14 +46,16 @@ DEV=$(
container_name: front-ms
image: node:lts-alpine
working_dir: /front-ms
env_file:
- ./.envs/.local/.front-ms
command: sh -c "yarn install && yarn dev --host=0.0.0.0 --port=8080"
volumes:
- ./front-ms:/front-ms
ports:
- 8080:8080

volumes:
postgres_data:
postgres-data:
EOF
)

Expand Down
6 changes: 4 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ services:
env_file:
- ./.envs/.local/.postgres
volumes:
- ./postgres_data:/var/lib/postgresql/data/
- ./postgres-data:/var/lib/postgresql/data/

auth-ms:
container_name: 'auth-ms'
Expand Down Expand Up @@ -36,11 +36,13 @@ services:
container_name: front-ms
image: node:lts-alpine
working_dir: /front-ms
env_file:
- ./.envs/.local/.front-ms
command: sh -c "yarn install && yarn dev --host=0.0.0.0 --port=8080"
volumes:
- ./front-ms:/front-ms
ports:
- 8080:8080

volumes:
postgres_data:
postgres-data:
2 changes: 1 addition & 1 deletion front-ms/src/api/auth/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export default {
getCredentials() {
const ACCESS_TOKEN = localStorage.getItem("access_token");
api.defaults.headers.Authorization = ACCESS_TOKEN;
return api.get(`/get_credentials`);
return api.get(`/get-credentials`);
},
login(data) {
api.defaults.headers.Authorization = "";
Expand Down
2 changes: 1 addition & 1 deletion race-ms/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf

*.pyc
# AWS User-specific
.idea/**/aws.xml

Expand Down
Loading
X Tutup