diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..0b2d814 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,26 @@ +name: Build + +on: + push: + branches: [master] + pull_request: + +jobs: + build: + name: Build + runs-on: build + steps: + - uses: actions/checkout@v4 + with: + path: decePubClient + + - name: Fetch PrivaPub's client models beside it + env: + READ_TOKEN: ${{ secrets.THEPRA_READ_TOKEN }} + run: | + rm -rf SocialPub + git -c http.extraHeader="Authorization: token $READ_TOKEN" clone -q --depth 1 \ + https://git.thepra.dev/thepra/SocialPub.git SocialPub + + - name: Build + run: dotnet build decePubClient/decePubClient.csproj -c Release diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..eb7f667 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,72 @@ +name: Deploy + +on: + workflow_dispatch: + push: + tags: + - 'v*' + +env: + WEB_ROOT: /var/www/decepub.thepra.dev + BACKUPS: /var/backups/decepub.thepra.dev + PUBLIC_URL: https://decepub.thepra.dev + +jobs: + site: + name: decepub.thepra.dev + runs-on: build + steps: + - uses: actions/checkout@v4 + with: + path: decePubClient + + - name: Fetch PrivaPub's client models beside it + env: + READ_TOKEN: ${{ secrets.THEPRA_READ_TOKEN }} + run: | + rm -rf SocialPub + git -c http.extraHeader="Authorization: token $READ_TOKEN" clone -q --depth 1 \ + https://git.thepra.dev/thepra/SocialPub.git SocialPub + + - name: Resolve the build identity + run: | + echo "BUILD_COMMIT=$(echo "$GITHUB_SHA" | cut -c1-8)" >> "$GITHUB_ENV" + echo "BUILD_REF=${GITHUB_REF_NAME:-master}" >> "$GITHUB_ENV" + echo "BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_ENV" + + - name: Publish + run: | + rm -rf "$GITHUB_WORKSPACE/publish" + dotnet publish decePubClient/decePubClient.csproj -c Release -o "$GITHUB_WORKSPACE/publish" + printf '{"commit":"%s","buildRef":"%s","builtAt":"%s"}\n' "$BUILD_COMMIT" "$BUILD_REF" "$BUILD_TIME" \ + > "$GITHUB_WORKSPACE/publish/wwwroot/build.json" + + - name: Assert the publish actually produced a site + run: | + W="$GITHUB_WORKSPACE/publish/wwwroot" + for f in index.html appsettings.json build.json _framework/blazor.webassembly.js css/style.min.css; do + [ -e "$W/$f" ] || { echo "::error::publish output is missing $f"; exit 1; } + done + ls "$W/_framework" | grep -q '\.wasm$' || { echo "::error::no .wasm in _framework"; exit 1; } + echo "publish OK, $(du -sh "$W" | cut -f1)" + + - name: Snapshot the live directory + run: | + STAMP=$(date +%Y%m%d-%H%M%S) + rsync -a "$WEB_ROOT/" "$BACKUPS/site-$STAMP/" + echo "SNAPSHOT=$BACKUPS/site-$STAMP" >> "$GITHUB_ENV" + ls -1dt "$BACKUPS"/site-* 2>/dev/null | tail -n +4 | xargs -r rm -rf || true + + - name: Sync + run: rsync -a --delete "$GITHUB_WORKSPACE/publish/wwwroot/" "$WEB_ROOT/" + + - name: Verify what is being served, roll back on failure + run: | + served=$(curl -fsS "$PUBLIC_URL/build.json" | python3 -c "import json,sys; print(json.load(sys.stdin).get('commit',''))" || true) + code=$(curl -s -o /dev/null -w '%{http_code}' "$PUBLIC_URL/some/client/route") + if [ "$served" != "$BUILD_COMMIT" ] || [ "$code" != "200" ]; then + echo "::error::served build '$served' (expected '$BUILD_COMMIT'), fallback route answered $code - rolling back" + rsync -a --delete "$SNAPSHOT/" "$WEB_ROOT/" + exit 1 + fi + echo "::notice::serving $served" diff --git a/Extensions/ExtensionMethods.cs b/Extensions/ExtensionMethods.cs index da33399..07d1a21 100644 --- a/Extensions/ExtensionMethods.cs +++ b/Extensions/ExtensionMethods.cs @@ -1,4 +1,4 @@ -using Blazored.LocalStorage; +using Blazored.LocalStorage; using collAnon.Client.Services; @@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; -using SocialPub.ClientModels; +using PrivaPub.ClientModels; using System.Collections.Specialized; using System.ComponentModel; diff --git a/Models/AppConfiguration.cs b/Models/AppConfiguration.cs index db7e396..727aac5 100644 --- a/Models/AppConfiguration.cs +++ b/Models/AppConfiguration.cs @@ -1,6 +1,7 @@ -namespace decePubClient.Models; +namespace decePubClient.Models; public class AppConfiguration { public string Version { get; set; } + public string ApiBaseAddress { get; set; } } \ No newline at end of file diff --git a/Models/PageSettings.cs b/Models/PageSettings.cs index 12c540b..f318d7c 100644 --- a/Models/PageSettings.cs +++ b/Models/PageSettings.cs @@ -1,5 +1,5 @@ - -using SocialPub.ClientModels.Resources; + +using PrivaPub.ClientModels.Resources; using System.ComponentModel.DataAnnotations; diff --git a/Program.cs b/Program.cs index 2cce5c6..348c28e 100644 --- a/Program.cs +++ b/Program.cs @@ -12,7 +12,7 @@ using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Authentication; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -using SocialPub.ClientModels; +using PrivaPub.ClientModels; using collAnon.Client.Services; var builder = WebAssemblyHostBuilder.CreateDefault(args); @@ -51,9 +51,10 @@ builder.Services.AddOptions() .AddLogging(lb => lb.SetMinimumLevel(LogLevel.Debug)) .AddIndexedDb(); +var apiBaseAddress = builder.Configuration["AppConfiguration:ApiBaseAddress"]; builder.Services.AddHttpClient("default", client => { - client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress); + client.BaseAddress = new Uri(string.IsNullOrEmpty(apiBaseAddress) ? builder.HostEnvironment.BaseAddress : apiBaseAddress.TrimEnd('/') + "/"); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); }); diff --git a/Services/CoalescingStringLocalizer.cs b/Services/CoalescingStringLocalizer.cs index bbde6d1..1370585 100644 --- a/Services/CoalescingStringLocalizer.cs +++ b/Services/CoalescingStringLocalizer.cs @@ -1,8 +1,8 @@ -using decePubClient.Resources; +using decePubClient.Resources; using Microsoft.Extensions.Localization; -using SocialPub.ClientModels.Resources; +using PrivaPub.ClientModels.Resources; namespace collAnon.Client.Services { diff --git a/decePubClient.csproj b/decePubClient.csproj index 404da06..f5151fa 100644 --- a/decePubClient.csproj +++ b/decePubClient.csproj @@ -1,7 +1,7 @@ - + - net7.0 + net10.0 disable enable service-worker-assets.js @@ -10,18 +10,15 @@ - - - - - - - - - + + + + + + - + @@ -77,7 +74,7 @@ - + diff --git a/deploy/max/setup.sh b/deploy/max/setup.sh new file mode 100755 index 0000000..71a832c --- /dev/null +++ b/deploy/max/setup.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# One-time root setup on Max for decePubClient at decepub.thepra.dev (static files, no unit). Idempotent. +# rsync -a -e $MAX/ssh.sh deploy/ root@nuvola.xyz:/root/decepub-deploy/ +# $MAX/run.sh bash /root/decepub-deploy/max/setup.sh +set -euo pipefail +SRC="${1:-/root/decepub-deploy}" +HOST=decepub.thepra.dev +RUNNER=build-runner +ACME=/root/.acme.sh/acme.sh + +echo "== directories" +install -d -o "$RUNNER" -g www-data -m 755 /var/www/$HOST +install -d -o "$RUNNER" -g "$RUNNER" -m 750 /var/backups/$HOST + +echo "== nginx snippet and bootstrap vhost" +install -m 644 "$SRC/nginx/decepub-headers.conf" /etc/nginx/snippets/decepub-headers.conf +if [ -f /root/.acme.sh/${HOST}_ecc/fullchain.cer ]; then + install -m 644 "$SRC/nginx/$HOST.conf" /etc/nginx/sites-available/$HOST.conf +else + awk '/^server \{/{n++} n==1' "$SRC/nginx/$HOST.conf" > /etc/nginx/sites-available/$HOST.conf +fi +ln -sf /etc/nginx/sites-available/$HOST.conf /etc/nginx/sites-enabled/$HOST.conf +nginx -t +systemctl reload nginx + +echo "== certificate" +if [ -f /root/.acme.sh/${HOST}_ecc/fullchain.cer ]; then + echo "$HOST: certificate present" +else + $ACME --issue --server letsencrypt -d $HOST -w /var/www/acme --renew-hook "systemctl reload nginx" +fi + +echo "== full vhost" +install -m 644 "$SRC/nginx/$HOST.conf" /etc/nginx/sites-available/$HOST.conf +nginx -t +systemctl reload nginx +echo "setup complete" diff --git a/deploy/nginx/decepub-headers.conf b/deploy/nginx/decepub-headers.conf new file mode 100644 index 0000000..ae227f4 --- /dev/null +++ b/deploy/nginx/decepub-headers.conf @@ -0,0 +1,5 @@ +add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; +add_header X-Content-Type-Options "nosniff" always; +add_header Referrer-Policy "strict-origin-when-cross-origin" always; +add_header X-Frame-Options "SAMEORIGIN" always; +add_header X-Robots-Tag "noindex, nofollow" always; diff --git a/deploy/nginx/decepub.thepra.dev.conf b/deploy/nginx/decepub.thepra.dev.conf new file mode 100644 index 0000000..4712a10 --- /dev/null +++ b/deploy/nginx/decepub.thepra.dev.conf @@ -0,0 +1,56 @@ +server { + listen 80; + listen [::]:80; + server_name decepub.thepra.dev; + + location ^~ /.well-known/acme-challenge/ { + root /var/www/acme; + default_type "text/plain"; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl; + listen 8444 ssl proxy_protocol; + listen [::]:443 ssl; + server_name decepub.thepra.dev; + http2 on; + + include /etc/nginx/ssl.conf; + ssl_certificate /root/.acme.sh/decepub.thepra.dev_ecc/fullchain.cer; + ssl_certificate_key /root/.acme.sh/decepub.thepra.dev_ecc/decepub.thepra.dev.key; + include /etc/nginx/snippets/decepub-headers.conf; + + access_log /var/log/nginx/decepub.thepra.dev.access.log; + error_log /var/log/nginx/decepub.thepra.dev.error.log; + + root /var/www/decepub.thepra.dev; + index index.html; + gzip_static on; + open_file_cache off; + + location ^~ /.well-known/acme-challenge/ { + root /var/www/acme; + default_type "text/plain"; + } + + location = /index.html { expires -1; } + location = /build.json { expires -1; } + location = /appsettings.json { expires -1; } + location = /service-worker.js { expires -1; } + location = /service-worker-assets.js { expires -1; } + + location ^~ /_framework/ { + expires 1h; + try_files $uri =404; + } + + location / { + expires -1; + try_files $uri $uri/ /index.html; + } +} diff --git a/global.json b/global.json new file mode 100644 index 0000000..d46d21e --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature", + "allowPrerelease": false + } +} diff --git a/wwwroot/appsettings.json b/wwwroot/appsettings.json index 64df850..d4507fe 100644 --- a/wwwroot/appsettings.json +++ b/wwwroot/appsettings.json @@ -1,16 +1,6 @@ { - "Local": { - "Authority": "https://openidconnect.net", - "ClientId": "1234.locahost", - "RedirectUri": "", - "MetadataUrl": "", - "PostLogoutRedirectUri": "", - "ResponseType": "", - "ResponseMode": "", - "AdditionalProviderParameters": [], - "DefaultScopes": [] - }, "AppConfiguration": { - "Version": "0.1" + "Version": "0.1", + "ApiBaseAddress": "https://privapub.thepra.dev" } } diff --git a/wwwroot/css/style.min.css.gz b/wwwroot/css/style.min.css.gz deleted file mode 100644 index 9aae4c8..0000000 Binary files a/wwwroot/css/style.min.css.gz and /dev/null differ diff --git a/wwwroot/index.html b/wwwroot/index.html index 1884d42..9bdc092 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -22,7 +22,6 @@ -