From 4cd9db36cd39489018b7acb3ad04c6aa6b8d529a Mon Sep 17 00:00:00 2001 From: Luka Radenovic Date: Mon, 27 Sep 2021 12:03:35 +0200 Subject: [PATCH] Initial commit --- .gitignore | 2 ++ Dockerfile | 28 ++++++++++++++++++++ api/__init__.py | 3 +++ api/apps.py | 66 ++++++++++++++++++++++++++++++++++++++++++++++++ api/auth.py | 21 +++++++++++++++ api/users.py | 38 ++++++++++++++++++++++++++++ app.py | 27 ++++++++++++++++++++ config.py | 3 +++ requirements.txt | 14 ++++++++++ run_app.sh | 4 +++ 10 files changed, 206 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 api/__init__.py create mode 100644 api/apps.py create mode 100644 api/auth.py create mode 100644 api/users.py create mode 100644 app.py create mode 100644 config.py create mode 100644 requirements.txt create mode 100755 run_app.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2969886 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.venv +*.pyc \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ffb278e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM python:3.6-slim + +RUN apt-get update +RUN apt-get install -y libpq-dev python-dev gcc + +## make a local directory +RUN mkdir /app + +# set "app" as the working directory from which CMD, RUN, ADD references +WORKDIR /app + +# copy requirements.txt to /app +ADD requirements.txt . + +# required to be able to install old MarkupSafe==1.0.0 version +RUN pip install --upgrade pip setuptools==45.2.0 + +# pip install the local requirements.txt +RUN pip install -r requirements.txt + +# now copy all the files in this directory to /code +ADD . . + +# Listen to port 80 at runtime +EXPOSE 5000 + +# Define our command to be run when launching the container +CMD ["gunicorn", "app:app", "-b", "0.0.0.0:5000", "--workers", "4", "--reload", "--capture-output", "--enable-stdio-inheritance", "--log-level", "DEBUG"] diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..4cd2077 --- /dev/null +++ b/api/__init__.py @@ -0,0 +1,3 @@ +from flask import Blueprint + +api_v1 = Blueprint('api_v1', __name__, url_prefix='/api/v1') diff --git a/api/apps.py b/api/apps.py new file mode 100644 index 0000000..af9bcd6 --- /dev/null +++ b/api/apps.py @@ -0,0 +1,66 @@ +from flask import jsonify +from flask_jwt_extended import jwt_required +from flask_cors import cross_origin + +from . import api_v1 + +CONFIG_DATA = [ + { + "id": "values.yml", + "description": "Some user friendly description", + "raw": "cronjob:\n # Set curl to accept insecure connections when acme staging is used\n curlInsecure: false", + "fields": [ + {"name": "cronjob", "type": "string", "value": ""}, + {"name": "curlInsecure", "type": "boolean", "value": "false"} + ] + } +] + +APPS_DATA = [ + {"id": 1, "name": "Nextcloud", "enabled": True, "status": "ON for everyone"}, + {"id": 2, "name": "Rocketchat", "enabled": True, "status": "ON for everyone"}, + {"id": 3, "name": "Wordpress", "enabled": False, "status": "ON for everyone"} +] + +APP_DATA = {"id": 1, "name": "Nextcloud", "selected": True, "status": "ON for everyone", "config": CONFIG_DATA}, + + +@api_v1.route('/apps', methods=['GET']) +@jwt_required() +@cross_origin() +def get_apps(): + return jsonify(APPS_DATA) + + +@api_v1.route('/apps/', methods=['GET']) +@jwt_required() +def get_app(slug): + return jsonify(APPS_DATA[0]) + + +@api_v1.route('/apps', methods=['POST']) +@jwt_required() +@cross_origin() +def post_app(): + return jsonify(APPS_DATA), 201 + + +@api_v1.route('/apps/', methods=['PUT']) +@jwt_required() +@cross_origin() +def put_app(slug): + return jsonify(APPS_DATA) + + +@api_v1.route('/apps//config', methods=['GET']) +@jwt_required() +@cross_origin() +def get_config(slug): + return jsonify(CONFIG_DATA) + + +@api_v1.route('/apps//config', methods=['DELETE']) +@jwt_required() +@cross_origin() +def delete_config(slug): + return jsonify(CONFIG_DATA) diff --git a/api/auth.py b/api/auth.py new file mode 100644 index 0000000..fb4cdd9 --- /dev/null +++ b/api/auth.py @@ -0,0 +1,21 @@ +from flask import request, jsonify +from flask_jwt_extended import create_access_token +from flask_cors import cross_origin + +from . import api_v1 + +USERNAME = 'admin' +PASSWORD = 'admin' + + +@api_v1.route('/login', methods=['POST']) +@cross_origin() +def login(): + username = request.json.get('username') + password = request.json.get('password') + + if username != USERNAME or password != PASSWORD: + return jsonify({'errorMessage': 'Invalid username or password'}), 401 + + access_token = create_access_token(identity=username) + return jsonify({'username': USERNAME, 'access_token': access_token}) diff --git a/api/users.py b/api/users.py new file mode 100644 index 0000000..b96a03f --- /dev/null +++ b/api/users.py @@ -0,0 +1,38 @@ +from flask import jsonify +from flask_jwt_extended import jwt_required +from flask_cors import cross_origin + +from . import api_v1 + + +USER_DATA = [ + {"id": 1, "email": "john@doe.com", "name": "John Doe", "status": "active", "last_login": "2021-08-03T07:40:51+00:00"} +] + + +@api_v1.route('/users', methods=['GET']) +@jwt_required() +@cross_origin() +def get_users(): + return jsonify(USER_DATA) + + +@api_v1.route('/users', methods=['POST']) +@jwt_required() +@cross_origin() +def post_user(): + return jsonify(USER_DATA), 201 + + +@api_v1.route('/users/', methods=['PUT']) +@jwt_required() +@cross_origin() +def put_user(id): + return jsonify(USER_DATA) + + +@api_v1.route('/users/', methods=['DELETE']) +@jwt_required() +@cross_origin() +def delete_user(id): + return jsonify(USER_DATA) diff --git a/app.py b/app.py new file mode 100644 index 0000000..8654fc4 --- /dev/null +++ b/app.py @@ -0,0 +1,27 @@ +from flask import Flask, jsonify +from flask_jwt_extended import JWTManager +from flask_cors import CORS, cross_origin + +from config import * + +from api import api_v1, auth, users, apps + +app = Flask(__name__) +cors = CORS(app) +app.config['SECRET_KEY'] = SECRET_KEY +app.register_blueprint(api_v1) + +jwt = JWTManager(app) + + +# When token is not valid or missing handler +@jwt.invalid_token_loader +@jwt.unauthorized_loader +@jwt.expired_token_loader +def expired_token_callback(*args): + return jsonify({'errorMessage': 'Unauthorized'}), 401 + + +@app.route('/') +def index(): + return 'Open App Stack API v1.0' diff --git a/config.py b/config.py new file mode 100644 index 0000000..5532012 --- /dev/null +++ b/config.py @@ -0,0 +1,3 @@ +import os + +SECRET_KEY = os.environ.get('SECRET_KEY') diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ebb162e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,14 @@ +cffi==1.14.6 +click==8.0.1 +cryptography==3.4.7 +Flask==2.0.1 +Flask-Cors==3.0.10 +Flask-JWT-Extended==4.2.3 +gunicorn==20.1.0 +itsdangerous==2.0.1 +Jinja2==3.0.1 +MarkupSafe==2.0.1 +pycparser==2.20 +PyJWT==2.1.0 +six==1.16.0 +Werkzeug==2.0.1 diff --git a/run_app.sh b/run_app.sh new file mode 100755 index 0000000..c025ded --- /dev/null +++ b/run_app.sh @@ -0,0 +1,4 @@ +export FLASK_APP=app.py +export FLASK_ENV=development +export SECRET_KEY="e38hq!@0n64g@qe6)5csk41t=ljo2vllog(%k7njnm4b@kh42c" +flask run