diff --git a/api/.dockerignore b/api/.dockerignore new file mode 100644 index 0000000..f5e82a9 --- /dev/null +++ b/api/.dockerignore @@ -0,0 +1,5 @@ +bin/ +obj/ +design/ +*.user +.vs/ diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..216633d --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,17 @@ +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /src + +COPY api.csproj ./ +RUN dotnet restore api.csproj + +COPY . ./ +RUN dotnet publish api.csproj -c Release -o /app/publish --no-restore + +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime +WORKDIR /app +COPY --from=build /app/publish . + +ENV ASPNETCORE_URLS=http://+:8005 +EXPOSE 8005 + +ENTRYPOINT ["dotnet", "api.dll"] diff --git a/api/appsettings.Development.json b/api/appsettings.Development.json index 707cbc5..cee8d31 100644 --- a/api/appsettings.Development.json +++ b/api/appsettings.Development.json @@ -1,11 +1,11 @@ { "ConnectionStrings": { - "DefaultConnection": "Server=localhost;Port=3306;Database=win_student_goal_tracker;Uid=root;Pwd=change_me;" + "DefaultConnection": "Server=localhost;Port=3306;Database=winstudentgoaltracker;Uid=root;Pwd=change_me;" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } - } -} + } +} diff --git a/api/appsettings.json b/api/appsettings.json index 88d864b..2276e9d 100644 --- a/api/appsettings.json +++ b/api/appsettings.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "DefaultConnection": "Server=localhost;Port=3306;Database=win_student_goal_tracker;Uid=root;Pwd=change_me;" + "DefaultConnection": "Server=localhost;Port=3306;Database=winstudentgoaltracker;Uid=root;Pwd=change_me;" }, "Jwt": { "Key": "super_secret_key_change_me_in_production_123!", diff --git a/db/docker-init/01-schema.sh b/db/docker-init/01-schema.sh new file mode 100755 index 0000000..96c6c54 --- /dev/null +++ b/db/docker-init/01-schema.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# Initializes the database schema from db/Objects SQL files. +# Runs in order: tables (FK checks off) → functions → views → procedures. +set -e + +DB="${MYSQL_DATABASE}" +OBJECTS_DIR="/db-objects" + +mysql_exec() { + mysql -u root -p"${MYSQL_ROOT_PASSWORD}" "$DB" "$@" +} + +echo "=== Initializing database schema ===" + +# ── Tables (all in one session with FK checks disabled) ────────────────────── +if [ -d "$OBJECTS_DIR/tables" ] && ls "$OBJECTS_DIR/tables"/*.sql &>/dev/null; then + echo "Loading tables..." + { + echo "SET FOREIGN_KEY_CHECKS=0;" + for f in "$OBJECTS_DIR/tables"/*.sql; do + [ -f "$f" ] || continue + cat "$f" + echo + done + echo "SET FOREIGN_KEY_CHECKS=1;" + } | mysql_exec + echo " Tables done." +fi + +# ── Functions ───────────────────────────────────────────────────────────────── +if [ -d "$OBJECTS_DIR/functions" ] && ls "$OBJECTS_DIR/functions"/*.sql &>/dev/null; then + echo "Loading functions..." + for f in "$OBJECTS_DIR/functions"/*.sql; do + [ -f "$f" ] || continue + mysql_exec < "$f" + done + echo " Functions done." +fi + +# ── Views ───────────────────────────────────────────────────────────────────── +if [ -d "$OBJECTS_DIR/views" ] && ls "$OBJECTS_DIR/views"/*.sql &>/dev/null; then + echo "Loading views..." + for f in "$OBJECTS_DIR/views"/*.sql; do + [ -f "$f" ] || continue + mysql_exec < "$f" + done + echo " Views done." +fi + +# ── Stored Procedures ───────────────────────────────────────────────────────── +if [ -d "$OBJECTS_DIR/procedures" ] && ls "$OBJECTS_DIR/procedures"/*.sql &>/dev/null; then + echo "Loading procedures..." + for f in "$OBJECTS_DIR/procedures"/*.sql; do + [ -f "$f" ] || continue + mysql_exec < "$f" + done + echo " Procedures done." +fi + +echo "=== Schema initialization complete ===" diff --git a/docker-compose.yml b/docker-compose.yml index ee3d774..215cb2b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,66 @@ services: - "3309:3306" volumes: - win_mysql_data:/var/lib/mysql + - ./db/docker-init:/docker-entrypoint-initdb.d:ro + - ./db/Objects:/db-objects:ro + healthcheck: + # Uses TCP (-h 127.0.0.1) so it only passes after init scripts complete + # and MySQL starts listening on the network. + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"] + interval: 10s + timeout: 5s + retries: 15 + start_period: 30s + networks: + - backend + + api: + build: + context: ./api + dockerfile: Dockerfile + container_name: winstudent-api + restart: unless-stopped + environment: + - ASPNETCORE_URLS=http://+:8005 + - ConnectionStrings__DefaultConnection=Server=mysql;Port=3306;Database=${MYSQL_DATABASE};Uid=${MYSQL_USER};Pwd=${MYSQL_PASSWORD}; + - Jwt__Key=${JWT_KEY} + - Jwt__Issuer=WinStudentGoalTrackerAPI + depends_on: + mysql: + condition: service_healthy + networks: + - backend + - web + labels: + - "traefik.enable=true" + - "traefik.http.routers.winstudent-api.rule=Host(`winapi.opelly.me`)" + - "traefik.http.routers.winstudent-api.entrypoints=websecure" + - "traefik.http.routers.winstudent-api.tls.certresolver=letsencrypt" + - "traefik.http.services.winstudent-api.loadbalancer.server.port=8005" + - "traefik.http.routers.winstudent-api.middlewares=gzip@file,security-headers@file" + + ui: + build: + context: ./ui/winstudentgoaltracker + dockerfile: Dockerfile + container_name: winstudent-ui + restart: unless-stopped + depends_on: + - api + networks: + - web + labels: + - "traefik.enable=true" + - "traefik.http.routers.winstudent-ui.rule=Host(`win.opelly.me`)" + - "traefik.http.routers.winstudent-ui.entrypoints=websecure" + - "traefik.http.routers.winstudent-ui.tls.certresolver=letsencrypt" + - "traefik.http.services.winstudent-ui.loadbalancer.server.port=8006" + - "traefik.http.routers.winstudent-ui.middlewares=gzip@file,security-headers@file" + +networks: + backend: + web: + external: true volumes: win_mysql_data: diff --git a/ui/winstudentgoaltracker/.dockerignore b/ui/winstudentgoaltracker/.dockerignore new file mode 100644 index 0000000..306e8de --- /dev/null +++ b/ui/winstudentgoaltracker/.dockerignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.angular/ diff --git a/ui/winstudentgoaltracker/Dockerfile b/ui/winstudentgoaltracker/Dockerfile new file mode 100644 index 0000000..cbda90e --- /dev/null +++ b/ui/winstudentgoaltracker/Dockerfile @@ -0,0 +1,14 @@ +FROM node:22-alpine AS build +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +COPY . ./ +RUN npm run build + +FROM nginx:alpine AS runtime +COPY --from=build /app/dist/winstudentgoaltracker/browser /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 8006 diff --git a/ui/winstudentgoaltracker/nginx.conf b/ui/winstudentgoaltracker/nginx.conf new file mode 100644 index 0000000..deb5c39 --- /dev/null +++ b/ui/winstudentgoaltracker/nginx.conf @@ -0,0 +1,11 @@ +server { + listen 8006; + + root /usr/share/nginx/html; + index index.html; + + # Angular SPA fallback routing + location / { + try_files $uri $uri/ /index.html; + } +}