This commit is contained in:
parent
93ee9ce58c
commit
6e3ba36851
25
.drone.yml
Normal file
25
.drone.yml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: default
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: code-analysis
|
||||||
|
image: aosapps/drone-sonar-plugin
|
||||||
|
settings:
|
||||||
|
sonar_host:
|
||||||
|
from_secret: SONAR_HOST
|
||||||
|
sonar_token:
|
||||||
|
from_secret: SONAR_CODE
|
||||||
|
|
||||||
|
- name: kaniko
|
||||||
|
image: banzaicloud/drone-kaniko
|
||||||
|
settings:
|
||||||
|
registry: registry.kmlabz.com
|
||||||
|
repo: onspot/${DRONE_REPO_NAME}
|
||||||
|
username:
|
||||||
|
from_secret: DOCKER_USERNAME
|
||||||
|
password:
|
||||||
|
from_secret: DOCKER_PASSWORD
|
||||||
|
tags:
|
||||||
|
- latest
|
||||||
|
- ${DRONE_BUILD_NUMBER}
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -128,4 +128,4 @@ dmypy.json
|
|||||||
|
|
||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
.idea/
|
||||||
|
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
FROM python:3.9-slim
|
||||||
|
|
||||||
|
ENV TZ Europe/Budapest
|
||||||
|
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ARG RELEASE_ID
|
||||||
|
ENV RELEASE_ID ${RELEASE_ID:-""}
|
||||||
|
|
||||||
|
COPY requirements.txt ./
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt && rm -rf requirements.txt
|
||||||
|
|
||||||
|
COPY ./src .
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
ENTRYPOINT ["gunicorn", "-b", "0.0.0.0:8080", "--workers", "1", "--threads", "1", "app:app"]
|
45
k8s/backend.yml
Normal file
45
k8s/backend.yml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: backend
|
||||||
|
namespace: onspot
|
||||||
|
labels:
|
||||||
|
app: backend
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: backend
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: backend
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: backend
|
||||||
|
image: registry.kmlabz.com/onspot/backend
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: regcred
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: backend
|
||||||
|
namespace: onspot
|
||||||
|
labels:
|
||||||
|
app: backend
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- name: backend
|
||||||
|
port: 80
|
||||||
|
targetPort: 8080
|
||||||
|
protocol: TCP
|
||||||
|
selector:
|
||||||
|
app: backend
|
||||||
|
type: ClusterIP
|
12
requirements.txt
Normal file
12
requirements.txt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
musicbrainzngs
|
||||||
|
flask
|
||||||
|
flask-restful
|
||||||
|
gunicorn
|
||||||
|
flask-jwt-extended
|
||||||
|
sentry-sdk[flask]
|
||||||
|
py-healthcheck
|
||||||
|
flask-redis
|
||||||
|
marshmallow
|
||||||
|
flask-marshmallow
|
||||||
|
spotipy
|
||||||
|
pycryptodome
|
57
src/aes_encrypt.py
Normal file
57
src/aes_encrypt.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
AES Encryption methods
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = '@tormakris'
|
||||||
|
__copyright__ = "Copyright 2020, onSpot Team"
|
||||||
|
__module_name__ = "aes_encrypt"
|
||||||
|
__version__text__ = "1"
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
|
||||||
|
from fred import flaskred
|
||||||
|
from schemas import UserSchema
|
||||||
|
|
||||||
|
|
||||||
|
class AESCrypto:
|
||||||
|
def __init__(self, encoded_secret_key: str, padding_character: bytes = 'a'.encode('UFT-8')):
|
||||||
|
self.padding_character = padding_character
|
||||||
|
self.encoded_secret_key = encoded_secret_key
|
||||||
|
|
||||||
|
def encrypt_message(self, private_msg) -> bytes:
|
||||||
|
secret_key = base64.b64decode(self.encoded_secret_key)
|
||||||
|
cipher = AES.new(secret_key, AES.MODE_EAX)
|
||||||
|
padded_private_msg = private_msg + (self.padding_character.decode('UFT-8') * ((16 - len(private_msg)) % 16))
|
||||||
|
encrypted_msg = cipher.encrypt(padded_private_msg)
|
||||||
|
encoded_encrypted_msg = base64.b64encode(encrypted_msg)
|
||||||
|
return encoded_encrypted_msg
|
||||||
|
|
||||||
|
def decrypt_message(self, encoded_encrypted_msg) -> str:
|
||||||
|
secret_key = base64.b64decode(self.encoded_secret_key)
|
||||||
|
encrypted_msg = base64.b64decode(encoded_encrypted_msg)
|
||||||
|
cipher = AES.new(secret_key, AES.MODE_EAX)
|
||||||
|
decrypted_msg = cipher.decrypt(encrypted_msg)
|
||||||
|
unpadded_private_msg = decrypted_msg.rstrip(self.padding_character)
|
||||||
|
return unpadded_private_msg.decode('UTF-8')
|
||||||
|
|
||||||
|
|
||||||
|
class EncryptedUserRedis:
|
||||||
|
def __init__(self, encoded_secret_key: str):
|
||||||
|
self.aes = AESCrypto(encoded_secret_key)
|
||||||
|
self.userschema = UserSchema(many=False)
|
||||||
|
|
||||||
|
def store(self, user: UserSchema) -> None:
|
||||||
|
plaindict = self.userschema.dump(user)
|
||||||
|
plaindict['password'] = self.aes.encrypt_message(user['password'])
|
||||||
|
flaskred.set(user['name'], json.dumps(plaindict).encode('UTF-8'))
|
||||||
|
|
||||||
|
def load(self, username: str) -> UserSchema:
|
||||||
|
encryptedstr = flaskred.get(username).decode('UTF-8')
|
||||||
|
encrypteddict = json.loads(encryptedstr)
|
||||||
|
user = UserSchema(name=encrypteddict['name'], password=self.aes.decrypt_message(encrypteddict['password']))
|
||||||
|
return user
|
75
src/app.py
Normal file
75
src/app.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from flask import Flask
|
||||||
|
from flask_restful import Api
|
||||||
|
import sentry_sdk
|
||||||
|
from sentry_sdk.integrations.flask import FlaskIntegration
|
||||||
|
from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
|
||||||
|
from healthcheck import HealthCheck
|
||||||
|
|
||||||
|
from jwtman import jwtman
|
||||||
|
from marshm import ma
|
||||||
|
from fred import flaskred
|
||||||
|
from config import SENTRY_DSN, JWT_SECRET_KEY, RELEASEMODE, RELEASE_ID, PORT, DEBUG, REDIS_URL
|
||||||
|
from errorhandlers import register_all_error_handlers
|
||||||
|
from resources import LoginApi
|
||||||
|
|
||||||
|
"""
|
||||||
|
Main Flask RESTful API
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "@tormakris"
|
||||||
|
__copyright__ = "Copyright 2020, onSpot Team"
|
||||||
|
__module_name__ = "app"
|
||||||
|
__version__text__ = "1"
|
||||||
|
|
||||||
|
if SENTRY_DSN:
|
||||||
|
sentry_sdk.init(
|
||||||
|
dsn=SENTRY_DSN,
|
||||||
|
integrations=[FlaskIntegration(), SqlalchemyIntegration()],
|
||||||
|
traces_sample_rate=1.0,
|
||||||
|
send_default_pii=True,
|
||||||
|
release=RELEASE_ID,
|
||||||
|
environment=RELEASEMODE,
|
||||||
|
_experiments={"auto_enabling_integrations": True}
|
||||||
|
)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config['JWT_SECRET_KEY'] = JWT_SECRET_KEY
|
||||||
|
app.config['REDIS_URL'] = REDIS_URL
|
||||||
|
api = Api(app)
|
||||||
|
health = HealthCheck()
|
||||||
|
ma.init_app(app)
|
||||||
|
flaskred.init_app(app)
|
||||||
|
jwtman.init_app(app)
|
||||||
|
|
||||||
|
formatter = logging.Formatter(
|
||||||
|
fmt="%(asctime)s - %(levelname)s - %(module)s - %(message)s"
|
||||||
|
)
|
||||||
|
|
||||||
|
handler = logging.StreamHandler()
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
logger.addHandler(handler)
|
||||||
|
|
||||||
|
api.add_resource(LogoffApi, '/api/auth/logoff')
|
||||||
|
api.add_resource(LoginApi, '/api/auth/login')
|
||||||
|
api.add_resource(MeApi, '/api/auth/me')
|
||||||
|
api.add_resource(ListsApi, '/api/lists')
|
||||||
|
api.add_resource(SingleListApi, '/api/lists/<listid>')
|
||||||
|
api.add_resource(TrackApi, '/api/lists/<listid>/<trackid>')
|
||||||
|
|
||||||
|
app.add_url_rule("/healthz", "healthcheck", view_func=lambda: health.run())
|
||||||
|
|
||||||
|
register_all_error_handlers(app)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(
|
||||||
|
debug=bool(DEBUG),
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=int(PORT),
|
||||||
|
)
|
27
src/config.py
Normal file
27
src/config.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
"""
|
||||||
|
Configuration
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
__author__ = "@tormakris"
|
||||||
|
__copyright__ = "Copyright 2020, onSpot Team"
|
||||||
|
__module_name__ = "config"
|
||||||
|
__version__text__ = "1"
|
||||||
|
|
||||||
|
|
||||||
|
PORT = os.environ.get("ONSPOT_PORT", 8080)
|
||||||
|
DEBUG = os.environ.get("ONSPOT_DEBUG", True)
|
||||||
|
|
||||||
|
SENTRY_DSN = os.environ.get("SENTRY_DSN")
|
||||||
|
RELEASE_ID = os.environ.get("RELEASE_ID", "test")
|
||||||
|
RELEASEMODE = os.environ.get("ONSPOT_RELEASEMODE", "dev")
|
||||||
|
|
||||||
|
JWT_SECRET_KEY = os.getenv("ONSPOT_JWT_SECRET_KEY", str(uuid4()))
|
||||||
|
|
||||||
|
REDIS_URL = os.getenv("ONSPOT_REDIS_URL")
|
||||||
|
|
||||||
|
ENCODED_SECRET_KEY = os.getenv("ONSPOT_ENCODED_SECRET_KEY")
|
27
src/errorhandlers.py
Normal file
27
src/errorhandlers.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
Flask error handler functions
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = '@tormakris'
|
||||||
|
__copyright__ = "Copyright 2020, onSpot Team"
|
||||||
|
__module_name__ = "errorhandlers"
|
||||||
|
__version__text__ = "1"
|
||||||
|
|
||||||
|
|
||||||
|
def get_standard_error_handler(code: int):
|
||||||
|
def error_handler(err):
|
||||||
|
return {"msg": str(err)}, code
|
||||||
|
|
||||||
|
return error_handler
|
||||||
|
|
||||||
|
|
||||||
|
# function to register all handlers
|
||||||
|
|
||||||
|
|
||||||
|
def register_all_error_handlers(app):
|
||||||
|
error_codes_to_override = [404, 403, 401, 405, 400, 409, 422]
|
||||||
|
|
||||||
|
for code in error_codes_to_override:
|
||||||
|
app.register_error_handler(code, get_standard_error_handler(code))
|
14
src/fred.py
Normal file
14
src/fred.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from flask_redis import FlaskRedis
|
||||||
|
|
||||||
|
"""
|
||||||
|
Redis
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = '@tormakris'
|
||||||
|
__copyright__ = "Copyright 2020, onSpot Team"
|
||||||
|
__module_name__ = "fred"
|
||||||
|
__version__text__ = "1"
|
||||||
|
|
||||||
|
flaskred = FlaskRedis()
|
14
src/jwtman.py
Normal file
14
src/jwtman.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from flask_jwt_extended import JWTManager
|
||||||
|
|
||||||
|
"""
|
||||||
|
JWTManager
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = '@tormakris'
|
||||||
|
__copyright__ = "Copyright 2020, onSpot Team"
|
||||||
|
__module_name__ = "jwtman"
|
||||||
|
__version__text__ = "1"
|
||||||
|
|
||||||
|
jwtman = JWTManager()
|
14
src/marshm.py
Normal file
14
src/marshm.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from flask_marshmallow import Marshmallow
|
||||||
|
|
||||||
|
"""
|
||||||
|
Marshmallow
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = '@tormakris'
|
||||||
|
__copyright__ = "Copyright 2020, onSpot Team"
|
||||||
|
__module_name__ = "marshm"
|
||||||
|
__version__text__ = "1"
|
||||||
|
|
||||||
|
ma = Marshmallow()
|
45
src/resources.py
Normal file
45
src/resources.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
|
||||||
|
from flask_restful import Resource
|
||||||
|
from flask import request, current_app, abort
|
||||||
|
|
||||||
|
from config import ENCODED_SECRET_KEY
|
||||||
|
from schemas import UserSchema, ListSchema, TrackSchema
|
||||||
|
from aes_encrypt import EncryptedUserRedis
|
||||||
|
|
||||||
|
"""
|
||||||
|
Flask Restful endpoints
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = '@tormakris'
|
||||||
|
__copyright__ = "Copyright 2020, onSpot Team"
|
||||||
|
__module_name__ = "resources"
|
||||||
|
__version__text__ = "1"
|
||||||
|
|
||||||
|
INVALID_JSON_SCHEMA_MSG = "invalid json schema"
|
||||||
|
|
||||||
|
|
||||||
|
class LoginApi(Resource):
|
||||||
|
"""
|
||||||
|
See: https://swagger.kmlabz.com/?urls.primaryName=onSpot%20Backend#/backend/logon
|
||||||
|
"""
|
||||||
|
|
||||||
|
userschema = UserSchema(many=False)
|
||||||
|
encryptor = EncryptedUserRedis(ENCODED_SECRET_KEY)
|
||||||
|
|
||||||
|
def post(self):
|
||||||
|
body = request.get_json()
|
||||||
|
|
||||||
|
try:
|
||||||
|
userobj = self.userschema.load(body)
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.warning(e)
|
||||||
|
abort(417, INVALID_JSON_SCHEMA_MSG)
|
||||||
|
|
||||||
|
self.encryptor.store(userobj)
|
||||||
|
|
||||||
|
expires = datetime.timedelta(days=7)
|
||||||
|
access_token = create_access_token(identity=str(userobj['name']), expires_delta=expires)
|
||||||
|
return {'token': access_token}, 200
|
49
src/schemas.py
Normal file
49
src/schemas.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from marshm import ma
|
||||||
|
from marshmallow import fields
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Marshmallow schemas
|
||||||
|
"""
|
||||||
|
|
||||||
|
__author__ = "@tormakris"
|
||||||
|
__copyright__ = "Copyright 2020, onSpot Team"
|
||||||
|
__module_name__ = "schemas"
|
||||||
|
__version__text__ = "1"
|
||||||
|
|
||||||
|
|
||||||
|
class UserSchema(ma.Schema):
|
||||||
|
"""
|
||||||
|
Parameters:
|
||||||
|
- name (string)
|
||||||
|
- passowrd (string)
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = fields.String(required=True)
|
||||||
|
password = fields.String(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class TrackSchema(ma.Schema):
|
||||||
|
"""
|
||||||
|
Parameters:
|
||||||
|
- id (integer)
|
||||||
|
- title (string)
|
||||||
|
- artist (string)
|
||||||
|
- album (string)
|
||||||
|
- spotify_id (string)
|
||||||
|
- cover_url (string)
|
||||||
|
"""
|
||||||
|
|
||||||
|
id = fields.Integer(required=True)
|
||||||
|
title = fields.String(required=True)
|
||||||
|
artist = fields.String(required=True)
|
||||||
|
album = fields.String(required=True)
|
||||||
|
spotify_id = fields.String(required=True)
|
||||||
|
cover_url = fields.String(required=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ListSchema(ma.Schema):
|
||||||
|
|
||||||
|
id = fields.Integer(required=True)
|
||||||
|
tracklist = fields.Nested(TrackSchema, many=True)
|
Loading…
Reference in New Issue
Block a user