Final changes required for deployment
- Use a plain HTML flask template for the main links page - Move password and port to environment variables - Relative paths for AJAX requests - Add a simple build script
This commit is contained in:
parent
2fabf06e94
commit
82a726ec60
3
.gitignore
vendored
3
.gitignore
vendored
@ -4,3 +4,6 @@ password.txt
|
||||
/.vs
|
||||
/.vscode
|
||||
data.json
|
||||
.env
|
||||
build/
|
||||
index.out.css
|
||||
|
15
README-deploy.md
Normal file
15
README-deploy.md
Normal file
@ -0,0 +1,15 @@
|
||||
### Steps to Deploy
|
||||
- move contents of frontend/ to /srv/www-csc-links/
|
||||
- create a `.env` file in backend/ with following contents:
|
||||
```
|
||||
PASSWORD=...
|
||||
PORT=...
|
||||
```
|
||||
- run the following in backend/:
|
||||
```
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
- run python app in backend/ (with the virtual env activated)
|
||||
- edit the `.htaccess` file in /srv/www-csc-links/ to point to the running python application
|
@ -1,16 +1,28 @@
|
||||
from flask import Flask, request, jsonify
|
||||
from flask import Flask, request, jsonify, render_template
|
||||
from flask_httpauth import HTTPBasicAuth
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
import json
|
||||
import sqlite3
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
DB_PATH = os.path.join(os.path.dirname(__file__), 'links.db')
|
||||
|
||||
load_dotenv()
|
||||
|
||||
app = Flask(__name__)
|
||||
auth = HTTPBasicAuth('CustomBasic')
|
||||
|
||||
password = os.environ.get("PASSWORD")
|
||||
if not password:
|
||||
raise Exception("PASSWORD must be set")
|
||||
|
||||
port = int(os.environ.get("PORT") or 3000)
|
||||
|
||||
out_path = os.environ.get("OUT_PATH") or "/srv/www-csc-links/index.html"
|
||||
|
||||
users = {
|
||||
"admin": generate_password_hash("test"),
|
||||
"admin": generate_password_hash(password),
|
||||
}
|
||||
|
||||
def get_data_from_query(query):
|
||||
@ -27,11 +39,13 @@ def get_data_from_query(query):
|
||||
con.close()
|
||||
return links_list
|
||||
|
||||
def regen_JSON():
|
||||
"""Gets links from DB and outputs them in JSON"""
|
||||
def regen_html(path):
|
||||
"""Gets links from DB and outputs them in HTML"""
|
||||
outfile = open(path, 'w')
|
||||
links_list = get_data_from_query('SELECT url, name FROM links WHERE active=1 ORDER BY position')
|
||||
links_json = json.dumps(links_list, indent=4)
|
||||
return links_json
|
||||
html = render_template('template.html', links_list=links_list)
|
||||
print(html, file=outfile)
|
||||
outfile.close()
|
||||
|
||||
@auth.verify_password
|
||||
def verify_password(username, password):
|
||||
@ -47,19 +61,21 @@ def get_links():
|
||||
@app.route('/editor/links', methods = ['POST'])
|
||||
@auth.login_required
|
||||
def update_links():
|
||||
|
||||
con = sqlite3.connect(DB_PATH)
|
||||
cur = con.cursor()
|
||||
try:
|
||||
cur.execute("begin")
|
||||
cur.execute('DELETE FROM links')
|
||||
|
||||
links = []
|
||||
data = request.json['links']
|
||||
|
||||
items = 'url', 'name', 'clicks', 'active'
|
||||
for i in range(len(data)):
|
||||
if not(all(e in data[i] for e in items)):
|
||||
return "Bad request, some items missing from link object", 400
|
||||
|
||||
links = []
|
||||
cur.execute("begin")
|
||||
cur.execute('DELETE FROM links')
|
||||
for i in range(len(data)):
|
||||
url = data[i]['url']
|
||||
name = data[i]['name']
|
||||
clicks = data[i]['clicks']
|
||||
@ -72,12 +88,7 @@ def update_links():
|
||||
cur.executemany('INSERT INTO links VALUES (?,?,?,?,?)', links)
|
||||
con.commit()
|
||||
con.close()
|
||||
data = regen_JSON()
|
||||
# TODO: Trigger a rebuild of the frontend
|
||||
outfile = open('data.json', 'w')
|
||||
print(data, file=outfile)
|
||||
outfile.close()
|
||||
|
||||
regen_html(out_path)
|
||||
except Exception as e:
|
||||
cur.execute("rollback")
|
||||
con.close()
|
||||
@ -108,4 +119,4 @@ def update_clicks():
|
||||
return 'ok'
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True)
|
||||
app.run(port=port, host="0.0.0.0")
|
||||
|
@ -1,14 +1,18 @@
|
||||
astroid==2.5.1
|
||||
click==7.1.2
|
||||
Flask==1.1.2
|
||||
Flask-HTTPAuth==4.2.0
|
||||
isort==5.7.0
|
||||
itsdangerous==1.1.0
|
||||
Jinja2==2.11.3
|
||||
lazy-object-proxy==1.5.2
|
||||
MarkupSafe==1.1.1
|
||||
mccabe==0.6.1
|
||||
pkg-resources==0.0.0
|
||||
pylint==2.7.2
|
||||
python-dotenv==0.17.1
|
||||
toml==0.10.2
|
||||
typed-ast==1.4.3
|
||||
types-click==0.1.4
|
||||
types-Flask==0.1.1
|
||||
types-Jinja2==0.1.0
|
||||
@ -17,4 +21,3 @@ types-typing-extensions==3.7.2
|
||||
types-Werkzeug==0.1.1
|
||||
Werkzeug==1.0.1
|
||||
wrapt==1.12.1
|
||||
Flask-HTTPAuth==4.2.0
|
||||
|
@ -7,11 +7,11 @@ con = sqlite3.connect(DB_PATH)
|
||||
|
||||
# array of links to store
|
||||
links = [
|
||||
('http://csclub.uwaterloo.ca/','CS Club Website',3,0,1),
|
||||
('https://www.instagram.com/uwcsclub/','Instagram',4,1,1),
|
||||
('https://www.facebook.com/uw.computerscienceclub','Facebook',5,2,1),
|
||||
('http://twitch.tv/uwcsclub','Twitch',6,3,1),
|
||||
('http://bit.ly/uwcsclub-yt','YouTube',7,4,1),
|
||||
('http://csclub.uwaterloo.ca/','CS Club Website',0,0,1),
|
||||
('https://www.instagram.com/uwcsclub/','Instagram',0,1,1),
|
||||
('https://www.facebook.com/uw.computerscienceclub','Facebook',0,2,1),
|
||||
('http://twitch.tv/uwcsclub','Twitch',0,3,1),
|
||||
('http://bit.ly/uwcsclub-yt','YouTube',0,4,1),
|
||||
]
|
||||
|
||||
# SQLite setup
|
||||
@ -23,9 +23,9 @@ if cur.fetchone():
|
||||
raise Exception('Links table already exists.')
|
||||
else:
|
||||
cur.execute('''CREATE TABLE links (
|
||||
url text NOT NULL,
|
||||
name text NOT NULL,
|
||||
clicks int NOT NULL,
|
||||
url text NOT NULL,
|
||||
name text NOT NULL,
|
||||
clicks int NOT NULL,
|
||||
position int NOT NULL UNIQUE,
|
||||
active int NOT NULL,
|
||||
UNIQUE(url, name)
|
||||
@ -33,4 +33,4 @@ else:
|
||||
cur.executemany('INSERT INTO links VALUES (?,?,?,?,?)', links)
|
||||
con.commit()
|
||||
|
||||
con.close()
|
||||
con.close()
|
||||
|
48
backend/templates/template.html
Normal file
48
backend/templates/template.html
Normal file
@ -0,0 +1,48 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width"/>
|
||||
<meta charSet="utf-8"/>
|
||||
<title>@uwcsclub | LinkList</title>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Karla:wght@700&display=swap" rel="stylesheet">
|
||||
<link href="index.out.css" rel="stylesheet">
|
||||
<script>
|
||||
const handleClick = function(name, url) {
|
||||
fetch("api/clicks", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ name, url }),
|
||||
})
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="__next">
|
||||
<div class="text-s flex flex-col items-center w-full top-6 font-karla">
|
||||
<img class="mb-3" src="images/csc-logo.png" alt="CSC Logo" width="100px" />
|
||||
<h1 class="font-bold">@uwcsclub</h1>
|
||||
<ul class="flex flex-col my-6 w-full">
|
||||
{% for link in links_list %}
|
||||
<li class="w-full flex justify-center">
|
||||
<a
|
||||
class="btn bg-gray-450 p-3 text-white font-karla font-bold text-center self-center my-1.5
|
||||
hover:bg-white hover:text-black border-2 border-gray-800 transition duration-300 ease-in-out
|
||||
w-11/12 sm:w-4/12 min-h-link"
|
||||
href="{{ link['url'] }}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onclick="return handleClick("{{ link['name'] }}", "{{ link['url'] }}")"
|
||||
>
|
||||
{{ link["name"] }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
22
build.bash
Executable file
22
build.bash
Executable file
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
rm -rf build
|
||||
mkdir build
|
||||
|
||||
# Build Frontend
|
||||
cd frontend/public
|
||||
NODE_ENV=production npx tailwindcss-cli@latest build index.in.css -o index.out.css
|
||||
cd ..
|
||||
npm run build && npm run export
|
||||
cd ..
|
||||
mv frontend/out build/frontend
|
||||
|
||||
# Backend
|
||||
cd backend
|
||||
source venv/bin/activate
|
||||
if [ ! -f links.db ]; then
|
||||
python3 setup_db.py
|
||||
fi
|
||||
cd ..
|
||||
rsync -ax --exclude venv backend/ build/backend
|
||||
|
||||
cp README-deploy.md build
|
@ -84,6 +84,9 @@ const Link: React.FC<LinkProps> = ({ index, link, onChange, onDelete }) => {
|
||||
<DeleteIcon />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex justify-between items-center px-2 pb-2">
|
||||
{`Clicks: ${link.clicks}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -16,6 +16,7 @@ const Editor: React.FC<EditorProps> = ({ links, setLinks }) => {
|
||||
const { displayDragDrop } = useDragDrop();
|
||||
const { headers } = useAuth();
|
||||
const [editableLinks, setEditableLinks] = useState<EditableLink[]>(links);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setEditableLinks(links);
|
||||
@ -44,7 +45,9 @@ const Editor: React.FC<EditorProps> = ({ links, setLinks }) => {
|
||||
]);
|
||||
|
||||
const onSubmit = async () => {
|
||||
const res = await fetch("/api/editor/links", {
|
||||
setIsSaving(true);
|
||||
|
||||
const res = await fetch("api/editor/links", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@ -52,8 +55,11 @@ const Editor: React.FC<EditorProps> = ({ links, setLinks }) => {
|
||||
},
|
||||
body: JSON.stringify({ links: editableLinks }),
|
||||
});
|
||||
|
||||
const updatedLinks = await res.json();
|
||||
setLinks(updatedLinks);
|
||||
|
||||
setIsSaving(false);
|
||||
};
|
||||
|
||||
const onCancel = () => {
|
||||
@ -111,7 +117,7 @@ const Editor: React.FC<EditorProps> = ({ links, setLinks }) => {
|
||||
<button
|
||||
className="block flex py-2 items-center justify-center rounded-lg bg-purple-600 hover:bg-purple-500 cursor-pointer text-white w-full disabled:opacity-50"
|
||||
onClick={onSubmit}
|
||||
disabled={equal(editableLinks, links)}
|
||||
disabled={isSaving || equal(editableLinks, links)}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
@ -119,7 +125,7 @@ const Editor: React.FC<EditorProps> = ({ links, setLinks }) => {
|
||||
<button
|
||||
className="block flex py-2 items-center justify-center rounded-lg bg-purple-600 hover:bg-purple-500 cursor-pointer text-white w-full disabled:opacity-50"
|
||||
onClick={onCancel}
|
||||
disabled={equal(editableLinks, links)}
|
||||
disabled={isSaving || equal(editableLinks, links)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
@ -6,25 +6,12 @@ export interface Link {
|
||||
}
|
||||
interface Props {
|
||||
links: Link[];
|
||||
logClicks?: boolean;
|
||||
}
|
||||
|
||||
export const Links: React.FC<Props> = ({ links, logClicks = false }) => {
|
||||
const handleClick = (name: string, url: string) => {
|
||||
if (logClicks) {
|
||||
fetch("/api/clicks", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ name, url }),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const Links: React.FC<Props> = ({ links }) => {
|
||||
return (
|
||||
<div className="text-s flex flex-col items-center w-full top-6 font-karla">
|
||||
<img className="mb-3" src="csc_logo.png" alt="CSC Logo" width="100px" />
|
||||
<img className="mb-3" src="images/csc-logo.png" alt="CSC Logo" width="100px" />
|
||||
<h1 className="font-bold">@uwcsclub</h1>
|
||||
<ul className="flex flex-col my-6 w-full">
|
||||
{links.map(({ name, url }, i) => (
|
||||
@ -36,7 +23,6 @@ export const Links: React.FC<Props> = ({ links, logClicks = false }) => {
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={() => handleClick(name, url)}
|
||||
>
|
||||
{name}
|
||||
</a>
|
||||
|
@ -19,19 +19,13 @@ export const AuthProvider: React.FC = ({ children }) => {
|
||||
const logout = () => setHeaders(undefined);
|
||||
|
||||
const login = async (password: string): Promise<boolean> => {
|
||||
const username = process.env.NEXT_PUBLIC_EDITOR_USERNAME;
|
||||
|
||||
if (!username) {
|
||||
throw new Error(
|
||||
"Missing NEXT_PUBLIC_EDITOR_USERNAME environment variable"
|
||||
);
|
||||
}
|
||||
const username = "admin";
|
||||
|
||||
const newHeaders = {
|
||||
Authorization: `CustomBasic ${btoa(`${username}:${password}`)}`,
|
||||
};
|
||||
|
||||
const res = await fetch("/api/editor/links", { headers: newHeaders });
|
||||
const res = await fetch("api/editor/links", { headers: newHeaders });
|
||||
|
||||
if (res.status === 200) {
|
||||
setHeaders(newHeaders);
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { useAuth } from "components/Login/AuthContext";
|
||||
import Image from "next/image";
|
||||
import Head from "next/head";
|
||||
|
||||
const LoginBox: React.FC = () => {
|
||||
@ -37,8 +36,8 @@ const LoginBox: React.FC = () => {
|
||||
</Head>
|
||||
<div className="flex flex-col justify-center items-center space-y-10">
|
||||
<div className="flex flex-row justify-center items-center space-x-5">
|
||||
<Image
|
||||
src="/images/csc-logo-fb-trans.png"
|
||||
<img
|
||||
src="images/csc-logo-trans.png"
|
||||
height={80}
|
||||
width={80}
|
||||
alt="CSC Logo"
|
||||
|
@ -5,7 +5,7 @@
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"export": "next export",
|
||||
"type-check": "tsc",
|
||||
"format": "prettier --write './**/*'",
|
||||
"format:check": "prettier --check './**/*'",
|
||||
|
@ -13,7 +13,7 @@ const EditorPage: React.FC = () => {
|
||||
async function fetchLinks() {
|
||||
if (!loggedIn) return;
|
||||
|
||||
const res = await fetch("/api/editor/links", { headers });
|
||||
const res = await fetch("api/editor/links", { headers });
|
||||
const links = await res.json();
|
||||
setLinks(links);
|
||||
}
|
||||
|
@ -1,23 +0,0 @@
|
||||
import React from "react";
|
||||
import { GetStaticProps } from "next";
|
||||
import { Link, Links } from "components/Links";
|
||||
|
||||
export const getStaticProps: GetStaticProps<Props> = async () => {
|
||||
const res = await fetch(`${process.env.DEV_URL}/links`);
|
||||
const links = await res.json();
|
||||
|
||||
return {
|
||||
props: { links },
|
||||
revalidate: 60,
|
||||
};
|
||||
};
|
||||
|
||||
interface Props {
|
||||
links: Link[];
|
||||
}
|
||||
|
||||
const Home: React.FC<Props> = ({ links }) => {
|
||||
return <Links links={links} logClicks={true} />;
|
||||
};
|
||||
|
||||
export default Home;
|
5
frontend/public/.htaccess
Normal file
5
frontend/public/.htaccess
Normal file
@ -0,0 +1,5 @@
|
||||
RewriteEngine On
|
||||
|
||||
RewriteCond %{SCRIPT_FILENAME} !-d
|
||||
RewriteCond %{SCRIPT_FILENAME} !-f
|
||||
RewriteRule "^api/(.*)$" "http://corn-syrup:5730/$1" [P]
|
Binary file not shown.
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
17
frontend/public/index.in.css
Normal file
17
frontend/public/index.in.css
Normal file
@ -0,0 +1,17 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
input,
|
||||
button {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* NextJS reset styles https://gist.github.com/dmurawsky/d45f068097d181c733a53687edce1919 */
|
||||
html,
|
||||
body,
|
||||
body > div:first-child,
|
||||
div#__next,
|
||||
div#__next > div {
|
||||
height: 100%;
|
||||
}
|
58
frontend/public/tailwind.config.js
Normal file
58
frontend/public/tailwind.config.js
Normal file
@ -0,0 +1,58 @@
|
||||
// eslint-disable-next-line no-undef
|
||||
module.exports = {
|
||||
purge: {
|
||||
content: [
|
||||
"../../backend/templates/template.html",
|
||||
],
|
||||
// These options are passed through directly to PurgeCSS
|
||||
options: {
|
||||
keyframes: true,
|
||||
fontFace: true,
|
||||
},
|
||||
},
|
||||
darkMode: false, // or 'media' or 'class'
|
||||
theme: {
|
||||
fontFamily: {
|
||||
body: ["Karla", "sans-serif"],
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
analytics: {
|
||||
"view-icon": "#39e09b",
|
||||
"click-icon": "#8a86e5",
|
||||
},
|
||||
gray: {
|
||||
450: "#3d3b3c",
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["Karla", "Helvetica", "Ubuntu", "sans-serif"],
|
||||
karla: ["Karla", "Verdana", "sans-serif"],
|
||||
},
|
||||
fontSize: {
|
||||
s: ".82rem",
|
||||
},
|
||||
screens: {
|
||||
"lt-lg": "992px",
|
||||
},
|
||||
flex: {
|
||||
analytics: "1 0 0",
|
||||
chevron: "0 0 16px",
|
||||
},
|
||||
borderRadius: {
|
||||
preview: "3.2rem",
|
||||
},
|
||||
borderWidth: {
|
||||
preview: "0.75rem",
|
||||
},
|
||||
minHeight: {
|
||||
link: "48px",
|
||||
},
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
extend: {
|
||||
opacity: ["disabled"],
|
||||
},
|
||||
},
|
||||
};
|
@ -1,4 +0,0 @@
|
||||
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.1 KiB |
@ -4,13 +4,6 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@font-face {
|
||||
font-family: "Karla";
|
||||
src: local("Karla"),
|
||||
url("/fonts/Karla-VariableFont_wght.ttf") format("truetype");
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
outline: none;
|
||||
|
Loading…
x
Reference in New Issue
Block a user