Browse Source

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
merge-requests/30/merge
Neil Parikh 1 year ago
parent
commit
82a726ec60
  1. 3
      .gitignore
  2. 15
      README-deploy.md
  3. 45
      backend/main.py
  4. 5
      backend/requirements.txt
  5. 18
      backend/setup_db.py
  6. 48
      backend/templates/template.html
  7. 22
      build.bash
  8. 3
      frontend/components/Editor/Link.tsx
  9. 12
      frontend/components/Editor/index.tsx
  10. 18
      frontend/components/Links/index.tsx
  11. 10
      frontend/components/Login/AuthContext.tsx
  12. 5
      frontend/components/Login/index.tsx
  13. 2
      frontend/package.json
  14. 2
      frontend/pages/editor.tsx
  15. 23
      frontend/pages/index.tsx
  16. 5
      frontend/public/.htaccess
  17. BIN
      frontend/public/favicon.ico
  18. BIN
      frontend/public/fonts/Karla-Bold.ttf
  19. BIN
      frontend/public/fonts/Karla-Regular.ttf
  20. BIN
      frontend/public/fonts/Karla-SemiBold.ttf
  21. BIN
      frontend/public/fonts/Karla-VariableFont_wght.ttf
  22. 0
      frontend/public/images/csc-logo-trans.png
  23. 0
      frontend/public/images/csc-logo.png
  24. 17
      frontend/public/index.in.css
  25. 58
      frontend/public/tailwind.config.js
  26. 4
      frontend/public/vercel.svg
  27. 7
      frontend/styles/globals.css

3
.gitignore vendored

@ -4,3 +4,6 @@ password.txt
/.vs
/.vscode
data.json
.env
build/
index.out.css

15
README-deploy.md

@ -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

45
backend/main.py

@ -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")

5
backend/requirements.txt

@ -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

18
backend/setup_db.py

@ -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

@ -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(&quot;{{ link['name'] }}&quot;, &quot;{{ link['url'] }}&quot;)"
>
{{ link["name"] }}
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
</body>
</html>

22
build.bash

@ -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

3
frontend/components/Editor/Link.tsx

@ -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>

12
frontend/components/Editor/index.tsx

@ -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>

18
frontend/components/Links/index.tsx

@ -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>

10
frontend/components/Login/AuthContext.tsx

@ -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);

5
frontend/components/Login/index.tsx

@ -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"

2
frontend/package.json

@ -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 './**/*'",

2
frontend/pages/editor.tsx

@ -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);
}

23
frontend/pages/index.tsx

@ -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

@ -0,0 +1,5 @@
RewriteEngine On
RewriteCond %{SCRIPT_FILENAME} !-d
RewriteCond %{SCRIPT_FILENAME} !-f
RewriteRule "^api/(.*)$" "http://corn-syrup:5730/$1" [P]

BIN
frontend/public/favicon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

BIN
frontend/public/fonts/Karla-Bold.ttf

Binary file not shown.

BIN
frontend/public/fonts/Karla-Regular.ttf

Binary file not shown.

BIN
frontend/public/fonts/Karla-SemiBold.ttf

Binary file not shown.

BIN
frontend/public/fonts/Karla-VariableFont_wght.ttf

Binary file not shown.

0
frontend/public/images/csc-logo-fb-trans.png → frontend/public/images/csc-logo-trans.png

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

0
frontend/public/csc_logo.png → frontend/public/images/csc-logo.png

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

17
frontend/public/index.in.css

@ -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

@ -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"],
},
},
};

4
frontend/public/vercel.svg

@ -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

7
frontend/styles/globals.css

@ -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…
Cancel
Save