release(v1.6.10): self-host ReDoc, gate sidebar toggle on auth, and enrich release workflow

This commit is contained in:
Ryan
2025-10-28 02:11:54 -04:00
committed by GitHub
parent ab327acc8a
commit b1bd903072
10 changed files with 2066 additions and 138 deletions

View File

@@ -1,110 +1,153 @@
--- ---
name: Release on version.js update name: Release on version.js update
on: on:
push: push:
branches: branches:
- master - master
paths: paths:
- public/js/version.js - public/js/version.js
workflow_run: workflow_run:
workflows: "Bump version and sync Changelog to Docker Repo" workflows: "Bump version and sync Changelog to Docker Repo"
types: completed types: completed
permissions: permissions:
contents: write contents: write
jobs: jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
concurrency: concurrency:
group: release-${{ github.ref }}-${{ github.sha }} group: release-${{ github.ref }}-${{ github.sha }}
cancel-in-progress: false cancel-in-progress: false
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Read version from version.js - name: Read version from version.js
id: ver id: ver
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
VER=$(grep -Eo "APP_VERSION\s*=\s*['\"]v[^'\"]+['\"]" public/js/version.js | sed -E "s/.*['\"](v[^'\"]+)['\"].*/\1/") VER=$(grep -Eo "APP_VERSION\s*=\s*['\"]v[^'\"]+['\"]" public/js/version.js | sed -E "s/.*['\"](v[^'\"]+)['\"].*/\1/")
if [[ -z "$VER" ]]; then if [[ -z "$VER" ]]; then
echo "Could not parse APP_VERSION from version.js" >&2 echo "Could not parse APP_VERSION from version.js" >&2
exit 1 exit 1
fi
echo "version=$VER" >> "$GITHUB_OUTPUT"
echo "Parsed version: $VER"
- name: Skip if tag already exists
id: tagcheck
shell: bash
run: |
set -euo pipefail
git fetch --tags --quiet
if git rev-parse -q --verify "refs/tags/${{ steps.ver.outputs.version }}" >/dev/null; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "Tag ${{ steps.ver.outputs.version }} already exists. Skipping release."
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
- name: Prepare release notes from CHANGELOG.md (optional)
if: steps.tagcheck.outputs.exists == 'false'
id: notes
shell: bash
run: |
set -euo pipefail
NOTES_PATH=""
if [[ -f CHANGELOG.md ]]; then
awk '
BEGIN{found=0}
/^## / && !found {found=1}
found && /^---$/ {exit}
found {print}
' CHANGELOG.md > RELEASE_BODY.md || true
# Trim trailing blank lines
sed -i -e :a -e '/^\n*$/{$d;N;ba' -e '}' RELEASE_BODY.md || true
if [[ -s RELEASE_BODY.md ]]; then
NOTES_PATH="RELEASE_BODY.md"
fi fi
fi echo "version=$VER" >> "$GITHUB_OUTPUT"
echo "path=$NOTES_PATH" >> "$GITHUB_OUTPUT" echo "Parsed version: $VER"
- name: (optional) Build archive to attach - name: Skip if tag already exists
if: steps.tagcheck.outputs.exists == 'false' id: tagcheck
shell: bash shell: bash
run: | run: |
set -euo pipefail set -euo pipefail
zip -r "FileRise-${{ steps.ver.outputs.version }}.zip" public/ README.md LICENSE >/dev/null || true git fetch --tags --quiet
if git rev-parse -q --verify "refs/tags/${{ steps.ver.outputs.version }}" >/dev/null; then
# Path A: we have extracted notes -> use body_path echo "exists=true" >> "$GITHUB_OUTPUT"
- name: Create GitHub Release (with CHANGELOG snippet) echo "Tag ${{ steps.ver.outputs.version }} already exists. Skipping release."
if: steps.tagcheck.outputs.exists == 'false' && steps.notes.outputs.path != '' else
uses: softprops/action-gh-release@v2 echo "exists=false" >> "$GITHUB_OUTPUT"
with: fi
tag_name: ${{ steps.ver.outputs.version }}
target_commitish: ${{ github.sha }} # Build the artifact first so we can checksum it
name: ${{ steps.ver.outputs.version }} - name: Build zip artifact
body_path: ${{ steps.notes.outputs.path }} if: steps.tagcheck.outputs.exists == 'false'
generate_release_notes: false shell: bash
files: | run: |
FileRise-${{ steps.ver.outputs.version }}.zip set -euo pipefail
zip -r "FileRise-${{ steps.ver.outputs.version }}.zip" public/ README.md LICENSE >/dev/null || true
# Path B: no notes -> let GitHub auto-generate from commits
- name: Create GitHub Release (auto notes) - name: Compute SHA-256 checksum
if: steps.tagcheck.outputs.exists == 'false' && steps.notes.outputs.path == '' if: steps.tagcheck.outputs.exists == 'false'
uses: softprops/action-gh-release@v2 id: sum
with: shell: bash
tag_name: ${{ steps.ver.outputs.version }} run: |
target_commitish: ${{ github.sha }} set -euo pipefail
name: ${{ steps.ver.outputs.version }} ZIP="FileRise-${{ steps.ver.outputs.version }}.zip"
generate_release_notes: true SHA=$(shasum -a 256 "$ZIP" | awk '{print $1}')
files: | echo "$SHA $ZIP" > "${ZIP}.sha256"
FileRise-${{ steps.ver.outputs.version }}.zip echo "sha=$SHA" >> "$GITHUB_OUTPUT"
echo "Computed SHA-256: $SHA"
- name: Extract notes from CHANGELOG (optional)
if: steps.tagcheck.outputs.exists == 'false'
id: notes
shell: bash
run: |
set -euo pipefail
NOTES_PATH=""
if [[ -f CHANGELOG.md ]]; then
awk '
BEGIN{found=0}
/^## / && !found {found=1}
found && /^---$/ {exit}
found {print}
' CHANGELOG.md > CHANGELOG_SNIPPET.md || true
sed -i -e :a -e '/^\n*$/{$d;N;ba' -e '}' CHANGELOG_SNIPPET.md || true
if [[ -s CHANGELOG_SNIPPET.md ]]; then
NOTES_PATH="CHANGELOG_SNIPPET.md"
fi
fi
echo "path=$NOTES_PATH" >> "$GITHUB_OUTPUT"
- name: Compute previous tag (for Full Changelog link)
if: steps.tagcheck.outputs.exists == 'false'
id: prev
shell: bash
run: |
set -euo pipefail
git fetch --tags --quiet
PREV=$(git tag --list "v*" --sort=-v:refname | sed -n '2p' || true)
if [[ -z "$PREV" ]]; then
PREV=$(git rev-list --max-parents=0 HEAD | tail -n1)
fi
echo "prev=$PREV" >> "$GITHUB_OUTPUT"
echo "Previous tag or baseline: $PREV"
- name: Build release body (snippet + full changelog + checksum)
if: steps.tagcheck.outputs.exists == 'false'
shell: bash
run: |
set -euo pipefail
VER="${{ steps.ver.outputs.version }}"
PREV="${{ steps.prev.outputs.prev }}"
REPO="${GITHUB_REPOSITORY}"
COMPARE_URL="https://github.com/${REPO}/compare/${PREV}...${VER}"
ZIP="FileRise-${VER}.zip"
SHA="${{ steps.sum.outputs.sha }}"
{
echo "## ${VER}"
echo
if [[ -s CHANGELOG_SNIPPET.md ]]; then
cat CHANGELOG_SNIPPET.md
echo
fi
echo "### Full Changelog"
echo "[${PREV} → ${VER}](${COMPARE_URL})"
echo
echo "### SHA-256 (zip)"
echo '```'
echo "${SHA} ${ZIP}"
echo '```'
} > RELEASE_BODY.md
echo "Release body:"
sed -n '1,200p' RELEASE_BODY.md
- name: Create GitHub Release
if: steps.tagcheck.outputs.exists == 'false'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.ver.outputs.version }}
target_commitish: ${{ github.sha }}
name: ${{ steps.ver.outputs.version }}
body_path: RELEASE_BODY.md
generate_release_notes: false
files: |
FileRise-${{ steps.ver.outputs.version }}.zip
FileRise-${{ steps.ver.outputs.version }}.zip.sha256

View File

@@ -52,7 +52,7 @@ jobs:
echo "Stamping ?v=${QVER} and {{APP_VER}}=${VER}" echo "Stamping ?v=${QVER} and {{APP_VER}}=${VER}"
# 1) Only stamp ?v= in HTML/CSS (avoid JS concatenation issues) # 1) Only stamp ?v= in HTML/CSS (avoid JS concatenation issues)
mapfile -t html_css < <(git ls-files -- 'public/*.html' 'public/**/*.html' 'public/*.css' 'public/**/*.css') mapfile -t html_css < <(git ls-files -- 'public/*.html' 'public/**/*.html' 'public/*.php' 'public/**/*.css')
for f in "${html_css[@]}"; do for f in "${html_css[@]}"; do
sed -E -i "s/(\?v=)[^\"'&<>\s]*/\1${QVER}/g" "$f" sed -E -i "s/(\?v=)[^\"'&<>\s]*/\1${QVER}/g" "$f"
sed -E -i "s/\{\{APP_VER\}\}/${VER}/g" "$f" sed -E -i "s/\{\{APP_VER\}\}/${VER}/g" "$f"

View File

@@ -1,5 +1,20 @@
# Changelog # Changelog
## Changes 10/28/2025 (v1.6.10)
release(v1.6.10): self-host ReDoc, gate sidebar toggle on auth, and enrich release workflow
- Vendor ReDoc and add MIT license file under public/vendor/redoc/; switch api.php to local bundle to satisfy CSP (script-src 'self').
- main.js: add/remove body.authenticated on login/logout so UI can reflect auth state.
- dragAndDrop.js: only render sidebarToggleFloating when authenticated; stop event bubbling, keep dark-mode styles.
- sync-changelog.yml: also stamp ?v= in PHP templates (public/**/*.php).
- release-on-version.yml: build zip first, compute SHA-256, assemble release body with latest CHANGELOG snippet, “Full Changelog” compare link, and attach .sha256 alongside the zip.
- THIRD_PARTY.md: document ReDoc vendoring and rationale.
Refs: #security #csp #release
---
## Changes 10/27/2025 (v1.6.9) ## Changes 10/27/2025 (v1.6.9)
release(v1.6.9): feat(core) localize assets, harden headers, and speed up load release(v1.6.9): feat(core) localize assets, harden headers, and speed up load

View File

@@ -139,7 +139,7 @@ docker run -d \
-e DATE_TIME_FORMAT="m/d/y h:iA" \ -e DATE_TIME_FORMAT="m/d/y h:iA" \
-e TOTAL_UPLOAD_SIZE="5G" \ -e TOTAL_UPLOAD_SIZE="5G" \
-e SECURE="false" \ -e SECURE="false" \
-e PERSISTENT_TOKENS_KEY="please_change_this_@@" \ -e PERSISTENT_TOKENS_KEY="default_please_change_this_key" \
-e PUID="1000" \ -e PUID="1000" \
-e PGID="1000" \ -e PGID="1000" \
-e CHOWN_ON_START="true" \ -e CHOWN_ON_START="true" \
@@ -186,7 +186,7 @@ services:
DATE_TIME_FORMAT: "m/d/y h:iA" DATE_TIME_FORMAT: "m/d/y h:iA"
TOTAL_UPLOAD_SIZE: "10G" TOTAL_UPLOAD_SIZE: "10G"
SECURE: "false" SECURE: "false"
PERSISTENT_TOKENS_KEY: "please_change_this_@@" PERSISTENT_TOKENS_KEY: "default_please_change_this_key"
# Ownership & indexing # Ownership & indexing
PUID: "1000" # Unraid users often use 99 PUID: "1000" # Unraid users often use 99
PGID: "1000" # Unraid users often use 100 PGID: "1000" # Unraid users often use 100

View File

@@ -37,6 +37,10 @@ If you believe any attribution is missing or incorrect, please open an issue.
- **Resumable.js 1.1.0** — MIT License - **Resumable.js 1.1.0** — MIT License
**Files:** `public/vendor/resumable/1.1.0/resumable.min.js` **Files:** `public/vendor/resumable/1.1.0/resumable.min.js`
- **ReDoc (redoc.standalone.js)** — MIT License
**Files:** `public/vendor/redoc/redoc.standalone.js`
**Notes:** Self-hosted to comply with `script-src 'self'` CSP.
> MIT-licensed code: see `licenses/mit.txt`. > MIT-licensed code: see `licenses/mit.txt`.
> Apache-2.0licensed code: see `licenses/apache-2.0.txt`. > Apache-2.0licensed code: see `licenses/apache-2.0.txt`.

View File

@@ -19,13 +19,15 @@ if (isset($_GET['spec'])) {
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/> <meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>FileRise API Docs</title> <title>FileRise API Docs</title>
<script defer src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"
integrity="sha384-70P5pmIdaQdVbxvjhrcTDv1uKcKqalZ3OHi7S2J+uzDl0PW8dO6L+pHOpm9EEjGJ" <!-- Local ReDoc bundle -->
crossorigin="anonymous"></script> <script defer src="/vendor/redoc/redoc.standalone.js?v=dev"></script>
<script defer src="/js/redoc-init.js"></script>
<!-- Your init (also local) -->
<script defer src="/js/redoc-init.js?v=dev"></script>
</head> </head>
<body> <body>
<redoc spec-url="api.php?spec=1"></redoc> <redoc spec-url="/api.php?spec=1"></redoc>
<div id="redoc-container"></div> <div id="redoc-container"></div>
</body> </body>
</html> </html>

View File

@@ -490,10 +490,17 @@ function mountHeaderToggle(btn) {
} }
function ensureZonesToggle() { function ensureZonesToggle() {
const isAuthed = document.body.classList.contains('authenticated');
let btn = document.getElementById('sidebarToggleFloating'); let btn = document.getElementById('sidebarToggleFloating');
const host = getHeaderHost(); const host = getHeaderHost();
if (!host) return; if (!host) return;
// If not authenticated, make sure the button is gone and bail.
if (!isAuthed) {
if (btn) btn.remove();
return;
}
// ensure the host is a positioning context // ensure the host is a positioning context
const hostStyle = getComputedStyle(host); const hostStyle = getComputedStyle(host);
if (hostStyle.position === 'static') { if (hostStyle.position === 'static') {
@@ -502,24 +509,25 @@ function ensureZonesToggle() {
if (!btn) { if (!btn) {
btn = document.createElement('button'); btn = document.createElement('button');
btn.id = 'sidebarToggleFloating'; btn.id = 'sidebarToggleFloating';
btn.type = 'button'; // not a submit btn.type = 'button';
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation(); // don't bubble into the <a href="index.html">
setSidebarCollapsed(!isSidebarCollapsed());
updateSidebarToggleUI(); // refresh icon/title
});
['mousedown','mouseup','pointerdown','pointerup'].forEach(evt =>
btn.addEventListener(evt, (e) => e.stopPropagation())
);
btn.setAttribute('aria-label', 'Toggle panels'); btn.setAttribute('aria-label', 'Toggle panels');
// Prevent accidental navigations / bubbling
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
setSidebarCollapsed(!isSidebarCollapsed());
updateSidebarToggleUI();
});
['mousedown','mouseup','pointerdown','pointerup'].forEach(evt =>
btn.addEventListener(evt, (e) => e.stopPropagation())
);
Object.assign(btn.style, { Object.assign(btn.style, {
position: 'absolute', // <-- key change (was fixed) position: 'absolute',
top: '8px', // adjust to line up with header content top: '8px',
left: '65px', // place to the right of your logo; tweak as needed left: '65px',
zIndex: '1000', zIndex: '1000',
width: '38px', width: '38px',
height: '38px', height: '38px',
@@ -535,7 +543,7 @@ btn.addEventListener('click', (e) => {
lineHeight: '0' lineHeight: '0'
}); });
// dark-mode polish (optional) // Dark mode polish
if (document.body.classList.contains('dark-mode')) { if (document.body.classList.contains('dark-mode')) {
btn.style.background = '#2c2c2c'; btn.style.background = '#2c2c2c';
btn.style.border = '1px solid #555'; btn.style.border = '1px solid #555';
@@ -543,17 +551,14 @@ btn.addEventListener('click', (e) => {
btn.style.color = '#e0e0e0'; btn.style.color = '#e0e0e0';
} }
btn.addEventListener('click', () => { // Insert right after the logo if present, else append to host
setZonesCollapsed(!isZonesCollapsed());
});
// Insert right after the logo if present, else just append to host
const afterLogo = host.querySelector('.header-logo'); const afterLogo = host.querySelector('.header-logo');
if (afterLogo && afterLogo.parentNode) { if (afterLogo && afterLogo.parentNode) {
afterLogo.parentNode.insertBefore(btn, afterLogo.nextSibling); afterLogo.parentNode.insertBefore(btn, afterLogo.nextSibling);
} else { } else {
host.appendChild(btn); host.appendChild(btn);
} }
themeToggleButton(btn); themeToggleButton(btn);
} }

View File

@@ -204,8 +204,11 @@ export function triggerLogout() {
credentials: "include", credentials: "include",
headers: { "X-CSRF-Token": getCsrfToken() } headers: { "X-CSRF-Token": getCsrfToken() }
}) })
.then(() => window.location.reload(true)) .then(() => {
.catch(() => { }); document.body.classList.remove('authenticated');
window.location.reload(true);
})
.catch(() => {});
} }
// Expose functions for inline handlers. // Expose functions for inline handlers.
@@ -239,9 +242,12 @@ document.addEventListener("DOMContentLoaded", function () {
// 3) If authenticated, start app // 3) If authenticated, start app
checkAuthentication().then(authenticated => { checkAuthentication().then(authenticated => {
if (authenticated) { if (authenticated) {
document.body.classList.add('authenticated');
const overlay = document.getElementById('loadingOverlay'); const overlay = document.getElementById('loadingOverlay');
if (overlay) overlay.remove(); if (overlay) overlay.remove();
initializeApp(); initializeApp();
} else {
document.body.classList.remove('authenticated');
} }
}); });

21
public/vendor/redoc/LICENSE vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015-present, Rebilly, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1832
public/vendor/redoc/redoc.standalone.js vendored Normal file

File diff suppressed because one or more lines are too long