skeleton done
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Torma Kristóf 2020-11-25 23:54:52 +01:00
parent 93ee9ce58c
commit 6e3ba36851
14 changed files with 424 additions and 1 deletions

25
.drone.yml Normal file
View 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
View File

@ -128,4 +128,4 @@ dmypy.json
# Pyre type checker
.pyre/
.idea/

19
Dockerfile Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)