feat: Initial code
All checks were successful
Build Docker / Build Image (push) Successful in 32s

This commit is contained in:
Nathan Woodburn 2023-11-08 17:55:49 +11:00
parent e3138cdbee
commit f424db847d
Signed by: nathanwoodburn
GPG Key ID: 203B000478AD0EF1
14 changed files with 436 additions and 0 deletions

View File

@ -0,0 +1,41 @@
name: Build Docker
run-name: Build Docker Images
on:
push:
jobs:
Build Image:
runs-on: [ubuntu-latest, amd]
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install Docker
run : |
apt-get install ca-certificates curl gnupg
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
apt-get update
apt-get install docker-ce-cli -y
- name: Build Docker image
run : |
echo "${{ secrets.DOCKERGIT_TOKEN }}" | docker login git.woodburn.au -u nathanwoodburn --password-stdin
echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}"
tag=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
tag=${tag//\//-}
tag_num=${GITHUB_RUN_NUMBER}
echo "tag_num=$tag_num"
if [[ "$tag" == "main" ]]; then
tag="latest"
else
tag_num="${tag}-${tag_num}"
fi
docker build -t shakecities:$tag_num .
docker tag shakecities:$tag_num git.woodburn.au/nathanwoodburn/shakecities:$tag_num
docker push git.woodburn.au/nathanwoodburn/shakecities:$tag_num
docker tag shakecities:$tag_num git.woodburn.au/nathanwoodburn/shakecities:$tag
docker push git.woodburn.au/nathanwoodburn/shakecities:$tag

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.env
__pycache__/
*.json

17
Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM --platform=$BUILDPLATFORM python:3.10-alpine AS builder
WORKDIR /app
COPY requirements.txt /app
RUN --mount=type=cache,target=/root/.cache/pip \
pip3 install -r requirements.txt
COPY . /app
# Add mount point for data volume
VOLUME /data
ENTRYPOINT ["python3"]
CMD ["server.py", "sldserver.py"]
FROM builder as dev-envs

69
accounts.py Normal file
View File

@ -0,0 +1,69 @@
import os
import dotenv
from passlib.hash import argon2
import json
dotenv.load_dotenv()
local = os.getenv('LOCAL')
def hash_password(password):
return argon2.using(rounds=16).hash(password)
# Verify a password against a hashed password
def verify_password(password, hashed_password):
return argon2.verify(password, hashed_password)
def generate_cookie():
token = os.urandom(24).hex()
# Verify token doesn't already exist
with open('users.json', 'r') as f:
users = json.load(f)
for user in users:
if token in user['tokens']:
print('Token already exists, generating new one')
return generate_cookie()
return token
# Create a new user
def create_user(email, domain, password):
# Hash password
hashed_password = hash_password(password)
# Create user
user = {
'email': email,
'domain': domain,
'password': hashed_password
}
# Create a cookie
token = generate_cookie()
user['tokens'] = [token]
# If file doesn't exist, create it
if not os.path.isfile('users.json'):
with open('users.json', 'w') as f:
json.dump([], f)
# Write to file
with open('users.json', 'r') as f:
users = json.load(f)
for u in users:
if u['email'] == email:
return {'success': False, 'message': 'Email already exists'}
users.append(user)
with open('users.json', 'w') as f:
json.dump(users, f)
return {'success': True, 'message': 'User created', 'token': token}
def validate_token(token):
with open('users.json', 'r') as f:
users = json.load(f)
for user in users:
if token in user['tokens']:
return user
return False

77
main.py Normal file
View File

@ -0,0 +1,77 @@
from flask import Flask, make_response, redirect, request, jsonify, render_template, send_from_directory
import os
import dotenv
import requests
import json
import schedule
import time
from email_validator import validate_email, EmailNotValidError
import accounts
app = Flask(__name__)
dotenv.load_dotenv()
#Assets routes
@app.route('/assets/<path:path>')
def assets(path):
return send_from_directory('templates/assets', path)
#! TODO make prettier
def error(message):
return jsonify({'success': False, 'message': message}), 400
@app.route('/')
def index():
if 'token' in request.cookies:
token = request.cookies['token']
# Verify token
user = accounts.validate_token(token)
if not user:
# Remove cookie
resp = make_response(redirect('/'))
resp.set_cookie('token', '', expires=0)
return resp
return render_template('index.html',account=user['email'],account_link="account")
return render_template('index.html',account="Login",account_link="login")
@app.route('/signup', methods=['POST'])
def signup():
email = request.form['email']
domain = request.form['domain']
password = request.form['password']
print("New signup for " + email + " | " + domain)
try:
valid = validate_email(email)
email = valid.email
user = accounts.create_user(email, domain, password)
if not user['success']:
return error(user['message'])
# Redirect to dashboard with cookie
resp = make_response(redirect('/edit'))
resp.set_cookie('token', user['token'])
return resp
except EmailNotValidError as e:
return jsonify({'success': False, 'message': 'Invalid email'}), 400
@app.route('/<path:path>')
def catch_all(path):
# If file exists, load it
if os.path.isfile('templates/' + path):
return render_template(path)
# Try with .html
if os.path.isfile('templates/' + path + '.html'):
return render_template(path + '.html')
return redirect('/') # 404 catch all
# 404 catch all
@app.errorhandler(404)
def not_found(e):
return redirect('/')
if __name__ == '__main__':
app.run(debug=False, port=5000, host='0.0.0.0')

10
requirements.txt Normal file
View File

@ -0,0 +1,10 @@
flask
python-dotenv
gunicorn
requests
pyotp
schedule
email-validator
py3dns
passlib
argon2-cffi

42
server.py Normal file
View File

@ -0,0 +1,42 @@
import time
from flask import Flask
from main import app
import main
from gunicorn.app.base import BaseApplication
import os
import dotenv
import sys
import json
class GunicornApp(BaseApplication):
def __init__(self, app, options=None):
self.options = options or {}
self.application = app
super().__init__()
def load_config(self):
for key, value in self.options.items():
if key in self.cfg.settings and value is not None:
self.cfg.set(key.lower(), value)
def load(self):
return self.application
if __name__ == '__main__':
workers = os.getenv('WORKERS')
threads = os.getenv('THREADS')
if workers is None:
workers = 1
if threads is None:
threads = 2
workers = int(workers)
threads = int(threads)
options = {
'bind': '0.0.0.0:5000',
'workers': workers,
'threads': threads,
}
gunicorn_app = GunicornApp(app, options)
print('Starting server with ' + str(workers) + ' workers and ' + str(threads) + ' threads', flush=True)
gunicorn_app.run()

48
slds.py Normal file
View File

@ -0,0 +1,48 @@
from flask import Flask, make_response, redirect, request, jsonify, render_template, send_from_directory
import os
import dotenv
import requests
import json
import schedule
import time
from email_validator import validate_email, EmailNotValidError
import accounts
app = Flask(__name__)
dotenv.load_dotenv()
#Assets routes
@app.route('/assets/<path:path>')
def assets(path):
return send_from_directory('templates/assets', path)
#! TODO make prettier
def error(message):
return jsonify({'success': False, 'message': message}), 400
@app.route('/')
def index():
host = request.host
return jsonify({'success': True, 'message': host})
@app.route('/<path:path>')
def catch_all(path):
# If file exists, load it
if os.path.isfile('templates/' + path):
return render_template(path)
# Try with .html
if os.path.isfile('templates/' + path + '.html'):
return render_template(path + '.html')
return redirect('/') # 404 catch all
# 404 catch all
@app.errorhandler(404)
def not_found(e):
return redirect('/')
if __name__ == '__main__':
app.run(debug=False, port=5000, host='0.0.0.0')

42
sldserver.py Normal file
View File

@ -0,0 +1,42 @@
import time
from flask import Flask
from slds import app
import slds
from gunicorn.app.base import BaseApplication
import os
import dotenv
import sys
import json
class GunicornApp(BaseApplication):
def __init__(self, app, options=None):
self.options = options or {}
self.application = app
super().__init__()
def load_config(self):
for key, value in self.options.items():
if key in self.cfg.settings and value is not None:
self.cfg.set(key.lower(), value)
def load(self):
return self.application
if __name__ == '__main__':
workers = os.getenv('WORKERS')
threads = os.getenv('THREADS')
if workers is None:
workers = 1
if threads is None:
threads = 2
workers = int(workers)
threads = int(threads)
options = {
'bind': '0.0.0.0:5001',
'workers': workers,
'threads': threads,
}
gunicorn_app = GunicornApp(app, options)
print('Starting server with ' + str(workers) + ' workers and ' + str(threads) + ' threads', flush=True)
gunicorn_app.run()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
templates/assets/css/styles.min.css vendored Normal file
View File

@ -0,0 +1 @@
.bs-icon{--bs-icon-size:.75rem;display:flex;flex-shrink:0;justify-content:center;align-items:center;font-size:var(--bs-icon-size);width:calc(var(--bs-icon-size) * 2);height:calc(var(--bs-icon-size) * 2);color:var(--bs-primary)}.bs-icon-xs{--bs-icon-size:1rem;width:calc(var(--bs-icon-size) * 1.5);height:calc(var(--bs-icon-size) * 1.5)}.bs-icon-sm{--bs-icon-size:1rem}.bs-icon-md{--bs-icon-size:1.5rem}.bs-icon-lg{--bs-icon-size:2rem}.bs-icon-xl{--bs-icon-size:2.5rem}.bs-icon.bs-icon-primary{color:var(--bs-white);background:var(--bs-primary)}.bs-icon.bs-icon-primary-light{color:var(--bs-primary);background:rgba(var(--bs-primary-rgb),.2)}.bs-icon.bs-icon-semi-white{color:var(--bs-primary);background:rgba(255,255,255,.5)}.bs-icon.bs-icon-rounded{border-radius:.5rem}.bs-icon.bs-icon-circle{border-radius:50%}

31
templates/index.html Normal file
View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html data-bs-theme="light" lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>shakecities</title>
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/styles.min.css">
</head>
<body>
<nav class="navbar navbar-expand-md bg-dark py-3" data-bs-theme="dark">
<div class="container"><a class="navbar-brand d-flex align-items-center" href="/"><span class="bs-icon-sm bs-icon-rounded bs-icon-primary d-flex justify-content-center align-items-center me-2 bs-icon"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-bezier">
<path fill-rule="evenodd" d="M0 10.5A1.5 1.5 0 0 1 1.5 9h1A1.5 1.5 0 0 1 4 10.5v1A1.5 1.5 0 0 1 2.5 13h-1A1.5 1.5 0 0 1 0 11.5v-1zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1zm10.5.5A1.5 1.5 0 0 1 13.5 9h1a1.5 1.5 0 0 1 1.5 1.5v1a1.5 1.5 0 0 1-1.5 1.5h-1a1.5 1.5 0 0 1-1.5-1.5v-1zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1zM6 4.5A1.5 1.5 0 0 1 7.5 3h1A1.5 1.5 0 0 1 10 4.5v1A1.5 1.5 0 0 1 8.5 7h-1A1.5 1.5 0 0 1 6 5.5v-1zM7.5 4a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1z"></path>
<path d="M6 4.5H1.866a1 1 0 1 0 0 1h2.668A6.517 6.517 0 0 0 1.814 9H2.5c.123 0 .244.015.358.043a5.517 5.517 0 0 1 3.185-3.185A1.503 1.503 0 0 1 6 5.5v-1zm3.957 1.358A1.5 1.5 0 0 0 10 5.5v-1h4.134a1 1 0 1 1 0 1h-2.668a6.517 6.517 0 0 1 2.72 3.5H13.5c-.123 0-.243.015-.358.043a5.517 5.517 0 0 0-3.185-3.185z"></path>
</svg></span><span>Brand</span></a><button data-bs-toggle="collapse" class="navbar-toggler" data-bs-target="#navcol-6"><span class="visually-hidden">Toggle navigation</span><span class="navbar-toggler-icon"></span></button>
<div class="collapse navbar-collapse flex-grow-0 order-md-first" id="navcol-6">
<ul class="navbar-nav me-auto">
<li class="nav-item"><a class="nav-link active" href="/signup">Create your page</a></li>
<li class="nav-item"><a class="nav-link" href="/edit">Edit your page</a></li>
</ul>
<div class="d-md-none my-2"><button class="btn btn-light me-2" type="button">Button</button><button class="btn btn-primary" type="button">Button</button></div>
</div>
<div class="d-none d-md-block"><a class="btn btn-primary" role="button" href="/{{account_link}}">{{account}}</a></div>
</div>
</nav>
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
</body>
</html>

41
templates/signup.html Normal file
View File

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html data-bs-theme="light" lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>shakecities</title>
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="assets/css/styles.min.css">
</head>
<body>
<nav class="navbar navbar-expand-md bg-dark py-3" data-bs-theme="dark">
<div class="container"><a class="navbar-brand d-flex align-items-center" href="/"><span class="bs-icon-sm bs-icon-rounded bs-icon-primary d-flex justify-content-center align-items-center me-2 bs-icon"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-bezier">
<path fill-rule="evenodd" d="M0 10.5A1.5 1.5 0 0 1 1.5 9h1A1.5 1.5 0 0 1 4 10.5v1A1.5 1.5 0 0 1 2.5 13h-1A1.5 1.5 0 0 1 0 11.5v-1zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1zm10.5.5A1.5 1.5 0 0 1 13.5 9h1a1.5 1.5 0 0 1 1.5 1.5v1a1.5 1.5 0 0 1-1.5 1.5h-1a1.5 1.5 0 0 1-1.5-1.5v-1zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1zM6 4.5A1.5 1.5 0 0 1 7.5 3h1A1.5 1.5 0 0 1 10 4.5v1A1.5 1.5 0 0 1 8.5 7h-1A1.5 1.5 0 0 1 6 5.5v-1zM7.5 4a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1z"></path>
<path d="M6 4.5H1.866a1 1 0 1 0 0 1h2.668A6.517 6.517 0 0 0 1.814 9H2.5c.123 0 .244.015.358.043a5.517 5.517 0 0 1 3.185-3.185A1.503 1.503 0 0 1 6 5.5v-1zm3.957 1.358A1.5 1.5 0 0 0 10 5.5v-1h4.134a1 1 0 1 1 0 1h-2.668a6.517 6.517 0 0 1 2.72 3.5H13.5c-.123 0-.243.015-.358.043a5.517 5.517 0 0 0-3.185-3.185z"></path>
</svg></span><span>Brand</span></a><button data-bs-toggle="collapse" class="navbar-toggler" data-bs-target="#navcol-6"><span class="visually-hidden">Toggle navigation</span><span class="navbar-toggler-icon"></span></button>
<div class="collapse navbar-collapse flex-grow-0 order-md-first" id="navcol-6">
<ul class="navbar-nav me-auto">
<li class="nav-item"><a class="nav-link active" href="/signup">Create your page</a></li>
<li class="nav-item"><a class="nav-link" href="/edit">Edit your page</a></li>
</ul>
<div class="d-md-none my-2"><button class="btn btn-light me-2" type="button">Button</button><button class="btn btn-primary" type="button">Button</button></div>
</div>
<div class="d-none d-md-block"><a class="btn btn-primary" role="button" href="#">{{account}}</a></div>
</div>
</nav>
<section style="width: 50%;margin: auto;margin-top: 50px;">
<div class="card bg-dark" style="padding-bottom: 40px;">
<div class="card-body">
<h4 class="card-title" style="color: rgb(255,255,255);">Sign up for your free page</h4>
</div>
<form style="width: 80%;margin: auto;text-align: right;margin-top: 20px;" method="post">
<p class="tld" style="position: relative;display: inline;color: rgb(0,0,0);margin-right: 10px;">.exampledomainnathan1</p><input class="form-control" type="text" id="domain" style="margin: auto;width: 100%;margin-top: -30px;" placeholder="yourname" name="domain" required=""><input class="form-control" type="email" style="margin: auto;width: 100%;margin-top: 10px;" placeholder="email" name="email" required=""><input class="form-control" type="password" style="margin: auto;width: 100%;margin-top: 10px;" name="password" placeholder="Password" required="" minlength="6"><input class="btn btn-primary" type="submit" style="margin-top: 10px;">
</form>
</div>
</section>
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
</body>
</html>