Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions client/.env.production
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VITE_GRAPHQL_URL=http://it2810-01.idi.ntnu.no:3001/
35 changes: 35 additions & 0 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"graphql": "^16.14.2",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-router": "^8.4.0",
"rxjs": "^7.8.2"
},
"devDependencies": {
Expand Down
17 changes: 17 additions & 0 deletions client/src/apollo/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';

/**
* Apollo Client handles communication with our GraphQL backend and is also
* our state management: InMemoryCache stores query results, so the same
* query isn't sent to the server again (fewer requests, more sustainable).
*
* The backend URL is read from the environment variable VITE_GRAPHQL_URL,
* so the same code works locally and on the VM. Without it, we use the
* local backend on port 3001.
*/
export const client = new ApolloClient({
link: new HttpLink({
uri: import.meta.env.VITE_GRAPHQL_URL ?? 'http://localhost:3001/',
}),
cache: new InMemoryCache(),
});
29 changes: 8 additions & 21 deletions client/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,17 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
import { BrowserRouter } from 'react-router';
import { ApolloProvider } from '@apollo/client/react';
import { client } from './apollo/client';
import App from './App';
import './index.css';
import App from './App.tsx';

/**
* Apollo Client handles communication with our GraphQL backend and is also
* our state management: InMemoryCache stores query results, so the same
* query isn't sent to the server again (fewer requests, more sustainable).
*
* The backend URL is read from the environment variable VITE_GRAPHQL_URL,
* so the same code works locally and on the VM. Without it, we use the
* local backend on port 3001.
*/
const client = new ApolloClient({
link: new HttpLink({
uri: import.meta.env.VITE_GRAPHQL_URL ?? 'http://localhost:3001/',
}),
cache: new InMemoryCache(),
});

createRoot(document.getElementById('root')!).render(
<StrictMode>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
<BrowserRouter basename="/project2">
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</BrowserRouter>
</StrictMode>,
);
2 changes: 2 additions & 0 deletions client/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ import { defineConfig } from 'vite'

// https://vite.dev/config/
export default defineConfig({
// The app is served from http://it2810-xx.idi.ntnu.no/project2/
base: '/project2/',
plugins: [react()],
})
20 changes: 20 additions & 0 deletions deploy/apache-project2.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Apache config for the Wine not? client, served at /project2.
# Installed to /etc/apache2/conf-available/project2.conf on the VM by deploy/deploy.sh on every deploy.

<Directory /var/www/html/project2>
# Unknown paths (e.g. /project2/wine/42) get index.html,
# so React Router can handle them and a page refresh doesn't give 404.
FallbackResource /project2/index.html

# index.html and version.txt are always re-checked,
# so a new deploy shows up immediately.
<FilesMatch "^(index\.html|version\.txt)$">
Header set Cache-Control "no-cache"
</FilesMatch>
</Directory>

# Vite gives built files hashed names, so browsers can cache them for a year.
# Returning visitors don't download them again (less data transferred).
<Directory /var/www/html/project2/assets>
Header set Cache-Control "public, max-age=31536000, immutable"
</Directory>
9 changes: 9 additions & 0 deletions deploy/backend.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Template for /etc/wine-not/backend.env on the VM. The real file, with the
# password, only exists on the VM and can be read with sudo.
# Same variable names as backend/.env on a laptop.
PORT=3001
DB_HOST=localhost
DB_PORT=5432
DB_NAME=winenot
DB_USER=winenot_app
DB_PASSWORD=<only in the real file on the VM>
72 changes: 72 additions & 0 deletions deploy/deploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env bash
# Builds the backend and the client on the VM and puts them live. Runs as root.
#
# Normally started by deploy/push-to-vm.sh on a laptop, which sends a new
# copy of main:
# bash deploy.sh --install <unpacked-folder> <commit>
#
# To rebuild the code that's already on the VM (e.g. after a migration),
# on the VM:
# sudo bash /opt/wine-not/deploy/deploy.sh
#
# Both parts are built before anything goes live. If a build fails, the
# script stops and the version that's already live keeps running.
set -euo pipefail

main() {
local app_dir=/opt/wine-not
local web_dir=/var/www/html/project2

# Only one deploy at a time; a second one waits for the first to finish.
exec 9>/run/wine-not-deploy.lock
flock 9

if [ "${1:-}" = "--install" ]; then
local src="$2" commit="$3"
echo "==> Copying commit $commit to $app_dir"
mkdir -p "$app_dir"
chmod 755 "$src" # the backend runs as user winenot and must read the code
# Replace the code, but keep installed packages, builds and the CSV data.
rsync -a --delete \
--exclude node_modules --exclude dist --exclude /backend/data \
"$src/" "$app_dir/"
echo "$commit" > "$app_dir/COMMIT"
fi

echo "==> Backend: install and build"
cd "$app_dir/backend"
npm ci
npm run build

echo "==> Client: install and build"
cd "$app_dir/client"
npm ci
npm run build

echo "==> Server setup: backend service and Apache config"
install -m 644 "$app_dir/deploy/wine-not-api.service" /etc/systemd/system/wine-not-api.service
systemctl daemon-reload
systemctl enable --quiet wine-not-api
install -m 644 "$app_dir/deploy/apache-project2.conf" /etc/apache2/conf-available/project2.conf
a2enmod --quiet headers
a2enconf --quiet project2
apache2ctl configtest

echo "==> Going live"
systemctl restart wine-not-api
mkdir -p "$web_dir"
rsync -a --delete "$app_dir/client/dist/" "$web_dir/"
systemctl restart apache2

# Shows which commit is live at /project2/version.txt
local commit_live
commit_live="$(cat "$app_dir/COMMIT" 2>/dev/null || echo unknown)"
echo "$commit_live deployed $(date '+%Y-%m-%d %H:%M')" > "$web_dir/version.txt"

sleep 2
echo "==> Done. Commit $commit_live. Backend status: $(systemctl is-active wine-not-api)"
}

# Everything is inside a function so bash reads the whole script before
# running it, even if the file is replaced while it runs.
main "$@"
43 changes: 43 additions & 0 deletions deploy/push-to-vm.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Deploys the newest version of main to the VM.
#
# Run it on your LAPTOP, from the repo, on the NTNU network or VPN:
# bash deploy/push-to-vm.sh <ntnu-username>
#
# The VM has no access to the Git server, so your laptop packs up what's on
# main, copies it to the VM and runs deploy/deploy.sh there.
# You may be asked for your NTNU password up to three times:
# once to copy, once to log in and once for sudo.
set -euo pipefail

VM_HOST="it2810-01.idi.ntnu.no"

if [ "$#" -ne 1 ]; then
echo "Usage: bash deploy/push-to-vm.sh <ntnu-username>" >&2
exit 1
fi
vm_user="$1"

cd "$(git rev-parse --show-toplevel)"

echo "==> Getting the newest main"
git fetch --quiet origin main
commit="$(git rev-parse --short origin/main)"
echo " Commit $commit: $(git log -1 --format=%s origin/main)"

# Only committed files on main are packed: no node_modules, .env or CSV data.
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
git archive --format=tar.gz -o "$tmp_dir/wine-not.tar.gz" origin/main

echo "==> Copying to $VM_HOST"
remote_archive="/tmp/wine-not-$commit.tar.gz"
scp -q "$tmp_dir/wine-not.tar.gz" "$vm_user@$VM_HOST:$remote_archive"

echo "==> Building and starting on the VM"
# Unpacks into a temporary folder and runs the deploy.sh from this commit,
# which copies the code to /opt/wine-not, builds it and puts it live.
ssh -t "$vm_user@$VM_HOST" \
"sudo bash -c 'set -e; d=\$(mktemp -d); tar -xzf $remote_archive -C \"\$d\"; rm -f $remote_archive; bash \"\$d/deploy/deploy.sh\" --install \"\$d\" $commit; rm -rf \"\$d\"'"

echo "==> Live at http://$VM_HOST/project2/"
19 changes: 19 additions & 0 deletions deploy/wine-not-api.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# systemd service that runs the Wine not? GraphQL backend on port 3001.
# Installed to /etc/systemd/system/ on the VM by deploy/deploy.sh on every deploy.
[Unit]
Description=Wine not? GraphQL backend
After=network.target postgresql.service

[Service]
User=winenot
Group=winenot
WorkingDirectory=/opt/wine-not/backend
# Database settings and port. This file exists only on the VM, never in Git.
EnvironmentFile=/etc/wine-not/backend.env
Environment=NODE_ENV=production
ExecStart=/usr/bin/node dist/index.js
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
28 changes: 28 additions & 0 deletions deploy/with-env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Runs one command with the backend's database settings loaded from
# /etc/wine-not/backend.env (the secret file that only exists on the VM).
# The DB_ settings are also given to psql (as PGHOST, PGUSER, ...),
# so psql connects to the same database as the backend.
#
# Usage on the VM, from /opt/wine-not/backend:
# sudo bash ../deploy/with-env.sh npm run import
# sudo bash ../deploy/with-env.sh psql -f db/migrations/<file>.sql
set -euo pipefail

if [ "$#" -eq 0 ]; then
echo "Usage: sudo bash ../deploy/with-env.sh <command> [args...]" >&2
exit 1
fi

set -a # export every variable defined in the file
# shellcheck source=/dev/null
. /etc/wine-not/backend.env
set +a

export PGHOST="${DB_HOST:-localhost}"
export PGPORT="${DB_PORT:-5432}"
export PGDATABASE="${DB_NAME:-}"
export PGUSER="${DB_USER:-}"
export PGPASSWORD="${DB_PASSWORD:-}"

exec "$@"