mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2026-08-14 12:11:46 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b4ea9614b | |||
| 1644505043 | |||
| e142524bfc | |||
| 8fac30fc02 | |||
| c3b1f933b1 | |||
| a839915e3e | |||
| 5d95f4cd39 | |||
| 01a3b08dac |
@@ -9,6 +9,8 @@
|
||||
NODE_ENV=production
|
||||
HOST=0.0.0.0
|
||||
PORT=4321
|
||||
# Optional application base path (use "/" for root, or "/mirror" for subpath deployments)
|
||||
BASE_URL=/
|
||||
|
||||
# Database Configuration
|
||||
# For self-hosted, SQLite is used by default
|
||||
@@ -31,6 +33,12 @@ BETTER_AUTH_URL=http://localhost:4321
|
||||
# PUBLIC_BETTER_AUTH_URL=https://gitea-mirror.example.com
|
||||
# BETTER_AUTH_TRUSTED_ORIGINS=https://gitea-mirror.example.com
|
||||
#
|
||||
# If your app is served from a path prefix (e.g. https://git.example.com/mirror), set:
|
||||
# BASE_URL=/mirror
|
||||
# BETTER_AUTH_URL=https://git.example.com
|
||||
# PUBLIC_BETTER_AUTH_URL=https://git.example.com
|
||||
# BETTER_AUTH_TRUSTED_ORIGINS=https://git.example.com
|
||||
#
|
||||
# BETTER_AUTH_URL - Used server-side for auth callbacks and redirects
|
||||
# PUBLIC_BETTER_AUTH_URL - Used client-side (browser) for auth API calls
|
||||
# BETTER_AUTH_TRUSTED_ORIGINS - Comma-separated list of origins allowed to make auth requests
|
||||
|
||||
+7
-4
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1.4
|
||||
|
||||
FROM oven/bun:1.3.10-debian AS base
|
||||
FROM oven/bun:1.3.12-debian AS base
|
||||
WORKDIR /app
|
||||
RUN apt-get update && apt-get -y upgrade && apt-get install -y --no-install-recommends \
|
||||
python3 make g++ gcc wget sqlite3 openssl ca-certificates \
|
||||
@@ -16,6 +16,7 @@ COPY . .
|
||||
RUN bun run build
|
||||
RUN mkdir -p dist/scripts && \
|
||||
for script in scripts/*.ts; do \
|
||||
if [ "$(basename "$script")" = "runtime-server.ts" ]; then continue; fi; \
|
||||
bun build "$script" --target=bun --outfile=dist/scripts/$(basename "${script%.ts}.js"); \
|
||||
done
|
||||
|
||||
@@ -31,7 +32,7 @@ FROM debian:trixie-slim AS git-lfs-builder
|
||||
RUN apt-get update && apt-get -y upgrade && apt-get install -y --no-install-recommends \
|
||||
wget ca-certificates git make \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
ARG GO_VERSION=1.25.8
|
||||
ARG GO_VERSION=1.25.9
|
||||
ARG GIT_LFS_VERSION=3.7.1
|
||||
RUN ARCH="$(dpkg --print-architecture)" \
|
||||
&& wget -qO /tmp/go.tar.gz "https://go.dev/dl/go${GO_VERSION}.linux-${ARCH}.tar.gz" \
|
||||
@@ -48,7 +49,7 @@ RUN git clone --branch "v${GIT_LFS_VERSION}" --depth 1 https://github.com/git-lf
|
||||
&& install -m 755 /tmp/git-lfs/bin/git-lfs /usr/local/bin/git-lfs
|
||||
|
||||
# ----------------------------
|
||||
FROM oven/bun:1.3.10-debian AS runner
|
||||
FROM oven/bun:1.3.12-debian AS runner
|
||||
WORKDIR /app
|
||||
RUN apt-get update && apt-get -y upgrade && apt-get install -y --no-install-recommends \
|
||||
git wget sqlite3 openssl ca-certificates \
|
||||
@@ -59,6 +60,7 @@ COPY --from=pruner /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
COPY --from=builder /app/docker-entrypoint.sh ./docker-entrypoint.sh
|
||||
COPY --from=builder /app/scripts/runtime-server.ts ./scripts/runtime-server.ts
|
||||
COPY --from=builder /app/drizzle ./drizzle
|
||||
|
||||
# Remove build-only packages that are not needed at runtime
|
||||
@@ -73,6 +75,7 @@ ENV NODE_ENV=production
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=4321
|
||||
ENV DATABASE_URL=file:data/gitea-mirror.db
|
||||
ENV BASE_URL=/
|
||||
|
||||
# Create directories and setup permissions
|
||||
RUN mkdir -p /app/certs && \
|
||||
@@ -90,6 +93,6 @@ VOLUME /app/data
|
||||
EXPOSE 4321
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:4321/api/health || exit 1
|
||||
CMD sh -c 'BASE="${BASE_URL:-/}"; if [ "$BASE" = "/" ]; then BASE=""; else BASE="${BASE%/}"; fi; wget --no-verbose --tries=1 --spider "http://localhost:4321${BASE}/api/health" || exit 1'
|
||||
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||
|
||||
@@ -300,7 +300,22 @@ CLEANUP_DRY_RUN=false # Set to true to test without changes
|
||||
|
||||
### Reverse Proxy Configuration
|
||||
|
||||
If using a reverse proxy (e.g., nginx proxy manager) and experiencing issues with JavaScript files not loading properly, try enabling HTTP/2 support in your proxy configuration. While not required by the application, some proxy configurations may have better compatibility with HTTP/2 enabled. See [issue #43](https://github.com/RayLabsHQ/gitea-mirror/issues/43) for reference.
|
||||
If you run behind a reverse proxy on a subpath (for example `https://git.example.com/mirror`), configure:
|
||||
|
||||
```bash
|
||||
# BASE_URL handles the path prefix — auth URLs stay as origin only
|
||||
BASE_URL=/mirror
|
||||
BETTER_AUTH_URL=https://git.example.com
|
||||
PUBLIC_BETTER_AUTH_URL=https://git.example.com
|
||||
BETTER_AUTH_TRUSTED_ORIGINS=https://git.example.com
|
||||
# → Auth endpoints resolve to: https://git.example.com/mirror/api/auth/*
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `BASE_URL` sets the application path prefix.
|
||||
- `BETTER_AUTH_URL` and `PUBLIC_BETTER_AUTH_URL` should be **origin only** (e.g. `https://git.example.com`). Do not include the base path — it is applied automatically from `BASE_URL`. Any path accidentally included is stripped.
|
||||
- `BETTER_AUTH_TRUSTED_ORIGINS` should also contain origins only (no path).
|
||||
- `BASE_URL` is runtime configuration, so prebuilt registry images can be reused across different subpaths.
|
||||
|
||||
### Mirror Token Rotation (GitHub Token Changed)
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"dotenv": "^17.3.1",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"fuse.js": "^7.1.0",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"lucide-react": "^0.577.0",
|
||||
@@ -82,9 +82,14 @@
|
||||
},
|
||||
"overrides": {
|
||||
"@esbuild-kit/esm-loader": "npm:tsx@^4.21.0",
|
||||
"@xmldom/xmldom": "^0.8.12",
|
||||
"defu": "^6.1.7",
|
||||
"devalue": "^5.6.4",
|
||||
"fast-xml-parser": "^5.5.6",
|
||||
"kysely": "^0.28.16",
|
||||
"lodash": "^4.18.1",
|
||||
"node-forge": "^1.3.3",
|
||||
"picomatch": "^4.0.4",
|
||||
"rollup": ">=4.59.0",
|
||||
"svgo": "^4.0.1",
|
||||
},
|
||||
@@ -721,7 +726,7 @@
|
||||
|
||||
"@xmldom/is-dom-node": ["@xmldom/is-dom-node@1.0.1", "", {}, "sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q=="],
|
||||
|
||||
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="],
|
||||
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.12", "", {}, "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg=="],
|
||||
|
||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
|
||||
@@ -859,7 +864,7 @@
|
||||
|
||||
"decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
|
||||
|
||||
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||
"defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
@@ -893,7 +898,7 @@
|
||||
|
||||
"drizzle-kit": ["drizzle-kit@0.31.9", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg=="],
|
||||
|
||||
"drizzle-orm": ["drizzle-orm@0.45.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="],
|
||||
"drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="],
|
||||
|
||||
"dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="],
|
||||
|
||||
@@ -1085,7 +1090,7 @@
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"kysely": ["kysely@0.28.12", "", {}, "sha512-kWiueDWXhbCchgiotwXkwdxZE/6h56IHAeFWg4euUfW0YsmO9sxbAxzx1KLLv2lox15EfuuxHQvgJ1qIfZuHGw=="],
|
||||
"kysely": ["kysely@0.28.16", "", {}, "sha512-3i5pmOiZvMDj00qhrIVbH0AnioVTx22DMP7Vn5At4yJO46iy+FM8Y/g61ltenLVSo3fiO8h8Q3QOFgf/gQ72ww=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
||||
@@ -1111,7 +1116,7 @@
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||
|
||||
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
"lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
|
||||
|
||||
"lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
|
||||
|
||||
@@ -1325,7 +1330,7 @@
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||
|
||||
"playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="],
|
||||
|
||||
@@ -1745,8 +1750,6 @@
|
||||
|
||||
"@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"astro/esbuild": ["esbuild@0.27.4", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", "@esbuild/android-arm": "0.27.4", "@esbuild/android-arm64": "0.27.4", "@esbuild/android-x64": "0.27.4", "@esbuild/darwin-arm64": "0.27.4", "@esbuild/darwin-x64": "0.27.4", "@esbuild/freebsd-arm64": "0.27.4", "@esbuild/freebsd-x64": "0.27.4", "@esbuild/linux-arm": "0.27.4", "@esbuild/linux-arm64": "0.27.4", "@esbuild/linux-ia32": "0.27.4", "@esbuild/linux-loong64": "0.27.4", "@esbuild/linux-mips64el": "0.27.4", "@esbuild/linux-ppc64": "0.27.4", "@esbuild/linux-riscv64": "0.27.4", "@esbuild/linux-s390x": "0.27.4", "@esbuild/linux-x64": "0.27.4", "@esbuild/netbsd-arm64": "0.27.4", "@esbuild/netbsd-x64": "0.27.4", "@esbuild/openbsd-arm64": "0.27.4", "@esbuild/openbsd-x64": "0.27.4", "@esbuild/openharmony-arm64": "0.27.4", "@esbuild/sunos-x64": "0.27.4", "@esbuild/win32-arm64": "0.27.4", "@esbuild/win32-ia32": "0.27.4", "@esbuild/win32-x64": "0.27.4" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ=="],
|
||||
|
||||
"astro/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
||||
|
||||
@@ -22,6 +22,7 @@ services:
|
||||
# BETTER_AUTH_URL=https://gitea-mirror.example.com
|
||||
# PUBLIC_BETTER_AUTH_URL=https://gitea-mirror.example.com
|
||||
# BETTER_AUTH_TRUSTED_ORIGINS=https://gitea-mirror.example.com
|
||||
# Path-prefix deployments (e.g. /mirror) are supported at runtime via BASE_URL.
|
||||
|
||||
# === CORE SETTINGS ===
|
||||
# These are technically required but have working defaults
|
||||
@@ -29,6 +30,7 @@ services:
|
||||
- DATABASE_URL=file:data/gitea-mirror.db
|
||||
- HOST=0.0.0.0
|
||||
- PORT=4321
|
||||
- BASE_URL=${BASE_URL:-/}
|
||||
- PUBLIC_BETTER_AUTH_URL=${PUBLIC_BETTER_AUTH_URL:-http://localhost:4321}
|
||||
# Optional concurrency controls (defaults match in-app defaults)
|
||||
# If you want perfect ordering of issues and PRs, set these at 1
|
||||
@@ -36,7 +38,11 @@ services:
|
||||
- MIRROR_PULL_REQUEST_CONCURRENCY=${MIRROR_PULL_REQUEST_CONCURRENCY:-5}
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=3", "--spider", "http://localhost:4321/api/health"]
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"BASE=\"${BASE_URL:-/}\"; if [ \"$${BASE}\" = \"/\" ]; then BASE=\"\"; else BASE=\"$${BASE%/}\"; fi; wget --no-verbose --tries=3 --spider \"http://localhost:4321$${BASE}/api/health\"",
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
|
||||
@@ -66,6 +66,7 @@ services:
|
||||
- DATABASE_URL=file:data/gitea-mirror.db
|
||||
- HOST=0.0.0.0
|
||||
- PORT=4321
|
||||
- BASE_URL=${BASE_URL:-/}
|
||||
- BETTER_AUTH_SECRET=dev-secret-key
|
||||
# GitHub/Gitea Mirror Config
|
||||
- GITHUB_USERNAME=${GITHUB_USERNAME:-your-github-username}
|
||||
@@ -89,7 +90,11 @@ services:
|
||||
# Optional: Skip TLS verification (insecure, use only for testing)
|
||||
# - GITEA_SKIP_TLS_VERIFY=${GITEA_SKIP_TLS_VERIFY:-false}
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4321/api/health"]
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"BASE=\"${BASE_URL:-/}\"; if [ \"$${BASE}\" = \"/\" ]; then BASE=\"\"; else BASE=\"$${BASE%/}\"; fi; wget --no-verbose --tries=1 --spider \"http://localhost:4321$${BASE}/api/health\"",
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
+11
-1
@@ -30,6 +30,7 @@ services:
|
||||
- DATABASE_URL=file:data/gitea-mirror.db
|
||||
- HOST=0.0.0.0
|
||||
- PORT=4321
|
||||
- BASE_URL=${BASE_URL:-/}
|
||||
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET:-your-secret-key-change-this-in-production}
|
||||
- BETTER_AUTH_URL=${BETTER_AUTH_URL:-http://localhost:4321}
|
||||
# REVERSE PROXY: If you access Gitea Mirror through a reverse proxy (e.g. Nginx, Caddy, Traefik),
|
||||
@@ -37,6 +38,11 @@ services:
|
||||
# BETTER_AUTH_URL=https://gitea-mirror.example.com
|
||||
# PUBLIC_BETTER_AUTH_URL=https://gitea-mirror.example.com
|
||||
# BETTER_AUTH_TRUSTED_ORIGINS=https://gitea-mirror.example.com
|
||||
# If deployed under a path prefix (e.g. https://git.example.com/mirror), also set:
|
||||
# BASE_URL=/mirror
|
||||
# BETTER_AUTH_URL=https://git.example.com
|
||||
# PUBLIC_BETTER_AUTH_URL=https://git.example.com
|
||||
# BETTER_AUTH_TRUSTED_ORIGINS=https://git.example.com
|
||||
- PUBLIC_BETTER_AUTH_URL=${PUBLIC_BETTER_AUTH_URL:-http://localhost:4321}
|
||||
- BETTER_AUTH_TRUSTED_ORIGINS=${BETTER_AUTH_TRUSTED_ORIGINS:-}
|
||||
# Optional: ENCRYPTION_SECRET will be auto-generated if not provided
|
||||
@@ -81,7 +87,11 @@ services:
|
||||
- HEADER_AUTH_AUTO_PROVISION=${HEADER_AUTH_AUTO_PROVISION:-false}
|
||||
- HEADER_AUTH_ALLOWED_DOMAINS=${HEADER_AUTH_ALLOWED_DOMAINS:-}
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=3", "--spider", "http://localhost:4321/api/health"]
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"BASE=\"${BASE_URL:-/}\"; if [ \"$${BASE}\" = \"/\" ]; then BASE=\"\"; else BASE=\"$${BASE%/}\"; fi; wget --no-verbose --tries=3 --spider \"http://localhost:4321$${BASE}/api/health\"",
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
|
||||
@@ -229,7 +229,13 @@ trap 'shutdown_handler' TERM INT HUP
|
||||
|
||||
# Start the application
|
||||
echo "Starting Gitea Mirror..."
|
||||
bun ./dist/server/entry.mjs &
|
||||
if [ -f "./scripts/runtime-server.ts" ]; then
|
||||
bun ./scripts/runtime-server.ts &
|
||||
elif [ -f "./dist/scripts/runtime-server.js" ]; then
|
||||
bun ./dist/scripts/runtime-server.js &
|
||||
else
|
||||
bun ./dist/server/entry.mjs &
|
||||
fi
|
||||
APP_PID=$!
|
||||
|
||||
# Wait for the application to finish
|
||||
|
||||
@@ -33,10 +33,11 @@ Essential application settings required for running Gitea Mirror.
|
||||
| `NODE_ENV` | Application environment | `production` | No |
|
||||
| `HOST` | Server host binding | `0.0.0.0` | No |
|
||||
| `PORT` | Server port | `4321` | No |
|
||||
| `BASE_URL` | Application base path. Use `/` for root deployments, or a prefix such as `/mirror` when serving behind a reverse-proxy path prefix. | `/` | No |
|
||||
| `DATABASE_URL` | Database connection URL | `sqlite://data/gitea-mirror.db` | No |
|
||||
| `BETTER_AUTH_SECRET` | Secret key for session signing (generate with: `openssl rand -base64 32`) | - | Yes |
|
||||
| `BETTER_AUTH_URL` | Primary base URL for authentication. This should be the main URL where your application is accessed. | `http://localhost:4321` | No |
|
||||
| `PUBLIC_BETTER_AUTH_URL` | Client-side auth URL for multi-origin access. Set this to your primary domain when you need to access the app from different origins (e.g., both IP and domain). The client will use this URL for all auth requests instead of the current browser origin. | - | No |
|
||||
| `BETTER_AUTH_URL` | Authentication origin (scheme + host only, e.g. `https://git.example.com`). Do **not** include a path — any path is automatically stripped, and `BASE_URL` is applied separately. | `http://localhost:4321` | No |
|
||||
| `PUBLIC_BETTER_AUTH_URL` | Client-side auth origin for multi-origin access (same rule: origin only, no path). Set this to your primary domain when you need to access the app from different origins (e.g., both IP and domain). The client will use this URL for all auth requests instead of the current browser origin. | - | No |
|
||||
| `BETTER_AUTH_TRUSTED_ORIGINS` | Trusted origins for authentication requests. Comma-separated list of URLs. Use this to specify additional access URLs (e.g., local IP + domain: `http://10.10.20.45:4321,https://gitea-mirror.mydomain.tld`), SSO providers, reverse proxies, etc. | - | No |
|
||||
| `ENCRYPTION_SECRET` | Optional encryption key for tokens (generate with: `openssl rand -base64 48`) | - | No |
|
||||
|
||||
@@ -302,6 +303,7 @@ services:
|
||||
environment:
|
||||
# Core Configuration
|
||||
- NODE_ENV=production
|
||||
- BASE_URL=/
|
||||
- DATABASE_URL=file:data/gitea-mirror.db
|
||||
- BETTER_AUTH_SECRET=your-secure-secret-here
|
||||
# Primary access URL:
|
||||
@@ -370,6 +372,24 @@ This setup allows you to:
|
||||
|
||||
**Important:** When accessing from different origins (IP vs domain), you'll need to log in separately on each origin as cookies cannot be shared across different origins for security reasons.
|
||||
|
||||
### Path Prefix Deployments
|
||||
|
||||
If you serve Gitea Mirror under a subpath such as `https://git.example.com/mirror`, set:
|
||||
|
||||
```bash
|
||||
# BASE_URL handles the path prefix — auth URLs stay as origin only
|
||||
BASE_URL=/mirror
|
||||
BETTER_AUTH_URL=https://git.example.com
|
||||
PUBLIC_BETTER_AUTH_URL=https://git.example.com
|
||||
BETTER_AUTH_TRUSTED_ORIGINS=https://git.example.com
|
||||
# → Auth endpoints resolve to: https://git.example.com/mirror/api/auth/*
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `BETTER_AUTH_URL` and `PUBLIC_BETTER_AUTH_URL` must be **origin only** (scheme + host). Do not include the base path — it is applied automatically from `BASE_URL`. Any path accidentally included is stripped.
|
||||
- `BETTER_AUTH_TRUSTED_ORIGINS` must also contain origins only (no path).
|
||||
- `BASE_URL` is applied at runtime, so prebuilt images can be reused with different path prefixes.
|
||||
|
||||
### Trusted Origins
|
||||
|
||||
The `BETTER_AUTH_TRUSTED_ORIGINS` variable serves multiple purposes:
|
||||
|
||||
+11
-6
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "gitea-mirror",
|
||||
"type": "module",
|
||||
"version": "3.14.2",
|
||||
"version": "3.15.2",
|
||||
"engines": {
|
||||
"bun": ">=1.2.9"
|
||||
},
|
||||
@@ -31,8 +31,8 @@
|
||||
"test-shutdown": "bun scripts/test-graceful-shutdown.ts",
|
||||
"test-shutdown-cleanup": "bun scripts/test-graceful-shutdown.ts --cleanup",
|
||||
"preview": "bunx --bun astro preview",
|
||||
"start": "bun dist/server/entry.mjs",
|
||||
"start:fresh": "bun run cleanup-db && bun run manage-db init && bun dist/server/entry.mjs",
|
||||
"start": "bun scripts/runtime-server.ts",
|
||||
"start:fresh": "bun run cleanup-db && bun run manage-db init && bun scripts/runtime-server.ts",
|
||||
"test": "bun test",
|
||||
"test:migrations": "bun scripts/validate-migrations.ts",
|
||||
"test:watch": "bun test --watch",
|
||||
@@ -45,11 +45,16 @@
|
||||
},
|
||||
"overrides": {
|
||||
"@esbuild-kit/esm-loader": "npm:tsx@^4.21.0",
|
||||
"@xmldom/xmldom": "^0.8.12",
|
||||
"defu": "^6.1.7",
|
||||
"devalue": "^5.6.4",
|
||||
"fast-xml-parser": "^5.5.6",
|
||||
"kysely": "^0.28.16",
|
||||
"lodash": "^4.18.1",
|
||||
"node-forge": "^1.3.3",
|
||||
"svgo": "^4.0.1",
|
||||
"rollup": ">=4.59.0"
|
||||
"picomatch": "^4.0.4",
|
||||
"rollup": ">=4.59.0",
|
||||
"svgo": "^4.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.7",
|
||||
@@ -92,7 +97,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"dotenv": "^17.3.1",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"fuse.js": "^7.1.0",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"lucide-react": "^0.577.0",
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { createServer } from "node:http";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
|
||||
function normalizeBasePath(basePath: string | undefined): string {
|
||||
if (!basePath || !basePath.trim()) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
let normalized = basePath.trim();
|
||||
if (!normalized.startsWith("/")) {
|
||||
normalized = `/${normalized}`;
|
||||
}
|
||||
|
||||
normalized = normalized.replace(/\/+$/, "");
|
||||
return normalized || "/";
|
||||
}
|
||||
|
||||
function rewriteRequestUrl(rawUrl: string, basePath: string): string | null {
|
||||
if (basePath === "/") {
|
||||
return rawUrl;
|
||||
}
|
||||
|
||||
const url = new URL(rawUrl, "http://localhost");
|
||||
const pathname = url.pathname;
|
||||
|
||||
if (pathname === basePath || pathname === `${basePath}/`) {
|
||||
url.pathname = "/";
|
||||
return `${url.pathname}${url.search}`;
|
||||
}
|
||||
|
||||
if (pathname.startsWith(`${basePath}/`)) {
|
||||
url.pathname = pathname.slice(basePath.length) || "/";
|
||||
return `${url.pathname}${url.search}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const basePath = normalizeBasePath(process.env.BASE_URL);
|
||||
const host = process.env.HOST || "0.0.0.0";
|
||||
const port = Number.parseInt(process.env.PORT || "4321", 10);
|
||||
|
||||
process.env.ASTRO_NODE_AUTOSTART = "disabled";
|
||||
const { handler } = await import("../dist/server/entry.mjs");
|
||||
|
||||
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
||||
if (!req.url) {
|
||||
res.statusCode = 400;
|
||||
res.end("Bad Request");
|
||||
return;
|
||||
}
|
||||
|
||||
const rewrittenUrl = rewriteRequestUrl(req.url, basePath);
|
||||
if (rewrittenUrl === null) {
|
||||
res.statusCode = 404;
|
||||
res.end("Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
req.url = rewrittenUrl;
|
||||
req.headers["x-gitea-mirror-base-rewritten"] = "1";
|
||||
|
||||
Promise.resolve((handler as unknown as (request: IncomingMessage, response: ServerResponse) => unknown)(req, res)).catch((error) => {
|
||||
console.error("Unhandled runtime server error:", error);
|
||||
if (!res.headersSent) {
|
||||
res.statusCode = 500;
|
||||
res.end("Internal Server Error");
|
||||
} else {
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(port, host, () => {
|
||||
console.log(`Runtime server listening on http://${host}:${port} (BASE_URL=${basePath})`);
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { Home, ArrowLeft, GitBranch, BookOpen, Settings, FileQuestion } from "lucide-react";
|
||||
import { withBase } from "@/lib/base-path";
|
||||
|
||||
export function NotFound() {
|
||||
return (
|
||||
@@ -21,7 +22,7 @@ export function NotFound() {
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<Button asChild className="w-full">
|
||||
<a href="/">
|
||||
<a href={withBase("/")}>
|
||||
<Home className="mr-2 h-4 w-4" />
|
||||
Go to Dashboard
|
||||
</a>
|
||||
@@ -45,21 +46,21 @@ export function NotFound() {
|
||||
{/* Quick Links */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<a
|
||||
href="/repositories"
|
||||
href={withBase("/repositories")}
|
||||
className="flex flex-col items-center gap-2 p-3 rounded-md hover:bg-muted transition-colors"
|
||||
>
|
||||
<GitBranch className="h-5 w-5 text-muted-foreground" />
|
||||
<span className="text-xs">Repositories</span>
|
||||
</a>
|
||||
<a
|
||||
href="/config"
|
||||
href={withBase("/config")}
|
||||
className="flex flex-col items-center gap-2 p-3 rounded-md hover:bg-muted transition-colors"
|
||||
>
|
||||
<Settings className="h-5 w-5 text-muted-foreground" />
|
||||
<span className="text-xs">Config</span>
|
||||
</a>
|
||||
<a
|
||||
href="/docs"
|
||||
href={withBase("/docs")}
|
||||
className="flex flex-col items-center gap-2 p-3 rounded-md hover:bg-muted transition-colors"
|
||||
>
|
||||
<BookOpen className="h-5 w-5 text-muted-foreground" />
|
||||
@@ -77,4 +78,4 @@ export function NotFound() {
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import { toast } from 'sonner';
|
||||
import { useLiveRefresh } from '@/hooks/useLiveRefresh';
|
||||
import { useConfigStatus } from '@/hooks/useConfigStatus';
|
||||
import { useNavigation } from '@/components/layout/MainLayout';
|
||||
import { withBase } from '@/lib/base-path';
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
@@ -321,7 +322,7 @@ export function ActivityLog() {
|
||||
setIsInitialLoading(true);
|
||||
setShowCleanupDialog(false);
|
||||
|
||||
const response = await fetch('/api/activities/cleanup', {
|
||||
const response = await fetch(withBase('/api/activities/cleanup'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ userId: user.id }),
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Separator } from '@/components/ui/separator';
|
||||
import { toast, Toaster } from 'sonner';
|
||||
import { showErrorToast } from '@/lib/utils';
|
||||
import { Loader2, Mail, Globe, Eye, EyeOff } from 'lucide-react';
|
||||
import { withBase } from '@/lib/base-path';
|
||||
|
||||
|
||||
export function LoginForm() {
|
||||
@@ -47,7 +48,7 @@ export function LoginForm() {
|
||||
toast.success('Login successful!');
|
||||
// Small delay before redirecting to see the success message
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
window.location.href = withBase('/');
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
showErrorToast(error, toast);
|
||||
@@ -64,12 +65,15 @@ export function LoginForm() {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseURL = typeof window !== 'undefined' ? window.location.origin : 'http://localhost:4321';
|
||||
const callbackURL =
|
||||
typeof window !== 'undefined'
|
||||
? new URL(withBase('/'), window.location.origin).toString()
|
||||
: `http://localhost:4321${withBase('/')}`;
|
||||
await authClient.signIn.sso({
|
||||
email: ssoEmail || undefined,
|
||||
domain: domain,
|
||||
providerId: providerId,
|
||||
callbackURL: `${baseURL}/`,
|
||||
callbackURL,
|
||||
scopes: ['openid', 'email', 'profile'], // TODO: This is not being respected by the SSO plugin.
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -85,7 +89,7 @@ export function LoginForm() {
|
||||
<CardHeader className="text-center">
|
||||
<div className="flex justify-center mb-4">
|
||||
<img
|
||||
src="/logo.png"
|
||||
src={withBase('/logo.png')}
|
||||
alt="Gitea Mirror Logo"
|
||||
className="h-8 w-10"
|
||||
/>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { toast, Toaster } from 'sonner';
|
||||
import { showErrorToast } from '@/lib/utils';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { Eye, EyeOff } from 'lucide-react';
|
||||
import { withBase } from '@/lib/base-path';
|
||||
|
||||
export function SignupForm() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -42,7 +43,7 @@ export function SignupForm() {
|
||||
toast.success('Account created successfully! Redirecting to dashboard...');
|
||||
// Small delay before redirecting to see the success message
|
||||
setTimeout(() => {
|
||||
window.location.href = '/';
|
||||
window.location.href = withBase('/');
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
showErrorToast(error, toast);
|
||||
@@ -57,7 +58,7 @@ export function SignupForm() {
|
||||
<CardHeader className="text-center">
|
||||
<div className="flex justify-center mb-4">
|
||||
<img
|
||||
src="/logo.png"
|
||||
src={withBase('/logo.png')}
|
||||
alt="Gitea Mirror Logo"
|
||||
className="h-8 w-10"
|
||||
/>
|
||||
|
||||
@@ -24,6 +24,7 @@ import { toast } from 'sonner';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { invalidateConfigCache } from '@/hooks/useConfigStatus';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { withBase } from '@/lib/base-path';
|
||||
|
||||
type ConfigState = {
|
||||
githubConfig: GitHubConfig;
|
||||
@@ -35,6 +36,8 @@ type ConfigState = {
|
||||
notificationConfig: NotificationConfig;
|
||||
};
|
||||
|
||||
const CONFIG_API_PATH = withBase('/api/config');
|
||||
|
||||
export function ConfigTabs() {
|
||||
const [config, setConfig] = useState<ConfigState>({
|
||||
githubConfig: {
|
||||
@@ -198,7 +201,7 @@ export function ConfigTabs() {
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/config', {
|
||||
const response = await fetch(CONFIG_API_PATH, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(reqPayload),
|
||||
@@ -264,7 +267,7 @@ export function ConfigTabs() {
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/config', {
|
||||
const response = await fetch(CONFIG_API_PATH, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(reqPayload),
|
||||
@@ -329,7 +332,7 @@ export function ConfigTabs() {
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/config', {
|
||||
const response = await fetch(CONFIG_API_PATH, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(reqPayload),
|
||||
@@ -378,7 +381,7 @@ export function ConfigTabs() {
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/config', {
|
||||
const response = await fetch(CONFIG_API_PATH, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(reqPayload),
|
||||
@@ -418,7 +421,7 @@ export function ConfigTabs() {
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/config', {
|
||||
const response = await fetch(CONFIG_API_PATH, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(reqPayload),
|
||||
@@ -453,7 +456,7 @@ export function ConfigTabs() {
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/config', {
|
||||
const response = await fetch(CONFIG_API_PATH, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(reqPayload),
|
||||
@@ -498,7 +501,7 @@ export function ConfigTabs() {
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/config', {
|
||||
const response = await fetch(CONFIG_API_PATH, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(reqPayload),
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Bell, Activity, Send } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import type { NotificationConfig } from "@/types/config";
|
||||
import { withBase } from "@/lib/base-path";
|
||||
|
||||
interface NotificationSettingsProps {
|
||||
notificationConfig: NotificationConfig;
|
||||
@@ -31,7 +32,7 @@ export function NotificationSettings({
|
||||
const handleTestNotification = async () => {
|
||||
setIsTesting(true);
|
||||
try {
|
||||
const resp = await fetch("/api/notifications/test", {
|
||||
const resp = await fetch(withBase("/api/notifications/test"), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ notificationConfig }),
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Badge } from '../ui/badge';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { MultiSelect } from '@/components/ui/multi-select';
|
||||
import { withBase } from '@/lib/base-path';
|
||||
|
||||
function isTrustedIssuer(issuer: string, allowedHosts: string[]): boolean {
|
||||
try {
|
||||
@@ -100,6 +101,9 @@ export function SSOSettings() {
|
||||
digestAlgorithm: 'sha256',
|
||||
identifierFormat: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
|
||||
});
|
||||
const appOrigin = typeof window !== 'undefined' ? window.location.origin : '';
|
||||
const buildAbsoluteAppUrl = (path: string) =>
|
||||
appOrigin ? new URL(withBase(path), appOrigin).toString() : withBase(path);
|
||||
|
||||
|
||||
|
||||
@@ -179,8 +183,8 @@ export function SSOSettings() {
|
||||
} else {
|
||||
requestData.entryPoint = providerForm.entryPoint;
|
||||
requestData.cert = providerForm.cert;
|
||||
requestData.callbackUrl = providerForm.callbackUrl || `${window.location.origin}/api/auth/sso/saml2/callback/${providerForm.providerId}`;
|
||||
requestData.audience = providerForm.audience || window.location.origin;
|
||||
requestData.callbackUrl = providerForm.callbackUrl || buildAbsoluteAppUrl(`/api/auth/sso/saml2/callback/${providerForm.providerId}`);
|
||||
requestData.audience = providerForm.audience || appOrigin;
|
||||
requestData.wantAssertionsSigned = providerForm.wantAssertionsSigned;
|
||||
requestData.signatureAlgorithm = providerForm.signatureAlgorithm;
|
||||
requestData.digestAlgorithm = providerForm.digestAlgorithm;
|
||||
@@ -517,7 +521,7 @@ export function SSOSettings() {
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<div className="space-y-2">
|
||||
<p>Redirect URL: {window.location.origin}/api/auth/sso/callback/{providerForm.providerId || '{provider-id}'}</p>
|
||||
<p>Redirect URL: {buildAbsoluteAppUrl(`/api/auth/sso/callback/${providerForm.providerId || '{provider-id}'}`)}</p>
|
||||
{isTrustedIssuer(providerForm.issuer, ['google.com']) && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Note: Google doesn't support the "offline_access" scope. Make sure to exclude it from the selected scopes.
|
||||
@@ -563,8 +567,8 @@ export function SSOSettings() {
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<div className="space-y-1">
|
||||
<p>Callback URL: {window.location.origin}/api/auth/sso/saml2/callback/{providerForm.providerId || '{provider-id}'}</p>
|
||||
<p>SP Metadata: {window.location.origin}/api/auth/sso/saml2/sp/metadata?providerId={providerForm.providerId || '{provider-id}'}</p>
|
||||
<p>Callback URL: {buildAbsoluteAppUrl(`/api/auth/sso/saml2/callback/${providerForm.providerId || '{provider-id}'}`)}</p>
|
||||
<p>SP Metadata: {buildAbsoluteAppUrl(`/api/auth/sso/saml2/sp/metadata?providerId=${providerForm.providerId || '{provider-id}'}`)}</p>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
@@ -724,4 +728,4 @@ export function SSOSettings() {
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useLiveRefresh } from "@/hooks/useLiveRefresh";
|
||||
import { usePageVisibility } from "@/hooks/usePageVisibility";
|
||||
import { useConfigStatus } from "@/hooks/useConfigStatus";
|
||||
import { useNavigation } from "@/components/layout/MainLayout";
|
||||
import { withBase } from "@/lib/base-path";
|
||||
|
||||
// Helper function to format last sync time
|
||||
function formatLastSyncTime(date: Date | null): string {
|
||||
@@ -110,7 +111,7 @@ export function Dashboard() {
|
||||
useEffectForToasts(() => {
|
||||
if (!user?.id) return;
|
||||
|
||||
const eventSource = new EventSource(`/api/events?userId=${user.id}`);
|
||||
const eventSource = new EventSource(`${withBase("/api/events")}?userId=${user.id}`);
|
||||
|
||||
eventSource.addEventListener("rate-limit", (event) => {
|
||||
try {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { MirrorJob } from "@/lib/db/schema";
|
||||
import { formatDate, getStatusColor } from "@/lib/utils";
|
||||
import { Button } from "../ui/button";
|
||||
import { Activity, Clock } from "lucide-react";
|
||||
import { withBase } from "@/lib/base-path";
|
||||
|
||||
interface RecentActivityProps {
|
||||
activities: MirrorJob[];
|
||||
@@ -14,7 +15,7 @@ export function RecentActivity({ activities }: RecentActivityProps) {
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>Recent Activity</CardTitle>
|
||||
<Button variant="outline" asChild>
|
||||
<a href="/activity">View All</a>
|
||||
<a href={withBase("/activity")}>View All</a>
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -27,7 +28,7 @@ export function RecentActivity({ activities }: RecentActivityProps) {
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<a href="/activity">
|
||||
<a href={withBase("/activity")}>
|
||||
<Activity className="h-3.5 w-3.5 mr-1.5" />
|
||||
View History
|
||||
</a>
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { Repository } from "@/lib/db/schema";
|
||||
import { getStatusColor } from "@/lib/utils";
|
||||
import { buildGiteaWebUrl } from "@/lib/gitea-url";
|
||||
import { useGiteaConfig } from "@/hooks/useGiteaConfig";
|
||||
import { withBase } from "@/lib/base-path";
|
||||
|
||||
interface RepositoryListProps {
|
||||
repositories: Repository[];
|
||||
@@ -42,7 +43,7 @@ export function RepositoryList({ repositories }: RepositoryListProps) {
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>Repositories</CardTitle>
|
||||
<Button variant="outline" asChild>
|
||||
<a href="/repositories">View All</a>
|
||||
<a href={withBase("/repositories")}>View All</a>
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -54,7 +55,7 @@ export function RepositoryList({ repositories }: RepositoryListProps) {
|
||||
Configure your GitHub connection to start mirroring repositories.
|
||||
</p>
|
||||
<Button asChild>
|
||||
<a href="/config">Configure GitHub</a>
|
||||
<a href={withBase("/config")}>Configure GitHub</a>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { withBase } from "@/lib/base-path";
|
||||
|
||||
interface HeaderProps {
|
||||
currentPage?: "dashboard" | "repositories" | "organizations" | "configuration" | "activity-log";
|
||||
@@ -101,14 +102,14 @@ export function Header({ currentPage, onNavigate, onMenuClick, onToggleCollapse,
|
||||
<button
|
||||
onClick={() => {
|
||||
if (currentPage !== 'dashboard') {
|
||||
window.history.pushState({}, '', '/');
|
||||
window.history.pushState({}, '', withBase('/'));
|
||||
onNavigate?.('dashboard');
|
||||
}
|
||||
}}
|
||||
className="flex items-center gap-2 py-1 hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<img
|
||||
src="/logo.png"
|
||||
src={withBase('/logo.png')}
|
||||
alt="Gitea Mirror Logo"
|
||||
className="h-5 w-6"
|
||||
/>
|
||||
@@ -163,7 +164,7 @@ export function Header({ currentPage, onNavigate, onMenuClick, onToggleCollapse,
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<a href="/login">Login</a>
|
||||
<a href={withBase('/login')}>Login</a>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Toaster } from "@/components/ui/sonner";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useRepoSync } from "@/hooks/useSyncRepo";
|
||||
import { useConfigStatus } from "@/hooks/useConfigStatus";
|
||||
import { stripBasePath, withBase } from "@/lib/base-path";
|
||||
|
||||
// Navigation context to signal when navigation happens
|
||||
const NavigationContext = createContext<{ navigationKey: number }>({ navigationKey: 0 });
|
||||
@@ -71,7 +72,7 @@ function AppWithProviders({ page: initialPage }: AppProps) {
|
||||
// Handle browser back/forward navigation
|
||||
useEffect(() => {
|
||||
const handlePopState = () => {
|
||||
const path = window.location.pathname;
|
||||
const path = stripBasePath(window.location.pathname);
|
||||
const pageMap: Record<string, AppProps['page']> = {
|
||||
'/': 'dashboard',
|
||||
'/repositories': 'repositories',
|
||||
@@ -125,7 +126,7 @@ function AppWithProviders({ page: initialPage }: AppProps) {
|
||||
if (!authLoading && !user) {
|
||||
// Use window.location for client-side redirect
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = '/login';
|
||||
window.location.href = withBase('/login');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { stripBasePath, withBase } from "@/lib/base-path";
|
||||
|
||||
interface SidebarProps {
|
||||
className?: string;
|
||||
@@ -24,14 +25,14 @@ export function Sidebar({ className, onNavigate, isOpen, isCollapsed = false, on
|
||||
|
||||
useEffect(() => {
|
||||
// Hydration happens here
|
||||
const path = window.location.pathname;
|
||||
const path = stripBasePath(window.location.pathname);
|
||||
setCurrentPath(path);
|
||||
}, []);
|
||||
|
||||
// Listen for URL changes (browser back/forward)
|
||||
useEffect(() => {
|
||||
const handlePopState = () => {
|
||||
setCurrentPath(window.location.pathname);
|
||||
setCurrentPath(stripBasePath(window.location.pathname));
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', handlePopState);
|
||||
@@ -45,7 +46,7 @@ export function Sidebar({ className, onNavigate, isOpen, isCollapsed = false, on
|
||||
if (currentPath === href) return;
|
||||
|
||||
// Update URL without page reload
|
||||
window.history.pushState({}, '', href);
|
||||
window.history.pushState({}, '', withBase(href));
|
||||
setCurrentPath(href);
|
||||
|
||||
// Map href to page name for the parent component
|
||||
@@ -163,7 +164,7 @@ export function Sidebar({ className, onNavigate, isOpen, isCollapsed = false, on
|
||||
Check out the documentation for help with setup and configuration.
|
||||
</p>
|
||||
<a
|
||||
href="/docs"
|
||||
href={withBase("/docs")}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 text-xs md:text-xs text-primary hover:underline py-2 md:py-0"
|
||||
@@ -177,7 +178,7 @@ export function Sidebar({ className, onNavigate, isOpen, isCollapsed = false, on
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger asChild>
|
||||
<a
|
||||
href="/docs"
|
||||
href={withBase("/docs")}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
|
||||
@@ -12,6 +12,7 @@ import { cn } from "@/lib/utils";
|
||||
import { buildGiteaWebUrl } from "@/lib/gitea-url";
|
||||
import { MirrorDestinationEditor } from "./MirrorDestinationEditor";
|
||||
import { useGiteaConfig } from "@/hooks/useGiteaConfig";
|
||||
import { withBase } from "@/lib/base-path";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -85,7 +86,7 @@ export function OrganizationList({
|
||||
|
||||
const handleUpdateDestination = async (orgId: string, newDestination: string | null) => {
|
||||
// Call API to update organization destination
|
||||
const response = await fetch(`/api/organizations/${orgId}`, {
|
||||
const response = await fetch(`${withBase("/api/organizations")}/${orgId}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -189,7 +190,7 @@ export function OrganizationList({
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<Building2 className="h-5 w-5 text-muted-foreground flex-shrink-0" />
|
||||
<a
|
||||
href={`/repositories?organization=${encodeURIComponent(org.name || '')}`}
|
||||
href={`${withBase('/repositories')}?organization=${encodeURIComponent(org.name || '')}`}
|
||||
className="font-medium hover:underline cursor-pointer truncate"
|
||||
>
|
||||
{org.name}
|
||||
@@ -264,7 +265,7 @@ export function OrganizationList({
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<a
|
||||
href={`/repositories?organization=${encodeURIComponent(org.name || '')}`}
|
||||
href={`${withBase('/repositories')}?organization=${encodeURIComponent(org.name || '')}`}
|
||||
className="text-xl font-semibold hover:underline cursor-pointer"
|
||||
>
|
||||
{org.name}
|
||||
|
||||
@@ -50,6 +50,7 @@ import AddRepositoryDialog from "./AddRepositoryDialog";
|
||||
import { useLiveRefresh } from "@/hooks/useLiveRefresh";
|
||||
import { useConfigStatus } from "@/hooks/useConfigStatus";
|
||||
import { useNavigation } from "@/components/layout/MainLayout";
|
||||
import { withBase } from "@/lib/base-path";
|
||||
|
||||
const REPOSITORY_SORT_OPTIONS = [
|
||||
{ value: "imported-desc", label: "Recently Imported" },
|
||||
@@ -1518,7 +1519,7 @@ export default function Repository() {
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
window.history.pushState({}, '', '/config');
|
||||
window.history.pushState({}, '', withBase('/config'));
|
||||
// We need to trigger a page change event for the navigation system
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
}}
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
import { InlineDestinationEditor } from "./InlineDestinationEditor";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { withBase } from "@/lib/base-path";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -102,7 +103,7 @@ export default function RepositoryTable({
|
||||
|
||||
const handleUpdateDestination = async (repoId: string, newDestination: string | null) => {
|
||||
// Call API to update repository destination
|
||||
const response = await fetch(`/api/repositories/${repoId}`, {
|
||||
const response = await fetch(`${withBase("/api/repositories")}/${repoId}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
---
|
||||
import { BASE_PATH_WINDOW_KEY } from '@/lib/base-path';
|
||||
|
||||
const normalizeBasePath = (value) => {
|
||||
if (!value || !value.trim()) {
|
||||
return '/';
|
||||
}
|
||||
|
||||
let normalized = value.trim();
|
||||
if (!normalized.startsWith('/')) {
|
||||
normalized = `/${normalized}`;
|
||||
}
|
||||
|
||||
normalized = normalized.replace(/\/+$/, '');
|
||||
return normalized || '/';
|
||||
};
|
||||
|
||||
const runtimeBasePath = normalizeBasePath(process.env.BASE_URL);
|
||||
---
|
||||
|
||||
<script is:inline>
|
||||
<script is:inline define:vars={{ BASE_PATH_WINDOW_KEY, runtimeBasePath }}>
|
||||
window[BASE_PATH_WINDOW_KEY] = runtimeBasePath;
|
||||
|
||||
const getThemePreference = () => {
|
||||
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
|
||||
return localStorage.getItem('theme');
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "react";
|
||||
import { authApi } from "@/lib/api";
|
||||
import type { ExtendedUser } from "@/types/user";
|
||||
import { withBase } from "@/lib/base-path";
|
||||
|
||||
interface AuthContextType {
|
||||
user: ExtendedUser | null;
|
||||
@@ -61,9 +62,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
// Redirect user based on error
|
||||
if (err?.message === "No users found") {
|
||||
window.location.href = "/signup";
|
||||
window.location.href = withBase("/signup");
|
||||
} else {
|
||||
window.location.href = "/login";
|
||||
window.location.href = withBase("/login");
|
||||
}
|
||||
console.error("Auth check failed", err);
|
||||
} finally {
|
||||
@@ -111,7 +112,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
try {
|
||||
await authApi.logout();
|
||||
setUser(null);
|
||||
window.location.href = "/login";
|
||||
window.location.href = withBase("/login");
|
||||
} catch (err) {
|
||||
console.error("Logout error:", err);
|
||||
} finally {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "react";
|
||||
import { authClient, useSession as useBetterAuthSession } from "@/lib/auth-client";
|
||||
import type { Session, AuthUser } from "@/lib/auth-client";
|
||||
import { withBase } from "@/lib/base-path";
|
||||
|
||||
interface AuthContextType {
|
||||
user: AuthUser | null;
|
||||
@@ -46,7 +47,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const result = await authClient.signIn.email({
|
||||
email,
|
||||
password,
|
||||
callbackURL: "/",
|
||||
callbackURL: withBase("/"),
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
@@ -73,7 +74,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
email,
|
||||
password,
|
||||
name: username, // Better Auth uses 'name' field for display name
|
||||
callbackURL: "/",
|
||||
callbackURL: withBase("/"),
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
@@ -94,7 +95,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
await authClient.signOut({
|
||||
fetchOptions: {
|
||||
onSuccess: () => {
|
||||
window.location.href = "/login";
|
||||
window.location.href = withBase("/login");
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -140,4 +141,4 @@ export function useAuth() {
|
||||
}
|
||||
|
||||
// Export the Better Auth session hook for direct use when needed
|
||||
export { useBetterAuthSession };
|
||||
export { useBetterAuthSession };
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState, useRef, useCallback } from "react";
|
||||
import type { MirrorJob } from "@/lib/db/schema";
|
||||
import { withBase } from "@/lib/base-path";
|
||||
|
||||
interface UseSSEOptions {
|
||||
userId?: string;
|
||||
@@ -41,7 +42,7 @@ export const useSSE = ({
|
||||
}
|
||||
|
||||
// Create new EventSource connection
|
||||
const eventSource = new EventSource(`/api/sse?userId=${userId}`);
|
||||
const eventSource = new EventSource(`${withBase("/api/sse")}?userId=${userId}`);
|
||||
eventSourceRef.current = eventSource;
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useAuth } from "./useAuth";
|
||||
import { withBase } from "@/lib/base-path";
|
||||
|
||||
interface UseRepoSyncOptions {
|
||||
userId?: string;
|
||||
@@ -51,7 +52,7 @@ export function useRepoSync({
|
||||
|
||||
const sync = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/job/schedule-sync-repo", {
|
||||
const response = await fetch(withBase("/api/job/schedule-sync-repo"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import '../styles/global.css';
|
||||
import '../styles/docs.css';
|
||||
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||
import { withBase } from '@/lib/base-path';
|
||||
|
||||
// Accept title as a prop with a default value
|
||||
const { title = 'Gitea Mirror' } = Astro.props;
|
||||
@@ -11,7 +12,7 @@ const { title = 'Gitea Mirror' } = Astro.props;
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href={withBase('/favicon.svg')} />
|
||||
<title>{title}</title>
|
||||
<ThemeScript />
|
||||
</head>
|
||||
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
import { withBase } from "@/lib/base-path";
|
||||
|
||||
// Base API URL
|
||||
const API_BASE = "/api";
|
||||
const API_BASE = withBase("/api");
|
||||
|
||||
// Helper function for API requests
|
||||
async function apiRequest<T>(
|
||||
|
||||
@@ -3,6 +3,12 @@ import { createAuthClient } from "better-auth/react";
|
||||
import { oidcClient } from "better-auth/client/plugins";
|
||||
import { ssoClient } from "@better-auth/sso/client";
|
||||
import type { Session as BetterAuthSession, User as BetterAuthUser } from "better-auth";
|
||||
import { withBase } from "@/lib/base-path";
|
||||
|
||||
function normalizeAuthBaseUrl(url: string): string {
|
||||
const validatedUrl = new URL(url.trim());
|
||||
return validatedUrl.origin;
|
||||
}
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
// Use PUBLIC_BETTER_AUTH_URL if set (for multi-origin access), otherwise use current origin
|
||||
@@ -18,9 +24,8 @@ export const authClient = createAuthClient({
|
||||
// Validate and clean the URL if provided
|
||||
if (url && typeof url === 'string' && url.trim() !== '') {
|
||||
try {
|
||||
// Validate URL format and remove trailing slash
|
||||
const validatedUrl = new URL(url.trim());
|
||||
return validatedUrl.origin; // Use origin to ensure clean URL without path
|
||||
// Validate URL format and preserve optional base path
|
||||
return normalizeAuthBaseUrl(url);
|
||||
} catch (e) {
|
||||
console.warn(`Invalid PUBLIC_BETTER_AUTH_URL: ${url}, falling back to default`);
|
||||
}
|
||||
@@ -34,7 +39,7 @@ export const authClient = createAuthClient({
|
||||
// Default for SSR - always return a valid URL
|
||||
return 'http://localhost:4321';
|
||||
})(),
|
||||
basePath: '/api/auth', // Explicitly set the base path
|
||||
basePath: withBase('/api/auth'), // Explicitly set the base path
|
||||
plugins: [
|
||||
oidcClient(),
|
||||
ssoClient(),
|
||||
|
||||
+5
-4
@@ -5,6 +5,7 @@ import { sso } from "@better-auth/sso";
|
||||
import { db, users } from "./db";
|
||||
import * as schema from "./db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { withBase } from "./base-path";
|
||||
|
||||
/**
|
||||
* Resolves the list of trusted origins for Better Auth CSRF validation.
|
||||
@@ -97,7 +98,7 @@ export const auth = betterAuth({
|
||||
try {
|
||||
// Validate URL format and ensure it's a proper origin
|
||||
const validatedUrl = new URL(url.trim());
|
||||
const cleanUrl = validatedUrl.origin; // Use origin to ensure no trailing paths
|
||||
const cleanUrl = validatedUrl.origin;
|
||||
console.info('Using BETTER_AUTH_URL:', cleanUrl);
|
||||
return cleanUrl;
|
||||
} catch (e) {
|
||||
@@ -107,7 +108,7 @@ export const auth = betterAuth({
|
||||
return defaultUrl;
|
||||
}
|
||||
})(),
|
||||
basePath: "/api/auth", // Specify the base path for auth endpoints
|
||||
basePath: withBase("/api/auth"), // Specify the base path for auth endpoints
|
||||
|
||||
// Trusted origins - this is how we support multiple access URLs.
|
||||
// Uses the function form so that the origin can be auto-detected from
|
||||
@@ -150,8 +151,8 @@ export const auth = betterAuth({
|
||||
plugins: [
|
||||
// OIDC Provider plugin - allows this app to act as an OIDC provider
|
||||
oidcProvider({
|
||||
loginPage: "/login",
|
||||
consentPage: "/oauth/consent",
|
||||
loginPage: withBase("/login"),
|
||||
consentPage: withBase("/oauth/consent"),
|
||||
// Allow dynamic client registration for flexibility
|
||||
allowDynamicClientRegistration: true,
|
||||
// Note: trustedClients would be configured here if Better Auth supports it
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
|
||||
const originalBaseUrl = process.env.BASE_URL;
|
||||
const originalWindow = (globalThis as { window?: unknown }).window;
|
||||
|
||||
async function loadModule(baseUrl?: string, runtimeWindowBasePath?: string) {
|
||||
if (baseUrl === undefined) {
|
||||
delete process.env.BASE_URL;
|
||||
} else {
|
||||
process.env.BASE_URL = baseUrl;
|
||||
}
|
||||
|
||||
if (runtimeWindowBasePath === undefined) {
|
||||
if (originalWindow === undefined) {
|
||||
delete (globalThis as { window?: unknown }).window;
|
||||
} else {
|
||||
(globalThis as { window?: unknown }).window = originalWindow;
|
||||
const restoredWindow = (globalThis as { window?: { __GITEA_MIRROR_BASE_PATH__?: string } }).window;
|
||||
if (typeof restoredWindow === "object" && restoredWindow !== null) {
|
||||
delete restoredWindow.__GITEA_MIRROR_BASE_PATH__;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(globalThis as { window?: { __GITEA_MIRROR_BASE_PATH__?: string } }).window = {
|
||||
__GITEA_MIRROR_BASE_PATH__: runtimeWindowBasePath,
|
||||
};
|
||||
}
|
||||
|
||||
return import(`./base-path.ts?case=${encodeURIComponent(baseUrl ?? "default")}-${Date.now()}-${Math.random()}`);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
if (originalBaseUrl === undefined) {
|
||||
delete process.env.BASE_URL;
|
||||
} else {
|
||||
process.env.BASE_URL = originalBaseUrl;
|
||||
}
|
||||
|
||||
if (originalWindow === undefined) {
|
||||
delete (globalThis as { window?: unknown }).window;
|
||||
} else {
|
||||
(globalThis as { window?: unknown }).window = originalWindow;
|
||||
}
|
||||
});
|
||||
|
||||
describe("base-path helpers", () => {
|
||||
test("defaults to root paths", async () => {
|
||||
const mod = await loadModule(undefined);
|
||||
|
||||
expect(mod.BASE_PATH).toBe("/");
|
||||
expect(mod.withBase("/api/health")).toBe("/api/health");
|
||||
expect(mod.withBase("repositories")).toBe("/repositories");
|
||||
expect(mod.stripBasePath("/config")).toBe("/config");
|
||||
});
|
||||
|
||||
test("normalizes prefixed base paths", async () => {
|
||||
const mod = await loadModule("mirror/");
|
||||
|
||||
expect(mod.BASE_PATH).toBe("/mirror");
|
||||
expect(mod.withBase("/api/health")).toBe("/mirror/api/health");
|
||||
expect(mod.withBase("repositories")).toBe("/mirror/repositories");
|
||||
expect(mod.stripBasePath("/mirror/config")).toBe("/config");
|
||||
expect(mod.stripBasePath("/mirror")).toBe("/");
|
||||
});
|
||||
|
||||
test("keeps absolute URLs unchanged", async () => {
|
||||
const mod = await loadModule("/mirror");
|
||||
|
||||
expect(mod.withBase("https://example.com/path")).toBe("https://example.com/path");
|
||||
});
|
||||
|
||||
test("uses browser runtime base path when process env is unset", async () => {
|
||||
const mod = await loadModule(undefined, "/runtime");
|
||||
|
||||
expect(mod.BASE_PATH).toBe("/runtime");
|
||||
expect(mod.withBase("/api/health")).toBe("/runtime/api/health");
|
||||
expect(mod.stripBasePath("/runtime/config")).toBe("/config");
|
||||
});
|
||||
|
||||
test("prefers process env base path over browser runtime value", async () => {
|
||||
const mod = await loadModule("/env", "/runtime");
|
||||
|
||||
expect(mod.BASE_PATH).toBe("/env");
|
||||
expect(mod.withBase("/api/health")).toBe("/env/api/health");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
const URL_SCHEME_REGEX = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
|
||||
const BASE_PATH_WINDOW_KEY = "__GITEA_MIRROR_BASE_PATH__";
|
||||
|
||||
function normalizeBasePath(basePath: string | null | undefined): string {
|
||||
if (!basePath) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
let normalized = basePath.trim();
|
||||
if (!normalized) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
if (!normalized.startsWith("/")) {
|
||||
normalized = `/${normalized}`;
|
||||
}
|
||||
|
||||
normalized = normalized.replace(/\/+$/, "");
|
||||
return normalized || "/";
|
||||
}
|
||||
|
||||
function resolveRuntimeBasePath(): string {
|
||||
if (typeof process !== "undefined" && typeof process.env?.BASE_URL === "string") {
|
||||
return normalizeBasePath(process.env.BASE_URL);
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
const runtimeBasePath = (window as Window & { [BASE_PATH_WINDOW_KEY]?: string })[BASE_PATH_WINDOW_KEY];
|
||||
if (typeof runtimeBasePath === "string") {
|
||||
return normalizeBasePath(runtimeBasePath);
|
||||
}
|
||||
}
|
||||
|
||||
return "/";
|
||||
}
|
||||
|
||||
export function getBasePath(): string {
|
||||
return resolveRuntimeBasePath();
|
||||
}
|
||||
|
||||
export const BASE_PATH = getBasePath();
|
||||
export { BASE_PATH_WINDOW_KEY };
|
||||
|
||||
export function withBase(path: string): string {
|
||||
const basePath = getBasePath();
|
||||
|
||||
if (!path) {
|
||||
return basePath === "/" ? "/" : `${basePath}/`;
|
||||
}
|
||||
|
||||
if (URL_SCHEME_REGEX.test(path) || path.startsWith("//")) {
|
||||
return path;
|
||||
}
|
||||
|
||||
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
||||
if (basePath === "/") {
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
return `${basePath}${normalizedPath}`;
|
||||
}
|
||||
|
||||
export function stripBasePath(pathname: string): string {
|
||||
const basePath = getBasePath();
|
||||
|
||||
if (!pathname) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
if (basePath === "/") {
|
||||
return pathname;
|
||||
}
|
||||
|
||||
if (pathname === basePath || pathname === `${basePath}/`) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
if (pathname.startsWith(`${basePath}/`)) {
|
||||
return pathname.slice(basePath.length) || "/";
|
||||
}
|
||||
|
||||
return pathname;
|
||||
}
|
||||
@@ -308,6 +308,13 @@ export async function initializeConfigFromEnv(): Promise<void> {
|
||||
mirrorPullRequests: envConfig.mirror.mirrorPullRequests ?? existingConfig?.[0]?.giteaConfig?.mirrorPullRequests ?? false,
|
||||
mirrorLabels: envConfig.mirror.mirrorLabels ?? existingConfig?.[0]?.giteaConfig?.mirrorLabels ?? false,
|
||||
mirrorMilestones: envConfig.mirror.mirrorMilestones ?? existingConfig?.[0]?.giteaConfig?.mirrorMilestones ?? false,
|
||||
// Backup options — preserve existing values so UI-configured settings survive restart
|
||||
backupStrategy: existingConfig?.[0]?.giteaConfig?.backupStrategy ?? 'on-force-push',
|
||||
backupBeforeSync: existingConfig?.[0]?.giteaConfig?.backupBeforeSync ?? true,
|
||||
backupRetentionCount: existingConfig?.[0]?.giteaConfig?.backupRetentionCount ?? 5,
|
||||
backupRetentionDays: existingConfig?.[0]?.giteaConfig?.backupRetentionDays ?? 30,
|
||||
backupDirectory: existingConfig?.[0]?.giteaConfig?.backupDirectory || undefined,
|
||||
blockSyncOnBackupFailure: existingConfig?.[0]?.giteaConfig?.blockSyncOnBackupFailure ?? true,
|
||||
};
|
||||
|
||||
// Build schedule config with support for interval as string or number
|
||||
|
||||
@@ -789,7 +789,7 @@ describe("Enhanced Gitea Operations", () => {
|
||||
expect(mockMirrorGitRepoLabelsToGitea).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("continues incremental issue and PR syncing when metadata was previously synced", async () => {
|
||||
test("skips issues and PRs when metadata shows they were already synced", async () => {
|
||||
const config: Partial<Config> = {
|
||||
userId: "user123",
|
||||
githubConfig: {
|
||||
@@ -848,9 +848,10 @@ describe("Enhanced Gitea Operations", () => {
|
||||
}
|
||||
);
|
||||
|
||||
// All metadata components were previously synced, so none should be called again
|
||||
expect(mockMirrorGitHubReleasesToGitea).not.toHaveBeenCalled();
|
||||
expect(mockMirrorGitRepoIssuesToGitea).toHaveBeenCalledTimes(1);
|
||||
expect(mockMirrorGitRepoPullRequestsToGitea).toHaveBeenCalledTimes(1);
|
||||
expect(mockMirrorGitRepoIssuesToGitea).not.toHaveBeenCalled();
|
||||
expect(mockMirrorGitRepoPullRequestsToGitea).not.toHaveBeenCalled();
|
||||
expect(mockMirrorGitRepoLabelsToGitea).not.toHaveBeenCalled();
|
||||
expect(mockMirrorGitRepoMilestonesToGitea).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -556,6 +556,9 @@ export async function syncGiteaRepoEnhanced({
|
||||
}
|
||||
|
||||
// Update mirror interval if needed
|
||||
// NOTE: Gitea/Forgejo's PATCH /repos/{owner}/{repo} API does not support
|
||||
// updating mirror credentials (mirror_username/mirror_password). Repos that
|
||||
// were originally migrated without credentials must be deleted and re-mirrored.
|
||||
if (config.giteaConfig?.mirrorInterval) {
|
||||
try {
|
||||
console.log(`[Sync] Updating mirror interval for ${repoOwner}/${repoName} to ${config.giteaConfig.mirrorInterval}`);
|
||||
@@ -603,10 +606,12 @@ export async function syncGiteaRepoEnhanced({
|
||||
!!config.giteaConfig?.mirrorReleases && !skipMetadataForStarred;
|
||||
const shouldMirrorIssuesThisRun =
|
||||
!!config.giteaConfig?.mirrorIssues &&
|
||||
!skipMetadataForStarred;
|
||||
!skipMetadataForStarred &&
|
||||
!metadataState.components.issues;
|
||||
const shouldMirrorPullRequests =
|
||||
!!config.giteaConfig?.mirrorPullRequests &&
|
||||
!skipMetadataForStarred;
|
||||
!skipMetadataForStarred &&
|
||||
!metadataState.components.pullRequests;
|
||||
const shouldMirrorLabels =
|
||||
!!config.giteaConfig?.mirrorLabels &&
|
||||
!skipMetadataForStarred &&
|
||||
@@ -680,6 +685,13 @@ export async function syncGiteaRepoEnhanced({
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
config.giteaConfig?.mirrorIssues &&
|
||||
metadataState.components.issues
|
||||
) {
|
||||
console.log(
|
||||
`[Sync] Issues already mirrored for ${repository.name}; skipping to avoid duplicates`
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldMirrorPullRequests) {
|
||||
@@ -710,6 +722,13 @@ export async function syncGiteaRepoEnhanced({
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
config.giteaConfig?.mirrorPullRequests &&
|
||||
metadataState.components.pullRequests
|
||||
) {
|
||||
console.log(
|
||||
`[Sync] Pull requests already mirrored for ${repository.name}; skipping`
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldMirrorLabels) {
|
||||
|
||||
+75
-5
@@ -815,8 +815,10 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
service: "git",
|
||||
};
|
||||
|
||||
// Add authentication for private repositories
|
||||
if (repository.isPrivate) {
|
||||
// Always send authentication credentials so Gitea/Forgejo stores them
|
||||
// for subsequent mirror fetches. This prevents "terminal prompts disabled"
|
||||
// errors on public repos and raises GitHub API rate limits.
|
||||
{
|
||||
const githubOwner =
|
||||
(
|
||||
config.githubConfig as typeof config.githubConfig & {
|
||||
@@ -1501,10 +1503,13 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
lfs: config.giteaConfig?.lfs || false,
|
||||
private: repository.isPrivate,
|
||||
description: repository.description?.trim() || "",
|
||||
service: "git",
|
||||
};
|
||||
|
||||
// Add authentication for private repositories
|
||||
if (repository.isPrivate) {
|
||||
// Always send authentication credentials so Gitea/Forgejo stores them
|
||||
// for subsequent mirror fetches. This prevents "terminal prompts disabled"
|
||||
// errors on public repos and raises GitHub API rate limits.
|
||||
{
|
||||
const githubOwner =
|
||||
(
|
||||
config.githubConfig as typeof config.githubConfig & {
|
||||
@@ -2715,7 +2720,7 @@ export async function mirrorGitHubReleasesToGitea({
|
||||
if (existingNote !== releaseNote || existingRelease.name !== (release.name || release.tag_name)) {
|
||||
console.log(`[Releases] Updating existing release ${release.tag_name} with new changelog/title`);
|
||||
|
||||
await httpPut(
|
||||
await httpPatch(
|
||||
`${config.giteaConfig.url}/api/v1/repos/${repoOwner}/${repoName}/releases/${existingRelease.id}`,
|
||||
{
|
||||
tag_name: release.tag_name,
|
||||
@@ -2829,6 +2834,71 @@ export async function mirrorGitHubReleasesToGitea({
|
||||
}
|
||||
|
||||
console.log(`✅ Mirrored/Updated ${mirroredCount} releases to Gitea (${skippedCount} already up-to-date)`);
|
||||
|
||||
// Enforce release retention limit by removing the oldest excess releases from Gitea
|
||||
try {
|
||||
// Paginate to fetch ALL Gitea releases (API max is 100 per page)
|
||||
const allGiteaReleases: Array<{ id: number; tag_name: string; created_at: string }> = [];
|
||||
let cleanupPage = 1;
|
||||
while (true) {
|
||||
const pageResponse = await httpGet(
|
||||
`${config.giteaConfig.url}/api/v1/repos/${repoOwner}/${repoName}/releases?per_page=100&page=${cleanupPage}`,
|
||||
{
|
||||
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
|
||||
}
|
||||
).catch(() => null);
|
||||
|
||||
if (!pageResponse?.data || !Array.isArray(pageResponse.data) || pageResponse.data.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
allGiteaReleases.push(...pageResponse.data);
|
||||
|
||||
if (pageResponse.data.length < 100) {
|
||||
break;
|
||||
}
|
||||
cleanupPage++;
|
||||
}
|
||||
|
||||
if (allGiteaReleases.length > releaseLimit) {
|
||||
const excessCount = allGiteaReleases.length - releaseLimit;
|
||||
|
||||
// Sort by created_at ascending (oldest first) so we delete the oldest excess
|
||||
const sorted = [...allGiteaReleases].sort(
|
||||
(a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
|
||||
);
|
||||
|
||||
const toDelete = sorted.slice(0, excessCount);
|
||||
|
||||
console.log(
|
||||
`[Releases] Enforcing retention limit (${releaseLimit}): ${allGiteaReleases.length} releases found, removing ${toDelete.length} oldest excess release(s)`
|
||||
);
|
||||
|
||||
for (const excess of toDelete) {
|
||||
try {
|
||||
await httpDelete(
|
||||
`${config.giteaConfig.url}/api/v1/repos/${repoOwner}/${repoName}/releases/${excess.id}`,
|
||||
{
|
||||
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
|
||||
}
|
||||
);
|
||||
console.log(`[Releases] Deleted excess release: ${excess.tag_name}`);
|
||||
} catch (deleteError) {
|
||||
console.error(
|
||||
`[Releases] Failed to delete excess release ${excess.tag_name}: ${
|
||||
deleteError instanceof Error ? deleteError.message : String(deleteError)
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.warn(
|
||||
`[Releases] Release retention cleanup failed: ${
|
||||
cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function mirrorGitRepoPullRequestsToGitea({
|
||||
|
||||
+2
-1
@@ -2,8 +2,9 @@ import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { httpRequest, HttpError } from "@/lib/http-client";
|
||||
import type { RepoStatus } from "@/types/Repository";
|
||||
import { withBase } from "@/lib/base-path";
|
||||
|
||||
export const API_BASE = "/api";
|
||||
export const API_BASE = withBase("/api");
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
|
||||
@@ -52,12 +52,12 @@ describe("buildGithubSourceAuthPayload", () => {
|
||||
expect(auth.auth_token).toBe("ghp_trimmed");
|
||||
});
|
||||
|
||||
test("throws when token is missing", () => {
|
||||
expect(() =>
|
||||
buildGithubSourceAuthPayload({
|
||||
token: " ",
|
||||
githubUsername: "user",
|
||||
})
|
||||
).toThrow("GitHub token is required to mirror private repositories.");
|
||||
test("returns empty object when token is missing", () => {
|
||||
const result = buildGithubSourceAuthPayload({
|
||||
token: " ",
|
||||
githubUsername: "user",
|
||||
});
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,8 @@ export interface GithubSourceAuthPayload {
|
||||
auth_token: string;
|
||||
}
|
||||
|
||||
export type GithubSourceAuthPayloadOrEmpty = GithubSourceAuthPayload | Record<string, never>;
|
||||
|
||||
const DEFAULT_GITHUB_AUTH_USERNAME = "x-access-token";
|
||||
|
||||
function normalize(value?: string | null): string {
|
||||
@@ -18,18 +20,19 @@ function normalize(value?: string | null): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build source credentials for private GitHub repository mirroring.
|
||||
* Build source credentials for GitHub repository mirroring.
|
||||
* GitHub expects username + token-as-password over HTTPS (not the GitLab-style "oauth2" username).
|
||||
* Returns an empty object when no token is available, allowing callers to use it unconditionally.
|
||||
*/
|
||||
export function buildGithubSourceAuthPayload({
|
||||
token,
|
||||
githubOwner,
|
||||
githubUsername,
|
||||
repositoryOwner,
|
||||
}: BuildGithubSourceAuthPayloadParams): GithubSourceAuthPayload {
|
||||
}: BuildGithubSourceAuthPayloadParams): GithubSourceAuthPayloadOrEmpty {
|
||||
const normalizedToken = normalize(token);
|
||||
if (!normalizedToken) {
|
||||
throw new Error("GitHub token is required to mirror private repositories.");
|
||||
return {};
|
||||
}
|
||||
|
||||
const authUsername =
|
||||
|
||||
+34
-1
@@ -9,6 +9,13 @@ import { auth } from './lib/auth';
|
||||
import { isHeaderAuthEnabled, authenticateWithHeaders } from './lib/auth-header';
|
||||
import { initializeConfigFromEnv } from './lib/env-config-loader';
|
||||
import { db, users } from './lib/db';
|
||||
import { getBasePath } from './lib/base-path';
|
||||
|
||||
const ASTRO_INTERNAL_ASSET_PATH_PATTERN = /(["'])\/(_astro\/|_server-islands\/|_image\b)/g;
|
||||
|
||||
function prefixAstroInternalAssetPaths(html: string, basePath: string): string {
|
||||
return html.replace(ASTRO_INTERNAL_ASSET_PATH_PATTERN, `$1${basePath}/$2`);
|
||||
}
|
||||
|
||||
// Flag to track if recovery has been initialized
|
||||
let recoveryInitialized = false;
|
||||
@@ -21,6 +28,8 @@ let envConfigInitialized = false;
|
||||
let envConfigCheckCount = 0; // Track attempts to avoid excessive checking
|
||||
|
||||
export const onRequest = defineMiddleware(async (context, next) => {
|
||||
const basePath = getBasePath();
|
||||
|
||||
// First, try Better Auth session (cookie-based)
|
||||
try {
|
||||
const session = await auth.api.getSession({
|
||||
@@ -217,5 +226,29 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
||||
}
|
||||
|
||||
// Continue with the request
|
||||
return next();
|
||||
const response = await next();
|
||||
|
||||
if (basePath === "/") {
|
||||
return response;
|
||||
}
|
||||
|
||||
const contentType = response.headers.get("content-type") ?? "";
|
||||
if (!contentType.includes("text/html")) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const body = await response.text();
|
||||
const rewrittenBody = prefixAstroInternalAssetPaths(body, basePath);
|
||||
if (rewrittenBody === body) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const headers = new Headers(response.headers);
|
||||
headers.delete("content-length");
|
||||
|
||||
return new Response(rewrittenBody, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers,
|
||||
});
|
||||
});
|
||||
|
||||
+3
-2
@@ -2,6 +2,7 @@
|
||||
import '../styles/global.css';
|
||||
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||
import { NotFound } from '@/components/NotFound';
|
||||
import { withBase } from '@/lib/base-path';
|
||||
|
||||
const generator = Astro.generator;
|
||||
---
|
||||
@@ -10,7 +11,7 @@ const generator = Astro.generator;
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href={withBase('/favicon.svg')} />
|
||||
<meta name="generator" content={generator} />
|
||||
<title>Page Not Found - Gitea Mirror</title>
|
||||
<ThemeScript />
|
||||
@@ -34,4 +35,4 @@ const generator = Astro.generator;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,7 @@ import '../styles/global.css';
|
||||
import App from '@/components/layout/MainLayout';
|
||||
import { db, mirrorJobs } from '@/lib/db';
|
||||
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||
import { withBase } from '@/lib/base-path';
|
||||
|
||||
// Fetch activity data from the database
|
||||
let activityData = [];
|
||||
@@ -53,7 +54,7 @@ const handleRefresh = () => {
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href={withBase('/favicon.svg')} />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Activity Log - Gitea Mirror</title>
|
||||
<ThemeScript />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { auth } from "@/lib/auth";
|
||||
import type { APIRoute } from "astro";
|
||||
import { stripBasePath, withBase } from "@/lib/base-path";
|
||||
|
||||
export const ALL: APIRoute = async (ctx) => {
|
||||
// If you want to use rate limiting, make sure to set the 'x-forwarded-for' header
|
||||
@@ -9,7 +10,11 @@ export const ALL: APIRoute = async (ctx) => {
|
||||
}
|
||||
|
||||
try {
|
||||
return await auth.handler(ctx.request);
|
||||
const requestUrl = new URL(ctx.request.url);
|
||||
requestUrl.pathname = withBase(stripBasePath(requestUrl.pathname));
|
||||
const authRequest = new Request(requestUrl, ctx.request);
|
||||
|
||||
return await auth.handler(authRequest);
|
||||
} catch (error) {
|
||||
console.error("Auth handler error:", error);
|
||||
|
||||
@@ -18,7 +23,7 @@ export const ALL: APIRoute = async (ctx) => {
|
||||
if (url.pathname.includes('/sso/callback')) {
|
||||
// Redirect to error page for SSO errors
|
||||
return Response.redirect(
|
||||
`${ctx.url.origin}/auth-error?error=sso_callback_failed&error_description=${encodeURIComponent(
|
||||
`${ctx.url.origin}${withBase('/auth-error')}?error=sso_callback_failed&error_description=${encodeURIComponent(
|
||||
error instanceof Error ? error.message : "SSO authentication failed"
|
||||
)}`,
|
||||
302
|
||||
@@ -34,4 +39,4 @@ export const ALL: APIRoute = async (ctx) => {
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { db, ssoProviders } from "@/lib/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { nanoid } from "nanoid";
|
||||
import { normalizeOidcProviderConfig, OidcConfigError } from "@/lib/sso/oidc-config";
|
||||
import { withBase } from "@/lib/base-path";
|
||||
|
||||
// POST /api/auth/sso/register - Register a new SSO provider using Better Auth
|
||||
export async function POST(context: APIContext) {
|
||||
@@ -87,7 +88,9 @@ export async function POST(context: APIContext) {
|
||||
registrationBody.samlConfig = {
|
||||
entryPoint,
|
||||
cert,
|
||||
callbackUrl: callbackUrl || `${context.url.origin}/api/auth/sso/saml2/callback/${providerId}`,
|
||||
callbackUrl:
|
||||
callbackUrl ||
|
||||
`${context.url.origin}${withBase(`/api/auth/sso/saml2/callback/${providerId}`)}`,
|
||||
audience: audience || context.url.origin,
|
||||
wantAssertionsSigned,
|
||||
signatureAlgorithm,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
import Layout from '@/layouts/main.astro';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { withBase } from '@/lib/base-path';
|
||||
|
||||
const error = Astro.url.searchParams.get('error');
|
||||
const errorDescription = Astro.url.searchParams.get('error_description');
|
||||
@@ -30,13 +31,13 @@ const errorDescription = Astro.url.searchParams.get('error_description');
|
||||
<div class="mt-6 flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => window.location.href = '/login'}
|
||||
onClick={() => window.location.href = withBase('/login')}
|
||||
>
|
||||
Back to Login
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => window.location.href = '/'}
|
||||
onClick={() => window.location.href = withBase('/')}
|
||||
>
|
||||
Go Home
|
||||
</Button>
|
||||
@@ -44,4 +45,4 @@ const errorDescription = Astro.url.searchParams.get('error_description');
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
||||
@@ -7,13 +7,14 @@ import { db, configs } from '@/lib/db';
|
||||
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { SaveConfigApiRequest,SaveConfigApiResponse } from '@/types/config';
|
||||
import { withBase } from '@/lib/base-path';
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href={withBase('/favicon.svg')} />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Configuration - Gitea Mirror</title>
|
||||
<ThemeScript />
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
import MainLayout from '../../layouts/main.astro';
|
||||
import { withBase } from '@/lib/base-path';
|
||||
---
|
||||
|
||||
<MainLayout title="Advanced Topics - Gitea Mirror">
|
||||
<main class="max-w-5xl mx-auto px-4 py-12">
|
||||
<div class="sticky top-4 z-10 mb-6">
|
||||
<a
|
||||
href="/docs/"
|
||||
href={withBase('/docs/')}
|
||||
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-card text-foreground hover:bg-muted transition-colors border border-border focus:ring-2 focus:ring-ring outline-none"
|
||||
>
|
||||
<span aria-hidden="true">←</span> Back to Documentation
|
||||
@@ -51,8 +52,9 @@ import MainLayout from '../../layouts/main.astro';
|
||||
{ var: 'NODE_ENV', desc: 'Application environment', default: 'production' },
|
||||
{ var: 'PORT', desc: 'Server port', default: '4321' },
|
||||
{ var: 'HOST', desc: 'Server host', default: '0.0.0.0' },
|
||||
{ var: 'BASE_URL', desc: 'Application base path ("/" or e.g. "/mirror")', default: '/' },
|
||||
{ var: 'BETTER_AUTH_SECRET', desc: 'Authentication secret key', default: 'Auto-generated' },
|
||||
{ var: 'BETTER_AUTH_URL', desc: 'Authentication base URL', default: 'http://localhost:4321' },
|
||||
{ var: 'BETTER_AUTH_URL', desc: 'Authentication origin (scheme + host, no path). Any path is stripped automatically.', default: 'http://localhost:4321' },
|
||||
{ var: 'NODE_EXTRA_CA_CERTS', desc: 'Path to CA certificate file', default: 'None' },
|
||||
{ var: 'DATABASE_URL', desc: 'SQLite database path', default: './data/gitea-mirror.db' },
|
||||
].map((item, i) => (
|
||||
@@ -225,10 +227,20 @@ import MainLayout from '../../layouts/main.astro';
|
||||
BETTER_AUTH_URL=https://gitea-mirror.example.com
|
||||
PUBLIC_BETTER_AUTH_URL=https://gitea-mirror.example.com
|
||||
BETTER_AUTH_TRUSTED_ORIGINS=https://gitea-mirror.example.com`}</code></pre>
|
||||
</div>
|
||||
<div class="bg-muted/30 rounded p-3 mt-3">
|
||||
<pre class="text-sm"><code>{`# If deployed under a path prefix (example: /mirror):
|
||||
# BASE_URL handles the path — auth URLs stay as origin only
|
||||
BASE_URL=/mirror
|
||||
BETTER_AUTH_URL=https://git.example.com
|
||||
PUBLIC_BETTER_AUTH_URL=https://git.example.com
|
||||
BETTER_AUTH_TRUSTED_ORIGINS=https://git.example.com
|
||||
# → Auth endpoints resolve to: https://git.example.com/mirror/api/auth/*`}</code></pre>
|
||||
</div>
|
||||
<ul class="mt-3 space-y-1 text-sm">
|
||||
<li><code class="bg-red-500/10 px-1 rounded">BETTER_AUTH_URL</code> — Server-side auth base URL for callbacks and redirects</li>
|
||||
<li><code class="bg-red-500/10 px-1 rounded">PUBLIC_BETTER_AUTH_URL</code> — Client-side (browser) URL for auth API calls</li>
|
||||
<li><code class="bg-red-500/10 px-1 rounded">BASE_URL</code> — Application base path for path-prefix deployments (e.g. <code>/mirror</code>). This is handled separately from the auth URLs.</li>
|
||||
<li><code class="bg-red-500/10 px-1 rounded">BETTER_AUTH_URL</code> — Server-side auth origin, scheme + host only (e.g. <code>https://git.example.com</code>). Do <strong>not</strong> include the base path — it is applied automatically from <code>BASE_URL</code>.</li>
|
||||
<li><code class="bg-red-500/10 px-1 rounded">PUBLIC_BETTER_AUTH_URL</code> — Client-side (browser) auth origin. Same rule: origin only, no path.</li>
|
||||
<li><code class="bg-red-500/10 px-1 rounded">BETTER_AUTH_TRUSTED_ORIGINS</code> — Comma-separated origins allowed to make auth requests</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -243,9 +255,10 @@ BETTER_AUTH_TRUSTED_ORIGINS=https://gitea-mirror.example.com`}</code></pre>
|
||||
image: ghcr.io/raylabshq/gitea-mirror:latest
|
||||
environment:
|
||||
- BETTER_AUTH_SECRET=your-secret-key-min-32-chars
|
||||
- BETTER_AUTH_URL=https://gitea-mirror.example.com
|
||||
- PUBLIC_BETTER_AUTH_URL=https://gitea-mirror.example.com
|
||||
- BETTER_AUTH_TRUSTED_ORIGINS=https://gitea-mirror.example.com
|
||||
- BASE_URL=/mirror
|
||||
- BETTER_AUTH_URL=https://git.example.com
|
||||
- PUBLIC_BETTER_AUTH_URL=https://git.example.com
|
||||
- BETTER_AUTH_TRUSTED_ORIGINS=https://git.example.com
|
||||
# ... other settings ...`}</code></pre>
|
||||
</div>
|
||||
|
||||
@@ -509,4 +522,4 @@ ls -t "$BACKUP_DIR"/backup_*.tar.gz | tail -n +8 | xargs rm -f`}</code></pre>
|
||||
</section>
|
||||
</article>
|
||||
</main>
|
||||
</MainLayout>
|
||||
</MainLayout>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
import MainLayout from '../../layouts/main.astro';
|
||||
import { withBase } from '@/lib/base-path';
|
||||
---
|
||||
|
||||
<MainLayout title="Architecture - Gitea Mirror">
|
||||
<main class="max-w-5xl mx-auto px-4 py-12">
|
||||
<div class="sticky top-4 z-10 mb-6">
|
||||
<a
|
||||
href="/docs/"
|
||||
href={withBase('/docs/')}
|
||||
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-card text-foreground hover:bg-muted transition-colors border border-border focus:ring-2 focus:ring-ring outline-none"
|
||||
>
|
||||
<span aria-hidden="true">←</span> Back to Documentation
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
import MainLayout from '../../layouts/main.astro';
|
||||
import { withBase } from '@/lib/base-path';
|
||||
---
|
||||
|
||||
<MainLayout title="Authentication & SSO - Gitea Mirror">
|
||||
<main class="max-w-5xl mx-auto px-4 py-12">
|
||||
<div class="sticky top-4 z-10 mb-6">
|
||||
<a
|
||||
href="/docs/"
|
||||
href={withBase('/docs/')}
|
||||
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-card text-foreground hover:bg-muted transition-colors border border-border focus:ring-2 focus:ring-ring outline-none"
|
||||
>
|
||||
<span aria-hidden="true">←</span> Back to Documentation
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
import MainLayout from '../../layouts/main.astro';
|
||||
import { withBase } from '@/lib/base-path';
|
||||
---
|
||||
|
||||
<MainLayout title="CA Certificates - Gitea Mirror">
|
||||
<main class="max-w-5xl mx-auto px-4 py-12">
|
||||
<div class="sticky top-4 z-10 mb-6">
|
||||
<a
|
||||
href="/docs/"
|
||||
href={withBase('/docs/')}
|
||||
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-card text-foreground hover:bg-muted transition-colors border border-border focus:ring-2 focus:ring-ring outline-none"
|
||||
>
|
||||
<span aria-hidden="true">←</span> Back to Documentation
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
---
|
||||
import MainLayout from '../../layouts/main.astro';
|
||||
import { withBase } from '@/lib/base-path';
|
||||
|
||||
const envVars = [
|
||||
{ name: 'NODE_ENV', desc: 'Runtime environment', default: 'development', example: 'production' },
|
||||
{ name: 'BASE_URL', desc: 'Application base path', default: '/', example: '/mirror' },
|
||||
{ name: 'DATABASE_URL', desc: 'SQLite database URL', default: 'file:data/gitea-mirror.db', example: 'file:path/to/database.db' },
|
||||
{ name: 'JWT_SECRET', desc: 'Secret key for JWT auth', default: 'Auto-generated', example: 'your-secure-string' },
|
||||
{ name: 'HOST', desc: 'Server host', default: 'localhost', example: '0.0.0.0' },
|
||||
@@ -35,7 +37,7 @@ const giteaOptions = [
|
||||
<main class="max-w-5xl mx-auto px-4 py-12">
|
||||
<div class="sticky top-4 z-10 mb-6">
|
||||
<a
|
||||
href="/docs/"
|
||||
href={withBase('/docs/')}
|
||||
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-card text-foreground hover:bg-muted transition-colors border border-border focus:ring-2 focus:ring-ring outline-none"
|
||||
>
|
||||
<span aria-hidden="true">←</span> Back to Documentation
|
||||
@@ -509,4 +511,4 @@ curl http://your-server:port/api/health`}</code></pre>
|
||||
</section>
|
||||
</article>
|
||||
</main>
|
||||
</MainLayout>
|
||||
</MainLayout>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
import MainLayout from '../../layouts/main.astro';
|
||||
import { LuSettings, LuRocket, LuBookOpen, LuShield, LuKey, LuNetwork } from 'react-icons/lu';
|
||||
import { withBase } from '@/lib/base-path';
|
||||
|
||||
// Define our documentation pages directly
|
||||
const docs = [
|
||||
@@ -69,7 +70,7 @@ const sortedDocs = docs.sort((a, b) => a.order - b.order);
|
||||
|
||||
return (
|
||||
<a
|
||||
href={doc.href}
|
||||
href={withBase(doc.href)}
|
||||
class="group block p-7 border border-border rounded-2xl bg-card hover:bg-muted transition-colors shadow-lg focus:ring-2 focus:ring-ring outline-none"
|
||||
tabindex="0"
|
||||
>
|
||||
@@ -85,4 +86,4 @@ const sortedDocs = docs.sort((a, b) => a.order - b.order);
|
||||
})}
|
||||
</div>
|
||||
</main>
|
||||
</MainLayout>
|
||||
</MainLayout>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
import MainLayout from '../../layouts/main.astro';
|
||||
import { withBase } from '@/lib/base-path';
|
||||
---
|
||||
|
||||
<MainLayout title="Quick Start Guide - Gitea Mirror">
|
||||
<main class="max-w-5xl mx-auto px-4 py-12">
|
||||
<div class="sticky top-4 z-10 mb-6">
|
||||
<a
|
||||
href="/docs/"
|
||||
href={withBase('/docs/')}
|
||||
class="inline-flex items-center gap-2 px-3 py-1.5 rounded-md bg-card text-foreground hover:bg-muted transition-colors border border-border focus:ring-2 focus:ring-ring outline-none"
|
||||
>
|
||||
<span aria-hidden="true">←</span> Back to Documentation
|
||||
@@ -418,11 +419,11 @@ bun run start</code></pre>
|
||||
<div class="space-y-3">
|
||||
<div class="flex gap-3">
|
||||
<span class="text-primary">📖</span>
|
||||
<span>Check out the <a href="/docs/configuration" class="text-primary hover:underline font-medium">Configuration Guide</a> for advanced settings</span>
|
||||
<span>Check out the <a href={withBase('/docs/configuration')} class="text-primary hover:underline font-medium">Configuration Guide</a> for advanced settings</span>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<span class="text-primary">🏗️</span>
|
||||
<span>Review the <a href="/docs/architecture" class="text-primary hover:underline font-medium">Architecture Documentation</a> to understand the system</span>
|
||||
<span>Review the <a href={withBase('/docs/architecture')} class="text-primary hover:underline font-medium">Architecture Documentation</a> to understand the system</span>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<span class="text-primary">📊</span>
|
||||
@@ -434,4 +435,4 @@ bun run start</code></pre>
|
||||
</section>
|
||||
</article>
|
||||
</main>
|
||||
</MainLayout>
|
||||
</MainLayout>
|
||||
|
||||
@@ -4,6 +4,7 @@ import App from '@/components/layout/MainLayout';
|
||||
import { db, repositories, mirrorJobs, users } from '@/lib/db';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||
import { withBase } from '@/lib/base-path';
|
||||
|
||||
// Check if any users exist in the database
|
||||
const userCountResult = await db.select({ count: sql<number>`count(*)` }).from(users);
|
||||
@@ -11,7 +12,7 @@ const userCount = userCountResult[0]?.count || 0;
|
||||
|
||||
// Redirect to signup if no users exist
|
||||
if (userCount === 0) {
|
||||
return Astro.redirect('/signup');
|
||||
return Astro.redirect(withBase('/signup'));
|
||||
}
|
||||
|
||||
// Fetch data from the database
|
||||
@@ -59,7 +60,7 @@ try {
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link rel="icon" type="image/x-icon" href={withBase('/favicon.ico')} />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Dashboard - Gitea Mirror</title>
|
||||
<ThemeScript />
|
||||
|
||||
@@ -4,6 +4,7 @@ import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||
import { LoginPage } from '@/components/auth/LoginPage';
|
||||
import { db, users } from '@/lib/db';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { withBase } from '@/lib/base-path';
|
||||
|
||||
// Check if any users exist in the database
|
||||
const userCountResult = await db
|
||||
@@ -13,7 +14,7 @@ const userCount = userCountResult[0].count;
|
||||
|
||||
// Redirect to signup if no users exist
|
||||
if (userCount === 0) {
|
||||
return Astro.redirect('/signup');
|
||||
return Astro.redirect(withBase('/signup'));
|
||||
}
|
||||
|
||||
const generator = Astro.generator;
|
||||
@@ -23,7 +24,7 @@ const generator = Astro.generator;
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href={withBase('/favicon.svg')} />
|
||||
<meta name="generator" content={generator} />
|
||||
<title>Login - Gitea Mirror</title>
|
||||
<ThemeScript />
|
||||
|
||||
@@ -3,11 +3,12 @@ import '@/styles/global.css';
|
||||
import ConsentPage from '@/components/oauth/ConsentPage';
|
||||
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||
import Providers from '@/components/layout/Providers';
|
||||
import { withBase } from '@/lib/base-path';
|
||||
|
||||
// Check if user is authenticated
|
||||
const sessionCookie = Astro.cookies.get('better-auth-session');
|
||||
if (!sessionCookie) {
|
||||
return Astro.redirect('/login');
|
||||
return Astro.redirect(withBase('/login'));
|
||||
}
|
||||
---
|
||||
|
||||
@@ -15,7 +16,7 @@ if (!sessionCookie) {
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href={withBase('/favicon.svg')} />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Authorize Application - Gitea Mirror</title>
|
||||
<ThemeScript />
|
||||
@@ -25,4 +26,4 @@ if (!sessionCookie) {
|
||||
<ConsentPage client:load />
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import '../styles/global.css';
|
||||
import App from '@/components/layout/MainLayout';
|
||||
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||
import { withBase } from '@/lib/base-path';
|
||||
|
||||
---
|
||||
|
||||
@@ -9,7 +10,7 @@ import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href={withBase('/favicon.svg')} />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Organizations - Gitea Mirror</title>
|
||||
<ThemeScript />
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
import '../styles/global.css';
|
||||
import App from '@/components/layout/MainLayout';
|
||||
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||
import { withBase } from '@/lib/base-path';
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href={withBase('/favicon.svg')} />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Repositories - Gitea Mirror</title>
|
||||
<ThemeScript />
|
||||
|
||||
@@ -4,6 +4,7 @@ import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||
import { SignupPage } from '@/components/auth/SignupPage';
|
||||
import { db, users } from '@/lib/db';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { withBase } from '@/lib/base-path';
|
||||
|
||||
// Check if any users exist in the database
|
||||
const userCountResult = await db
|
||||
@@ -13,7 +14,7 @@ const userCount = userCountResult[0]?.count;
|
||||
|
||||
// Redirect to login if users already exist
|
||||
if (userCount !== null && Number(userCount) > 0) {
|
||||
return Astro.redirect('/login');
|
||||
return Astro.redirect(withBase('/login'));
|
||||
}
|
||||
|
||||
const generator = Astro.generator;
|
||||
@@ -23,7 +24,7 @@ const generator = Astro.generator;
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href={withBase('/favicon.svg')} />
|
||||
<meta name="generator" content={generator} />
|
||||
<title>Setup Admin Account - Gitea Mirror</title>
|
||||
<ThemeScript />
|
||||
|
||||
Generated
+18
-18
@@ -25,7 +25,7 @@ importers:
|
||||
version: 1.12.69
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.2.1
|
||||
version: 4.2.1(vite@7.3.1(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
version: 4.2.1(vite@7.3.2(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
'@types/canvas-confetti':
|
||||
specifier: ^1.9.0
|
||||
version: 1.9.0
|
||||
@@ -1053,8 +1053,8 @@ packages:
|
||||
decode-named-character-reference@1.3.0:
|
||||
resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
|
||||
|
||||
defu@6.1.4:
|
||||
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
||||
defu@6.1.7:
|
||||
resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
|
||||
|
||||
dequal@2.0.3:
|
||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||
@@ -2010,8 +2010,8 @@ packages:
|
||||
vfile@6.0.3:
|
||||
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
|
||||
|
||||
vite@7.3.1:
|
||||
resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
|
||||
vite@7.3.2:
|
||||
resolution: {integrity: sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -2146,12 +2146,12 @@ snapshots:
|
||||
'@astrojs/internal-helpers': 0.8.0
|
||||
'@types/react': 19.2.14
|
||||
'@types/react-dom': 19.2.3(@types/react@19.2.14)
|
||||
'@vitejs/plugin-react': 5.2.0(vite@7.3.1(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
'@vitejs/plugin-react': 5.2.0(vite@7.3.2(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
devalue: 5.6.4
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
ultrahtml: 1.6.0
|
||||
vite: 7.3.1(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.31.1)
|
||||
vite: 7.3.2(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.31.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- jiti
|
||||
@@ -2748,12 +2748,12 @@ snapshots:
|
||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.2.1
|
||||
'@tailwindcss/oxide-win32-x64-msvc': 4.2.1
|
||||
|
||||
'@tailwindcss/vite@4.2.1(vite@7.3.1(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.31.1))':
|
||||
'@tailwindcss/vite@4.2.1(vite@7.3.2(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.31.1))':
|
||||
dependencies:
|
||||
'@tailwindcss/node': 4.2.1
|
||||
'@tailwindcss/oxide': 4.2.1
|
||||
tailwindcss: 4.2.1
|
||||
vite: 7.3.1(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.31.1)
|
||||
vite: 7.3.2(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.31.1)
|
||||
|
||||
'@types/babel__core@7.20.5':
|
||||
dependencies:
|
||||
@@ -2823,7 +2823,7 @@ snapshots:
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
|
||||
'@vitejs/plugin-react@5.2.0(vite@7.3.1(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.31.1))':
|
||||
'@vitejs/plugin-react@5.2.0(vite@7.3.2(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.31.1))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0)
|
||||
@@ -2831,7 +2831,7 @@ snapshots:
|
||||
'@rolldown/pluginutils': 1.0.0-rc.3
|
||||
'@types/babel__core': 7.20.5
|
||||
react-refresh: 0.18.0
|
||||
vite: 7.3.1(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.31.1)
|
||||
vite: 7.3.2(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.31.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -2906,8 +2906,8 @@ snapshots:
|
||||
unist-util-visit: 5.1.0
|
||||
unstorage: 1.17.4
|
||||
vfile: 6.0.3
|
||||
vite: 7.3.1(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.31.1)
|
||||
vitefu: 1.1.2(vite@7.3.1(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
vite: 7.3.2(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.31.1)
|
||||
vitefu: 1.1.2(vite@7.3.2(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.31.1))
|
||||
xxhash-wasm: 1.1.0
|
||||
yargs-parser: 22.0.0
|
||||
zod: 4.3.6
|
||||
@@ -3044,7 +3044,7 @@ snapshots:
|
||||
dependencies:
|
||||
character-entities: 2.0.2
|
||||
|
||||
defu@6.1.4: {}
|
||||
defu@6.1.7: {}
|
||||
|
||||
dequal@2.0.3: {}
|
||||
|
||||
@@ -3208,7 +3208,7 @@ snapshots:
|
||||
dependencies:
|
||||
cookie-es: 1.2.2
|
||||
crossws: 0.3.5
|
||||
defu: 6.1.4
|
||||
defu: 6.1.7
|
||||
destr: 2.0.5
|
||||
iron-webcrypto: 1.2.1
|
||||
node-mock-http: 1.0.4
|
||||
@@ -4416,7 +4416,7 @@ snapshots:
|
||||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.3
|
||||
|
||||
vite@7.3.1(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.31.1):
|
||||
vite@7.3.2(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.31.1):
|
||||
dependencies:
|
||||
esbuild: 0.27.3
|
||||
fdir: 6.5.0(picomatch@4.0.4)
|
||||
@@ -4430,9 +4430,9 @@ snapshots:
|
||||
jiti: 2.6.1
|
||||
lightningcss: 1.31.1
|
||||
|
||||
vitefu@1.1.2(vite@7.3.1(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.31.1)):
|
||||
vitefu@1.1.2(vite@7.3.2(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.31.1)):
|
||||
optionalDependencies:
|
||||
vite: 7.3.1(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.31.1)
|
||||
vite: 7.3.2(@types/node@24.7.1)(jiti@2.6.1)(lightningcss@1.31.1)
|
||||
|
||||
web-namespaces@2.0.1: {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user