Commit ff9526fa authored by Moritz Sokoll's avatar Moritz Sokoll 🦀
Browse files

Merge branch 'dev' into 'experimental'

Merge dev  to experimental

See merge request !1
parents 3d144a8b 66aa04f8
Pipeline #193 passed with stage
in 51 seconds
......@@ -15,5 +15,3 @@ build:
script:
- pwd
- make build
tags:
- x86_64
# Bug
## Information
- Version (branch)
- Codebase (api or frontend)
- Error Codes (if known)
- Action Performed
# Feature: (Feature Name)
## Description
(Description for the feature)
## Reasons
- (List of reasons to why this Feature should be added)
FRONTENDDIR = src
APIDIR = api
WEBPACK = npx webpack
SASS = sassc
COMPOSE = docker-compose
docker-build-main:
make stylesheets
cd src
cd $(FRONTENDDIR)
npx webpack
docker build -t uludev/pyblog:latest src
docker build -t uludev/pyblog:latest $(FRONTENDDIR)
docker-build-api:
docker build -t uludev/pyblogapi:latest api
docker-run-main:
docker run --name blog -d -p5000:5000 -v`pwd`/src/pages:/app/pages pyblog:latest
docker run --name blog -d -p5000:5000 -v`pwd`/$(FRONTENDDIR)/pages:/app/pages pyblog:latest
docker-run-api:
docker run --name blog_api -d -p5001:5001 -v`pwd`/api/dbconfig.json:/app/dbconfig.json pyblogapi:latest
docker run --name blog_api -d -p5001:5001 -v`pwd`/$(APIDIR)/dbconfig.json:/app/dbconfig.json pyblogapi:latest
stylesheets:
sassc src/sass/navbar.scss src/static/css/navbar.css
sassc src/sass/main.scss src/static/css/main.css
sassc src/sass/postcard.scss src/static/css/postcard.css
sassc src/sass/login.scss src/static/css/login.css
$(SASS) $(FRONTENDDIR)/sass/navbar.scss $(FRONTENDDIR)/static/css/navbar.css
$(SASS) $(FRONTENDDIR)/sass/main.scss $(FRONTENDDIR)/static/css/main.css
$(SASS) $(FRONTENDDIR)/sass/postcard.scss $(FRONTENDDIR)/static/css/postcard.css
$(SASS) $(FRONTENDDIR)/sass/login.scss $(FRONTENDDIR)/static/css/login.css
start:
docker-compose up -d
$(COMPOSE) up -d
stop:
docker-compose down
$(COMPOSE) down
restart:
docker-compose down
docker-compose up -d
$(COMPOSE) down
$(COMPOSE) up -d
build:
make stylesheets
cd src
npx webpack
cd $(FRONTENDDIR)
$(WEBPACK)
......@@ -8,4 +8,4 @@ RUN pip3 install -U pip
RUN pip3 install -r requirements.txt
EXPOSE 5001
CMD [ "python3", "app.py" ]
CMD [ "uvicorn", "main:app", "--port", "5001", "--host", "0.0.0.0" ]
# imports
from flask import Flask, jsonify, request, Response, make_response, session
from flask_session import Session
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from typing import Optional
import mysql.connector as sql
from libapi.conf import ConfigFile
from libapi.token import TokenStore
from pydantic import BaseModel
import secrets
# create the Flask application instance
api = Flask(__name__)
api.config["SESSION_PERMANENT"] = False
api.config["SESSION_TYPE"] = "filesystem"
Session(api)
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=['*'],
allow_credentials=True,
allow_methods=['*'],
allow_headers=['*']
)
#set the directory containing the query files
querydir = "sql-src"
......@@ -32,137 +39,115 @@ def createDbconn():
)
return conn
# Create the Login request model
class Login(BaseModel):
uname: str
pwd: str
class Token(BaseModel):
token: str
# route the index
@api.route("/")
@app.get('/')
def apiIndex():
return jsonify({'path': 'index', 'content': 'PyBlogs api index response'})
return {'path': 'index', 'content': 'PyBlogs api index response'}
#/blog returns a blog post based on an id. e.g: /blog?id=1 returns the blog post with id 1
@api.route("/blog", methods=["GET"])
def apiBlog():
if not "id" in request.args:
return jsonify({'error': '400 bad request'}), 400
id = int(request.args["id"])
conn = createDbconn()
cursor = conn.cursor()
cursor.execute(open(f"{querydir}/getpost.sql", "r").read().format(id))
resp = cursor.fetchone()
response = make_response(jsonify({"content": resp[2], "title": resp[3], "author": resp[1], "id": resp[0]}))
response.headers["Access-Control-Allow-Origin"] = '*'
return response
@app.get("/blog/{id}")
def apiBlog(id: int):
while createDbconn().cursor() as cursor:
cursor.execute(open(f"{querydir}/getpost.sql", "r").read().format(id))
resp = cursor.fetchone()
return {"content": resp[2], "title": resp[3], "author": resp[1], "id": resp[0]}
#@api.route("/add", methods=["POST"])
#def addpost():
# pass
#TODO: implement login & register for user authentication | refactor sql statements to use '{}' instead of '?'
@api.route("/register", methods=["POST"])
def registerUser():
if not "pwd" in request.args or "uname" not in request.args:
return jsonify({"error": "400 bad request", "reason": "missing parameter"}), 400
cursor.execute(open(f"{querydir}/adduser.sql", "r").read(), (request.args["uname"], request.args["pwd"]))
cursor.execute(open(f"{querydir}/auth.sql", "r").read(), request.args["pwd"])
@app.post("/register?uname={uname}&?pwd={pwd}")
def registerUser(uname: str, pwd:str):
cursor.execute(open(f"{querydir}/adduser.sql", "r").read(), (uname, pwd))
cursor.execute(open(f"{querydir}/auth.sql", "r").read(), pwd)
response = cursor.fetchone()
if len(response) < 1:
return jsonify({"error": "500 failed to authenticate"}), 500
id = response[0]
session[str(id)] = request.form["username"]
return jsonify({"username": request.form["username"], "id": id})
return jsonify({"username": uname, "id": id})
@api.route("/login", methods=["POST"])
def loginUser():
tokenstore.delExpired()
if not "pwd" in request.form or "uname" not in request.form:
return jsonify({"error": "400 bad request"}), 400
@app.post("/login/")
def loginUser(logindata: Login):
conn = createDbconn()
cursor = conn.cursor()
cursor.execute(open(f"{querydir}/auth.sql", "r").read(), (request.form["pwd"], request.form["uname"]))
cursor.execute(open(f"{querydir}/auth.sql", "r").read(), (logindata.pwd, logindata.uname))
response = cursor.fetchone()
if len(response) < 1:
return jsonify({"error": "500 failed to authenticate"}), 500
return {"error": "403 forbidden"}, 403
id = response[0]
token = tokenstore.generate(str(id), 10800, 64)
response = make_response(jsonify({"token": token, "uname": request.form["uname"]}))
response.headers["Access-Control-Allow-Origin"] = '*'
return response
@api.route("/logout", methods=["POST"])
def logoutUser():
tokenstore.delExpired()
if not "token" in request.form:
return jsonify({"error": "400 bad request"}), 400
try:
tokenstore.remove(request.form["token"])
return jsonify({"status": "done"})
except ValueError:
return jsonify({"error": "400 bad request"})
token = secrets.token_urlsafe(64)
cursor.execute(open(f"{querydir}/gettoken.sql", "r").read())
usedtokens = [i[0] for i in cursor.fetchall()]
while token in usedtokens:
token = secrets.token_urlsafe(64)
continue
cursor.execute(open(f"{querydir}/addtoken.sql", "r").read(), (id, token))
return {"token": token, "uname": logindata.uname}
@app.post("/logout")
def logoutUser(token: Token):
conn = createDbconn()
cursor = conn.cursor()
cursor.execute(open(f"{querydir}/logout.sql", "r").read(), {"token": Token.token})
@api.route("/auth", methods=["POST"])
def authenticateUser():
if not "pwd" in request.args or "uname" not in request.args:
return jsonify({"error": "400 bad request", "reason": "missing parameter"}), 400
if not request.form["uname"] in session:
return jsonify({"authenticated": False}), 200
return jsonify({"username": request.form["uname"], "id": id})
#@api.route("/auth", methods=["POST"])
#def authenticateUser():
# if not "pwd" in request.args or "uname" not in request.args:
# return jsonify({"error": "400 bad request", "reason": "missing parameter"}), 400
# return jsonify({"authenticated": False}), 200
# return jsonify({"username": request.form["uname"], "id": id})
#/search gives metadata about all posts containig a query string. e.g. /search?q=tech will return post metadata about all posts where the title or the author contains 'tech'
@api.route("/search", methods=["GET"])
def searchPosts():
if not "q" in request.args:
return jsonify({"error": "400 bad request"}), 400
@app.get("/search/{q}")
def searchPosts(q: str):
conn = createDbconn()
cursor = conn.cursor()
cursor.execute(open(f"{querydir}/searchposts.sql", "r").read(), (f"%{request.args['q']}%", f"%{request.args['q']}%"))
cursor.execute(open(f"{querydir}/searchposts.sql", "r").read(), (f"%{q}%", f"%{q}%"))
res = cursor.fetchall()
responsedbcontent = []
for i in range(len(res)):
responsedbcontent.append({"title": res[i][2], "author": res[i][1], "id": res[i][0]})
response = make_response(jsonify(responsedbcontent))
response.headers["Access-Control-Allow-Origin"] = '*'
return response
@api.route("/user", methods=["GET"])
def userInformation():
if not "name" in request.args:
return jsonify({"error": "400 bad request"}), 400
else:
conn = createDbconn()
cursor = conn.cursor()
cursor.execute(open(f"{querydir}/getuinf.sql", "r").read().format(request.args["name"]))
res = cursor.fetchone()
response = make_response(jsonify({"name": res[1], "id": res[0], "bio": res[2]}))
response.headers["Access-Control-Allow-Origin"] = '*'
return response
return responsedbcontent
@app.get("/user/{name}")
def userInformation(name: str):
conn = createDbconn()
cursor = conn.cursor()
cursor.execute(open(f"{querydir}/getuinf.sql", "r").read().format(name))
res = cursor.fetchone()
return {"name": res[1], "id": res[0], "bio": res[2]}
# Endpoint for returning all posts of a specific user
@api.route("/uposts", methods=["GET"])
def userPosts():
if not "name" in request.args:
return jsonify({"error": "400 bad request"})
else:
conn = createDbconn()
cursor = conn.cursor()
cursor.execute(open(f"{querydir}/userposts.sql", "r").read().strip(), (request.args["name"], request.args["name"]))
res = cursor.fetchall()
responsecontent = []
for i in range(len(res)):
responsecontent.append({"title": res[i][1], "id": res[i][0]})
response = make_response(jsonify(responsecontent))
response.headers["Access-Control-Allow-Origin"] = '*'
return response
if __name__ == "__main__":
api.run(servconf["host"], port=int(servconf["port"]), debug=bool(servconf["debug"]))
@app.get("/uposts/{name}")
def userPosts(name: str):
conn = createDbconn()
cursor = conn.cursor()
cursor.execute(open(f"{querydir}/userposts.sql", "r").read().strip(), (name, name))
res = cursor.fetchall()
responsecontent = []
for i in range(len(res)):
responsecontent.append({"title": res[i][1], "id": res[i][0]})
return responsecontent
flask
mysql-connector
Flask-Session
fastapi
uvicorn[standard]
pydantic
INSERT INTO pyblog_auth (uuid, token) VALUES (%s, %s);
INSERT INTO pyblog_users (name, bio, password) VALUES (?, 'new user', SHA(?, 256));
INSERT INTO pyblog_users (name, bio, password) VALUES (%s, 'new user', SHA(%s, 256));
SELECT token FROM pyblog_auth;
DELETE FROM pyblog_auth WHERE token = %(token)s AND uuid = %(uuid)s;
......@@ -7,10 +7,9 @@ services:
volumes:
- ./src/pages:/app/pages
- ./src/static:/app/static
ports:
- 0.0.0.0:5000:5000
depends_on:
- blogapi
scale: 3
blogapi:
image: uludev/pyblogapi:latest
......@@ -18,8 +17,7 @@ services:
volumes:
- ./api/dbconfig:/app/dbconfig
- ./api/sql-src:/app/sql-src
ports:
- 0.0.0.0:5001:5001
scale: 3
depends_on:
- blogdb
......@@ -30,6 +28,16 @@ services:
- blogdb_content:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: example
nginx:
image: nginx
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
ports:
- 5000:5000
- 5001:5001
depends_on:
- blog
- blogapi
volumes:
blogdb_content:
......
events {}
http {
upstream blog {
server blog_blog_1:5000;
server blog_blog_2:5000;
server blog_blog_3:5000;
}
upstream api {
server blog_blogapi_1:5001;
server blog_blogapi_2:5001;
server blog_blogapi_3:5001;
}
server {
listen 5000;
location / {
proxy_pass http://blog;
}
}
server {
listen 5001;
location / {
proxy_pass http://api;
}
}
}
# Makefile for Docker Container
# Makefile for Docker Container + frontend development
SASS = sassc
TSC = tsc
WEBPACK = npx webpack
stylesheets:
sassc sass/navbar.scss static/css/navbar.css
sassc sass/main.scss static/css/main.css
sassc sass/postcard.scss static/css/postcard.css
sassc sass/login.scss static/css/login.css
$(SASS) sass/navbar.scss static/css/navbar.css
$(SASS) sass/main.scss static/css/main.css
$(SASS) sass/postcard.scss static/css/postcard.css
$(SASS) sass/login.scss static/css/login.css
scripts:
$(TSC)
bundle:
$(TSC)
$(WEBPACK)
build:
tsc
npx webpack
$(TSC)
$(WEBPACK)
make stylesheets
from flask import Flask, request, redirect, session, jsonify, make_response, Response
from flask_session import Session
import requests
from datetime import timedelta
from flask import Flask, request
app = Flask(__name__)
app.config["SESSION_TYPE"] = "filesystem"
app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(hours=3)
Session(app)
@app.route("/")
def index():
return open("/app/pages/index.html", "r").read()
@app.route("/view")
@app.route("/view", methods=["GET"])
def viewBlogPage():
if request.method != "GET":
return "please use the HTTP GET method to access this site"
if not "id" in request.args:
return "provide a blog id to view any article"
id = request.args['id']
......@@ -40,32 +31,6 @@ def usershow():
def login():
return open("/app/pages/login.html", "r").read()
@app.route("/auth", methods=["POST"])
def authorize():
if not "uname" in request.form or not "pwd" in request.form:
return "Failed to Authorize", 400
authresp = requests.post("http://blogapi:5001/login", data={"uname": request.form["uname"], "pwd": request.form["pwd"]})
if not authresp.ok:
return "Failed to Authorize", 500
resjson = authresp.json()
if not "token" in resjson:
return "Failed to Authorize", 500
session.permanent = True
session["token"] = [resjson["token"], resjson["uname"]]
return redirect("/")
@app.route("/token", methods=["GET"])
def retToken():
try:
response = make_response(jsonify({"token": session["token"][0], "uname": session["token"][1]}))
response.headers["Access-Control-Allow-Origin"] = '*'
return response
except:
response = make_response(jsonify({"error": "404 not found"}))
response.headers["Access-Control-Allow-Origin"] = '*'
return response, 404
if __name__ == "__main__":
app.run("0.0.0.0", 5000, debug=True)
......@@ -4,17 +4,10 @@
<title>Login | PyBlog</title>
<link rel="stylesheet" href="/static/css/main.css">
<link rel="stylesheet" href="/static/css/login.css">
<script type="module">
import * as theme from '/static/theme.js';
import * as banner from '/static/banner.js';
window.onload = () => {
theme.setTheme();
banner.runBanner();
}
</script>
<script src="/static/login-bundle.js"></script>
</head>
<body>
<form class="login" method="POST" action="/auth">
<form class="login" action="/" name="loginform">
<h2>Login</h2>
<input type="text" placeholder="Username" name="uname" class="textedit">
<input type="password" placeholder="Password" name="pwd" class="textedit">
......
......@@ -9,32 +9,7 @@
<link rel="stylesheet" href="/static/css/navbar.css">
<meta name="viewport" content="width=device-width, inital-scale=1.0">
</head>
<body onload="loadUser(); loadUserPosts(); setTheme(); runBanner();">
<nav class="nav">
<ul>
<li onclick="document.location.href='/';" class="nav-titlediv">
<img src="/static/book.png" style="width: 5rem; height: 5rem;"></img>
<h2 class="nav-title">PyBlog</h2>
</li>
<li><a class="navbar" href="/new">new article</a></li>
<li>
<form action="/search" class="nav-searchform">
<input type="text" name="q" placeholder="Search...">
<button>Search</button>
</form>
</li>
<li><a class="navbar" href="/about">about</a></li>
<li><a class="navbar" href="/login">login</a></li>
<li class="navbar">
<label class="cmode-switch">
<input type="checkbox" name="theme">
<span class="cmode-slider"></span>
<br>
<p>dark-mode</p>
</label>
</li>
</ul>
</nav>
<body>
<main>
<h1 id="uname">User</h1>
<p id="bio">Bio</p>
......
flask
requests
Flask-Session
......@@ -2,7 +2,7 @@ html {
height: 100%;
display: grid;
align-items: center;
justify-items: center;
justify-content: center;
}
form {
background-color: var(--nav-bg);
......
......@@ -50,6 +50,30 @@ textarea {
}
#reslist {
list-style: none;
display: grid;
gap: 20px;
place-items: center;
li {
border-radius: 5px;
box-shadow: 0 0 10px black;
padding: 2px 2px;
min-width: 240px;
max-width: 1000px;
transition: .4s;
cursor: pointer;
a {
text-decoration: none;
color: var(--text);
}
&:hover {
background-color: var(--text);
color: var(--bg);
a {
color: var(--bg);
}
}
}
}
html.transition,
html.transition *,
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment