Compare commits
226 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f17dd038f | ||
|
|
921ab948a1 | ||
|
|
e7a102ee45 | ||
|
|
025df12bef | ||
|
|
60913a9f4d | ||
|
|
985c7e061c | ||
|
|
4d75d3514f | ||
|
|
5245d67f37 | ||
|
|
2cd7d911ed | ||
|
|
1c2391ea2e | ||
|
|
190e786449 | ||
|
|
fb27ddfee5 | ||
|
|
fd5e68c1d4 | ||
|
|
ea22df1296 | ||
|
|
080ad5deb4 | ||
|
|
71245cf56e | ||
|
|
1ccf670f81 | ||
|
|
cb266b9af0 | ||
|
|
fa5f7da5c4 | ||
|
|
3c808eb0c0 | ||
|
|
5e37c3bb84 | ||
|
|
847e94ca28 | ||
|
|
da497d54c8 | ||
|
|
79e0086a72 | ||
|
|
dc340666ef | ||
|
|
8b50a07c68 | ||
|
|
7dab4fb1d5 | ||
|
|
847823bbf8 | ||
|
|
e4e54722cf | ||
|
|
1eddbad908 | ||
|
|
a7083beff5 | ||
|
|
b21cd0b866 | ||
|
|
df644be769 | ||
|
|
204869fa3e | ||
|
|
e470256475 | ||
|
|
b65c360d61 | ||
|
|
ce46d33d29 | ||
|
|
f63633f97e | ||
|
|
3b53a29e71 | ||
|
|
64e73f9ca8 | ||
|
|
7d23894e5f | ||
|
|
8f2a4683c1 | ||
|
|
b5323ff8b4 | ||
|
|
7fee2adb51 | ||
|
|
af139ecb2d | ||
|
|
fb827724b6 | ||
|
|
2812b576d0 | ||
|
|
347188f43d | ||
|
|
beda2ce66c | ||
|
|
21e2f4717c | ||
|
|
b8dea1ee9c | ||
|
|
b27ff817f7 | ||
|
|
56bee451de | ||
|
|
0e9d54b517 | ||
|
|
7a04665b70 | ||
|
|
3a3ff314e0 | ||
|
|
fed74ee901 | ||
|
|
85ea502276 | ||
|
|
ffb7bd3cb0 | ||
|
|
b39d7a2179 | ||
|
|
bf99a95dc6 | ||
|
|
2ea917fdaa | ||
|
|
b841057f1a | ||
|
|
d588ce91b4 | ||
|
|
553396483e | ||
|
|
ebeabdb4fc | ||
|
|
ff209a6376 | ||
|
|
096e0c03ac | ||
|
|
63f20a7f04 | ||
|
|
34f741beef | ||
|
|
1f98f441f3 | ||
|
|
9c1ac76ff9 | ||
|
|
cf5027bafc | ||
|
|
6fd2774d43 | ||
|
|
8f379baad4 | ||
|
|
91fa3604b6 | ||
|
|
c0fff30fcb | ||
|
|
18de63d192 | ||
|
|
1fe20c3e54 | ||
|
|
7386b54a46 | ||
|
|
432a2bc54d | ||
|
|
f9d18f34ab | ||
|
|
cd86a09bbd | ||
|
|
1e2c1c686d | ||
|
|
f701574e67 | ||
|
|
4528be8cc6 | ||
|
|
80fd43ef42 | ||
|
|
3c52fe58aa | ||
|
|
319e7925ff | ||
|
|
5add8766a4 | ||
|
|
6ce70bb5bf | ||
|
|
f3aae2ec94 | ||
|
|
46d5ec46fc | ||
|
|
0caa53b67f | ||
|
|
18ecdbc252 | ||
|
|
51a6c8ca58 | ||
|
|
41b8806268 | ||
|
|
ac5c7800c1 | ||
|
|
13e7661f07 | ||
|
|
37e5b68bd5 | ||
|
|
89ca5abe7d | ||
|
|
2b78a6a4a8 | ||
|
|
c2f6e73054 | ||
|
|
c4b353aae8 | ||
|
|
4a54cf9009 | ||
|
|
fab4efd93a | ||
|
|
9f21cd6b1a | ||
|
|
9ef6017a23 | ||
|
|
502796371f | ||
|
|
b956b71c5f | ||
|
|
26b82e0f65 | ||
|
|
7c124a37d7 | ||
|
|
3e14edc571 | ||
|
|
a188869cae | ||
|
|
afac3b5ddc | ||
|
|
2ce4bb4373 | ||
|
|
5c9a3afaae | ||
|
|
de4e111095 | ||
|
|
8c4d9508c7 | ||
|
|
921eb5e07d | ||
|
|
ac1b09f7a1 | ||
|
|
9ee67ce77d | ||
|
|
92db61a2c9 | ||
|
|
cbf6e11de3 | ||
|
|
18855f09c4 | ||
|
|
b8965a9fd4 | ||
|
|
598e81ff45 | ||
|
|
fef6cbb60d | ||
|
|
c793be5863 | ||
|
|
d097ded6ee | ||
|
|
1b01a5e653 | ||
|
|
56988818d2 | ||
|
|
5a49726b0e | ||
|
|
987c4ec3ec | ||
|
|
444442fcca | ||
|
|
3fe2461031 | ||
|
|
ea7777a20f | ||
|
|
a3247c9c22 | ||
|
|
099bf7d36f | ||
|
|
10a14d88ef | ||
|
|
36f8d41d38 | ||
|
|
dd19131029 | ||
|
|
be5f2e6c3d | ||
|
|
d9bfc59a2d | ||
|
|
29a08ee3e3 | ||
|
|
b425cbce71 | ||
|
|
f54a7e6d71 | ||
|
|
d49599ff05 | ||
|
|
d99f597988 | ||
|
|
7dfb6b5d18 | ||
|
|
46e6b4b927 | ||
|
|
8bd3b8d3b1 | ||
|
|
78be49d4a7 | ||
|
|
c58bde1cc3 | ||
|
|
b4a2a14dd3 | ||
|
|
3fb71b666d | ||
|
|
e404490e75 | ||
|
|
b3856b4223 | ||
|
|
ad7418aef2 | ||
|
|
389f8dd292 | ||
|
|
067b5d8ccd | ||
|
|
6127a916f4 | ||
|
|
12ee065833 | ||
|
|
926737f1c5 | ||
|
|
fe94d97779 | ||
|
|
38a0d1b494 | ||
|
|
698eb0b507 | ||
|
|
0fb5f9e190 | ||
|
|
dacec93f55 | ||
|
|
b41438f686 | ||
|
|
df1738a44d | ||
|
|
afaac70bb8 | ||
|
|
da95c1d5fd | ||
|
|
8dc50f7ebf | ||
|
|
eafc44d112 | ||
|
|
25cff6fe8e | ||
|
|
29fe7ba895 | ||
|
|
fbcedc404a | ||
|
|
122848c970 | ||
|
|
4c15ecb1bf | ||
|
|
3209f70566 | ||
|
|
677bc0cb5b | ||
|
|
5693ae7822 | ||
|
|
814be1e9d0 | ||
|
|
4e3c4c2c67 | ||
|
|
46d6374ff0 | ||
|
|
4cd98dffc4 | ||
|
|
87ca3bc12f | ||
|
|
dd6554509c | ||
|
|
55465197d1 | ||
|
|
e255142e70 | ||
|
|
f2b64a61b8 | ||
|
|
0fba2cecac | ||
|
|
1aef433918 | ||
|
|
3f704ebb23 | ||
|
|
5797b9bba1 | ||
|
|
bb045b037b | ||
|
|
1a77a63a9a | ||
|
|
3a9b8380d4 | ||
|
|
5d5429ac71 | ||
|
|
de314cf174 | ||
|
|
e637d573a2 | ||
|
|
5f45a9a03d | ||
|
|
0920314679 | ||
|
|
1f6add5fff | ||
|
|
3ff15a46e7 | ||
|
|
465c812e7e | ||
|
|
794ea52e4d | ||
|
|
7b8ca7c3b8 | ||
|
|
f2c7728394 | ||
|
|
dcdb06ac39 | ||
|
|
9fa10dae00 | ||
|
|
99dd501a52 | ||
|
|
d4aa665873 | ||
|
|
0244133e7b | ||
|
|
6ea5e9efb0 | ||
|
|
8d7ca8dd8f | ||
|
|
8d2919717f | ||
|
|
1e06e2bd4b | ||
|
|
67080a7ce9 | ||
|
|
9d5db86bdf | ||
|
|
3458891511 | ||
|
|
d388f2e691 | ||
|
|
7bd862606b | ||
|
|
251baeb1aa | ||
|
|
e6a31512ac |
@@ -1,5 +0,0 @@
|
||||
Evaluate all the updates being made.
|
||||
Update CHANGELOG.md
|
||||
Use the chnages in the git log to determine if its a major, minor or a patch release.
|
||||
Update the package.json first before you push the tag.
|
||||
Never mention Claude Code in the release notes or in commit messages.
|
||||
@@ -1,3 +0,0 @@
|
||||
Generate release notes for the latest release.
|
||||
Use a temp md file to write the release notes.
|
||||
Do not check that file into git.
|
||||
139
.env.example
@@ -18,6 +18,7 @@ DATABASE_URL=sqlite://data/gitea-mirror.db
|
||||
# Generate with: openssl rand -base64 32
|
||||
BETTER_AUTH_SECRET=change-this-to-a-secure-random-string-in-production
|
||||
BETTER_AUTH_URL=http://localhost:4321
|
||||
# PUBLIC_BETTER_AUTH_URL=https://your-domain.com # Optional: Set this if accessing from different origins (e.g., IP and domain)
|
||||
# ENCRYPTION_SECRET=optional-encryption-key-for-token-encryption # Generate with: openssl rand -base64 48
|
||||
|
||||
# ===========================================
|
||||
@@ -26,45 +27,143 @@ BETTER_AUTH_URL=http://localhost:4321
|
||||
|
||||
# Docker Registry Configuration
|
||||
DOCKER_REGISTRY=ghcr.io
|
||||
DOCKER_IMAGE=arunavo4/gitea-mirror
|
||||
DOCKER_IMAGE=raylabshq/gitea-mirror:
|
||||
DOCKER_TAG=latest
|
||||
|
||||
# ===========================================
|
||||
# MIRROR CONFIGURATION (Optional)
|
||||
# Can also be configured via web UI
|
||||
# GITHUB CONFIGURATION
|
||||
# All settings can also be configured via web UI
|
||||
# ===========================================
|
||||
|
||||
# GitHub Configuration
|
||||
# Basic GitHub Settings
|
||||
# GITHUB_USERNAME=your-github-username
|
||||
# GITHUB_TOKEN=your-github-personal-access-token
|
||||
# SKIP_FORKS=false
|
||||
# GITHUB_TYPE=personal # Options: personal, organization
|
||||
|
||||
# Repository Selection
|
||||
# PRIVATE_REPOSITORIES=false
|
||||
# MIRROR_ISSUES=false
|
||||
# MIRROR_WIKI=false
|
||||
# PUBLIC_REPOSITORIES=true
|
||||
# INCLUDE_ARCHIVED=false
|
||||
# SKIP_FORKS=false
|
||||
# MIRROR_STARRED=false
|
||||
# STARRED_REPOS_ORG=starred # Organization name for starred repos
|
||||
|
||||
# Organization Settings
|
||||
# MIRROR_ORGANIZATIONS=false
|
||||
# PRESERVE_ORG_STRUCTURE=false
|
||||
# ONLY_MIRROR_ORGS=false
|
||||
# SKIP_STARRED_ISSUES=false
|
||||
|
||||
# Gitea Configuration
|
||||
# Mirror Strategy
|
||||
# MIRROR_STRATEGY=preserve # Options: preserve, single-org, flat-user, mixed
|
||||
|
||||
# Advanced GitHub Settings
|
||||
# SKIP_STARRED_ISSUES=false # Enable lightweight mode for starred repos
|
||||
|
||||
# ===========================================
|
||||
# GITEA CONFIGURATION
|
||||
# All settings can also be configured via web UI
|
||||
# ===========================================
|
||||
|
||||
# Basic Gitea Settings
|
||||
# GITEA_URL=http://gitea:3000
|
||||
# GITEA_TOKEN=your-local-gitea-token
|
||||
# GITEA_USERNAME=your-local-gitea-username
|
||||
# GITEA_ORGANIZATION=github-mirrors
|
||||
# GITEA_ORG_VISIBILITY=public
|
||||
# DELAY=3600
|
||||
# GITEA_ORGANIZATION=github-mirrors # Default organization for single-org strategy
|
||||
|
||||
# Repository Settings
|
||||
# GITEA_ORG_VISIBILITY=public # Options: public, private, limited, default
|
||||
# GITEA_MIRROR_INTERVAL=8h # Mirror sync interval (e.g., 30m, 1h, 8h, 24h) - automatically enables scheduler
|
||||
# GITEA_LFS=false # Enable LFS support
|
||||
# GITEA_CREATE_ORG=true # Auto-create organizations
|
||||
# GITEA_PRESERVE_VISIBILITY=false # Preserve GitHub repo visibility in Gitea
|
||||
|
||||
# Template Settings (for using repository templates)
|
||||
# GITEA_TEMPLATE_OWNER=template-owner
|
||||
# GITEA_TEMPLATE_REPO=template-repo
|
||||
|
||||
# Topic Settings
|
||||
# GITEA_ADD_TOPICS=true # Add topics to repositories
|
||||
# GITEA_TOPIC_PREFIX=gh- # Prefix for topics
|
||||
|
||||
# Fork Handling
|
||||
# GITEA_FORK_STRATEGY=reference # Options: skip, reference, full-copy
|
||||
|
||||
# ===========================================
|
||||
# OPTIONAL FEATURES
|
||||
# MIRROR OPTIONS
|
||||
# Control what gets mirrored from GitHub
|
||||
# ===========================================
|
||||
|
||||
# Database Cleanup Configuration
|
||||
# Release and Metadata
|
||||
# MIRROR_RELEASES=false # Mirror GitHub releases
|
||||
# RELEASE_LIMIT=10 # Maximum number of releases to mirror per repository
|
||||
# MIRROR_WIKI=false # Mirror wiki content
|
||||
|
||||
# Issue Tracking (requires MIRROR_METADATA=true)
|
||||
# MIRROR_METADATA=false # Master toggle for metadata mirroring
|
||||
# MIRROR_ISSUES=false # Mirror issues
|
||||
# MIRROR_PULL_REQUESTS=false # Mirror pull requests
|
||||
# MIRROR_LABELS=false # Mirror labels
|
||||
# MIRROR_MILESTONES=false # Mirror milestones
|
||||
|
||||
# ===========================================
|
||||
# AUTOMATION CONFIGURATION
|
||||
# Schedule automatic mirroring
|
||||
# ===========================================
|
||||
|
||||
# Basic Schedule Settings
|
||||
# SCHEDULE_ENABLED=false # When true, auto-imports and mirrors all repos on startup (v3.5.3+)
|
||||
# SCHEDULE_INTERVAL=3600 # Interval in seconds or cron expression (e.g., "0 2 * * *")
|
||||
# GITEA_MIRROR_INTERVAL=8h # Mirror sync interval (5m, 30m, 1h, 8h, 24h, 1d, 7d) - also triggers auto-start
|
||||
# AUTO_IMPORT_REPOS=true # Automatically discover and import new GitHub repositories during syncs
|
||||
# DELAY=3600 # Legacy: same as SCHEDULE_INTERVAL, kept for backward compatibility
|
||||
|
||||
# Execution Settings
|
||||
# SCHEDULE_CONCURRENT=false # Allow concurrent mirror operations
|
||||
# SCHEDULE_BATCH_SIZE=10 # Number of repos to process in parallel
|
||||
# SCHEDULE_PAUSE_BETWEEN_BATCHES=5000 # Pause between batches (ms)
|
||||
|
||||
# Retry Configuration
|
||||
# SCHEDULE_RETRY_ATTEMPTS=3
|
||||
# SCHEDULE_RETRY_DELAY=60000 # Delay between retries (ms)
|
||||
# SCHEDULE_TIMEOUT=3600000 # Max time for a mirror operation (ms)
|
||||
# SCHEDULE_AUTO_RETRY=true
|
||||
|
||||
# Update Detection
|
||||
# SCHEDULE_ONLY_MIRROR_UPDATED=false # Only mirror repos with updates
|
||||
# SCHEDULE_UPDATE_INTERVAL=86400000 # Check for updates interval (ms)
|
||||
# SCHEDULE_SKIP_RECENTLY_MIRRORED=true
|
||||
# SCHEDULE_RECENT_THRESHOLD=3600000 # Skip if mirrored within this time (ms)
|
||||
|
||||
# Maintenance
|
||||
# SCHEDULE_CLEANUP_BEFORE_MIRROR=false # Run cleanup before mirroring
|
||||
|
||||
# Notifications
|
||||
# SCHEDULE_NOTIFY_ON_FAILURE=true
|
||||
# SCHEDULE_NOTIFY_ON_SUCCESS=false
|
||||
# SCHEDULE_LOG_LEVEL=info # Options: error, warn, info, debug
|
||||
# SCHEDULE_TIMEZONE=UTC
|
||||
|
||||
# ===========================================
|
||||
# DATABASE CLEANUP CONFIGURATION
|
||||
# Automatic cleanup of old events and data
|
||||
# ===========================================
|
||||
|
||||
# Basic Cleanup Settings
|
||||
# CLEANUP_ENABLED=false
|
||||
# CLEANUP_RETENTION_DAYS=7
|
||||
# CLEANUP_RETENTION_DAYS=7 # Days to keep events
|
||||
|
||||
# TLS/SSL Configuration
|
||||
# GITEA_SKIP_TLS_VERIFY=false # WARNING: Only use for testing
|
||||
# Repository Cleanup (v3.4.0+)
|
||||
# CLEANUP_DELETE_FROM_GITEA=false # Delete repos from Gitea
|
||||
# CLEANUP_DELETE_IF_NOT_IN_GITHUB=false # Auto-remove repos that no longer exist in GitHub
|
||||
# CLEANUP_ORPHANED_REPO_ACTION=archive # Options: skip, archive, delete
|
||||
# CLEANUP_DRY_RUN=true # Test mode without actual deletion (set to false for production)
|
||||
|
||||
# Protected Repositories (comma-separated)
|
||||
# CLEANUP_PROTECTED_REPOS=important-repo,critical-project
|
||||
|
||||
# Cleanup Execution
|
||||
# CLEANUP_BATCH_SIZE=10
|
||||
# CLEANUP_PAUSE_BETWEEN_DELETES=2000 # Pause between deletions (ms)
|
||||
|
||||
# ===========================================
|
||||
# AUTHENTICATION CONFIGURATION
|
||||
@@ -79,3 +178,9 @@ DOCKER_TAG=latest
|
||||
# HEADER_AUTH_AUTO_PROVISION=false
|
||||
# HEADER_AUTH_ALLOWED_DOMAINS=example.com,company.org
|
||||
|
||||
# ===========================================
|
||||
# OPTIONAL FEATURES
|
||||
# ===========================================
|
||||
|
||||
# TLS/SSL Configuration
|
||||
# GITEA_SKIP_TLS_VERIFY=false # WARNING: Only use for testing
|
||||
BIN
.github/assets/activity.png
vendored
|
Before Width: | Height: | Size: 854 KiB After Width: | Height: | Size: 834 KiB |
BIN
.github/assets/configuration-2.png
vendored
Normal file
|
After Width: | Height: | Size: 986 KiB |
BIN
.github/assets/configuration.png
vendored
|
Before Width: | Height: | Size: 950 KiB After Width: | Height: | Size: 905 KiB |
BIN
.github/assets/configuration_mobile.png
vendored
|
Before Width: | Height: | Size: 255 KiB After Width: | Height: | Size: 270 KiB |
BIN
.github/assets/dashboard.png
vendored
|
Before Width: | Height: | Size: 943 KiB After Width: | Height: | Size: 908 KiB |
BIN
.github/assets/dashboard_mobile.png
vendored
|
Before Width: | Height: | Size: 215 KiB After Width: | Height: | Size: 241 KiB |
BIN
.github/assets/logo-new.png
vendored
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
.github/assets/logo-no-bg.png
vendored
|
Before Width: | Height: | Size: 1.5 MiB |
BIN
.github/assets/logo.png
vendored
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 24 KiB |
BIN
.github/assets/organisation.png
vendored
|
Before Width: | Height: | Size: 844 KiB After Width: | Height: | Size: 825 KiB |
BIN
.github/assets/organisation_mobile.png
vendored
|
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 246 KiB |
BIN
.github/assets/repositories.png
vendored
|
Before Width: | Height: | Size: 970 KiB After Width: | Height: | Size: 952 KiB |
BIN
.github/assets/repositories_mobile.png
vendored
|
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 237 KiB |
59
.github/ci/values-ci.yaml
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
image:
|
||||
registry: ghcr.io
|
||||
repository: raylabshq/gitea-mirror
|
||||
tag: ""
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 8080
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
hosts:
|
||||
- host: ci.example.com
|
||||
|
||||
route:
|
||||
enabled: true
|
||||
forceHTTPS: true
|
||||
domain: ["ci.example.com"]
|
||||
gateway: "dummy-gw"
|
||||
gatewayNamespace: "default"
|
||||
http:
|
||||
gatewaySection: "http"
|
||||
https:
|
||||
gatewaySection: "https"
|
||||
|
||||
gitea-mirror:
|
||||
nodeEnv: production
|
||||
core:
|
||||
databaseUrl: "file:data/gitea-mirror.db"
|
||||
betterAuthSecret: "dummy"
|
||||
betterAuthUrl: "http://localhost:4321"
|
||||
betterAuthTrustedOrigins: "http://localhost:4321"
|
||||
github:
|
||||
username: "ci-user"
|
||||
token: "not-used-in-template"
|
||||
type: "personal"
|
||||
privateRepositories: true
|
||||
skipForks: false
|
||||
starredCodeOnly: false
|
||||
gitea:
|
||||
url: "https://gitea.example.com"
|
||||
token: "not-used-in-template"
|
||||
username: "ci-user"
|
||||
organization: "github-mirrors"
|
||||
visibility: "public"
|
||||
mirror:
|
||||
releases: true
|
||||
wiki: true
|
||||
metadata: true
|
||||
issues: true
|
||||
pullRequests: true
|
||||
starred: false
|
||||
automation:
|
||||
schedule_enabled: true
|
||||
schedule_interval: "3600"
|
||||
cleanup:
|
||||
enabled: true
|
||||
interval: "2592000"
|
||||
7
.github/workflows/README.md
vendored
@@ -85,3 +85,10 @@ If a workflow fails:
|
||||
- Security vulnerabilities
|
||||
|
||||
For persistent issues, consider opening an issue in the repository.
|
||||
|
||||
|
||||
### Helm Test (`helm-test.yml`)
|
||||
|
||||
This workflow run on the main branch and pull requests. it:
|
||||
- Run yamllint to keep the formating unified
|
||||
- Run helm template with different value files
|
||||
|
||||
2
.github/workflows/astro-build-test.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: '1.2.9'
|
||||
bun-version: '1.2.16'
|
||||
|
||||
- name: Check lockfile and install dependencies
|
||||
run: |
|
||||
|
||||
82
.github/workflows/docker-build.yml
vendored
@@ -10,6 +10,10 @@ on:
|
||||
- 'package.json'
|
||||
- 'bun.lock*'
|
||||
- '.github/workflows/docker-build.yml'
|
||||
- 'docker-entrypoint.sh'
|
||||
- 'drizzle/**'
|
||||
- 'scripts/**'
|
||||
- 'src/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'Dockerfile'
|
||||
@@ -17,6 +21,10 @@ on:
|
||||
- 'package.json'
|
||||
- 'bun.lock*'
|
||||
- '.github/workflows/docker-build.yml'
|
||||
- 'docker-entrypoint.sh'
|
||||
- 'drizzle/**'
|
||||
- 'scripts/**'
|
||||
- 'src/**'
|
||||
schedule:
|
||||
- cron: '0 0 * * 0' # Weekly security scan on Sunday at midnight
|
||||
|
||||
@@ -48,7 +56,6 @@ jobs:
|
||||
|
||||
- name: Log into registry
|
||||
uses: docker/login-action@v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -89,6 +96,7 @@ jobs:
|
||||
type=sha,prefix=,suffix=,format=short
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
type=raw,value=${{ steps.tag_version.outputs.VERSION }}
|
||||
type=ref,event=pr,prefix=pr-
|
||||
|
||||
# Build and push Docker image
|
||||
- name: Build and push Docker image
|
||||
@@ -97,20 +105,83 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
load: ${{ github.event_name == 'pull_request' }}
|
||||
tags: ${{ github.event_name == 'pull_request' && 'gitea-mirror:scan' || steps.meta.outputs.tags }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
# Load image locally for security scanning (PRs only)
|
||||
- name: Load image for scanning
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
load: true
|
||||
tags: gitea-mirror:scan
|
||||
cache-from: type=gha
|
||||
|
||||
# Wait for image to be available in registry
|
||||
- name: Wait for image availability
|
||||
if: github.event_name != 'pull_request'
|
||||
run: |
|
||||
echo "Waiting for image to be available in registry..."
|
||||
sleep 5
|
||||
|
||||
# Add comment to PR with image details
|
||||
- name: Comment PR with image tag
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
const imageTag = `pr-${prNumber}`;
|
||||
const imagePath = `${{ env.REGISTRY }}/${{ env.IMAGE }}:${imageTag}`.toLowerCase();
|
||||
|
||||
const comment = `## 🐳 Docker Image Built Successfully
|
||||
|
||||
Your PR image is available for testing:
|
||||
|
||||
**Image Tag:** \`${imageTag}\`
|
||||
**Full Image Path:** \`${imagePath}\`
|
||||
|
||||
### Pull and Test
|
||||
\`\`\`bash
|
||||
docker pull ${imagePath}
|
||||
docker run -d \
|
||||
-p 4321:4321 \
|
||||
-e BETTER_AUTH_SECRET=your-secret-here \
|
||||
-e BETTER_AUTH_URL=http://localhost:4321 \
|
||||
--name gitea-mirror-test ${imagePath}
|
||||
\`\`\`
|
||||
|
||||
### Docker Compose Testing
|
||||
\`\`\`yaml
|
||||
services:
|
||||
gitea-mirror:
|
||||
image: ${imagePath}
|
||||
ports:
|
||||
- "4321:4321"
|
||||
environment:
|
||||
- BETTER_AUTH_SECRET=your-secret-here
|
||||
- BETTER_AUTH_URL=http://localhost:4321
|
||||
- BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:4321
|
||||
\`\`\`
|
||||
|
||||
> 💡 **Note:** PR images are tagged as \`pr-<number>\` and only built for \`linux/amd64\` to speed up CI.
|
||||
> Production images (\`latest\`, version tags) are multi-platform (\`linux/amd64\`, \`linux/arm64\`).
|
||||
|
||||
---
|
||||
📦 View in [GitHub Packages](https://github.com/${{ github.repository }}/pkgs/container/gitea-mirror)`;
|
||||
|
||||
github.rest.issues.createComment({
|
||||
issue_number: prNumber,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: comment
|
||||
});
|
||||
|
||||
# Docker Scout comprehensive security analysis
|
||||
- name: Docker Scout - Vulnerability Analysis & Recommendations
|
||||
uses: docker/scout-action@v1
|
||||
@@ -159,4 +230,3 @@ jobs:
|
||||
continue-on-error: true
|
||||
with:
|
||||
sarif_file: scout-results.sarif
|
||||
|
||||
|
||||
61
.github/workflows/helm-test.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name: Helm Chart CI
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'helm/gitea-mirror/**'
|
||||
- '.github/workflows/helm-test.yml'
|
||||
- '.github/ci/values-ci.yaml'
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'helm/gitea-mirror/**'
|
||||
- '.github/workflows/helm-test.yml'
|
||||
- '.github/ci/values-ci.yaml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
yamllint:
|
||||
name: Lint YAML
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install yamllint
|
||||
run: pip install --disable-pip-version-check yamllint
|
||||
- name: Run yamllint
|
||||
run: |
|
||||
yamllint -c helm/gitea-mirror/.yamllint helm/gitea-mirror
|
||||
|
||||
helm-template:
|
||||
name: Helm lint & template
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Helm
|
||||
uses: azure/setup-helm@v4
|
||||
with:
|
||||
version: v3.19.0
|
||||
- name: Helm lint
|
||||
run: |
|
||||
helm lint ./helm/gitea-mirror
|
||||
- name: Template with defaults
|
||||
run: |
|
||||
helm template test ./helm/gitea-mirror > /tmp/render-defaults.yaml
|
||||
test -s /tmp/render-defaults.yaml
|
||||
- name: Template with CI values
|
||||
run: |
|
||||
helm template test ./helm/gitea-mirror -f .github/ci/values-ci.yaml > /tmp/render-ci.yaml
|
||||
test -s /tmp/render-ci.yaml
|
||||
- name: Show a summary
|
||||
run: |
|
||||
echo "Rendered with defaults:"
|
||||
awk 'NR<=50{print} NR==51{print "..."; exit}' /tmp/render-defaults.yaml
|
||||
echo ""
|
||||
echo "Rendered with CI values:"
|
||||
awk 'NR<=50{print} NR==51{print "..."; exit}' /tmp/render-ci.yaml
|
||||
46
AGENTS.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
- `src/` – app code
|
||||
- `components/` (React, PascalCase files), `pages/` (Astro/API routes), `lib/` (domain + utilities, kebab-case), `hooks/`, `layouts/`, `styles/`, `tests/`, `types/`, `data/`, `content/`.
|
||||
- `scripts/` – operational TS scripts (DB init, recovery): e.g., `scripts/manage-db.ts`.
|
||||
- `drizzle/` – SQL migrations; `data/` – runtime SQLite (`gitea-mirror.db`).
|
||||
- `public/` – static assets; `dist/` – build output.
|
||||
- Key config: `astro.config.mjs`, `tsconfig.json` (alias `@/* → src/*`), `bunfig.toml` (test preload), `.env(.example)`.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
- Prereq: Bun `>= 1.2.9` (see `package.json`).
|
||||
- Setup: `bun run setup` – install deps and init DB.
|
||||
- Dev: `bun run dev` – start Astro dev server.
|
||||
- Build: `bun run build` – produce `dist/`.
|
||||
- Preview/Start: `bun run preview` (static preview) or `bun run start` (SSR entry).
|
||||
- Database: `bun run db:generate|migrate|push|studio` and `bun run manage-db init|check|fix|reset-users`.
|
||||
- Tests: `bun test` | `bun run test:watch` | `bun run test:coverage`.
|
||||
- Docker: see `docker-compose.yml` and variants in repo root.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
- Language: TypeScript, Astro, React.
|
||||
- Indentation: 2 spaces; keep existing semicolon/quote style in touched files.
|
||||
- Components: PascalCase `.tsx` in `src/components/` (e.g., `MainLayout.tsx`).
|
||||
- Modules/utils: kebab-case in `src/lib/` (e.g., `gitea-enhanced.ts`).
|
||||
- Imports: prefer alias `@/…` (configured in `tsconfig.json`).
|
||||
- Do not introduce new lint/format configs; follow current patterns.
|
||||
|
||||
## Testing Guidelines
|
||||
- Runner: Bun test (`bun:test`) with preload `src/tests/setup.bun.ts` (see `bunfig.toml`).
|
||||
- Location/Names: `**/*.test.ts(x)` under `src/**` (examples in `src/lib/**`).
|
||||
- Scope: add unit tests for new logic and API route tests for handlers.
|
||||
- Aim for meaningful coverage on DB, auth, and mirroring paths.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
- Commits: short, imperative, scoped when helpful (e.g., `lib: fix token parsing`, `ui: align buttons`).
|
||||
- PRs must include:
|
||||
- Summary, rationale, and testing steps/commands.
|
||||
- Linked issues (e.g., `Closes #123`).
|
||||
- Screenshots/gifs for UI changes.
|
||||
- Notes on DB/migration or .env impacts; update `docs/`/CHANGELOG if applicable.
|
||||
|
||||
## Security & Configuration Tips
|
||||
- Never commit secrets. Copy `.env.example` → `.env` and fill values; prefer `bun run startup-env-config` to validate.
|
||||
- SQLite files live in `data/`; avoid committing generated DBs.
|
||||
- Certificates (if used) reside in `certs/`; manage locally or via Docker secrets.
|
||||
165
CHANGELOG.md
@@ -7,6 +7,171 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Git LFS (Large File Storage) support for mirroring (#74)
|
||||
- New UI checkbox "Mirror LFS" in Mirror Options
|
||||
- Automatic LFS object transfer when enabled
|
||||
- Documentation for Gitea server LFS requirements
|
||||
- Repository "ignored" status to skip specific repos from mirroring (#75)
|
||||
- Repositories can be marked as ignored to exclude from all operations
|
||||
- Scheduler automatically skips ignored repositories
|
||||
- Enhanced error handling for all metadata mirroring operations
|
||||
- Individual try-catch blocks for issues, PRs, labels, milestones
|
||||
- Operations continue even if individual components fail
|
||||
- Support for BETTER_AUTH_TRUSTED_ORIGINS environment variable (#63)
|
||||
- Enables access via multiple URLs (local IP + domain)
|
||||
- Comma-separated trusted origins configuration
|
||||
- Proper documentation for multi-URL access patterns
|
||||
- Comprehensive fix report documentation
|
||||
|
||||
### Fixed
|
||||
- Fixed metadata mirroring authentication errors (#68)
|
||||
- Changed field checking from `username` to `defaultOwner` in metadata functions
|
||||
- Added proper field validation for all metadata operations
|
||||
- Fixed automatic mirroring scheduler issues (#72)
|
||||
- Improved interval parsing and error handling
|
||||
- Fixed OIDC authentication 500 errors with Authentik (#73)
|
||||
- Added URL validation in Better Auth configuration
|
||||
- Prevented undefined URL errors in auth callback
|
||||
- Fixed SSL certificate handling in Docker (#48)
|
||||
- NODE_EXTRA_CA_CERTS no longer gets overridden
|
||||
- Proper preservation of custom CA certificates
|
||||
- Fixed reverse proxy base domain issues (#63)
|
||||
- Better handling of custom subdomains
|
||||
- Support for trusted origins configuration
|
||||
- Fixed configuration persistence bugs (#49)
|
||||
- Config merging now preserves all fields
|
||||
- Retention period settings no longer reset
|
||||
- Fixed sync failures with improved error handling (#51)
|
||||
- Comprehensive error wrapping for all operations
|
||||
- Better error messages and logging
|
||||
|
||||
### Improved
|
||||
- Enhanced logging throughout metadata mirroring operations
|
||||
- Detailed success/failure messages for each component
|
||||
- Configuration details logged for debugging
|
||||
- Better configuration state management
|
||||
- Proper merging of loaded configs with defaults
|
||||
- Preservation of user settings on refresh
|
||||
- Updated documentation
|
||||
- Added LFS feature documentation
|
||||
- Updated README with new features
|
||||
- Enhanced CLAUDE.md with repository status definitions
|
||||
|
||||
## [3.7.1] - 2025-09-14
|
||||
|
||||
### Fixed
|
||||
- Cleanup archiving for mirror repositories now works reliably (refs #84; awaiting user confirmation).
|
||||
- Gitea rejects names violating the AlphaDashDot rule; archiving a mirror now uses a sanitized rename strategy (`archived-<name>`), with a timestamped fallback on conflicts or validation errors.
|
||||
- Owner resolution during cleanup no longer uses the GitHub owner by mistake. It prefers `mirroredLocation`, falls back to computed Gitea owner via configuration, and verifies location with a presence check to avoid `GetUserByName` 404s.
|
||||
- Repositories UI crash resolved when cleanup marked repos as archived.
|
||||
- Added `"archived"` to repository/job status enums, fixing Zod validation errors on the Repositories page.
|
||||
|
||||
### Changed
|
||||
- Archiving logic for mirror repos is non-destructive by design: data is preserved, repo is renamed with an archive marker, and mirror interval is reduced (best‑effort) to minimize sync attempts.
|
||||
- Cleanup service updates DB to `status: "archived"` and `isArchived: true` on successful archive path.
|
||||
|
||||
### Notes
|
||||
- This release addresses the scenario where a GitHub source disappears (deleted/banned), ensuring Gitea backups are preserved even when using `CLEANUP_DELETE_IF_NOT_IN_GITHUB=true` with `CLEANUP_ORPHANED_REPO_ACTION=archive`.
|
||||
- No database migration required.
|
||||
|
||||
## [3.2.6] - 2025-08-09
|
||||
|
||||
### Fixed
|
||||
- Added missing release asset mirroring functionality (APK, ZIP, Binary files)
|
||||
- Release assets (attachments) are now properly downloaded from GitHub and uploaded to Gitea
|
||||
- Fixed missing metadata component configuration checks
|
||||
|
||||
### Added
|
||||
- Full support for mirroring release assets/attachments
|
||||
- Debug logging for metadata component configuration to help troubleshoot mirroring issues
|
||||
- Download and upload progress logging for release assets
|
||||
|
||||
### Improved
|
||||
- Enhanced release mirroring to include all associated binary files and attachments
|
||||
- Better visibility into which metadata components are enabled/disabled
|
||||
- More detailed logging during the release asset transfer process
|
||||
|
||||
### Notes
|
||||
This patch adds the missing functionality to mirror release assets (APK, ZIP, Binary files, etc.) that was reported in Issue #68. Previously only release metadata was being mirrored, now all attachments are properly transferred to Gitea.
|
||||
|
||||
## [3.2.5] - 2025-08-09
|
||||
|
||||
### Fixed
|
||||
- Fixed critical authentication issue in releases mirroring that was still using encrypted tokens
|
||||
- Added missing repository existence check for releases mirroring function
|
||||
- Fixed "user does not exist [uid: 0]" error specifically affecting GitHub releases synchronization
|
||||
|
||||
### Improved
|
||||
- Enhanced releases mirroring with duplicate detection to avoid errors on re-runs
|
||||
- Better error handling and logging for release operations with [Releases] prefix
|
||||
- Added individual release error handling to continue mirroring even if some releases fail
|
||||
|
||||
### Notes
|
||||
This patch completes the authentication fixes started in v3.2.4, specifically addressing the releases mirroring function that was accidentally missed in the previous update.
|
||||
|
||||
## [3.2.4] - 2025-08-09
|
||||
|
||||
### Fixed
|
||||
- Fixed critical authentication issue causing "user does not exist [uid: 0]" errors during metadata mirroring (Issue #68)
|
||||
- Fixed inconsistent token handling across Gitea API calls
|
||||
- Fixed metadata mirroring functions attempting to operate on non-existent repositories
|
||||
- Fixed organization creation failing silently without proper error messages
|
||||
|
||||
### Added
|
||||
- Pre-flight authentication validation for all Gitea operations
|
||||
- Repository existence verification before metadata mirroring
|
||||
- Graceful fallback to user account when organization creation fails due to permissions
|
||||
- Authentication validation utilities for debugging configuration issues
|
||||
- Diagnostic test scripts for troubleshooting authentication problems
|
||||
|
||||
### Improved
|
||||
- Enhanced error messages with specific guidance for authentication failures
|
||||
- Better identification and logging of permission-related errors
|
||||
- More robust organization creation with retry logic and better error handling
|
||||
- Consistent token decryption across all API operations
|
||||
- Clearer error reporting for metadata mirroring failures
|
||||
|
||||
### Security
|
||||
- Fixed potential exposure of encrypted tokens in API calls
|
||||
- Improved token handling to ensure proper decryption before use
|
||||
|
||||
## [3.2.0] - 2025-07-31
|
||||
|
||||
### Fixed
|
||||
- Fixed Zod validation error in activity logs by correcting invalid "success" status values to "synced"
|
||||
- Resolved activity fetch API errors that occurred after mirroring operations
|
||||
|
||||
### Changed
|
||||
- Improved error handling and validation for mirror job status tracking
|
||||
- Enhanced reliability of organization creation and mirroring processes
|
||||
|
||||
### Internal
|
||||
- Consolidated Gitea integration modules for better maintainability
|
||||
- Improved test coverage for mirror operations
|
||||
|
||||
## [3.1.1] - 2025-07-30
|
||||
|
||||
### Fixed
|
||||
- Various bug fixes and stability improvements
|
||||
|
||||
## [3.1.0] - 2025-07-21
|
||||
|
||||
### Added
|
||||
- Support for GITHUB_EXCLUDED_ORGS environment variable to filter out specific organizations during discovery
|
||||
- New textarea UI component for improved form inputs in configuration
|
||||
|
||||
### Fixed
|
||||
- Fixed test failures related to mirror strategy configuration location
|
||||
- Corrected organization repository routing logic for different mirror strategies
|
||||
- Fixed starred repositories organization routing bug
|
||||
- Resolved SSO and OIDC authentication issues
|
||||
|
||||
### Improved
|
||||
- Enhanced organization configuration for better repository routing control
|
||||
- Better handling of mirror strategies in test suite
|
||||
- Improved error handling in authentication flows
|
||||
|
||||
## [3.0.0] - 2025-07-17
|
||||
|
||||
### 🔴 Breaking Changes
|
||||
|
||||
362
CLAUDE.md
@@ -4,128 +4,314 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Project Overview
|
||||
|
||||
Gitea Mirror is a web application that automatically mirrors repositories from GitHub to self-hosted Gitea instances. It uses Astro for SSR, React for UI, SQLite for data storage, and Bun as the JavaScript runtime.
|
||||
Gitea Mirror is a self-hosted web application that automatically mirrors repositories from GitHub to Gitea instances. It's built with Astro (SSR mode), React, and runs on the Bun runtime with SQLite for data persistence.
|
||||
|
||||
## Essential Commands
|
||||
**Key capabilities:**
|
||||
- Mirrors public, private, and starred GitHub repos to Gitea
|
||||
- Supports metadata mirroring (issues, PRs as issues, labels, milestones, releases, wiki)
|
||||
- Git LFS support
|
||||
- Multiple authentication methods (email/password, OIDC/SSO, header auth)
|
||||
- Scheduled automatic syncing with configurable intervals
|
||||
- Auto-discovery of new repos and cleanup of deleted repos
|
||||
- Multi-user support with encrypted token storage (AES-256-GCM)
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Setup and Installation
|
||||
```bash
|
||||
# Install dependencies
|
||||
bun install
|
||||
|
||||
# Initialize database (first time setup)
|
||||
bun run setup
|
||||
|
||||
# Clean start (reset database)
|
||||
bun run dev:clean
|
||||
```
|
||||
|
||||
### Development
|
||||
```bash
|
||||
bun run dev # Start development server (port 3000)
|
||||
bun run build # Build for production
|
||||
bun run preview # Preview production build
|
||||
# Start development server (http://localhost:4321)
|
||||
bun run dev
|
||||
|
||||
# Build for production
|
||||
bun run build
|
||||
|
||||
# Preview production build
|
||||
bun run preview
|
||||
|
||||
# Start production server
|
||||
bun run start
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
bun test # Run all tests
|
||||
bun test:watch # Run tests in watch mode
|
||||
bun test:coverage # Run tests with coverage
|
||||
# Run all tests
|
||||
bun test
|
||||
|
||||
# Run tests in watch mode
|
||||
bun test:watch
|
||||
|
||||
# Run tests with coverage
|
||||
bun test:coverage
|
||||
```
|
||||
|
||||
**Test configuration:**
|
||||
- Test runner: Bun's built-in test runner (configured in `bunfig.toml`)
|
||||
- Setup file: `src/tests/setup.bun.ts` (auto-loaded via bunfig.toml)
|
||||
- Timeout: 5000ms default
|
||||
- Tests are colocated with source files using `*.test.ts` pattern
|
||||
|
||||
### Database Management
|
||||
```bash
|
||||
bun run init-db # Initialize database
|
||||
bun run reset-users # Reset user accounts (development)
|
||||
bun run cleanup-db # Remove database files
|
||||
# Database operations via Drizzle
|
||||
bun run db:generate # Generate migrations from schema
|
||||
bun run db:migrate # Run migrations
|
||||
bun run db:push # Push schema changes directly
|
||||
bun run db:studio # Open Drizzle Studio (database GUI)
|
||||
bun run db:check # Check schema consistency
|
||||
|
||||
# Database utilities via custom scripts
|
||||
bun run manage-db init # Initialize database
|
||||
bun run manage-db check # Check database health
|
||||
bun run manage-db fix # Fix database issues
|
||||
bun run manage-db reset-users # Reset all users
|
||||
bun run cleanup-db # Delete database file
|
||||
```
|
||||
|
||||
### Production
|
||||
### Utility Scripts
|
||||
```bash
|
||||
bun run start # Start production server
|
||||
# Recovery and diagnostic scripts
|
||||
bun run startup-recovery # Recover from crashes
|
||||
bun run startup-recovery-force # Force recovery
|
||||
bun run test-recovery # Test recovery mechanism
|
||||
bun run test-shutdown # Test graceful shutdown
|
||||
|
||||
# Environment configuration
|
||||
bun run startup-env-config # Load config from env vars
|
||||
```
|
||||
|
||||
## Architecture & Key Concepts
|
||||
## Architecture
|
||||
|
||||
### Technology Stack
|
||||
- **Frontend**: Astro (SSR) + React + Tailwind CSS v4 + Shadcn UI
|
||||
- **Backend**: Bun runtime + SQLite + Drizzle ORM
|
||||
- **APIs**: GitHub (Octokit) and Gitea APIs
|
||||
- **Auth**: JWT tokens with bcryptjs password hashing
|
||||
### Tech Stack
|
||||
- **Frontend:** Astro v5 (SSR mode) + React v19 + Shadcn UI + Tailwind CSS v4
|
||||
- **Backend:** Astro API routes (Node adapter, standalone mode)
|
||||
- **Runtime:** Bun (>=1.2.9)
|
||||
- **Database:** SQLite via Drizzle ORM
|
||||
- **Authentication:** Better Auth (session-based)
|
||||
- **APIs:** GitHub (Octokit with throttling plugin), Gitea REST API
|
||||
|
||||
### Project Structure
|
||||
- `/src/pages/api/` - API endpoints (Astro API routes)
|
||||
- `/src/components/` - React components organized by feature
|
||||
- `/src/lib/db/` - Database queries and schema (Drizzle ORM)
|
||||
- `/src/hooks/` - Custom React hooks for data fetching
|
||||
- `/data/` - SQLite database storage location
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/ # React components (UI, features)
|
||||
│ ├── ui/ # Shadcn UI components
|
||||
│ ├── repositories/ # Repository management components
|
||||
│ ├── organizations/ # Organization management components
|
||||
│ └── ...
|
||||
├── pages/ # Astro pages and API routes
|
||||
│ ├── api/ # API endpoints (Better Auth integration)
|
||||
│ │ ├── auth/ # Authentication endpoints
|
||||
│ │ ├── github/ # GitHub operations
|
||||
│ │ ├── gitea/ # Gitea operations
|
||||
│ │ ├── sync/ # Mirror sync operations
|
||||
│ │ ├── job/ # Job management
|
||||
│ │ └── ...
|
||||
│ └── *.astro # Page components
|
||||
├── lib/ # Core business logic
|
||||
│ ├── db/ # Database (Drizzle ORM)
|
||||
│ │ ├── schema.ts # Database schema with Zod validation
|
||||
│ │ ├── index.ts # Database instance and table exports
|
||||
│ │ └── adapter.ts # Better Auth SQLite adapter
|
||||
│ ├── github.ts # GitHub API client (Octokit)
|
||||
│ ├── gitea.ts # Gitea API client
|
||||
│ ├── gitea-enhanced.ts # Enhanced Gitea operations (metadata)
|
||||
│ ├── scheduler-service.ts # Automatic mirroring scheduler
|
||||
│ ├── cleanup-service.ts # Activity log cleanup
|
||||
│ ├── repository-cleanup-service.ts # Orphaned repo cleanup
|
||||
│ ├── auth.ts # Better Auth configuration
|
||||
│ ├── config.ts # Configuration management
|
||||
│ ├── helpers.ts # Mirror job creation
|
||||
│ ├── utils/ # Utility functions
|
||||
│ │ ├── encryption.ts # AES-256-GCM token encryption
|
||||
│ │ ├── config-encryption.ts # Config token encryption
|
||||
│ │ ├── duration-parser.ts # Parse intervals (e.g., "8h", "30m")
|
||||
│ │ ├── concurrency.ts # Concurrency control utilities
|
||||
│ │ └── mirror-strategies.ts # Mirror strategy logic
|
||||
│ └── ...
|
||||
├── types/ # TypeScript type definitions
|
||||
├── tests/ # Test utilities and setup
|
||||
└── middleware.ts # Astro middleware (auth, session)
|
||||
|
||||
scripts/ # Utility scripts
|
||||
├── manage-db.ts # Database management CLI
|
||||
├── startup-recovery.ts # Crash recovery
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Key Architectural Patterns
|
||||
|
||||
1. **API Routes**: All API endpoints follow the pattern `/api/[resource]/[action]` and use `createSecureErrorResponse` for consistent error handling:
|
||||
```typescript
|
||||
import { createSecureErrorResponse } from '@/lib/utils/error-handler';
|
||||
#### 1. Database Schema and Validation
|
||||
- **Location:** `src/lib/db/schema.ts`
|
||||
- **Pattern:** Drizzle ORM tables + Zod schemas for validation
|
||||
- **Key tables:**
|
||||
- `configs` - User configuration (GitHub/Gitea settings, mirror options)
|
||||
- `repositories` - Tracked repositories with metadata
|
||||
- `organizations` - GitHub organizations with destination overrides
|
||||
- `mirrorJobs` - Mirror job queue and history
|
||||
- `activities` - Activity log for dashboard
|
||||
- `user`, `session`, `account` - Better Auth tables
|
||||
|
||||
export async function POST({ request }: APIContext) {
|
||||
try {
|
||||
// Implementation
|
||||
} catch (error) {
|
||||
return createSecureErrorResponse(error);
|
||||
}
|
||||
}
|
||||
```
|
||||
**Important:** All config tokens (GitHub/Gitea) are encrypted at rest using AES-256-GCM. Use helper functions from `src/lib/utils/config-encryption.ts` to decrypt.
|
||||
|
||||
2. **Database Queries**: Located in `/src/lib/db/queries/` organized by domain (users, repositories, etc.)
|
||||
#### 2. Mirror Job System
|
||||
- **Location:** `src/lib/helpers.ts` (createMirrorJob)
|
||||
- **Flow:**
|
||||
1. User triggers mirror via API endpoint
|
||||
2. `createMirrorJob()` creates job record with status "pending"
|
||||
3. Job processor (in API routes) performs GitHub → Gitea operations
|
||||
4. Job status updated throughout: "mirroring" → "success"/"failed"
|
||||
5. Events published via SSE for real-time UI updates
|
||||
|
||||
3. **Real-time Updates**: Server-Sent Events (SSE) endpoint at `/api/events` for live dashboard updates
|
||||
#### 3. GitHub ↔ Gitea Mirroring
|
||||
- **GitHub Client:** `src/lib/github.ts` - Octokit with rate limit tracking
|
||||
- **Gitea Client:** `src/lib/gitea.ts` - Basic repo operations
|
||||
- **Enhanced Gitea:** `src/lib/gitea-enhanced.ts` - Metadata mirroring (issues, PRs, releases)
|
||||
|
||||
4. **Authentication Flow**:
|
||||
- First user signup creates admin account
|
||||
- JWT tokens stored in cookies
|
||||
- Protected routes check auth via `getUserFromCookie()`
|
||||
**Mirror strategies (configured per user):**
|
||||
- `preserve` - Maintain GitHub org structure in Gitea
|
||||
- `single-org` - All repos into one Gitea org
|
||||
- `flat-user` - All repos under user account
|
||||
- `mixed` - Personal repos in one org, org repos preserve structure
|
||||
|
||||
5. **Mirror Process**:
|
||||
- Discovers repos from GitHub (user/org)
|
||||
- Creates/updates mirror in Gitea
|
||||
- Tracks status in database
|
||||
- Supports scheduled automatic mirroring
|
||||
**Metadata mirroring:**
|
||||
- Issues transferred with comments, labels, assignees
|
||||
- PRs converted to issues (Gitea API limitation - cannot create PRs)
|
||||
- Tagged with "pull-request" label
|
||||
- Title prefixed with `[PR #number] [STATUS]`
|
||||
- Body includes commit history, file changes, merge status
|
||||
- Releases mirrored with assets
|
||||
- Labels and milestones preserved
|
||||
- Wiki content cloned if enabled
|
||||
- **Sequential processing:** Issues/PRs mirrored one at a time to prevent out-of-order creation (see `src/lib/gitea-enhanced.ts`)
|
||||
|
||||
6. **Mirror Strategies**: Three ways to organize repositories in Gitea:
|
||||
- **preserve**: Maintains GitHub structure (default)
|
||||
- **single-org**: All repos go to one organization
|
||||
- **flat-user**: All repos go under user account
|
||||
- Starred repos always go to separate organization (starredReposOrg)
|
||||
- Routing logic in `getGiteaRepoOwner()` function
|
||||
#### 4. Scheduler Service
|
||||
- **Location:** `src/lib/scheduler-service.ts`
|
||||
- **Features:**
|
||||
- Cron-based or interval-based scheduling (uses `duration-parser.ts`)
|
||||
- Auto-start on boot when `SCHEDULE_ENABLED=true` or `GITEA_MIRROR_INTERVAL` is set
|
||||
- Auto-import new GitHub repos
|
||||
- Auto-cleanup orphaned repos (archive or delete)
|
||||
- Respects per-repo mirror intervals (not Gitea's default 24h)
|
||||
- **Concurrency control:** Uses `src/lib/utils/concurrency.ts` for batch processing
|
||||
|
||||
### Database Schema (SQLite)
|
||||
- `users` - User accounts and authentication
|
||||
- `configs` - GitHub/Gitea connection settings
|
||||
- `repositories` - Repository mirror status and metadata
|
||||
- `organizations` - Organization structure preservation
|
||||
- `mirror_jobs` - Scheduled mirror operations
|
||||
- `events` - Activity log and notifications
|
||||
#### 5. Authentication System
|
||||
- **Location:** `src/lib/auth.ts`, `src/lib/auth-client.ts`
|
||||
- **Better Auth integration:**
|
||||
- Email/password (always enabled)
|
||||
- OIDC/SSO providers (configurable via UI)
|
||||
- Header authentication for reverse proxies (Authentik, Authelia)
|
||||
- **Session management:** Cookie-based, validated in Astro middleware
|
||||
- **User helpers:** `src/lib/utils/auth-helpers.ts`
|
||||
|
||||
### Testing Approach
|
||||
- Uses Bun's native test runner (`bun:test`)
|
||||
- Test files use `.test.ts` or `.test.tsx` extension
|
||||
- Setup file at `/src/tests/setup.bun.ts`
|
||||
- Mock utilities available for API testing.
|
||||
#### 6. Environment Configuration
|
||||
- **Startup:** `src/lib/env-config-loader.ts` + `scripts/startup-env-config.ts`
|
||||
- **Pattern:** Environment variables can pre-configure settings, but users can override via web UI
|
||||
- **Encryption:** `ENCRYPTION_SECRET` for tokens, `BETTER_AUTH_SECRET` for sessions
|
||||
|
||||
### Development Tips
|
||||
- Environment variables in `.env` (copy from `.env.example`)
|
||||
- JWT_SECRET auto-generated if not provided
|
||||
- Database auto-initializes on first run
|
||||
- Use `bun run dev:clean` for fresh database start
|
||||
- Tailwind CSS v4 configured with Vite plugin
|
||||
#### 7. Real-time Updates
|
||||
- **Events:** `src/lib/events.ts` + `src/lib/events/realtime.ts`
|
||||
- **Pattern:** Server-Sent Events (SSE) for live dashboard updates
|
||||
- **Endpoints:** `/api/sse` - client subscribes to job/repo events
|
||||
|
||||
### Common Tasks
|
||||
### Testing Patterns
|
||||
|
||||
**Adding a new API endpoint:**
|
||||
1. Create file in `/src/pages/api/[resource]/[action].ts`
|
||||
2. Use `createSecureErrorResponse` for error handling
|
||||
3. Add corresponding database query in `/src/lib/db/queries/`
|
||||
4. Update types in `/src/types/` if needed
|
||||
**Unit tests:**
|
||||
- Colocated with source: `filename.test.ts` alongside `filename.ts`
|
||||
- Use Bun's built-in assertions and mocking
|
||||
- Mock external APIs (GitHub, Gitea) using `src/tests/mock-fetch.ts`
|
||||
|
||||
**Adding a new component:**
|
||||
1. Create in appropriate `/src/components/[feature]/` directory
|
||||
2. Use Shadcn UI components from `/src/components/ui/`
|
||||
3. Follow existing naming patterns (e.g., `RepositoryCard`, `ConfigTabs`)
|
||||
**Integration tests:**
|
||||
- Located in `src/tests/`
|
||||
- Test database operations with in-memory SQLite
|
||||
- Example: `src/lib/db/index.test.ts`
|
||||
|
||||
**Modifying database schema:**
|
||||
1. Update schema in `/src/lib/db/schema.ts`
|
||||
2. Run `bun run init-db` to recreate database
|
||||
3. Update related queries in `/src/lib/db/queries/`
|
||||
**Test utilities:**
|
||||
- `src/tests/setup.bun.ts` - Global test setup (loaded via bunfig.toml)
|
||||
- `src/tests/mock-fetch.ts` - Fetch mocking utilities
|
||||
|
||||
## Security Guidelines
|
||||
### Important Development Notes
|
||||
|
||||
- **Confidentiality Guidelines**:
|
||||
- Dont ever say Claude Code or generated with AI anyhwere.
|
||||
1. **Path Aliases:** Use `@/` for imports (configured in `tsconfig.json`)
|
||||
```typescript
|
||||
import { db } from '@/lib/db';
|
||||
```
|
||||
|
||||
2. **Token Encryption:** Always use encryption helpers when dealing with tokens:
|
||||
```typescript
|
||||
import { getDecryptedGitHubToken, getDecryptedGiteaToken } from '@/lib/utils/config-encryption';
|
||||
```
|
||||
|
||||
3. **API Route Pattern:** Astro API routes in `src/pages/api/` should:
|
||||
- Check authentication via Better Auth
|
||||
- Validate input with Zod schemas
|
||||
- Handle errors gracefully
|
||||
- Return JSON responses
|
||||
|
||||
4. **Database Migrations:**
|
||||
- Schema changes: Update `src/lib/db/schema.ts`
|
||||
- Generate migration: `bun run db:generate`
|
||||
- Review generated SQL in `drizzle/` directory
|
||||
- Apply: `bun run db:migrate` (or `db:push` for dev)
|
||||
|
||||
5. **Concurrency Control:**
|
||||
- Use utilities from `src/lib/utils/concurrency.ts` for batch operations
|
||||
- Respect rate limits (GitHub: 5000 req/hr authenticated, Gitea: varies)
|
||||
- Issue/PR mirroring is sequential to maintain chronological order
|
||||
|
||||
6. **Duration Parsing:**
|
||||
- Use `parseInterval()` from `src/lib/utils/duration-parser.ts`
|
||||
- Supports: "30m", "8h", "24h", "7d", cron expressions, or milliseconds
|
||||
|
||||
7. **Graceful Shutdown:**
|
||||
- Services implement cleanup handlers (see `src/lib/shutdown-manager.ts`)
|
||||
- Recovery system in `src/lib/recovery.ts` handles interrupted jobs
|
||||
|
||||
## Common Development Workflows
|
||||
|
||||
### Adding a new mirror option
|
||||
1. Update Zod schema in `src/lib/db/schema.ts` (e.g., `giteaConfigSchema`)
|
||||
2. Update TypeScript types in `src/types/config.ts`
|
||||
3. Add UI control in settings page component
|
||||
4. Update API handler in `src/pages/api/config/`
|
||||
5. Implement logic in `src/lib/gitea.ts` or `src/lib/gitea-enhanced.ts`
|
||||
|
||||
### Debugging mirror failures
|
||||
1. Check mirror jobs: `bun run db:studio` → `mirrorJobs` table
|
||||
2. Review activity logs: Dashboard → Activity tab
|
||||
3. Check console logs for API errors (GitHub/Gitea rate limits, auth issues)
|
||||
4. Use diagnostic scripts: `bun run test-recovery`
|
||||
|
||||
### Adding authentication provider
|
||||
1. Update Better Auth config in `src/lib/auth.ts`
|
||||
2. Add provider configuration UI in settings
|
||||
3. Test with `src/tests/test-gitea-auth.ts` patterns
|
||||
4. Update documentation in `docs/SSO-OIDC-SETUP.md`
|
||||
|
||||
## Docker Deployment
|
||||
|
||||
- **Dockerfile:** Multi-stage build (bun base → build → production)
|
||||
- **Entrypoint:** `docker-entrypoint.sh` - handles CA certs, user permissions, database init
|
||||
- **Compose files:**
|
||||
- `docker-compose.alt.yml` - Quick start (pre-built image, minimal config)
|
||||
- `docker-compose.yml` - Full setup (build from source, all env vars)
|
||||
- `docker-compose.dev.yml` - Development with hot reload
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- **Environment Variables:** See `docs/ENVIRONMENT_VARIABLES.md` for complete list
|
||||
- **Development Workflow:** See `docs/DEVELOPMENT_WORKFLOW.md`
|
||||
- **SSO Setup:** See `docs/SSO-OIDC-SETUP.md`
|
||||
- **Contributing:** See `CONTRIBUTING.md` for code guidelines and scope
|
||||
- **Graceful Shutdown:** See `docs/GRACEFUL_SHUTDOWN.md` for crash recovery details
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1.4
|
||||
|
||||
FROM oven/bun:1.2.18-alpine AS base
|
||||
FROM oven/bun:1.2.23-alpine AS base
|
||||
WORKDIR /app
|
||||
RUN apk add --no-cache libc6-compat python3 make g++ gcc wget sqlite openssl ca-certificates
|
||||
|
||||
@@ -55,4 +55,4 @@ 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
|
||||
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||
@@ -1,248 +0,0 @@
|
||||
# Migration Guide
|
||||
|
||||
This guide covers database migrations and version upgrades for Gitea Mirror.
|
||||
|
||||
## Version 3.0 Migration Guide
|
||||
|
||||
### Overview of v3 Changes
|
||||
|
||||
Version 3.0 introduces significant security improvements and authentication changes:
|
||||
- **Token Encryption**: All GitHub and Gitea tokens are now encrypted in the database
|
||||
- **Better Auth**: Complete authentication system overhaul with session-based auth
|
||||
- **SSO/OIDC Support**: Enterprise authentication options
|
||||
- **Enhanced Security**: Improved error handling and security practices
|
||||
|
||||
### Breaking Changes in v3
|
||||
|
||||
#### 1. Authentication System Overhaul
|
||||
- Users now log in with **email** instead of username
|
||||
- Session-based authentication replaces JWT tokens
|
||||
- New auth endpoints: `/api/auth/[...all]` instead of `/api/auth/login`
|
||||
- Password reset may be required for existing users
|
||||
|
||||
#### 2. Token Encryption
|
||||
- All stored GitHub and Gitea tokens are encrypted using AES-256-GCM
|
||||
- Requires encryption secret configuration
|
||||
- Existing unencrypted tokens must be migrated
|
||||
|
||||
#### 3. Environment Variables
|
||||
**Required changes:**
|
||||
- `JWT_SECRET` → `BETTER_AUTH_SECRET` (backward compatible)
|
||||
- New: `BETTER_AUTH_URL` (required)
|
||||
- New: `ENCRYPTION_SECRET` (recommended)
|
||||
|
||||
#### 4. Database Schema Updates
|
||||
New tables added:
|
||||
- `sessions` - User session management
|
||||
- `accounts` - Authentication accounts
|
||||
- `verification_tokens` - Email verification
|
||||
- `oauth_applications` - OAuth app registrations
|
||||
- `sso_providers` - SSO configuration
|
||||
|
||||
### Migration Steps from v2 to v3
|
||||
|
||||
**⚠️ IMPORTANT: Backup your database before upgrading!**
|
||||
|
||||
```bash
|
||||
cp data/gitea-mirror.db data/gitea-mirror.db.backup
|
||||
```
|
||||
|
||||
#### Automated Migration (Docker Compose)
|
||||
|
||||
For Docker Compose users, v3 migration is **fully automated**:
|
||||
|
||||
1. **Update your docker-compose.yml** to use v3:
|
||||
```yaml
|
||||
services:
|
||||
gitea-mirror:
|
||||
image: ghcr.io/raylabshq/gitea-mirror:v3
|
||||
```
|
||||
|
||||
2. **Pull and restart the container**:
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose down
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**That's it!** The container will automatically:
|
||||
- ✅ Generate BETTER_AUTH_SECRET (from existing JWT_SECRET if available)
|
||||
- ✅ Generate ENCRYPTION_SECRET for token encryption
|
||||
- ✅ Create Better Auth database tables
|
||||
- ✅ Migrate existing users to Better Auth system
|
||||
- ✅ Encrypt all stored GitHub/Gitea tokens
|
||||
- ✅ Apply all necessary database migrations
|
||||
|
||||
#### Manual Migration (Non-Docker)
|
||||
|
||||
#### Step 1: Update Environment Variables
|
||||
Add to your `.env` file:
|
||||
```bash
|
||||
# Set your application URL (required)
|
||||
BETTER_AUTH_URL=http://localhost:4321 # or your production URL
|
||||
|
||||
# Optional: These will be auto-generated if not provided
|
||||
# BETTER_AUTH_SECRET=your-existing-jwt-secret # Will use existing JWT_SECRET
|
||||
# ENCRYPTION_SECRET=your-48-character-secret # Will be auto-generated
|
||||
```
|
||||
|
||||
#### Step 2: Stop the Application
|
||||
```bash
|
||||
# Stop your running instance
|
||||
pkill -f "bun run start" # or your process manager command
|
||||
```
|
||||
|
||||
#### Step 3: Update to v3
|
||||
```bash
|
||||
# Pull latest changes
|
||||
git pull origin v3
|
||||
|
||||
# Install dependencies
|
||||
bun install
|
||||
```
|
||||
|
||||
#### Step 4: Run Migrations
|
||||
```bash
|
||||
# Option 1: Automatic migration on startup
|
||||
bun run build
|
||||
bun run start # Migrations run automatically
|
||||
|
||||
# Option 2: Manual migration
|
||||
bun run migrate:better-auth # Migrate users to Better Auth
|
||||
bun run migrate:encrypt-tokens # Encrypt stored tokens
|
||||
```
|
||||
|
||||
### Post-Migration Tasks
|
||||
|
||||
1. **All users must log in again** - Sessions are invalidated
|
||||
2. **Users log in with email** - Not username anymore
|
||||
3. **Check token encryption** - Verify GitHub/Gitea connections still work
|
||||
4. **Update API integrations** - Switch to new auth endpoints
|
||||
|
||||
### Troubleshooting v3 Migration
|
||||
|
||||
#### Users Can't Log In
|
||||
- Ensure they're using email, not username
|
||||
- They may need to reset password if migration failed
|
||||
- Check Better Auth migration logs
|
||||
|
||||
#### Token Decryption Errors
|
||||
- Verify ENCRYPTION_SECRET is set correctly
|
||||
- Re-run token encryption migration
|
||||
- Users may need to re-enter tokens
|
||||
|
||||
#### Database Errors
|
||||
- Ensure all migrations completed
|
||||
- Check disk space for new tables
|
||||
- Review migration logs in console
|
||||
|
||||
### Rollback Procedure
|
||||
If migration fails:
|
||||
```bash
|
||||
# Stop application
|
||||
pkill -f "bun run start"
|
||||
|
||||
# Restore database backup
|
||||
cp data/gitea-mirror.db.backup data/gitea-mirror.db
|
||||
|
||||
# Checkout previous version
|
||||
git checkout v2.22.0
|
||||
|
||||
# Restart with old version
|
||||
bun run start
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Drizzle Kit Migration Guide
|
||||
|
||||
This project uses Drizzle Kit for database migrations, providing better schema management and migration tracking.
|
||||
|
||||
## Overview
|
||||
|
||||
- **Database**: SQLite (with preparation for future PostgreSQL migration)
|
||||
- **ORM**: Drizzle ORM with Drizzle Kit for migrations
|
||||
- **Schema Location**: `/src/lib/db/schema.ts`
|
||||
- **Migrations Folder**: `/drizzle`
|
||||
- **Configuration**: `/drizzle.config.ts`
|
||||
|
||||
## Available Commands
|
||||
|
||||
### Database Management
|
||||
- `bun run init-db` - Initialize database with all migrations
|
||||
- `bun run check-db` - Check database status and recent migrations
|
||||
- `bun run reset-users` - Remove all users and related data
|
||||
- `bun run cleanup-db` - Remove database files
|
||||
|
||||
### Drizzle Kit Commands
|
||||
- `bun run db:generate` - Generate new migration files from schema changes
|
||||
- `bun run db:migrate` - Apply pending migrations to database
|
||||
- `bun run db:push` - Push schema changes directly (development)
|
||||
- `bun run db:pull` - Pull schema from database
|
||||
- `bun run db:check` - Check for migration issues
|
||||
- `bun run db:studio` - Open Drizzle Studio for database browsing
|
||||
|
||||
## Making Schema Changes
|
||||
|
||||
1. **Update Schema**: Edit `/src/lib/db/schema.ts`
|
||||
2. **Generate Migration**: Run `bun run db:generate`
|
||||
3. **Review Migration**: Check the generated SQL in `/drizzle` folder
|
||||
4. **Apply Migration**: Run `bun run db:migrate` or restart the application
|
||||
|
||||
## Migration Process
|
||||
|
||||
The application automatically runs migrations on startup:
|
||||
- Checks for pending migrations
|
||||
- Creates migrations table if needed
|
||||
- Applies all pending migrations in order
|
||||
- Tracks migration history
|
||||
|
||||
## Schema Organization
|
||||
|
||||
### Tables
|
||||
- `users` - User authentication and accounts
|
||||
- `configs` - GitHub/Gitea configurations
|
||||
- `repositories` - Repository mirror tracking
|
||||
- `organizations` - GitHub organizations
|
||||
- `mirror_jobs` - Job tracking with resilience
|
||||
- `events` - Real-time event notifications
|
||||
|
||||
### Indexes
|
||||
All performance-critical indexes are automatically created:
|
||||
- User lookups
|
||||
- Repository status queries
|
||||
- Organization filtering
|
||||
- Job tracking
|
||||
- Event channels
|
||||
|
||||
## Future PostgreSQL Migration
|
||||
|
||||
The setup is designed for easy PostgreSQL migration:
|
||||
|
||||
1. Update `drizzle.config.ts`:
|
||||
```typescript
|
||||
export default defineConfig({
|
||||
dialect: "postgresql",
|
||||
schema: "./src/lib/db/schema.ts",
|
||||
out: "./drizzle",
|
||||
dbCredentials: {
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
2. Update connection in `/src/lib/db/index.ts`
|
||||
3. Generate new migrations: `bun run db:generate`
|
||||
4. Apply to PostgreSQL: `bun run db:migrate`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Migration Errors
|
||||
- Check `/drizzle` folder for migration files
|
||||
- Verify database permissions
|
||||
- Review migration SQL for conflicts
|
||||
|
||||
### Schema Conflicts
|
||||
- Use `bun run db:check` to identify issues
|
||||
- Review generated migrations before applying
|
||||
- Keep schema.ts as single source of truth
|
||||
157
README.md
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<img src=".github/assets/logo-no-bg.png" alt="Gitea Mirror Logo" width="120" />
|
||||
<img src=".github/assets/logo.png" alt="Gitea Mirror Logo" width="120" />
|
||||
<h1>Gitea Mirror</h1>
|
||||
<p><i>Automatically mirror repositories from GitHub to your self-hosted Gitea instance.</i></p>
|
||||
<p align="center">
|
||||
@@ -10,10 +10,6 @@
|
||||
</p>
|
||||
</p>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Upgrading to v3?** Please read the [Migration Guide](MIGRATION_GUIDE.md) for breaking changes and upgrade instructions.
|
||||
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
```bash
|
||||
@@ -35,9 +31,16 @@ First user signup becomes admin. Configure GitHub and Gitea through the web inte
|
||||
- 🔁 Mirror public, private, and starred GitHub repos to Gitea
|
||||
- 🏢 Mirror entire organizations with flexible strategies
|
||||
- 🎯 Custom destination control for repos and organizations
|
||||
- 🔐 Secure authentication with JWT tokens
|
||||
- 📦 **Git LFS support** - Mirror large files with Git LFS
|
||||
- 📝 **Metadata mirroring** - Issues, pull requests (as issues), labels, milestones, wiki
|
||||
- 🚫 **Repository ignore** - Mark specific repos to skip
|
||||
- 🔐 Secure authentication with Better Auth (email/password, SSO, OIDC)
|
||||
- 📊 Real-time dashboard with activity logs
|
||||
- ⏱️ Scheduled automatic mirroring
|
||||
- ⏱️ Scheduled automatic mirroring with configurable intervals
|
||||
- 🔄 **Auto-discovery** - Automatically import new GitHub repositories (v3.4.0+)
|
||||
- 🧹 **Repository cleanup** - Auto-remove repos deleted from GitHub (v3.4.0+)
|
||||
- 🎯 **Proper mirror intervals** - Respects configured sync intervals (v3.4.0+)
|
||||
- 🗑️ Automatic database cleanup with configurable retention
|
||||
- 🐳 Dockerized with multi-arch support (AMD64/ARM64)
|
||||
|
||||
## 📸 Screenshots
|
||||
@@ -109,7 +112,7 @@ docker compose up -d
|
||||
#### Using Pre-built Image Directly
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/raylabshq/gitea-mirror:v3.0.0
|
||||
docker pull ghcr.io/raylabshq/gitea-mirror:v3.1.1
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
@@ -126,8 +129,8 @@ PORT=4321
|
||||
PUID=1000
|
||||
PGID=1000
|
||||
|
||||
# JWT secret (auto-generated if not set)
|
||||
JWT_SECRET=your-secret-key-change-this-in-production
|
||||
# Session secret (auto-generated if not set)
|
||||
BETTER_AUTH_SECRET=your-secret-key-change-this-in-production
|
||||
```
|
||||
|
||||
All other settings are configured through the web interface after starting.
|
||||
@@ -136,6 +139,8 @@ All other settings are configured through the web interface after starting.
|
||||
|
||||
Supports extensive environment variables for automated deployment. See the full [docker-compose.yml](docker-compose.yml) for all available options including GitHub tokens, Gitea URLs, mirror settings, and more.
|
||||
|
||||
📚 **For a complete list of all supported environment variables, see the [Environment Variables Documentation](docs/ENVIRONMENT_VARIABLES.md).**
|
||||
|
||||
### LXC Container (Proxmox)
|
||||
|
||||
```bash
|
||||
@@ -174,6 +179,88 @@ bun run dev
|
||||
- Override individual repository destinations in the table view
|
||||
- Starred repositories automatically go to a dedicated organization
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Git LFS (Large File Storage)
|
||||
Mirror Git LFS objects along with your repositories:
|
||||
- Enable "Mirror LFS" option in Settings → Mirror Options
|
||||
- Requires Gitea server with LFS enabled (`LFS_START_SERVER = true`)
|
||||
- Requires Git v2.1.2+ on the server
|
||||
|
||||
### Metadata Mirroring
|
||||
Transfer complete repository metadata from GitHub to Gitea:
|
||||
- **Issues** - Mirror all issues with comments and labels
|
||||
- **Pull Requests** - Transfer PR discussions to Gitea
|
||||
- **Labels** - Preserve repository labels
|
||||
- **Milestones** - Keep project milestones
|
||||
- **Wiki** - Mirror wiki content
|
||||
- **Releases** - Transfer GitHub releases with assets
|
||||
|
||||
Enable in Settings → Mirror Options → Mirror metadata
|
||||
|
||||
### Repository Management
|
||||
- **Ignore Status** - Mark repositories to skip from mirroring
|
||||
- **Automatic Cleanup** - Configure retention period for activity logs
|
||||
- **Scheduled Sync** - Set custom intervals for automatic mirroring
|
||||
|
||||
### Automatic Syncing & Synchronization
|
||||
|
||||
Gitea Mirror provides powerful automatic synchronization features:
|
||||
|
||||
#### Features (v3.4.0+)
|
||||
- **Auto-discovery**: Automatically discovers and imports new GitHub repositories
|
||||
- **Repository cleanup**: Removes repositories that no longer exist in GitHub
|
||||
- **Proper intervals**: Mirrors respect your configured sync intervals (not Gitea's default 24h)
|
||||
- **Smart scheduling**: Only syncs repositories that need updating
|
||||
- **Auto-start on boot** (v3.5.3+): Automatically imports and mirrors all repositories when `SCHEDULE_ENABLED=true` or `GITEA_MIRROR_INTERVAL` is set - no manual clicks required!
|
||||
|
||||
#### Configuration via Web Interface (Recommended)
|
||||
Navigate to the Configuration page and enable "Automatic Syncing" with your preferred interval.
|
||||
|
||||
#### Configuration via Environment Variables
|
||||
|
||||
**🚀 Set it and forget it!** With these environment variables, Gitea Mirror will automatically:
|
||||
1. **Import** all your GitHub repositories on startup (no manual import needed!)
|
||||
2. **Mirror** them to Gitea immediately
|
||||
3. **Keep them synchronized** based on your interval
|
||||
4. **Auto-discover** new repos you create/star on GitHub
|
||||
5. **Clean up** repos you delete from GitHub
|
||||
|
||||
```bash
|
||||
# Option 1: Enable automatic scheduling (triggers auto-start)
|
||||
SCHEDULE_ENABLED=true
|
||||
SCHEDULE_INTERVAL=3600 # Check every hour (or use cron: "0 * * * *")
|
||||
|
||||
# Option 2: Set mirror interval (also triggers auto-start)
|
||||
GITEA_MIRROR_INTERVAL=8h # Every 8 hours
|
||||
# Other examples: 5m, 30m, 1h, 24h, 1d, 7d
|
||||
|
||||
# Advanced: Use cron expressions for specific times
|
||||
SCHEDULE_INTERVAL="0 2 * * *" # Daily at 2 AM (optimize bandwidth usage)
|
||||
|
||||
# Auto-import new repositories (default: true)
|
||||
AUTO_IMPORT_REPOS=true
|
||||
|
||||
# Auto-cleanup orphaned repositories
|
||||
CLEANUP_DELETE_IF_NOT_IN_GITHUB=true
|
||||
CLEANUP_ORPHANED_REPO_ACTION=archive # 'archive' (recommended) or 'delete'
|
||||
CLEANUP_DRY_RUN=false # Set to true to test without changes
|
||||
```
|
||||
|
||||
**Important Notes**:
|
||||
- **Auto-Start**: When `SCHEDULE_ENABLED=true` or `GITEA_MIRROR_INTERVAL` is set, the service automatically imports all GitHub repositories and mirrors them on startup. No manual "Import" or "Mirror" button clicks required!
|
||||
- The scheduler checks every minute for tasks to run. The `GITEA_MIRROR_INTERVAL` determines how often each repository is actually synced. For example, with `8h`, each repo syncs every 8 hours from its last successful sync.
|
||||
|
||||
**🛡️ Backup Protection Features**:
|
||||
- **No Accidental Deletions**: Repository cleanup is automatically skipped if GitHub is inaccessible (account deleted, banned, or API errors)
|
||||
- **Archive Never Deletes Data**: The `archive` action preserves all repository data:
|
||||
- Regular repositories: Made read-only using Gitea's archive feature
|
||||
- Mirror repositories: Renamed with `archived-` prefix (Gitea API limitation prevents archiving mirrors)
|
||||
- Failed operations: Repository remains fully accessible even if marking as archived fails
|
||||
- **Manual Sync on Demand**: Archived mirrors stay in Gitea with automatic syncs disabled; trigger `Manual Sync` from the Repositories page whenever you need fresh data.
|
||||
- **The Whole Point of Backups**: Your Gitea mirrors are preserved even when GitHub sources disappear - that's why you have backups!
|
||||
- **Strongly Recommended**: Always use `CLEANUP_ORPHANED_REPO_ACTION=archive` (default) instead of `delete`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Reverse Proxy Configuration
|
||||
@@ -201,7 +288,7 @@ bun run build
|
||||
- **Frontend**: Astro, React, Shadcn UI, Tailwind CSS v4
|
||||
- **Backend**: Bun runtime, SQLite, Drizzle ORM
|
||||
- **APIs**: GitHub (Octokit), Gitea REST API
|
||||
- **Auth**: JWT tokens with bcryptjs password hashing
|
||||
- **Auth**: Better Auth with session-based authentication
|
||||
|
||||
## Security
|
||||
|
||||
@@ -209,21 +296,12 @@ bun run build
|
||||
- All GitHub and Gitea API tokens are encrypted at rest using AES-256-GCM
|
||||
- Encryption is automatic and transparent to users
|
||||
- Set `ENCRYPTION_SECRET` environment variable for production deployments
|
||||
- Falls back to `BETTER_AUTH_SECRET` or `JWT_SECRET` if not set
|
||||
- Falls back to `BETTER_AUTH_SECRET` if not set
|
||||
|
||||
### Password Security
|
||||
- User passwords are hashed using bcrypt (via Better Auth)
|
||||
- User passwords are securely hashed by Better Auth
|
||||
- Never stored in plaintext
|
||||
- Secure session management with JWT tokens
|
||||
|
||||
### Upgrading to v3
|
||||
|
||||
**Important**: If upgrading from v2.x to v3.0, please read the [Migration Guide](MIGRATION_GUIDE.md) for breaking changes and upgrade instructions.
|
||||
|
||||
For quick token encryption migration:
|
||||
```bash
|
||||
bun run migrate:encrypt-tokens
|
||||
```
|
||||
- Secure cookie-based session management
|
||||
|
||||
## Authentication
|
||||
|
||||
@@ -290,6 +368,31 @@ Gitea Mirror can also act as an OIDC provider for other applications. Register O
|
||||
- Create service-to-service authentication
|
||||
- Build integrations with your Gitea Mirror instance
|
||||
|
||||
## Known Limitations
|
||||
|
||||
### Pull Request Mirroring Implementation
|
||||
Pull requests **cannot be created as actual PRs** in Gitea due to API limitations. Instead, they are mirrored as **enriched issues** with comprehensive metadata.
|
||||
|
||||
**Why real PR mirroring isn't possible:**
|
||||
- Gitea's API doesn't support creating pull requests from external sources
|
||||
- Real PRs require actual Git branches with commits to exist in the repository
|
||||
- Would require complex branch synchronization and commit replication
|
||||
- The mirror relationship is one-way (GitHub → Gitea) for repository content
|
||||
|
||||
**How we handle Pull Requests:**
|
||||
PRs are mirrored as issues with rich metadata including:
|
||||
- 🏷️ Special "pull-request" label for identification
|
||||
- 📌 [PR #number] prefix in title with status indicators ([MERGED], [CLOSED])
|
||||
- 👤 Original author and creation date
|
||||
- 📝 Complete commit history (up to 10 commits with links)
|
||||
- 📊 File changes summary with additions/deletions
|
||||
- 📁 List of modified files (up to 20 files)
|
||||
- 💬 Original PR description and comments
|
||||
- 🔀 Base and head branch information
|
||||
- ✅ Merge status tracking
|
||||
|
||||
This approach preserves all important PR information while working within Gitea's API constraints. The PRs appear in Gitea's issue tracker with clear visual distinction and comprehensive details.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please read our [Contributing Guidelines](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
|
||||
@@ -300,11 +403,11 @@ GNU General Public License v3.0 - see [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://www.star-history.com/#RayLabsHQ/gitea-mirror&Date">
|
||||
<a href="https://www.star-history.com/#RayLabsHQ/gitea-mirror&type=date&legend=bottom-right">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=RayLabsHQ/gitea-mirror&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=RayLabsHQ/gitea-mirror&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=RayLabsHQ/gitea-mirror&type=Date" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=RayLabsHQ/gitea-mirror&type=date&theme=dark&legend=bottom-right" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=RayLabsHQ/gitea-mirror&type=date&legend=bottom-right" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=RayLabsHQ/gitea-mirror&type=date&legend=bottom-right" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ export default defineConfig({
|
||||
plugins: [tailwindcss()],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
external: ['bun']
|
||||
}
|
||||
}
|
||||
external: ['bun', 'bun:*'],
|
||||
},
|
||||
},
|
||||
},
|
||||
integrations: [react()]
|
||||
});
|
||||
});
|
||||
|
||||
6
bunfig.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
[test]
|
||||
# Set test timeout to 5 seconds (5000ms) to prevent hanging tests
|
||||
timeout = 5000
|
||||
|
||||
# Preload the setup file
|
||||
preload = ["./src/tests/setup.bun.ts"]
|
||||
@@ -1,5 +1,7 @@
|
||||
# Gitea Mirror alternate deployment configuration
|
||||
# Standard deployment with host path and minimal environments
|
||||
# Minimal Gitea Mirror deployment
|
||||
# Only includes what CANNOT be configured via the Web UI
|
||||
# Everything else can be set up through the web interface after deployment
|
||||
|
||||
services:
|
||||
gitea-mirror:
|
||||
image: ghcr.io/raylabshq/gitea-mirror:latest
|
||||
@@ -11,14 +13,49 @@ services:
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
environment:
|
||||
# === ABSOLUTELY REQUIRED ===
|
||||
# This MUST be set and CANNOT be changed via UI
|
||||
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET} # Min 32 chars, required for sessions
|
||||
- BETTER_AUTH_URL=${BETTER_AUTH_URL:-http://localhost:4321}
|
||||
- BETTER_AUTH_TRUSTED_ORIGINS=${BETTER_AUTH_TRUSTED_ORIGINS:-http://localhost:4321}
|
||||
|
||||
# === CORE SETTINGS ===
|
||||
# These are technically required but have working defaults
|
||||
- NODE_ENV=production
|
||||
- DATABASE_URL=file:data/gitea-mirror.db
|
||||
- HOST=0.0.0.0
|
||||
- PORT=4321
|
||||
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET:-your-secret-key-change-this-in-production}
|
||||
- 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
|
||||
- MIRROR_ISSUE_CONCURRENCY=${MIRROR_ISSUE_CONCURRENCY:-3}
|
||||
- MIRROR_PULL_REQUEST_CONCURRENCY=${MIRROR_PULL_REQUEST_CONCURRENCY:-5}
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=3", "--spider", "http://localhost:4321/api/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 15s
|
||||
|
||||
# === QUICK START ===
|
||||
#
|
||||
# 1. Create a .env file with only ONE required variable:
|
||||
# BETTER_AUTH_SECRET=your-32-character-minimum-secret-key-here
|
||||
#
|
||||
# 2. Run:
|
||||
# docker-compose -f docker-compose.alt.yml up -d
|
||||
#
|
||||
# 3. Access at http://localhost:4321
|
||||
#
|
||||
# 4. Sign up for an account (first user becomes admin)
|
||||
#
|
||||
# 5. Configure everything else through the web UI:
|
||||
# - GitHub credentials
|
||||
# - Gitea credentials
|
||||
# - Mirror settings
|
||||
# - Scheduling options
|
||||
# - Auto-import settings
|
||||
# - Cleanup preferences
|
||||
#
|
||||
# That's it! Everything else can be configured via the web interface.
|
||||
|
||||
@@ -70,6 +70,7 @@ services:
|
||||
# GitHub/Gitea Mirror Config
|
||||
- GITHUB_USERNAME=${GITHUB_USERNAME:-your-github-username}
|
||||
- GITHUB_TOKEN=${GITHUB_TOKEN:-your-github-token}
|
||||
- GITHUB_EXCLUDED_ORGS=${GITHUB_EXCLUDED_ORGS:-}
|
||||
- SKIP_FORKS=${SKIP_FORKS:-false}
|
||||
- PRIVATE_REPOSITORIES=${PRIVATE_REPOSITORIES:-false}
|
||||
- MIRROR_ISSUES=${MIRROR_ISSUES:-false}
|
||||
|
||||
@@ -24,6 +24,8 @@ services:
|
||||
# Option 2: Mount system CA bundle (if your CA is already in system store)
|
||||
# - /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro
|
||||
environment:
|
||||
# For a complete list of all supported environment variables, see:
|
||||
# docs/ENVIRONMENT_VARIABLES.md or .env.example
|
||||
- NODE_ENV=production
|
||||
- DATABASE_URL=file:data/gitea-mirror.db
|
||||
- HOST=0.0.0.0
|
||||
@@ -35,6 +37,7 @@ services:
|
||||
# GitHub/Gitea Mirror Config
|
||||
- GITHUB_USERNAME=${GITHUB_USERNAME:-}
|
||||
- GITHUB_TOKEN=${GITHUB_TOKEN:-}
|
||||
- GITHUB_EXCLUDED_ORGS=${GITHUB_EXCLUDED_ORGS:-}
|
||||
- SKIP_FORKS=${SKIP_FORKS:-false}
|
||||
- PRIVATE_REPOSITORIES=${PRIVATE_REPOSITORIES:-false}
|
||||
- MIRROR_ISSUES=${MIRROR_ISSUES:-false}
|
||||
@@ -44,12 +47,23 @@ services:
|
||||
- PRESERVE_ORG_STRUCTURE=${PRESERVE_ORG_STRUCTURE:-false}
|
||||
- ONLY_MIRROR_ORGS=${ONLY_MIRROR_ORGS:-false}
|
||||
- SKIP_STARRED_ISSUES=${SKIP_STARRED_ISSUES:-false}
|
||||
- MIRROR_ISSUE_CONCURRENCY=${MIRROR_ISSUE_CONCURRENCY:-3}
|
||||
- MIRROR_PULL_REQUEST_CONCURRENCY=${MIRROR_PULL_REQUEST_CONCURRENCY:-5}
|
||||
- GITEA_URL=${GITEA_URL:-}
|
||||
- GITEA_TOKEN=${GITEA_TOKEN:-}
|
||||
- GITEA_USERNAME=${GITEA_USERNAME:-}
|
||||
- GITEA_ORGANIZATION=${GITEA_ORGANIZATION:-github-mirrors}
|
||||
- GITEA_ORG_VISIBILITY=${GITEA_ORG_VISIBILITY:-public}
|
||||
- DELAY=${DELAY:-3600}
|
||||
# Scheduling and Sync Configuration (Issue #72 fixes)
|
||||
- SCHEDULE_ENABLED=${SCHEDULE_ENABLED:-false}
|
||||
- GITEA_MIRROR_INTERVAL=${GITEA_MIRROR_INTERVAL:-8h}
|
||||
- AUTO_IMPORT_REPOS=${AUTO_IMPORT_REPOS:-true}
|
||||
- AUTO_MIRROR_REPOS=${AUTO_MIRROR_REPOS:-false}
|
||||
# Repository Cleanup Configuration
|
||||
- CLEANUP_DELETE_IF_NOT_IN_GITHUB=${CLEANUP_DELETE_IF_NOT_IN_GITHUB:-false}
|
||||
- CLEANUP_ORPHANED_REPO_ACTION=${CLEANUP_ORPHANED_REPO_ACTION:-archive}
|
||||
- CLEANUP_DRY_RUN=${CLEANUP_DRY_RUN:-true}
|
||||
# Optional: Skip TLS verification (insecure, use only for testing)
|
||||
# - GITEA_SKIP_TLS_VERIFY=${GITEA_SKIP_TLS_VERIFY:-false}
|
||||
# Header Authentication (for Reverse Proxy SSO)
|
||||
|
||||
@@ -35,8 +35,8 @@ else
|
||||
echo "No custom CA certificates found in /app/certs"
|
||||
fi
|
||||
|
||||
# Check if system CA bundle is mounted and use it
|
||||
if [ -f "/etc/ssl/certs/ca-certificates.crt" ] && [ ! -L "/etc/ssl/certs/ca-certificates.crt" ]; then
|
||||
# Check if system CA bundle is mounted and use it (only if not already set)
|
||||
if [ -z "$NODE_EXTRA_CA_CERTS" ] && [ -f "/etc/ssl/certs/ca-certificates.crt" ] && [ ! -L "/etc/ssl/certs/ca-certificates.crt" ]; then
|
||||
# Check if it's a mounted file (not the default symlink)
|
||||
if [ "$(stat -c '%d' /etc/ssl/certs/ca-certificates.crt 2>/dev/null)" != "$(stat -c '%d' / 2>/dev/null)" ] || \
|
||||
[ "$(stat -f '%d' /etc/ssl/certs/ca-certificates.crt 2>/dev/null)" != "$(stat -f '%d' / 2>/dev/null)" ]; then
|
||||
@@ -120,231 +120,13 @@ fi
|
||||
# Dependencies are already installed during the Docker build process
|
||||
|
||||
# Initialize the database if it doesn't exist
|
||||
# Note: Drizzle migrations will be run automatically when the app starts (see src/lib/db/index.ts)
|
||||
if [ ! -f "/app/data/gitea-mirror.db" ]; then
|
||||
echo "Initializing database..."
|
||||
if [ -f "dist/scripts/init-db.js" ]; then
|
||||
bun dist/scripts/init-db.js
|
||||
elif [ -f "dist/scripts/manage-db.js" ]; then
|
||||
bun dist/scripts/manage-db.js init
|
||||
elif [ -f "scripts/manage-db.ts" ]; then
|
||||
bun scripts/manage-db.ts init
|
||||
else
|
||||
echo "Warning: Could not find database initialization scripts in dist/scripts."
|
||||
echo "Creating and initializing database manually..."
|
||||
|
||||
# Create the database file
|
||||
touch /app/data/gitea-mirror.db
|
||||
|
||||
# Initialize the database with required tables
|
||||
sqlite3 /app/data/gitea-mirror.db <<EOF
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS configs (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
github_config TEXT NOT NULL,
|
||||
gitea_config TEXT NOT NULL,
|
||||
include TEXT NOT NULL DEFAULT '["*"]',
|
||||
exclude TEXT NOT NULL DEFAULT '[]',
|
||||
schedule_config TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS repositories (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
config_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
full_name TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
clone_url TEXT NOT NULL,
|
||||
owner TEXT NOT NULL,
|
||||
organization TEXT,
|
||||
mirrored_location TEXT DEFAULT '',
|
||||
is_private INTEGER NOT NULL DEFAULT 0,
|
||||
is_fork INTEGER NOT NULL DEFAULT 0,
|
||||
forked_from TEXT,
|
||||
has_issues INTEGER NOT NULL DEFAULT 0,
|
||||
is_starred INTEGER NOT NULL DEFAULT 0,
|
||||
is_archived INTEGER NOT NULL DEFAULT 0,
|
||||
size INTEGER NOT NULL DEFAULT 0,
|
||||
has_lfs INTEGER NOT NULL DEFAULT 0,
|
||||
has_submodules INTEGER NOT NULL DEFAULT 0,
|
||||
default_branch TEXT NOT NULL,
|
||||
visibility TEXT NOT NULL DEFAULT 'public',
|
||||
status TEXT NOT NULL DEFAULT 'imported',
|
||||
last_mirrored INTEGER,
|
||||
error_message TEXT,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (config_id) REFERENCES configs(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS organizations (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
config_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
avatar_url TEXT NOT NULL,
|
||||
membership_role TEXT NOT NULL DEFAULT 'member',
|
||||
is_included INTEGER NOT NULL DEFAULT 1,
|
||||
status TEXT NOT NULL DEFAULT 'imported',
|
||||
last_mirrored INTEGER,
|
||||
error_message TEXT,
|
||||
repository_count INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (config_id) REFERENCES configs(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS mirror_jobs (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
repository_id TEXT,
|
||||
repository_name TEXT,
|
||||
organization_id TEXT,
|
||||
organization_name TEXT,
|
||||
details TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'imported',
|
||||
message TEXT NOT NULL,
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- New fields for job resilience
|
||||
job_type TEXT NOT NULL DEFAULT 'mirror',
|
||||
batch_id TEXT,
|
||||
total_items INTEGER,
|
||||
completed_items INTEGER DEFAULT 0,
|
||||
item_ids TEXT, -- JSON array as text
|
||||
completed_item_ids TEXT DEFAULT '[]', -- JSON array as text
|
||||
in_progress INTEGER NOT NULL DEFAULT 0, -- Boolean as integer
|
||||
started_at TIMESTAMP,
|
||||
completed_at TIMESTAMP,
|
||||
last_checkpoint TIMESTAMP,
|
||||
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_user_id ON mirror_jobs(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_batch_id ON mirror_jobs(batch_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_in_progress ON mirror_jobs(in_progress);
|
||||
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_job_type ON mirror_jobs(job_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_mirror_jobs_timestamp ON mirror_jobs(timestamp);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
channel TEXT NOT NULL,
|
||||
payload TEXT NOT NULL,
|
||||
read INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_events_user_channel ON events(user_id, channel);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_read ON events(read);
|
||||
EOF
|
||||
echo "Database initialized with required tables."
|
||||
fi
|
||||
echo "Database not found. It will be created and initialized via Drizzle migrations on first app startup..."
|
||||
# Create empty database file so migrations can run
|
||||
touch /app/data/gitea-mirror.db
|
||||
else
|
||||
echo "Database already exists, checking for issues..."
|
||||
if [ -f "dist/scripts/fix-db-issues.js" ]; then
|
||||
bun dist/scripts/fix-db-issues.js
|
||||
elif [ -f "dist/scripts/manage-db.js" ]; then
|
||||
bun dist/scripts/manage-db.js fix
|
||||
elif [ -f "scripts/manage-db.ts" ]; then
|
||||
bun scripts/manage-db.ts fix
|
||||
fi
|
||||
|
||||
# Run database migrations
|
||||
echo "Running database migrations..."
|
||||
|
||||
# Update mirror_jobs table with new columns for resilience
|
||||
if [ -f "dist/scripts/update-mirror-jobs-table.js" ]; then
|
||||
echo "Updating mirror_jobs table..."
|
||||
bun dist/scripts/update-mirror-jobs-table.js
|
||||
elif [ -f "scripts/update-mirror-jobs-table.ts" ]; then
|
||||
echo "Updating mirror_jobs table using TypeScript script..."
|
||||
bun scripts/update-mirror-jobs-table.ts
|
||||
else
|
||||
echo "Warning: Could not find mirror_jobs table update script."
|
||||
fi
|
||||
|
||||
# Run v3 migrations if needed
|
||||
echo "Checking for v3 migrations..."
|
||||
|
||||
# Check if we need to run Better Auth migration (check if accounts table exists)
|
||||
if ! sqlite3 /app/data/gitea-mirror.db "SELECT name FROM sqlite_master WHERE type='table' AND name='accounts';" | grep -q accounts; then
|
||||
echo "🔄 v3 Migration: Creating Better Auth tables..."
|
||||
# Create Better Auth tables
|
||||
sqlite3 /app/data/gitea-mirror.db <<EOF
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id TEXT PRIMARY KEY,
|
||||
userId TEXT NOT NULL,
|
||||
accountId TEXT NOT NULL,
|
||||
providerId TEXT NOT NULL,
|
||||
accessToken TEXT,
|
||||
refreshToken TEXT,
|
||||
expiresAt INTEGER,
|
||||
password TEXT,
|
||||
createdAt INTEGER NOT NULL,
|
||||
updatedAt INTEGER NOT NULL,
|
||||
FOREIGN KEY (userId) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
userId TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
expiresAt INTEGER NOT NULL,
|
||||
createdAt INTEGER NOT NULL,
|
||||
updatedAt INTEGER NOT NULL,
|
||||
FOREIGN KEY (userId) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS verification_tokens (
|
||||
id TEXT PRIMARY KEY,
|
||||
identifier TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
expires INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_userId ON accounts(userId);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token);
|
||||
CREATE INDEX IF NOT EXISTS idx_verification_identifier_token ON verification_tokens(identifier, token);
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Run Better Auth user migration
|
||||
if [ -f "dist/scripts/migrate-better-auth.js" ]; then
|
||||
echo "🔄 v3 Migration: Migrating users to Better Auth..."
|
||||
bun dist/scripts/migrate-better-auth.js
|
||||
elif [ -f "scripts/migrate-better-auth.ts" ]; then
|
||||
echo "🔄 v3 Migration: Migrating users to Better Auth..."
|
||||
bun scripts/migrate-better-auth.ts
|
||||
fi
|
||||
|
||||
# Run token encryption migration
|
||||
if [ -f "dist/scripts/migrate-tokens-encryption.js" ]; then
|
||||
echo "🔄 v3 Migration: Encrypting stored tokens..."
|
||||
bun dist/scripts/migrate-tokens-encryption.js
|
||||
elif [ -f "scripts/migrate-tokens-encryption.ts" ]; then
|
||||
echo "🔄 v3 Migration: Encrypting stored tokens..."
|
||||
bun scripts/migrate-tokens-encryption.ts
|
||||
fi
|
||||
echo "Database already exists, Drizzle will check for pending migrations on startup..."
|
||||
fi
|
||||
|
||||
# Extract version from package.json and set as environment variable
|
||||
@@ -355,6 +137,28 @@ fi
|
||||
|
||||
|
||||
|
||||
# Initialize configuration from environment variables if provided
|
||||
echo "Checking for environment configuration..."
|
||||
if [ -f "dist/scripts/startup-env-config.js" ]; then
|
||||
echo "Loading configuration from environment variables..."
|
||||
bun dist/scripts/startup-env-config.js
|
||||
ENV_CONFIG_EXIT_CODE=$?
|
||||
elif [ -f "scripts/startup-env-config.ts" ]; then
|
||||
echo "Loading configuration from environment variables..."
|
||||
bun scripts/startup-env-config.ts
|
||||
ENV_CONFIG_EXIT_CODE=$?
|
||||
else
|
||||
echo "Environment configuration script not found. Skipping."
|
||||
ENV_CONFIG_EXIT_CODE=0
|
||||
fi
|
||||
|
||||
# Log environment config result
|
||||
if [ $ENV_CONFIG_EXIT_CODE -eq 0 ]; then
|
||||
echo "✅ Environment configuration loaded successfully"
|
||||
else
|
||||
echo "⚠️ Environment configuration loading completed with warnings"
|
||||
fi
|
||||
|
||||
# Run startup recovery to handle any interrupted jobs
|
||||
echo "Running startup recovery..."
|
||||
if [ -f "dist/scripts/startup-recovery.js" ]; then
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
# Better Auth Migration Guide
|
||||
|
||||
This document describes the migration from the legacy authentication system to Better Auth.
|
||||
|
||||
## Overview
|
||||
|
||||
Gitea Mirror has been migrated to use Better Auth, a modern authentication library that provides:
|
||||
- Built-in support for email/password authentication
|
||||
- Session management with secure cookies
|
||||
- Database adapter with Drizzle ORM
|
||||
- Ready for OAuth2, OIDC, and SSO integrations
|
||||
- Type-safe authentication throughout the application
|
||||
|
||||
## Key Changes
|
||||
|
||||
### 1. Database Schema
|
||||
|
||||
New tables added:
|
||||
- `sessions` - User session management
|
||||
- `accounts` - Authentication providers (credentials, OAuth, etc.)
|
||||
- `verification_tokens` - Email verification and password reset tokens
|
||||
|
||||
Modified tables:
|
||||
- `users` - Added `emailVerified` field
|
||||
|
||||
### 2. Authentication Flow
|
||||
|
||||
**Login:**
|
||||
- Users now log in with email instead of username
|
||||
- Endpoint: `/api/auth/sign-in/email`
|
||||
- Session cookies are automatically managed
|
||||
|
||||
**Registration:**
|
||||
- Users register with username, email, and password
|
||||
- Username is stored as an additional field
|
||||
- Endpoint: `/api/auth/sign-up/email`
|
||||
|
||||
### 3. API Routes
|
||||
|
||||
All auth routes are now handled by Better Auth's catch-all handler:
|
||||
- `/api/auth/[...all].ts` handles all authentication endpoints
|
||||
|
||||
Legacy routes have been backed up to `/src/pages/api/auth/legacy-backup/`
|
||||
|
||||
### 4. Session Management
|
||||
|
||||
Sessions are now managed by Better Auth:
|
||||
- Middleware automatically populates `context.locals.user` and `context.locals.session`
|
||||
- Use `useAuth()` hook in React components for client-side auth
|
||||
- Sessions expire after 30 days by default
|
||||
|
||||
## Future OIDC/SSO Configuration
|
||||
|
||||
The project is now ready for OIDC and SSO integrations. To enable:
|
||||
|
||||
### 1. Enable SSO Plugin
|
||||
|
||||
```typescript
|
||||
// src/lib/auth.ts
|
||||
import { sso } from "better-auth/plugins/sso";
|
||||
|
||||
export const auth = betterAuth({
|
||||
// ... existing config
|
||||
plugins: [
|
||||
sso({
|
||||
provisionUser: async (data) => {
|
||||
// Custom user provisioning logic
|
||||
return data;
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Register OIDC Providers
|
||||
|
||||
```typescript
|
||||
// Example: Register an OIDC provider
|
||||
await authClient.sso.register({
|
||||
issuer: "https://idp.example.com",
|
||||
domain: "example.com",
|
||||
clientId: "your-client-id",
|
||||
clientSecret: "your-client-secret",
|
||||
providerId: "example-provider",
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Enable OIDC Provider Mode
|
||||
|
||||
To make Gitea Mirror act as an OIDC provider:
|
||||
|
||||
```typescript
|
||||
// src/lib/auth.ts
|
||||
import { oidcProvider } from "better-auth/plugins/oidc";
|
||||
|
||||
export const auth = betterAuth({
|
||||
// ... existing config
|
||||
plugins: [
|
||||
oidcProvider({
|
||||
loginPage: "/signin",
|
||||
consentPage: "/oauth/consent",
|
||||
metadata: {
|
||||
issuer: process.env.BETTER_AUTH_URL || "http://localhost:3000",
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Database Migration for SSO
|
||||
|
||||
When enabling SSO/OIDC, run migrations to add required tables:
|
||||
|
||||
```bash
|
||||
# Generate the schema
|
||||
bun drizzle-kit generate
|
||||
|
||||
# Apply the migration
|
||||
bun drizzle-kit migrate
|
||||
```
|
||||
|
||||
New tables that will be added:
|
||||
- `sso_providers` - SSO provider configurations
|
||||
- `oauth_applications` - OAuth2 client applications
|
||||
- `oauth_access_tokens` - OAuth2 access tokens
|
||||
- `oauth_consents` - User consent records
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Required environment variables:
|
||||
|
||||
```env
|
||||
# Better Auth configuration
|
||||
BETTER_AUTH_SECRET=your-secret-key
|
||||
BETTER_AUTH_URL=http://localhost:3000
|
||||
|
||||
# Legacy (kept for compatibility)
|
||||
JWT_SECRET=your-secret-key
|
||||
```
|
||||
|
||||
## Migration Script
|
||||
|
||||
To migrate existing users to Better Auth:
|
||||
|
||||
```bash
|
||||
bun run migrate:better-auth
|
||||
```
|
||||
|
||||
This script:
|
||||
1. Creates credential accounts for existing users
|
||||
2. Moves password hashes to the accounts table
|
||||
3. Preserves user creation dates
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Login Issues
|
||||
- Ensure users log in with email, not username
|
||||
- Check that BETTER_AUTH_SECRET is set
|
||||
- Verify database migrations have been applied
|
||||
|
||||
### Session Issues
|
||||
- Clear browser cookies if experiencing session problems
|
||||
- Check middleware is properly configured
|
||||
- Ensure auth routes are accessible at `/api/auth/*`
|
||||
|
||||
### Development Tips
|
||||
- Use `bun db:studio` to inspect database tables
|
||||
- Check `/api/auth/session` to verify current session
|
||||
- Enable debug logging in Better Auth for troubleshooting
|
||||
|
||||
## Resources
|
||||
|
||||
- [Better Auth Documentation](https://better-auth.com)
|
||||
- [Better Auth Astro Integration](https://better-auth.com/docs/integrations/astro)
|
||||
- [Better Auth Plugins](https://better-auth.com/docs/plugins)
|
||||
@@ -1,205 +0,0 @@
|
||||
# Build Guide
|
||||
|
||||
This guide covers building the open-source version of Gitea Mirror.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Bun** >= 1.2.9 (primary runtime)
|
||||
- **Node.js** >= 20 (for compatibility)
|
||||
- **Git**
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://github.com/yourusername/gitea-mirror.git
|
||||
cd gitea-mirror
|
||||
|
||||
# Install dependencies
|
||||
bun install
|
||||
|
||||
# Initialize database
|
||||
bun run init-db
|
||||
|
||||
# Build for production
|
||||
bun run build
|
||||
|
||||
# Start the application
|
||||
bun run start
|
||||
```
|
||||
|
||||
## Build Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `bun run build` | Production build |
|
||||
| `bun run dev` | Development server |
|
||||
| `bun run preview` | Preview production build |
|
||||
| `bun test` | Run tests |
|
||||
| `bun run cleanup-db` | Remove database files |
|
||||
|
||||
## Build Output
|
||||
|
||||
The build creates:
|
||||
- `dist/` - Production-ready server files
|
||||
- `.astro/` - Build cache (git-ignored)
|
||||
- `data/` - SQLite database location
|
||||
|
||||
## Development Build
|
||||
|
||||
For active development with hot reload:
|
||||
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
Access the application at http://localhost:4321
|
||||
|
||||
## Production Build
|
||||
|
||||
```bash
|
||||
# Build
|
||||
bun run build
|
||||
|
||||
# Test the build
|
||||
bun run preview
|
||||
|
||||
# Run in production
|
||||
bun run start
|
||||
```
|
||||
|
||||
## Docker Build
|
||||
|
||||
```dockerfile
|
||||
# Build Docker image
|
||||
docker build -t gitea-mirror:latest .
|
||||
|
||||
# Run container
|
||||
docker run -p 3000:3000 gitea-mirror:latest
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Create a `.env` file:
|
||||
|
||||
```env
|
||||
# Database
|
||||
DATABASE_PATH=./data/gitea-mirror.db
|
||||
|
||||
# Authentication
|
||||
JWT_SECRET=your-secret-here
|
||||
|
||||
# GitHub Configuration
|
||||
GITHUB_TOKEN=ghp_...
|
||||
GITHUB_WEBHOOK_SECRET=...
|
||||
|
||||
# Gitea Configuration
|
||||
GITEA_URL=https://your-gitea.com
|
||||
GITEA_TOKEN=...
|
||||
```
|
||||
|
||||
## Common Build Issues
|
||||
|
||||
### Missing Dependencies
|
||||
|
||||
```bash
|
||||
# Solution
|
||||
bun install
|
||||
```
|
||||
|
||||
### Database Not Initialized
|
||||
|
||||
```bash
|
||||
# Solution
|
||||
bun run init-db
|
||||
```
|
||||
|
||||
### Port Already in Use
|
||||
|
||||
```bash
|
||||
# Change port
|
||||
PORT=3001 bun run dev
|
||||
```
|
||||
|
||||
### Build Cache Issues
|
||||
|
||||
```bash
|
||||
# Clear cache
|
||||
rm -rf .astro/ dist/
|
||||
bun run build
|
||||
```
|
||||
|
||||
## Build Optimization
|
||||
|
||||
### Development Speed
|
||||
|
||||
- Use `bun run dev` for hot reload
|
||||
- Skip type checking during rapid development
|
||||
- Keep `.astro/` cache between builds
|
||||
|
||||
### Production Optimization
|
||||
|
||||
- Minification enabled automatically
|
||||
- Tree shaking removes unused code
|
||||
- Image optimization with Sharp
|
||||
|
||||
## Validation
|
||||
|
||||
After building, verify:
|
||||
|
||||
```bash
|
||||
# Check build output
|
||||
ls -la dist/
|
||||
|
||||
# Test server starts
|
||||
bun run start
|
||||
|
||||
# Check health endpoint
|
||||
curl http://localhost:3000/api/health
|
||||
```
|
||||
|
||||
## CI/CD Build
|
||||
|
||||
Example GitHub Actions workflow:
|
||||
|
||||
```yaml
|
||||
name: Build and Test
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- run: bun install
|
||||
- run: bun run build
|
||||
- run: bun test
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build Fails
|
||||
|
||||
1. Check Bun version: `bun --version`
|
||||
2. Clear dependencies: `rm -rf node_modules && bun install`
|
||||
3. Check for syntax errors: `bunx tsc --noEmit`
|
||||
|
||||
### Runtime Errors
|
||||
|
||||
1. Check environment variables
|
||||
2. Verify database exists
|
||||
3. Check file permissions
|
||||
|
||||
## Performance
|
||||
|
||||
Expected build times:
|
||||
- Clean build: ~5-10 seconds
|
||||
- Incremental build: ~2-5 seconds
|
||||
- Development startup: ~1-2 seconds
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Configure with [Configuration Guide](./CONFIGURATION.md)
|
||||
- Deploy with [Deployment Guide](./DEPLOYMENT.md)
|
||||
- Set up authentication with [SSO Guide](./SSO-OIDC-SETUP.md)
|
||||
@@ -1 +0,0 @@
|
||||
../certs/README.md
|
||||
@@ -16,27 +16,22 @@ This guide covers the development workflow for the open-source Gitea Mirror.
|
||||
|
||||
1. **Clone the repository**:
|
||||
```bash
|
||||
git clone https://github.com/yourusername/gitea-mirror.git
|
||||
git clone https://github.com/RayLabsHQ/gitea-mirror.git
|
||||
cd gitea-mirror
|
||||
```
|
||||
|
||||
2. **Install dependencies**:
|
||||
2. **Install dependencies and seed the SQLite database**:
|
||||
```bash
|
||||
bun install
|
||||
bun run setup
|
||||
```
|
||||
|
||||
3. **Initialize database**:
|
||||
```bash
|
||||
bun run init-db
|
||||
```
|
||||
|
||||
4. **Configure environment**:
|
||||
3. **Configure environment (optional)**:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your settings
|
||||
```
|
||||
|
||||
5. **Start development server**:
|
||||
4. **Start the development server**:
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
@@ -45,29 +40,33 @@ bun run dev
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `bun run dev` | Start development server with hot reload |
|
||||
| `bun run build` | Build for production |
|
||||
| `bun run preview` | Preview production build |
|
||||
| `bun test` | Run all tests |
|
||||
| `bun run dev` | Start the Bun + Astro dev server with hot reload |
|
||||
| `bun run build` | Build the production bundle |
|
||||
| `bun run preview` | Preview the production build locally |
|
||||
| `bun test` | Run the Bun test suite |
|
||||
| `bun test:watch` | Run tests in watch mode |
|
||||
| `bun run db:studio` | Open database GUI |
|
||||
| `bun run db:studio` | Launch Drizzle Kit Studio |
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
gitea-mirror/
|
||||
├── src/
|
||||
│ ├── components/ # React components
|
||||
│ ├── pages/ # Astro pages & API routes
|
||||
│ ├── lib/ # Core logic
|
||||
│ │ ├── db/ # Database queries
|
||||
│ │ ├── utils/ # Helper functions
|
||||
│ │ └── modules/ # Module system
|
||||
│ ├── hooks/ # React hooks
|
||||
│ └── types/ # TypeScript types
|
||||
├── public/ # Static assets
|
||||
├── scripts/ # Utility scripts
|
||||
└── tests/ # Test files
|
||||
├── src/ # Application UI, API routes, and services
|
||||
│ ├── components/ # React components rendered inside Astro pages
|
||||
│ ├── pages/ # Astro pages and API routes (e.g., /api/*)
|
||||
│ ├── lib/ # Core logic: GitHub/Gitea clients, scheduler, recovery, db helpers
|
||||
│ │ ├── db/ # Drizzle adapter + schema
|
||||
│ │ ├── modules/ # Module wiring (jobs, integrations)
|
||||
│ │ └── utils/ # Shared utilities
|
||||
│ ├── hooks/ # React hooks
|
||||
│ ├── content/ # In-app documentation and templated content
|
||||
│ ├── layouts/ # Shared layout components
|
||||
│ ├── styles/ # Tailwind CSS entrypoints
|
||||
│ └── types/ # TypeScript types
|
||||
├── scripts/ # Bun scripts for DB management and maintenance
|
||||
├── www/ # Marketing site (Astro + MDX use cases)
|
||||
├── public/ # Static assets served by Vite/Astro
|
||||
└── tests/ # Dedicated integration/unit test helpers
|
||||
```
|
||||
|
||||
## Feature Development
|
||||
@@ -80,10 +79,10 @@ git checkout -b feature/my-feature
|
||||
```
|
||||
|
||||
2. **Plan your changes**:
|
||||
- UI components in `/src/components/`
|
||||
- API endpoints in `/src/pages/api/`
|
||||
- Database queries in `/src/lib/db/queries/`
|
||||
- Types in `/src/types/`
|
||||
- UI components live in `src/components/`
|
||||
- API endpoints live in `src/pages/api/`
|
||||
- Database logic is under `src/lib/db/` (schema + adapter)
|
||||
- Shared types are in `src/types/`
|
||||
|
||||
3. **Implement the feature**:
|
||||
|
||||
@@ -120,7 +119,7 @@ describe('My Feature', () => {
|
||||
|
||||
5. **Update documentation**:
|
||||
- Add JSDoc comments
|
||||
- Update README if needed
|
||||
- Update README/docs if needed
|
||||
- Document API changes
|
||||
|
||||
## Database Development
|
||||
@@ -352,4 +351,4 @@ git push origin v2.23.0
|
||||
|
||||
- Check existing [issues](https://github.com/yourusername/gitea-mirror/issues)
|
||||
- Join [discussions](https://github.com/yourusername/gitea-mirror/discussions)
|
||||
- Read the [FAQ](./FAQ.md)
|
||||
- Read the [FAQ](./FAQ.md)
|
||||
|
||||
416
docs/ENVIRONMENT_VARIABLES.md
Normal file
@@ -0,0 +1,416 @@
|
||||
# Environment Variables Documentation
|
||||
|
||||
This document provides a comprehensive list of all environment variables supported by Gitea Mirror. These can be used to configure the application via Docker or other deployment methods.
|
||||
|
||||
## Environment Variables and UI Interaction
|
||||
|
||||
When environment variables are set:
|
||||
1. They are loaded on application startup
|
||||
2. Values are stored in the database on first load
|
||||
3. The UI will display these values and they can be modified
|
||||
4. UI changes are saved to the database and persist
|
||||
5. Environment variables provide initial defaults but don't override UI changes
|
||||
|
||||
**Note**: Some critical settings like `GITEA_LFS`, `MIRROR_RELEASES`, and `MIRROR_METADATA` will be visible and configurable in the UI even when set via environment variables.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Core Configuration](#core-configuration)
|
||||
- [GitHub Configuration](#github-configuration)
|
||||
- [Gitea Configuration](#gitea-configuration)
|
||||
- [Mirror Options](#mirror-options)
|
||||
- [Automation Configuration](#automation-configuration)
|
||||
- [Database Cleanup Configuration](#database-cleanup-configuration)
|
||||
- [Authentication Configuration](#authentication-configuration)
|
||||
- [Docker Configuration](#docker-configuration)
|
||||
|
||||
## Core Configuration
|
||||
|
||||
Essential application settings required for running Gitea Mirror.
|
||||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `NODE_ENV` | Application environment | `production` | No |
|
||||
| `HOST` | Server host binding | `0.0.0.0` | No |
|
||||
| `PORT` | Server port | `4321` | 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_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 |
|
||||
|
||||
## GitHub Configuration
|
||||
|
||||
Settings for connecting to and configuring GitHub repository sources.
|
||||
|
||||
### Basic Settings
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `GITHUB_USERNAME` | Your GitHub username | - | - |
|
||||
| `GITHUB_TOKEN` | GitHub personal access token (requires repo and admin:org scopes) | - | - |
|
||||
| `GITHUB_TYPE` | GitHub account type | `personal` | `personal`, `organization` |
|
||||
|
||||
### Repository Selection
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `PRIVATE_REPOSITORIES` | Include private repositories | `false` | `true`, `false` |
|
||||
| `PUBLIC_REPOSITORIES` | Include public repositories | `true` | `true`, `false` |
|
||||
| `INCLUDE_ARCHIVED` | Include archived repositories | `false` | `true`, `false` |
|
||||
| `SKIP_FORKS` | Skip forked repositories | `false` | `true`, `false` |
|
||||
| `MIRROR_STARRED` | Mirror starred repositories | `false` | `true`, `false` |
|
||||
| `STARRED_REPOS_ORG` | Organization name for starred repos | `starred` | Any string |
|
||||
|
||||
### Organization Settings
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `MIRROR_ORGANIZATIONS` | Mirror organization repositories | `false` | `true`, `false` |
|
||||
| `PRESERVE_ORG_STRUCTURE` | Preserve GitHub organization structure in Gitea | `false` | `true`, `false` |
|
||||
| `ONLY_MIRROR_ORGS` | Only mirror organization repos (skip personal) | `false` | `true`, `false` |
|
||||
| `MIRROR_STRATEGY` | Repository organization strategy | `preserve` | `preserve`, `single-org`, `flat-user`, `mixed` |
|
||||
|
||||
### Advanced Settings
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `SKIP_STARRED_ISSUES` | Enable lightweight mode for starred repos (skip issues) | `false` | `true`, `false` |
|
||||
|
||||
## Gitea Configuration
|
||||
|
||||
Settings for the destination Gitea instance.
|
||||
|
||||
### Connection Settings
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `GITEA_URL` | Gitea instance URL | - | Valid URL |
|
||||
| `GITEA_TOKEN` | Gitea access token | - | - |
|
||||
| `GITEA_USERNAME` | Gitea username | - | - |
|
||||
| `GITEA_ORGANIZATION` | Default organization for single-org strategy | `github-mirrors` | Any string |
|
||||
|
||||
### Repository Settings
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `GITEA_ORG_VISIBILITY` | Default organization visibility | `public` | `public`, `private`, `limited`, `default` |
|
||||
| `GITEA_MIRROR_INTERVAL` | Mirror sync interval - **automatically enables scheduled mirroring when set** | `8h` | Duration string (e.g., `30m`, `1h`, `8h`, `24h`, `1d`) or seconds |
|
||||
| `GITEA_LFS` | Enable LFS support (requires LFS on Gitea server) - Shows in UI | `false` | `true`, `false` |
|
||||
| `GITEA_CREATE_ORG` | Auto-create organizations | `true` | `true`, `false` |
|
||||
| `GITEA_PRESERVE_VISIBILITY` | Preserve GitHub repo visibility in Gitea | `false` | `true`, `false` |
|
||||
|
||||
### Template Settings
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `GITEA_TEMPLATE_OWNER` | Template repository owner | - | Any string |
|
||||
| `GITEA_TEMPLATE_REPO` | Template repository name | - | Any string |
|
||||
|
||||
### Topic Settings
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `GITEA_ADD_TOPICS` | Add topics to repositories | `true` | `true`, `false` |
|
||||
| `GITEA_TOPIC_PREFIX` | Prefix for repository topics | - | Any string |
|
||||
|
||||
### Fork Handling
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `GITEA_FORK_STRATEGY` | How to handle forked repositories | `reference` | `skip`, `reference`, `full-copy` |
|
||||
|
||||
### Additional Settings
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `GITEA_SKIP_TLS_VERIFY` | Skip TLS certificate verification (WARNING: insecure) | `false` | `true`, `false` |
|
||||
|
||||
## Mirror Options
|
||||
|
||||
Control what content gets mirrored from GitHub to Gitea.
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `MIRROR_RELEASES` | Mirror GitHub releases | `false` | `true`, `false` |
|
||||
| `RELEASE_LIMIT` | Maximum number of releases to mirror per repository | `10` | Number (1-100) |
|
||||
| `MIRROR_WIKI` | Mirror wiki content | `false` | `true`, `false` |
|
||||
| `MIRROR_METADATA` | Master toggle for metadata mirroring | `false` | `true`, `false` |
|
||||
| `MIRROR_ISSUES` | Mirror issues (requires MIRROR_METADATA=true) | `false` | `true`, `false` |
|
||||
| `MIRROR_PULL_REQUESTS` | Mirror pull requests (requires MIRROR_METADATA=true) | `false` | `true`, `false` |
|
||||
| `MIRROR_LABELS` | Mirror labels (requires MIRROR_METADATA=true) | `false` | `true`, `false` |
|
||||
| `MIRROR_MILESTONES` | Mirror milestones (requires MIRROR_METADATA=true) | `false` | `true`, `false` |
|
||||
| `MIRROR_ISSUE_CONCURRENCY` | Number of issues processed in parallel. Set above `1` to speed up mirroring at the risk of out-of-order creation. | `3` | Integer ≥ 1 |
|
||||
| `MIRROR_PULL_REQUEST_CONCURRENCY` | Number of pull requests processed in parallel. Values above `1` may cause ordering differences. | `5` | Integer ≥ 1 |
|
||||
|
||||
> **Ordering vs Throughput:** Metadata now mirrors sequentially by default to preserve chronology. Increase the concurrency variables only if you can tolerate minor out-of-order entries.
|
||||
|
||||
## Automation Configuration
|
||||
|
||||
Configure automatic scheduled mirroring.
|
||||
|
||||
### Basic Schedule Settings
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `SCHEDULE_ENABLED` | Enable automatic mirroring. **When set to `true`, automatically imports and mirrors all repositories on startup** (v3.5.3+) | `false` | `true`, `false` |
|
||||
| `SCHEDULE_INTERVAL` | Interval in seconds or cron expression. **Supports cron syntax for scheduled runs** (e.g., `"0 2 * * *"` for 2 AM daily) | `3600` | Number (seconds) or cron string |
|
||||
| `DELAY` | Legacy: same as SCHEDULE_INTERVAL | `3600` | Number (seconds) |
|
||||
|
||||
> **🚀 Auto-Start Feature (v3.5.3+)**
|
||||
> Setting either `SCHEDULE_ENABLED=true` or `GITEA_MIRROR_INTERVAL` triggers auto-start functionality where the service will:
|
||||
> 1. **Import** all GitHub repositories on startup
|
||||
> 2. **Mirror** them to Gitea immediately
|
||||
> 3. **Continue syncing** at the configured interval
|
||||
> 4. **Auto-discover** new repositories
|
||||
> 5. **Clean up** deleted repositories (if configured)
|
||||
>
|
||||
> This eliminates the need for manual button clicks - perfect for Docker/Kubernetes deployments!
|
||||
|
||||
> **⏰ Scheduling with Cron Expressions**
|
||||
> Use cron expressions in `SCHEDULE_INTERVAL` to run at specific times:
|
||||
> - `"0 2 * * *"` - Daily at 2 AM
|
||||
> - `"0 */6 * * *"` - Every 6 hours
|
||||
> - `"0 0 * * 0"` - Weekly on Sunday at midnight
|
||||
> - `"0 3 * * 1-5"` - Weekdays at 3 AM (Monday-Friday)
|
||||
>
|
||||
> This is useful for optimizing bandwidth usage during low-activity periods.
|
||||
|
||||
### Execution Settings
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `SCHEDULE_CONCURRENT` | Allow concurrent mirror operations | `false` | `true`, `false` |
|
||||
| `SCHEDULE_BATCH_SIZE` | Number of repos to process in parallel | `10` | Number |
|
||||
| `SCHEDULE_PAUSE_BETWEEN_BATCHES` | Pause between batches (milliseconds) | `5000` | Number |
|
||||
|
||||
### Retry Configuration
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `SCHEDULE_RETRY_ATTEMPTS` | Number of retry attempts | `3` | Number |
|
||||
| `SCHEDULE_RETRY_DELAY` | Delay between retries (milliseconds) | `60000` | Number |
|
||||
| `SCHEDULE_TIMEOUT` | Max time for a mirror operation (milliseconds) | `3600000` | Number |
|
||||
| `SCHEDULE_AUTO_RETRY` | Automatically retry failed operations | `true` | `true`, `false` |
|
||||
|
||||
### Update Detection
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `AUTO_IMPORT_REPOS` | Automatically discover and import new GitHub repositories during scheduled syncs | `true` | `true`, `false` |
|
||||
| `AUTO_MIRROR_REPOS` | Automatically mirror newly imported repositories during scheduled syncs (no manual “Mirror All” required) | `false` | `true`, `false` |
|
||||
| `SCHEDULE_ONLY_MIRROR_UPDATED` | Only mirror repos with updates | `false` | `true`, `false` |
|
||||
| `SCHEDULE_UPDATE_INTERVAL` | Check for updates interval (milliseconds) | `86400000` | Number |
|
||||
| `SCHEDULE_SKIP_RECENTLY_MIRRORED` | Skip recently mirrored repos | `true` | `true`, `false` |
|
||||
| `SCHEDULE_RECENT_THRESHOLD` | Skip if mirrored within this time (milliseconds) | `3600000` | Number |
|
||||
|
||||
### Maintenance & Notifications
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `SCHEDULE_CLEANUP_BEFORE_MIRROR` | Run cleanup before mirroring | `false` | `true`, `false` |
|
||||
| `SCHEDULE_NOTIFY_ON_FAILURE` | Send notifications on failure | `true` | `true`, `false` |
|
||||
| `SCHEDULE_NOTIFY_ON_SUCCESS` | Send notifications on success | `false` | `true`, `false` |
|
||||
| `SCHEDULE_LOG_LEVEL` | Logging level | `info` | `error`, `warn`, `info`, `debug` |
|
||||
| `SCHEDULE_TIMEZONE` | Timezone for scheduling | `UTC` | Valid timezone string |
|
||||
|
||||
## Database Cleanup Configuration
|
||||
|
||||
Configure automatic cleanup of old events and data.
|
||||
|
||||
### Basic Settings
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `CLEANUP_ENABLED` | Enable automatic cleanup | `false` | `true`, `false` |
|
||||
| `CLEANUP_RETENTION_DAYS` | Days to keep events | `7` | Number |
|
||||
|
||||
### Repository Cleanup
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `CLEANUP_DELETE_FROM_GITEA` | Delete repositories from Gitea | `false` | `true`, `false` |
|
||||
| `CLEANUP_DELETE_IF_NOT_IN_GITHUB` | Delete repos not found in GitHub (automatically enables cleanup) | `true` | `true`, `false` |
|
||||
| `CLEANUP_ORPHANED_REPO_ACTION` | Action for orphaned repositories. **Note**: `archive` is recommended to preserve backups | `archive` | `skip`, `archive`, `delete` |
|
||||
| `CLEANUP_DRY_RUN` | Test mode without actual deletion | `false` | `true`, `false` |
|
||||
| `CLEANUP_PROTECTED_REPOS` | Comma-separated list of protected repository names | - | Comma-separated strings |
|
||||
|
||||
**🛡️ Safety Features (Backup Protection)**:
|
||||
- **GitHub Failures Don't Delete Backups**: Cleanup is automatically skipped if GitHub API returns errors (404, 403, connection issues)
|
||||
- **Archive Never Deletes**: The `archive` action ALWAYS preserves repository data, it never deletes
|
||||
- **Graceful Degradation**: If marking as archived fails, the repository remains fully accessible in Gitea
|
||||
- **The Purpose of Backups**: Your mirrors are preserved even when GitHub sources disappear - that's the whole point!
|
||||
|
||||
**Archive Behavior (Aligned with Gitea API)**:
|
||||
- **Regular repositories**: Uses Gitea's native archive feature (PATCH `/repos/{owner}/{repo}` with `archived: true`)
|
||||
- Makes repository read-only while preserving all data
|
||||
- **Mirror repositories**: Uses rename strategy (Gitea API returns 422 for archiving mirrors)
|
||||
- Renamed with `archived-` prefix for clear identification
|
||||
- Description updated with preservation notice and timestamp
|
||||
- Mirror interval set to 8760h (1 year) to minimize sync attempts
|
||||
- Repository remains fully accessible and cloneable
|
||||
- **Manual Sync Option**: Archived mirrors are still available on the Repositories page with automatic syncs disabled—use the `Manual Sync` action to refresh them on demand.
|
||||
|
||||
### Execution Settings
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `CLEANUP_BATCH_SIZE` | Number of items to process per batch | `10` | Number |
|
||||
| `CLEANUP_PAUSE_BETWEEN_DELETES` | Pause between deletions (milliseconds) | `2000` | Number |
|
||||
|
||||
## Authentication Configuration
|
||||
|
||||
Configure authentication methods and SSO.
|
||||
|
||||
### Header Authentication (Reverse Proxy SSO)
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `HEADER_AUTH_ENABLED` | Enable header-based authentication | `false` | `true`, `false` |
|
||||
| `HEADER_AUTH_USER_HEADER` | Header containing username | `X-Authentik-Username` | Header name |
|
||||
| `HEADER_AUTH_EMAIL_HEADER` | Header containing email | `X-Authentik-Email` | Header name |
|
||||
| `HEADER_AUTH_NAME_HEADER` | Header containing display name | `X-Authentik-Name` | Header name |
|
||||
| `HEADER_AUTH_AUTO_PROVISION` | Auto-create users from headers | `false` | `true`, `false` |
|
||||
| `HEADER_AUTH_ALLOWED_DOMAINS` | Comma-separated list of allowed email domains | - | Comma-separated domains |
|
||||
|
||||
## Docker Configuration
|
||||
|
||||
Settings specific to Docker deployments.
|
||||
|
||||
| Variable | Description | Default | Options |
|
||||
|----------|-------------|---------|---------|
|
||||
| `DOCKER_REGISTRY` | Docker registry URL | `ghcr.io` | Registry URL |
|
||||
| `DOCKER_IMAGE` | Docker image name | `raylabshq/gitea-mirror:` | Image name |
|
||||
| `DOCKER_TAG` | Docker image tag | `latest` | Tag name |
|
||||
|
||||
## Example Docker Compose Configuration
|
||||
|
||||
Here's an example of how to use these environment variables in a `docker-compose.yml` file:
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
gitea-mirror:
|
||||
image: ghcr.io/raylabshq/gitea-mirror:latest
|
||||
container_name: gitea-mirror
|
||||
environment:
|
||||
# Core Configuration
|
||||
- NODE_ENV=production
|
||||
- DATABASE_URL=file:data/gitea-mirror.db
|
||||
- BETTER_AUTH_SECRET=your-secure-secret-here
|
||||
# Primary access URL:
|
||||
- BETTER_AUTH_URL=https://gitea-mirror.mydomain.tld
|
||||
# Additional access URLs (local network + SSO providers):
|
||||
# - BETTER_AUTH_TRUSTED_ORIGINS=http://10.10.20.45:4321,http://192.168.1.100:4321,https://auth.provider.com
|
||||
|
||||
# GitHub Configuration
|
||||
- GITHUB_USERNAME=your-username
|
||||
- GITHUB_TOKEN=ghp_your_token_here
|
||||
- PRIVATE_REPOSITORIES=true
|
||||
- MIRROR_STARRED=true
|
||||
- SKIP_FORKS=false
|
||||
|
||||
# Gitea Configuration
|
||||
- GITEA_URL=http://gitea:3000
|
||||
- GITEA_USERNAME=admin
|
||||
- GITEA_TOKEN=your-gitea-token
|
||||
- GITEA_ORGANIZATION=github-mirrors
|
||||
- GITEA_ORG_VISIBILITY=public
|
||||
|
||||
# Mirror Options
|
||||
- MIRROR_RELEASES=true
|
||||
- MIRROR_WIKI=true
|
||||
- MIRROR_METADATA=true
|
||||
- MIRROR_ISSUES=true
|
||||
- MIRROR_PULL_REQUESTS=true
|
||||
|
||||
# Automation
|
||||
- SCHEDULE_ENABLED=true
|
||||
- SCHEDULE_INTERVAL=3600
|
||||
|
||||
# Cleanup
|
||||
- CLEANUP_ENABLED=true
|
||||
- CLEANUP_RETENTION_DAYS=30
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
ports:
|
||||
- "4321:4321"
|
||||
```
|
||||
|
||||
## Authentication URL Configuration
|
||||
|
||||
### Multiple Access URLs
|
||||
|
||||
To allow access to Gitea Mirror through multiple URLs (e.g., local IP and public domain), you need to configure both server and client settings:
|
||||
|
||||
**Example Configuration:**
|
||||
```bash
|
||||
# Primary URL (required) - where the auth server is hosted
|
||||
BETTER_AUTH_URL=https://gitea-mirror.mydomain.tld
|
||||
|
||||
# Client-side URL (optional) - tells the browser where to send auth requests
|
||||
# Set this to your primary domain when accessing from different origins
|
||||
PUBLIC_BETTER_AUTH_URL=https://gitea-mirror.mydomain.tld
|
||||
|
||||
# Additional trusted origins (optional) - origins allowed to make auth requests
|
||||
BETTER_AUTH_TRUSTED_ORIGINS=http://10.10.20.45:4321,http://192.168.1.100:4321
|
||||
```
|
||||
|
||||
This setup allows you to:
|
||||
- Access via local network IP: `http://10.10.20.45:4321`
|
||||
- Access via public domain: `https://gitea-mirror.mydomain.tld`
|
||||
- Auth requests from the IP will be sent to the domain (via `PUBLIC_BETTER_AUTH_URL`)
|
||||
- Each origin requires separate login due to browser cookie isolation
|
||||
|
||||
**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.
|
||||
|
||||
### Trusted Origins
|
||||
|
||||
The `BETTER_AUTH_TRUSTED_ORIGINS` variable serves multiple purposes:
|
||||
|
||||
1. **SSO/OIDC Providers**: When using external authentication providers (Google, Authentik, Okta)
|
||||
2. **Reverse Proxies**: When running behind nginx, Traefik, or other proxies
|
||||
3. **Cross-Origin Requests**: When the frontend and backend are on different domains
|
||||
4. **Development**: When testing from different URLs
|
||||
|
||||
**Example Scenarios:**
|
||||
```bash
|
||||
# For Authentik SSO integration
|
||||
BETTER_AUTH_TRUSTED_ORIGINS=https://authentik.company.com,https://auth.company.com
|
||||
|
||||
# For reverse proxy setup
|
||||
BETTER_AUTH_TRUSTED_ORIGINS=https://proxy.internal,https://public.domain.com
|
||||
|
||||
# For development with multiple environments
|
||||
BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:3000,http://192.168.1.100:3000
|
||||
```
|
||||
|
||||
**Important Notes:**
|
||||
- All URLs from `BETTER_AUTH_URL` are automatically trusted
|
||||
- URLs must be complete with protocol (http/https)
|
||||
- Multiple origins are separated by commas
|
||||
- No trailing slashes needed
|
||||
|
||||
## Notes
|
||||
|
||||
1. **First Run**: Environment variables are loaded when the container starts. The configuration is applied after the first user account is created.
|
||||
|
||||
2. **UI Priority**: Manual changes made through the web UI will be preserved. Environment variables only set values for empty fields.
|
||||
|
||||
3. **Token Security**: All tokens are encrypted before being stored in the database.
|
||||
|
||||
4. **Auto-Enabling Features**: Certain environment variables automatically enable features when set:
|
||||
- `GITEA_MIRROR_INTERVAL` - Automatically enables scheduled mirroring
|
||||
- `CLEANUP_DELETE_IF_NOT_IN_GITHUB=true` - Automatically enables repository cleanup
|
||||
- `SCHEDULE_INTERVAL` or `DELAY` - Automatically enables the scheduler
|
||||
|
||||
5. **Backward Compatibility**: The `DELAY` variable is maintained for backward compatibility but `SCHEDULE_INTERVAL` is preferred.
|
||||
|
||||
6. **Required Scopes**: The GitHub token requires the following scopes:
|
||||
- `repo` (full control of private repositories)
|
||||
- `admin:org` (read organization data)
|
||||
- Additional scopes may be required for specific features
|
||||
|
||||
For more examples and detailed configuration, see the `.env.example` file in the repository.
|
||||
@@ -1,77 +0,0 @@
|
||||
# Extending Gitea Mirror
|
||||
|
||||
Gitea Mirror is designed with extensibility in mind through a module system.
|
||||
|
||||
## Module System
|
||||
|
||||
The application provides a module interface that allows extending functionality:
|
||||
|
||||
```typescript
|
||||
export interface Module {
|
||||
name: string;
|
||||
version: string;
|
||||
init(app: AppContext): Promise<void>;
|
||||
cleanup?(): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
## Creating Custom Modules
|
||||
|
||||
You can create custom modules to add features:
|
||||
|
||||
```typescript
|
||||
// my-module.ts
|
||||
export class MyModule implements Module {
|
||||
name = 'my-module';
|
||||
version = '1.0.0';
|
||||
|
||||
async init(app: AppContext) {
|
||||
// Add your functionality
|
||||
app.addRoute('/api/my-endpoint', this.handler);
|
||||
}
|
||||
|
||||
async handler(context) {
|
||||
return new Response('Hello from my module!');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Module Context
|
||||
|
||||
Modules receive an `AppContext` with:
|
||||
- Database access
|
||||
- Event system
|
||||
- Route registration
|
||||
- Configuration
|
||||
|
||||
## Private Extensions
|
||||
|
||||
If you're developing private extensions:
|
||||
|
||||
1. Create a separate package/repository
|
||||
2. Implement the module interface
|
||||
3. Use Bun's linking feature for development:
|
||||
```bash
|
||||
# In your extension
|
||||
bun link
|
||||
|
||||
# In gitea-mirror
|
||||
bun link your-extension
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Keep modules focused on a single feature
|
||||
- Use TypeScript for type safety
|
||||
- Handle errors gracefully
|
||||
- Clean up resources in `cleanup()`
|
||||
- Document your module's API
|
||||
|
||||
## Community Modules
|
||||
|
||||
Share your modules with the community:
|
||||
- Create a GitHub repository
|
||||
- Tag it with `gitea-mirror-module`
|
||||
- Submit a PR to list it in our docs
|
||||
|
||||
For more details on the module system, see the source code in `/src/lib/modules/`.
|
||||
125
docs/README.md
@@ -1,118 +1,39 @@
|
||||
# Gitea Mirror Documentation
|
||||
|
||||
Welcome to the Gitea Mirror documentation. This guide covers everything you need to know about developing, building, and deploying the open-source version of Gitea Mirror.
|
||||
This folder contains engineering and operations references for the open-source Gitea Mirror project. Each guide focuses on the parts of the system that still require bespoke explanation beyond the in-app help and the main `README.md`.
|
||||
|
||||
## Documentation Overview
|
||||
## Available Guides
|
||||
|
||||
### Getting Started
|
||||
### Core workflow
|
||||
- **[DEVELOPMENT_WORKFLOW.md](./DEVELOPMENT_WORKFLOW.md)** – Set up a local environment, run scripts, and understand the repo layout (app + marketing site).
|
||||
- **[ENVIRONMENT_VARIABLES.md](./ENVIRONMENT_VARIABLES.md)** – Complete reference for every configuration flag supported by the app and Docker images.
|
||||
|
||||
- **[Development Workflow](./DEVELOPMENT_WORKFLOW.md)** - Set up your development environment and start contributing
|
||||
- **[Build Guide](./BUILD_GUIDE.md)** - Build Gitea Mirror from source
|
||||
- **[Configuration Guide](./CONFIGURATION.md)** - Configure all available options
|
||||
### Reliability & recovery
|
||||
- **[GRACEFUL_SHUTDOWN.md](./GRACEFUL_SHUTDOWN.md)** – How signal handling, shutdown coordination, and job persistence work in v3.
|
||||
- **[RECOVERY_IMPROVEMENTS.md](./RECOVERY_IMPROVEMENTS.md)** – Deep dive into the startup recovery workflow and supporting scripts.
|
||||
|
||||
### Deployment
|
||||
### Authentication
|
||||
- **[SSO-OIDC-SETUP.md](./SSO-OIDC-SETUP.md)** – Configure OIDC/SSO providers through the admin UI.
|
||||
- **[SSO_TESTING.md](./SSO_TESTING.md)** – Recipes for local and staging SSO testing (Google, Keycloak, mock providers).
|
||||
|
||||
- **[Deployment Guide](./DEPLOYMENT.md)** - Deploy to production environments
|
||||
- **[Docker Guide](./DOCKER.md)** - Container-based deployment
|
||||
- **[Reverse Proxy Setup](./REVERSE_PROXY.md)** - Configure with nginx/Caddy
|
||||
If you are looking for customer-facing playbooks, see the MDX use cases under `www/src/pages/use-cases/`.
|
||||
|
||||
### Features
|
||||
## Quick start for local development
|
||||
|
||||
- **[SSO/OIDC Setup](./SSO-OIDC-SETUP.md)** - Configure authentication providers
|
||||
- **[Sponsor Integration](./SPONSOR_INTEGRATION.md)** - GitHub Sponsors integration
|
||||
- **[Webhook Configuration](./WEBHOOKS.md)** - Set up GitHub webhooks
|
||||
|
||||
### Architecture
|
||||
|
||||
- **[Architecture Overview](./ARCHITECTURE.md)** - System design and components
|
||||
- **[API Documentation](./API.md)** - REST API endpoints
|
||||
- **[Database Schema](./DATABASE.md)** - SQLite structure
|
||||
|
||||
### Maintenance
|
||||
|
||||
- **[Migration Guide](../MIGRATION_GUIDE.md)** - Upgrade from previous versions
|
||||
- **[Better Auth Migration](./BETTER_AUTH_MIGRATION.md)** - Migrate authentication system
|
||||
- **[Troubleshooting](./TROUBLESHOOTING.md)** - Common issues and solutions
|
||||
- **[Backup & Restore](./BACKUP.md)** - Data management
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Clone and install**:
|
||||
```bash
|
||||
git clone https://github.com/yourusername/gitea-mirror.git
|
||||
git clone https://github.com/RayLabsHQ/gitea-mirror.git
|
||||
cd gitea-mirror
|
||||
bun install
|
||||
bun run setup # installs deps and seeds the SQLite DB
|
||||
bun run dev # starts the Astro/Bun app on http://localhost:4321
|
||||
```
|
||||
|
||||
2. **Configure**:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your GitHub and Gitea tokens
|
||||
```
|
||||
The first user you create locally becomes the administrator. All other configuration—GitHub owners, Gitea targets, scheduling, cleanup—is done through the **Configuration** screen in the UI.
|
||||
|
||||
3. **Initialize and run**:
|
||||
```bash
|
||||
bun run init-db
|
||||
bun run dev
|
||||
```
|
||||
## Contributing & support
|
||||
|
||||
4. **Access**: Open http://localhost:4321
|
||||
- 🎯 Contribution guide: [../CONTRIBUTING.md](../CONTRIBUTING.md)
|
||||
- 📘 Code of conduct: [../CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md)
|
||||
- 🐞 Issues & feature requests: <https://github.com/RayLabsHQ/gitea-mirror/issues>
|
||||
- 💬 Discussions: <https://github.com/RayLabsHQ/gitea-mirror/discussions>
|
||||
|
||||
## Key Features
|
||||
|
||||
- 🔄 **Automatic Mirroring** - Keep repositories synchronized
|
||||
- 🗂️ **Organization Support** - Mirror entire organizations
|
||||
- ⭐ **Starred Repos** - Mirror your starred repositories
|
||||
- 🔐 **Self-Hosted** - Full control over your data
|
||||
- 🚀 **Fast** - Built with Bun for optimal performance
|
||||
- 🔒 **Secure** - JWT authentication, encrypted tokens
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Runtime**: Bun
|
||||
- **Framework**: Astro with React
|
||||
- **Database**: SQLite with Drizzle ORM
|
||||
- **Styling**: Tailwind CSS v4
|
||||
- **Authentication**: Better Auth
|
||||
|
||||
## System Requirements
|
||||
|
||||
- Bun >= 1.2.9
|
||||
- Node.js >= 20 (optional, for compatibility)
|
||||
- SQLite 3
|
||||
- 512MB RAM minimum
|
||||
- 1GB disk space
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions! Please see our [Contributing Guide](../CONTRIBUTING.md) for details.
|
||||
|
||||
### Development Setup
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Add tests
|
||||
5. Submit a pull request
|
||||
|
||||
### Code of Conduct
|
||||
|
||||
Please read our [Code of Conduct](../CODE_OF_CONDUCT.md) before contributing.
|
||||
|
||||
## Support
|
||||
|
||||
- **Issues**: [GitHub Issues](https://github.com/yourusername/gitea-mirror/issues)
|
||||
- **Discussions**: [GitHub Discussions](https://github.com/yourusername/gitea-mirror/discussions)
|
||||
- **Wiki**: [GitHub Wiki](https://github.com/yourusername/gitea-mirror/wiki)
|
||||
|
||||
## Security
|
||||
|
||||
For security issues, please see [SECURITY.md](../SECURITY.md).
|
||||
|
||||
## License
|
||||
|
||||
Gitea Mirror is open source software licensed under the [MIT License](../LICENSE).
|
||||
|
||||
---
|
||||
|
||||
For detailed information on any topic, please refer to the specific documentation guides listed above.
|
||||
Security disclosures should follow the process in [../SECURITY.md](../SECURITY.md).
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
# Graceful Shutdown Process
|
||||
|
||||
This document details how the gitea-mirror application handles graceful shutdown during active mirroring operations, with specific focus on job interruption and recovery.
|
||||
|
||||
## Overview
|
||||
|
||||
The graceful shutdown system is designed for **fast, clean termination** without waiting for long-running jobs to complete. It prioritizes **quick shutdown times** (under 30 seconds) while **preserving all progress** for seamless recovery.
|
||||
|
||||
## Key Principle
|
||||
|
||||
**The application does NOT wait for jobs to finish before shutting down.** Instead, it saves the current state and resumes after restart.
|
||||
|
||||
## Shutdown Scenario Example
|
||||
|
||||
### Initial State
|
||||
- **Job**: Mirror 500 repositories
|
||||
- **Progress**: 200 repositories completed
|
||||
- **Remaining**: 300 repositories pending
|
||||
- **Action**: User initiates shutdown (SIGTERM, Ctrl+C, Docker stop)
|
||||
|
||||
### Shutdown Process (Under 30 seconds)
|
||||
|
||||
#### Step 1: Signal Detection (Immediate)
|
||||
```
|
||||
📡 Received SIGTERM signal
|
||||
🛑 Graceful shutdown initiated by signal: SIGTERM
|
||||
📊 Shutdown status: 1 active jobs, 2 callbacks
|
||||
```
|
||||
|
||||
#### Step 2: Job State Saving (1-10 seconds)
|
||||
```
|
||||
📝 Step 1: Saving active job states...
|
||||
Saving state for job abc-123...
|
||||
✅ Saved state for job abc-123
|
||||
```
|
||||
|
||||
**What gets saved:**
|
||||
- `inProgress: false` - Mark job as not currently running
|
||||
- `completedItems: 200` - Number of repos successfully mirrored
|
||||
- `totalItems: 500` - Total repos in the job
|
||||
- `completedItemIds: [repo1, repo2, ..., repo200]` - List of completed repos
|
||||
- `itemIds: [repo1, repo2, ..., repo500]` - Full list of repos
|
||||
- `lastCheckpoint: 2025-05-24T17:30:00Z` - Exact shutdown time
|
||||
- `message: "Job interrupted by application shutdown - will resume on restart"`
|
||||
- `status: "imported"` - Keeps status as resumable (not "failed")
|
||||
|
||||
#### Step 3: Service Cleanup (1-5 seconds)
|
||||
```
|
||||
🔧 Step 2: Executing shutdown callbacks...
|
||||
🛑 Shutting down cleanup service...
|
||||
✅ Cleanup service stopped
|
||||
✅ Shutdown callback 1 completed
|
||||
```
|
||||
|
||||
#### Step 4: Clean Exit (Immediate)
|
||||
```
|
||||
💾 Step 3: Closing database connections...
|
||||
✅ Graceful shutdown completed successfully
|
||||
```
|
||||
|
||||
**Total shutdown time: ~15 seconds** (well under the 30-second limit)
|
||||
|
||||
## What Happens to the Remaining 300 Repos?
|
||||
|
||||
### During Shutdown
|
||||
- **NOT processed** - The remaining 300 repos are not mirrored
|
||||
- **NOT lost** - Their IDs are preserved in the job state
|
||||
- **NOT marked as failed** - Job status remains "imported" for recovery
|
||||
|
||||
### After Restart
|
||||
The recovery system automatically:
|
||||
|
||||
1. **Detects interrupted job** during startup
|
||||
2. **Calculates remaining work**: 500 - 200 = 300 repos
|
||||
3. **Extracts remaining repo IDs**: repos 201-500 from the original list
|
||||
4. **Resumes processing** from exactly where it left off
|
||||
5. **Continues until completion** of all 500 repos
|
||||
|
||||
## Timeout Configuration
|
||||
|
||||
### Shutdown Timeouts
|
||||
```typescript
|
||||
const SHUTDOWN_TIMEOUT = 30000; // 30 seconds max shutdown time
|
||||
const JOB_SAVE_TIMEOUT = 10000; // 10 seconds to save job state
|
||||
```
|
||||
|
||||
### Timeout Behavior
|
||||
- **Normal case**: Shutdown completes in 10-20 seconds
|
||||
- **Slow database**: Up to 30 seconds allowed
|
||||
- **Timeout exceeded**: Force exit with code 1
|
||||
- **Container kill**: Orchestrator should allow 45+ seconds grace period
|
||||
|
||||
## Job State Persistence
|
||||
|
||||
### Database Schema
|
||||
The `mirror_jobs` table stores complete job state:
|
||||
|
||||
```sql
|
||||
-- Job identification
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
job_type TEXT NOT NULL DEFAULT 'mirror',
|
||||
|
||||
-- Progress tracking
|
||||
total_items INTEGER,
|
||||
completed_items INTEGER DEFAULT 0,
|
||||
item_ids TEXT, -- JSON array of all repo IDs
|
||||
completed_item_ids TEXT DEFAULT '[]', -- JSON array of completed repo IDs
|
||||
|
||||
-- State management
|
||||
in_progress INTEGER NOT NULL DEFAULT 0, -- Boolean: currently running
|
||||
started_at TIMESTAMP,
|
||||
completed_at TIMESTAMP,
|
||||
last_checkpoint TIMESTAMP, -- Last progress save
|
||||
|
||||
-- Status and messaging
|
||||
status TEXT NOT NULL DEFAULT 'imported',
|
||||
message TEXT NOT NULL
|
||||
```
|
||||
|
||||
### Recovery Query
|
||||
The recovery system finds interrupted jobs:
|
||||
|
||||
```sql
|
||||
SELECT * FROM mirror_jobs
|
||||
WHERE in_progress = 0
|
||||
AND status = 'imported'
|
||||
AND completed_at IS NULL
|
||||
AND total_items > completed_items;
|
||||
```
|
||||
|
||||
## Shutdown-Aware Processing
|
||||
|
||||
### Concurrency Check
|
||||
During job execution, each repo processing checks for shutdown:
|
||||
|
||||
```typescript
|
||||
// Before processing each repository
|
||||
if (isShuttingDown()) {
|
||||
throw new Error('Processing interrupted by application shutdown');
|
||||
}
|
||||
```
|
||||
|
||||
### Checkpoint Intervals
|
||||
Jobs save progress periodically (every 10 repos by default):
|
||||
|
||||
```typescript
|
||||
checkpointInterval: 10, // Save progress every 10 repositories
|
||||
```
|
||||
|
||||
This ensures minimal work loss even if shutdown occurs between checkpoints.
|
||||
|
||||
## Container Integration
|
||||
|
||||
### Docker Entrypoint
|
||||
The Docker entrypoint properly forwards signals:
|
||||
|
||||
```bash
|
||||
# Set up signal handlers
|
||||
trap 'shutdown_handler' TERM INT HUP
|
||||
|
||||
# Start application in background
|
||||
bun ./dist/server/entry.mjs &
|
||||
APP_PID=$!
|
||||
|
||||
# Wait for application to finish
|
||||
wait "$APP_PID"
|
||||
```
|
||||
|
||||
### Kubernetes Configuration
|
||||
Recommended pod configuration:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
spec:
|
||||
terminationGracePeriodSeconds: 45 # Allow time for graceful shutdown
|
||||
containers:
|
||||
- name: gitea-mirror
|
||||
# ... other configuration
|
||||
```
|
||||
|
||||
## Monitoring and Logging
|
||||
|
||||
### Shutdown Logs
|
||||
```
|
||||
🛑 Graceful shutdown initiated by signal: SIGTERM
|
||||
📊 Shutdown status: 1 active jobs, 2 callbacks
|
||||
📝 Step 1: Saving active job states...
|
||||
Saving state for 1 active jobs...
|
||||
✅ Completed saving all active jobs
|
||||
🔧 Step 2: Executing shutdown callbacks...
|
||||
✅ Completed all shutdown callbacks
|
||||
💾 Step 3: Closing database connections...
|
||||
✅ Graceful shutdown completed successfully
|
||||
```
|
||||
|
||||
### Recovery Logs
|
||||
```
|
||||
⚠️ Jobs found that need recovery. Starting recovery process...
|
||||
Resuming job abc-123 with 300 remaining items...
|
||||
✅ Recovery completed successfully
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### For Operations
|
||||
1. **Monitor shutdown times** - Should complete under 30 seconds
|
||||
2. **Check recovery logs** - Verify jobs resume correctly after restart
|
||||
3. **Set appropriate grace periods** - Allow 45+ seconds in orchestrators
|
||||
4. **Plan maintenance windows** - Jobs will resume but may take time to complete
|
||||
|
||||
### For Development
|
||||
1. **Test shutdown scenarios** - Use `bun run test-shutdown`
|
||||
2. **Monitor job progress** - Check checkpoint frequency and timing
|
||||
3. **Verify recovery** - Ensure interrupted jobs resume correctly
|
||||
4. **Handle edge cases** - Test shutdown during different job phases
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Shutdown Takes Too Long
|
||||
- **Check**: Database performance during job state saving
|
||||
- **Solution**: Increase `SHUTDOWN_TIMEOUT` environment variable
|
||||
- **Monitor**: Job complexity and checkpoint frequency
|
||||
|
||||
### Jobs Don't Resume
|
||||
- **Check**: Recovery logs for errors during startup
|
||||
- **Verify**: Database contains interrupted jobs with correct status
|
||||
- **Test**: Run `bun run startup-recovery` manually
|
||||
|
||||
### Container Force-Killed
|
||||
- **Check**: Container orchestrator termination grace period
|
||||
- **Increase**: Grace period to 45+ seconds
|
||||
- **Monitor**: Application shutdown completion time
|
||||
|
||||
This design ensures **production-ready graceful shutdown** with **zero data loss** and **fast recovery times** suitable for modern containerized deployments.
|
||||
@@ -1,91 +0,0 @@
|
||||
# GitHub Sponsors Integration
|
||||
|
||||
This guide shows how GitHub Sponsors is integrated into the open-source version of Gitea Mirror.
|
||||
|
||||
## Components
|
||||
|
||||
### GitHubSponsors Card
|
||||
|
||||
A card component that displays in the sidebar or dashboard:
|
||||
|
||||
```tsx
|
||||
import { GitHubSponsors } from '@/components/sponsors/GitHubSponsors';
|
||||
|
||||
// In your layout or dashboard
|
||||
<GitHubSponsors />
|
||||
```
|
||||
|
||||
### SponsorButton
|
||||
|
||||
A smaller button for headers or navigation:
|
||||
|
||||
```tsx
|
||||
import { SponsorButton } from '@/components/sponsors/GitHubSponsors';
|
||||
|
||||
// In your header
|
||||
<SponsorButton />
|
||||
```
|
||||
|
||||
## Integration Points
|
||||
|
||||
### 1. Dashboard Sidebar
|
||||
|
||||
Add the sponsor card to the dashboard sidebar for visibility:
|
||||
|
||||
```tsx
|
||||
// src/components/layout/DashboardLayout.tsx
|
||||
<aside>
|
||||
{/* Other sidebar content */}
|
||||
<GitHubSponsors />
|
||||
</aside>
|
||||
```
|
||||
|
||||
### 2. Header Navigation
|
||||
|
||||
Add the sponsor button to the main navigation:
|
||||
|
||||
```tsx
|
||||
// src/components/layout/Header.tsx
|
||||
<nav>
|
||||
{/* Other nav items */}
|
||||
<SponsorButton />
|
||||
</nav>
|
||||
```
|
||||
|
||||
### 3. Settings Page
|
||||
|
||||
Add a support section in settings:
|
||||
|
||||
```tsx
|
||||
// src/components/settings/SupportSection.tsx
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Support Development</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<GitHubSponsors />
|
||||
</CardContent>
|
||||
</Card>
|
||||
```
|
||||
|
||||
## Behavior
|
||||
|
||||
- **Only appears in self-hosted mode**: The components automatically hide in hosted mode
|
||||
- **Non-intrusive**: Designed to be helpful without being annoying
|
||||
- **Multiple options**: GitHub Sponsors, Buy Me a Coffee, and starring the repo
|
||||
|
||||
## Customization
|
||||
|
||||
You can customize the sponsor components by:
|
||||
|
||||
1. Updating the GitHub Sponsors URL
|
||||
2. Adding/removing donation platforms
|
||||
3. Changing the styling to match your theme
|
||||
4. Adjusting the placement based on user feedback
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Don't be pushy**: Show sponsor options tastefully
|
||||
2. **Provide value first**: Ensure the tool is useful before asking for support
|
||||
3. **Be transparent**: Explain how sponsorships help the project
|
||||
4. **Thank sponsors**: Acknowledge supporters in README or releases
|
||||
193
docs/SSO_TESTING.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# Local SSO Testing Guide
|
||||
|
||||
This guide explains how to test SSO authentication locally with Gitea Mirror.
|
||||
|
||||
## Option 1: Using Google OAuth (Recommended for Quick Testing)
|
||||
|
||||
### Setup Steps:
|
||||
|
||||
1. **Create a Google OAuth Application**
|
||||
- Go to [Google Cloud Console](https://console.cloud.google.com/)
|
||||
- Create a new project or select existing
|
||||
- Enable Google+ API
|
||||
- Go to "Credentials" → "Create Credentials" → "OAuth client ID"
|
||||
- Choose "Web application"
|
||||
- Add authorized redirect URIs:
|
||||
- `http://localhost:3000/api/auth/sso/callback/google-sso`
|
||||
- `http://localhost:9876/api/auth/sso/callback/google-sso`
|
||||
|
||||
2. **Configure in Gitea Mirror**
|
||||
- Go to Configuration → Authentication tab
|
||||
- Click "Add Provider"
|
||||
- Select "OIDC / OAuth2"
|
||||
- Fill in:
|
||||
- Provider ID: `google-sso`
|
||||
- Email Domain: `gmail.com` (or your domain)
|
||||
- Issuer URL: `https://accounts.google.com`
|
||||
- Click "Discover" to auto-fill endpoints
|
||||
- Client ID: (from Google Console)
|
||||
- Client Secret: (from Google Console)
|
||||
- Save the provider
|
||||
|
||||
## Option 2: Using Keycloak (Local Identity Provider)
|
||||
|
||||
### Setup with Docker:
|
||||
|
||||
```bash
|
||||
# Run Keycloak
|
||||
docker run -d --name keycloak \
|
||||
-p 8080:8080 \
|
||||
-e KEYCLOAK_ADMIN=admin \
|
||||
-e KEYCLOAK_ADMIN_PASSWORD=admin \
|
||||
quay.io/keycloak/keycloak:latest start-dev
|
||||
|
||||
# Access at http://localhost:8080
|
||||
# Login with admin/admin
|
||||
```
|
||||
|
||||
### Configure Keycloak:
|
||||
|
||||
1. Create a new realm (e.g., "gitea-mirror")
|
||||
2. Create a client:
|
||||
- Client ID: `gitea-mirror`
|
||||
- Client Protocol: `openid-connect`
|
||||
- Access Type: `confidential`
|
||||
- Valid Redirect URIs: `http://localhost:*/api/auth/sso/callback/keycloak`
|
||||
3. Get credentials from the "Credentials" tab
|
||||
4. Create test users in "Users" section
|
||||
|
||||
### Configure in Gitea Mirror:
|
||||
|
||||
- Provider ID: `keycloak`
|
||||
- Email Domain: `example.com`
|
||||
- Issuer URL: `http://localhost:8080/realms/gitea-mirror`
|
||||
- Client ID: `gitea-mirror`
|
||||
- Client Secret: (from Keycloak)
|
||||
- Click "Discover" to auto-fill endpoints
|
||||
|
||||
## Option 3: Using Mock SSO Provider (Development)
|
||||
|
||||
For testing without external dependencies, you can use a mock OIDC provider.
|
||||
|
||||
### Using oidc-provider-example:
|
||||
|
||||
```bash
|
||||
# Clone and run mock provider
|
||||
git clone https://github.com/panva/node-oidc-provider-example.git
|
||||
cd node-oidc-provider-example
|
||||
npm install
|
||||
npm start
|
||||
|
||||
# Runs on http://localhost:3001
|
||||
```
|
||||
|
||||
### Configure in Gitea Mirror:
|
||||
|
||||
- Provider ID: `mock-provider`
|
||||
- Email Domain: `test.com`
|
||||
- Issuer URL: `http://localhost:3001`
|
||||
- Client ID: `foo`
|
||||
- Client Secret: `bar`
|
||||
- Authorization Endpoint: `http://localhost:3001/auth`
|
||||
- Token Endpoint: `http://localhost:3001/token`
|
||||
|
||||
## Testing the SSO Flow
|
||||
|
||||
1. **Logout** from Gitea Mirror if logged in
|
||||
2. Go to `/login`
|
||||
3. Click on the **SSO** tab
|
||||
4. Either:
|
||||
- Click the provider button (e.g., "Sign in with gmail.com")
|
||||
- Or enter your email and click "Continue with SSO"
|
||||
5. You'll be redirected to the identity provider
|
||||
6. Complete authentication
|
||||
7. You'll be redirected back and logged in
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues:
|
||||
|
||||
1. **"Invalid origin" error**
|
||||
- Check that `trustedOrigins` in `/src/lib/auth.ts` includes your dev URL
|
||||
- Restart the dev server after changes
|
||||
|
||||
2. **Provider not showing in login**
|
||||
- Check browser console for errors
|
||||
- Verify provider was saved successfully
|
||||
- Check `/api/sso/providers` returns your providers
|
||||
|
||||
3. **Redirect URI mismatch**
|
||||
- Ensure the redirect URI in your OAuth app matches exactly:
|
||||
`http://localhost:PORT/api/auth/sso/callback/PROVIDER_ID`
|
||||
|
||||
4. **CORS errors**
|
||||
- Add your identity provider domain to CORS allowed origins if needed
|
||||
|
||||
### Debug Mode:
|
||||
|
||||
Enable debug logging by setting environment variable:
|
||||
```bash
|
||||
DEBUG=better-auth:* bun run dev
|
||||
```
|
||||
|
||||
## Testing Different Scenarios
|
||||
|
||||
### 1. New User Registration
|
||||
- Use an email not in the system
|
||||
- SSO should create a new user automatically
|
||||
|
||||
### 2. Existing User Login
|
||||
- Create a user with email/password first
|
||||
- Login with SSO using same email
|
||||
- Should link to existing account
|
||||
|
||||
### 3. Domain-based Routing
|
||||
- Configure multiple providers with different domains
|
||||
- Test that entering email routes to correct provider
|
||||
|
||||
### 4. Organization Provisioning
|
||||
- Set organizationId on provider
|
||||
- Test that users are added to correct organization
|
||||
|
||||
## Security Testing
|
||||
|
||||
1. **Token Expiration**
|
||||
- Wait for session to expire
|
||||
- Test refresh flow
|
||||
|
||||
2. **Invalid State**
|
||||
- Modify state parameter in callback
|
||||
- Should reject authentication
|
||||
|
||||
3. **PKCE Flow**
|
||||
- Enable/disable PKCE
|
||||
- Verify code challenge works
|
||||
|
||||
## Using with Better Auth CLI
|
||||
|
||||
Better Auth provides CLI tools for testing:
|
||||
|
||||
```bash
|
||||
# List registered providers
|
||||
bun run auth:providers list
|
||||
|
||||
# Test provider configuration
|
||||
bun run auth:providers test google-sso
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
For production-like testing:
|
||||
|
||||
```env
|
||||
BETTER_AUTH_URL=http://localhost:3000
|
||||
BETTER_AUTH_SECRET=your-secret-key
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
After successful SSO setup:
|
||||
1. Test user attribute mapping
|
||||
2. Configure role-based access
|
||||
3. Set up SAML if needed
|
||||
4. Test with your organization's actual IdP
|
||||
127
docs/testing.md
@@ -1,127 +0,0 @@
|
||||
# Testing in Gitea Mirror
|
||||
|
||||
This document provides guidance on testing in the Gitea Mirror project.
|
||||
|
||||
## Current Status
|
||||
|
||||
The project now uses Bun's built-in test runner, which is Jest-compatible and provides a fast, reliable testing experience. We've migrated away from Vitest due to compatibility issues with Bun.
|
||||
|
||||
## Running Tests
|
||||
|
||||
To run tests, use the following commands:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
bun test
|
||||
|
||||
# Run tests in watch mode (automatically re-run when files change)
|
||||
bun test --watch
|
||||
|
||||
# Run tests with coverage reporting
|
||||
bun test --coverage
|
||||
```
|
||||
|
||||
## Test File Naming Conventions
|
||||
|
||||
Bun's test runner automatically discovers test files that match the following patterns:
|
||||
|
||||
- `*.test.{js|jsx|ts|tsx}`
|
||||
- `*_test.{js|jsx|ts|tsx}`
|
||||
- `*.spec.{js|jsx|ts|tsx}`
|
||||
- `*_spec.{js|jsx|ts|tsx}`
|
||||
|
||||
## Writing Tests
|
||||
|
||||
The project uses Bun's test runner with a Jest-compatible API. Here's an example test:
|
||||
|
||||
```typescript
|
||||
// example.test.ts
|
||||
import { describe, test, expect } from "bun:test";
|
||||
|
||||
describe("Example Test", () => {
|
||||
test("should pass", () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Testing React Components
|
||||
|
||||
For testing React components, we use React Testing Library:
|
||||
|
||||
```typescript
|
||||
// component.test.tsx
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import MyComponent from "../components/MyComponent";
|
||||
|
||||
describe("MyComponent", () => {
|
||||
test("renders correctly", () => {
|
||||
render(<MyComponent />);
|
||||
expect(screen.getByText("Hello World")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Test Setup
|
||||
|
||||
The test setup is defined in `src/tests/setup.bun.ts` and includes:
|
||||
|
||||
- Automatic cleanup after each test
|
||||
- Setup for any global test environment needs
|
||||
|
||||
## Mocking
|
||||
|
||||
Bun's test runner provides built-in mocking capabilities:
|
||||
|
||||
```typescript
|
||||
import { test, expect, mock } from "bun:test";
|
||||
|
||||
// Create a mock function
|
||||
const mockFn = mock(() => "mocked value");
|
||||
|
||||
test("mock function", () => {
|
||||
const result = mockFn();
|
||||
expect(result).toBe("mocked value");
|
||||
expect(mockFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Mock a module
|
||||
mock.module("./some-module", () => {
|
||||
return {
|
||||
someFunction: () => "mocked module function"
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
## CI Integration
|
||||
|
||||
The CI workflow has been updated to use Bun's test runner. Tests are automatically run as part of the CI pipeline.
|
||||
|
||||
## Test Coverage
|
||||
|
||||
To generate test coverage reports, run:
|
||||
|
||||
```bash
|
||||
bun test --coverage
|
||||
```
|
||||
|
||||
This will generate a coverage report in the `coverage` directory.
|
||||
|
||||
## Types of Tests
|
||||
|
||||
The project includes several types of tests:
|
||||
|
||||
1. **Unit Tests**: Testing individual functions and utilities
|
||||
2. **API Tests**: Testing API endpoints
|
||||
3. **Component Tests**: Testing React components
|
||||
4. **Integration Tests**: Testing how components work together
|
||||
|
||||
## Future Improvements
|
||||
|
||||
When expanding the test suite, consider:
|
||||
|
||||
1. Adding more comprehensive API endpoint tests
|
||||
2. Increasing component test coverage
|
||||
3. Setting up end-to-end tests with a tool like Playwright
|
||||
4. Adding performance tests for critical paths
|
||||
10
drizzle/0002_bored_captain_cross.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
CREATE TABLE `verifications` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`identifier` text NOT NULL,
|
||||
`value` text NOT NULL,
|
||||
`expires_at` integer NOT NULL,
|
||||
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||
`updated_at` integer DEFAULT (unixepoch()) NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `idx_verifications_identifier` ON `verifications` (`identifier`);
|
||||
3
drizzle/0003_open_spacker_dave.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE `organizations` ADD `public_repository_count` integer;--> statement-breakpoint
|
||||
ALTER TABLE `organizations` ADD `private_repository_count` integer;--> statement-breakpoint
|
||||
ALTER TABLE `organizations` ADD `fork_repository_count` integer;
|
||||
18
drizzle/0004_grey_butterfly.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
CREATE TABLE `rate_limits` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`user_id` text NOT NULL,
|
||||
`provider` text DEFAULT 'github' NOT NULL,
|
||||
`limit` integer NOT NULL,
|
||||
`remaining` integer NOT NULL,
|
||||
`used` integer NOT NULL,
|
||||
`reset` integer NOT NULL,
|
||||
`retry_after` integer,
|
||||
`status` text DEFAULT 'ok' NOT NULL,
|
||||
`last_checked` integer NOT NULL,
|
||||
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `idx_rate_limits_user_provider` ON `rate_limits` (`user_id`,`provider`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_rate_limits_status` ON `rate_limits` (`status`);
|
||||
11
drizzle/0005_polite_preak.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- Step 1: Remove duplicate repositories, keeping the most recently updated one
|
||||
-- This handles cases where users have duplicate entries from before the unique constraint
|
||||
DELETE FROM repositories
|
||||
WHERE rowid NOT IN (
|
||||
SELECT MAX(rowid)
|
||||
FROM repositories
|
||||
GROUP BY user_id, full_name
|
||||
);
|
||||
--> statement-breakpoint
|
||||
-- Step 2: Now create the unique index safely
|
||||
CREATE UNIQUE INDEX uniq_repositories_user_full_name ON repositories (user_id, full_name);
|
||||
1784
drizzle/meta/0002_snapshot.json
Normal file
1805
drizzle/meta/0003_snapshot.json
Normal file
1933
drizzle/meta/0004_snapshot.json
Normal file
1941
drizzle/meta/0005_snapshot.json
Normal file
@@ -15,6 +15,34 @@
|
||||
"when": 1752173351102,
|
||||
"tag": "0001_polite_exodus",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "6",
|
||||
"when": 1753539600567,
|
||||
"tag": "0002_bored_captain_cross",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "6",
|
||||
"when": 1757390828679,
|
||||
"tag": "0003_open_spacker_dave",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 4,
|
||||
"version": "6",
|
||||
"when": 1757392620734,
|
||||
"tag": "0004_grey_butterfly",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "6",
|
||||
"when": 1757786449446,
|
||||
"tag": "0005_polite_preak",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
21
helm/gitea-mirror/.yamllint
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
extends: default
|
||||
|
||||
ignore: |
|
||||
.yamllint
|
||||
node_modules
|
||||
templates
|
||||
unittests/bash
|
||||
|
||||
rules:
|
||||
truthy:
|
||||
allowed-values: ['true', 'false']
|
||||
check-keys: False
|
||||
level: error
|
||||
line-length: disable
|
||||
document-start: disable
|
||||
comments:
|
||||
min-spaces-from-content: 1
|
||||
braces:
|
||||
max-spaces-inside: 2
|
||||
|
||||
12
helm/gitea-mirror/Chart.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
apiVersion: v2
|
||||
name: gitea-mirror
|
||||
description: Kubernetes helm chart for gitea-mirror
|
||||
type: application
|
||||
version: 0.0.1
|
||||
appVersion: 3.7.2
|
||||
icon: https://github.com/RayLabsHQ/gitea-mirror/blob/main/.github/assets/logo.png
|
||||
keywords:
|
||||
- git
|
||||
- gitea
|
||||
sources:
|
||||
- https://github.com/RayLabsHQ/gitea-mirror
|
||||
307
helm/gitea-mirror/README.md
Normal file
@@ -0,0 +1,307 @@
|
||||
# gitea-mirror (Helm Chart)
|
||||
|
||||
Deploy **gitea-mirror** to Kubernetes using Helm. The chart packages a Deployment, Service, optional Ingress or Gateway API HTTPRoutes, ConfigMap and Secret, a PVC (optional), and an optional ServiceAccount.
|
||||
|
||||
- **Chart name:** `gitea-mirror`
|
||||
- **Type:** `application`
|
||||
- **App version:** `3.7.2` (default image tag, can be overridden)
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Kubernetes 1.23+
|
||||
- Helm 3.8+
|
||||
- (Optional) Gateway API (v1) if you plan to use `route.*` HTTPRoutes, see https://github.com/kubernetes-sigs/gateway-api/
|
||||
- (Optional) An Ingress controller if you plan to use `ingress.*`
|
||||
|
||||
---
|
||||
|
||||
## Quick start
|
||||
|
||||
From the repo root (chart path: `helm/gitea-mirror`):
|
||||
|
||||
```bash
|
||||
# Create a namespace (optional)
|
||||
kubectl create namespace gitea-mirror
|
||||
|
||||
# Install with minimal required secrets/values
|
||||
helm upgrade --install gitea-mirror ./helm/gitea-mirror --namespace gitea-mirror --set "gitea-mirror.github.username=<your-gh-username>" --set "gitea-mirror.github.token=<your-gh-token>" --set "gitea-mirror.gitea.url=https://gitea.example.com" --set "gitea-mirror.gitea.token=<your-gitea-token>"
|
||||
```
|
||||
|
||||
The default Service is `ClusterIP` on port `4321`. You can expose it via Ingress or Gateway API; see below.
|
||||
|
||||
---
|
||||
|
||||
## Upgrading
|
||||
|
||||
Standard Helm upgrade:
|
||||
|
||||
```bash
|
||||
helm upgrade gitea-mirror ./helm/gitea-mirror -n gitea-mirror
|
||||
```
|
||||
|
||||
If you change persistence settings or storage class, a rollout may require PVC recreation.
|
||||
|
||||
---
|
||||
|
||||
## Uninstalling
|
||||
|
||||
```bash
|
||||
helm uninstall gitea-mirror -n gitea-mirror
|
||||
```
|
||||
|
||||
If you enabled persistence with a PVC the data may persist; delete the PVC manually if you want a clean slate.
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Global image & pod settings
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `image.registry` | string | `ghcr.io` | Container registry. |
|
||||
| `image.repository` | string | `raylabshq/gitea-mirror` | Image repository. |
|
||||
| `image.tag` | string | `""` | Image tag; when empty, uses the chart `appVersion` (`3.7.2`). |
|
||||
| `image.pullPolicy` | string | `IfNotPresent` | K8s image pull policy. |
|
||||
| `imagePullSecrets` | list | `[]` | Image pull secrets. |
|
||||
| `podSecurityContext.runAsUser` | int | `1001` | UID. |
|
||||
| `podSecurityContext.runAsGroup` | int | `1001` | GID. |
|
||||
| `podSecurityContext.fsGroup` | int | `1001` | FS group. |
|
||||
| `podSecurityContext.fsGroupChangePolicy` | string | `OnRootMismatch` | FS group change policy. |
|
||||
| `nodeSelector` / `tolerations` / `affinity` / `topologySpreadConstraints` | — | — | Standard scheduling knobs. |
|
||||
| `extraVolumes` / `extraVolumeMounts` | list | `[]` | Append custom volumes/mounts. |
|
||||
| `priorityClassName` | string | `""` | Optional Pod priority class. |
|
||||
|
||||
### Deployment
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `deployment.port` | int | `4321` | Container port & named `http` port. |
|
||||
| `deployment.strategy.type` | string | `Recreate` | Update strategy (`Recreate` or `RollingUpdate`). |
|
||||
| `deployment.strategy.rollingUpdate.maxUnavailable/maxSurge` | string/int | — | Used when `type=RollingUpdate`. |
|
||||
| `deployment.env` | list | `[]` | Extra environment variables. |
|
||||
| `deployment.resources` | map | `{}` | CPU/memory requests & limits. |
|
||||
| `deployment.terminationGracePeriodSeconds` | int | `60` | Grace period. |
|
||||
| `livenessProbe.*` | — | enabled, `/api/health` | Liveness probe (HTTP GET to `/api/health`). |
|
||||
| `readinessProbe.*` | — | enabled, `/api/health` | Readiness probe. |
|
||||
| `startupProbe.*` | — | enabled, `/api/health` | Startup probe. |
|
||||
|
||||
> The Pod mounts a volume at `/app/data` (PVC or `emptyDir` depending on `persistence.enabled`).
|
||||
|
||||
### Service
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `service.type` | string | `ClusterIP` | Service type. |
|
||||
| `service.port` | int | `4321` | Service port. |
|
||||
| `service.clusterIP` | string | `None` | ClusterIP (only when `type=ClusterIP`). |
|
||||
| `service.externalTrafficPolicy` | string | `""` | External traffic policy (LB). |
|
||||
| `service.loadBalancerIP` | string | `""` | LoadBalancer IP. |
|
||||
| `service.loadBalancerClass` | string | `""` | LoadBalancer class. |
|
||||
| `service.annotations` / `service.labels` | map | `{}` | Extra metadata. |
|
||||
|
||||
### Ingress (optional)
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `ingress.enabled` | bool | `false` | Enable Ingress. |
|
||||
| `ingress.className` | string | `""` | IngressClass name. |
|
||||
| `ingress.hosts[0].host` | string | `mirror.example.com` | Hostname. |
|
||||
| `ingress.tls` | list | `[]` | TLS blocks (secret name etc.). |
|
||||
| `ingress.annotations` | map | `{}` | Controller-specific annotations. |
|
||||
|
||||
> The Ingress exposes `/` to the chart’s Service.
|
||||
|
||||
### Gateway API HTTPRoutes (optional)
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `route.enabled` | bool | `false` | Enable Gateway API HTTPRoutes. |
|
||||
| `route.forceHTTPS` | bool | `true` | If true, create an HTTP route that redirects to HTTPS (301). |
|
||||
| `route.domain` | list | `["mirror.example.com"]` | Hostnames. |
|
||||
| `route.gateway` | string | `""` | Gateway name. |
|
||||
| `route.gatewayNamespace` | string | `""` | Gateway namespace. |
|
||||
| `route.http.gatewaySection` | string | `""` | SectionName for HTTP listener. |
|
||||
| `route.https.gatewaySection` | string | `""` | SectionName for HTTPS listener. |
|
||||
| `route.http.filters` / `route.https.filters` | list | `[]` | Additional filters. (Defaults add HSTS header on HTTPS.) |
|
||||
|
||||
### Persistence
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `persistence.enabled` | bool | `true` | Enable persistent storage. |
|
||||
| `persistence.create` | bool | `true` | Create a PVC from the chart. |
|
||||
| `persistence.claimName` | string | `gitea-mirror-storage` | PVC name. |
|
||||
| `persistence.storageClass` | string | `""` | StorageClass to use. |
|
||||
| `persistence.accessModes` | list | `["ReadWriteOnce"]` | Access modes. |
|
||||
| `persistence.size` | string | `1Gi` | Requested size. |
|
||||
| `persistence.volumeName` | string | `""` | Bind to existing PV by name (optional). |
|
||||
| `persistence.annotations` | map | `{}` | PVC annotations. |
|
||||
|
||||
### ServiceAccount (optional)
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `serviceAccount.create` | bool | `false` | Create a ServiceAccount. |
|
||||
| `serviceAccount.name` | string | `""` | SA name (defaults to release fullname). |
|
||||
| `serviceAccount.automountServiceAccountToken` | bool | `false` | Automount token. |
|
||||
| `serviceAccount.annotations` / `labels` | map | `{}` | Extra metadata. |
|
||||
|
||||
---
|
||||
|
||||
## Application configuration (`gitea-mirror.*`)
|
||||
|
||||
These values populate a **ConfigMap** (non-secret) and a **Secret** (for tokens and sensitive fields). Environment variables from both are consumed by the container.
|
||||
|
||||
### Core
|
||||
|
||||
| Key | Default | Mapped env |
|
||||
| --- | --- | --- |
|
||||
| `gitea-mirror.nodeEnv` | `production` | `NODE_ENV` |
|
||||
| `gitea-mirror.core.databaseUrl` | `file:data/gitea-mirror.db` | `DATABASE_URL` |
|
||||
| `gitea-mirror.core.encryptionSecret` | `""` | `ENCRYPTION_SECRET` (Secret) |
|
||||
| `gitea-mirror.core.betterAuthSecret` | `""` | `BETTER_AUTH_SECRET` |
|
||||
| `gitea-mirror.core.betterAuthUrl` | `http://localhost:4321` | `BETTER_AUTH_URL` |
|
||||
| `gitea-mirror.core.betterAuthTrustedOrigins` | `http://localhost:4321` | `BETTER_AUTH_TRUSTED_ORIGINS` |
|
||||
|
||||
### GitHub
|
||||
|
||||
| Key | Default | Mapped env |
|
||||
| --- | --- | --- |
|
||||
| `gitea-mirror.github.username` | `""` | `GITHUB_USERNAME` |
|
||||
| `gitea-mirror.github.token` | `""` | `GITHUB_TOKEN` (Secret) |
|
||||
| `gitea-mirror.github.type` | `personal` | `GITHUB_TYPE` |
|
||||
| `gitea-mirror.github.privateRepositories` | `true` | `PRIVATE_REPOSITORIES` |
|
||||
| `gitea-mirror.github.skipForks` | `false` | `SKIP_FORKS` |
|
||||
| `gitea-mirror.github.starredCodeOnly` | `false` | `SKIP_STARRED_ISSUES` |
|
||||
| `gitea-mirror.github.mirrorStarred` | `false` | `MIRROR_STARRED` |
|
||||
|
||||
### Gitea
|
||||
|
||||
| Key | Default | Mapped env |
|
||||
| --- | --- | --- |
|
||||
| `gitea-mirror.gitea.url` | `""` | `GITEA_URL` |
|
||||
| `gitea-mirror.gitea.token` | `""` | `GITEA_TOKEN` (Secret) |
|
||||
| `gitea-mirror.gitea.username` | `""` | `GITEA_USERNAME` |
|
||||
| `gitea-mirror.gitea.organization` | `github-mirrors` | `GITEA_ORGANIZATION` |
|
||||
| `gitea-mirror.gitea.visibility` | `public` | `GITEA_ORG_VISIBILITY` |
|
||||
|
||||
### Mirror options
|
||||
|
||||
| Key | Default | Mapped env |
|
||||
| --- | --- | --- |
|
||||
| `gitea-mirror.mirror.releases` | `true` | `MIRROR_RELEASES` |
|
||||
| `gitea-mirror.mirror.wiki` | `true` | `MIRROR_WIKI` |
|
||||
| `gitea-mirror.mirror.metadata` | `true` | `MIRROR_METADATA` |
|
||||
| `gitea-mirror.mirror.issues` | `true` | `MIRROR_ISSUES` |
|
||||
| `gitea-mirror.mirror.pullRequests` | `true` | `MIRROR_PULL_REQUESTS` |
|
||||
| `gitea-mirror.mirror.starred` | _(see note above)_ | `MIRROR_STARRED` |
|
||||
|
||||
### Automation & cleanup
|
||||
|
||||
| Key | Default | Mapped env |
|
||||
| --- | --- | --- |
|
||||
| `gitea-mirror.automation.schedule_enabled` | `true` | `SCHEDULE_ENABLED` |
|
||||
| `gitea-mirror.automation.schedule_interval` | `3600` | `SCHEDULE_INTERVAL` (seconds) |
|
||||
| `gitea-mirror.cleanup.enabled` | `true` | `CLEANUP_ENABLED` |
|
||||
| `gitea-mirror.cleanup.retentionDays` | `30` | `CLEANUP_RETENTION_DAYS` |
|
||||
|
||||
> **Secrets:** If you set `gitea-mirror.existingSecret` (name of an existing Secret), the chart will **not** create its own Secret and will reference yours instead. Otherwise it creates a Secret with `GITHUB_TOKEN`, `GITEA_TOKEN`, `ENCRYPTION_SECRET`.
|
||||
|
||||
---
|
||||
|
||||
## Exposing the service
|
||||
|
||||
### Using Ingress
|
||||
|
||||
```yaml
|
||||
ingress:
|
||||
enabled: true
|
||||
className: "nginx"
|
||||
hosts:
|
||||
- host: mirror.example.com
|
||||
tls:
|
||||
- secretName: mirror-tls
|
||||
hosts:
|
||||
- mirror.example.com
|
||||
```
|
||||
|
||||
This creates an Ingress routing `/` to the service on port `4321`.
|
||||
|
||||
### Using Gateway API (HTTPRoute)
|
||||
|
||||
```yaml
|
||||
route:
|
||||
enabled: true
|
||||
domain: ["mirror.example.com"]
|
||||
gateway: "my-gateway"
|
||||
gatewayNamespace: "gateway-system"
|
||||
http:
|
||||
gatewaySection: "http"
|
||||
https:
|
||||
gatewaySection: "https"
|
||||
# Example extra filter already included by default: add HSTS header
|
||||
```
|
||||
|
||||
If `forceHTTPS: true`, the chart emits an HTTP route that redirects to HTTPS with 301. An HTTPS route is always created when `route.enabled=true`.
|
||||
|
||||
---
|
||||
|
||||
## Persistence & data
|
||||
|
||||
By default, the chart provisions a PVC named `gitea-mirror-storage` with `1Gi` and mounts it at `/app/data`. To use an existing PV or tune storage, adjust `persistence.*` in `values.yaml`. If you disable persistence, an `emptyDir` will be used instead.
|
||||
|
||||
---
|
||||
|
||||
## Environment & health endpoints
|
||||
|
||||
The container listens on `PORT` (defaults to `deployment.port` = `4321`) and exposes `GET /api/health` for liveness/readiness/startup probes.
|
||||
|
||||
---
|
||||
|
||||
## Examples
|
||||
|
||||
### Minimal (tokens via chart-managed Secret)
|
||||
|
||||
```yaml
|
||||
gitea-mirror:
|
||||
github:
|
||||
username: "gitea-mirror"
|
||||
token: "<gh-token>"
|
||||
gitea:
|
||||
url: "https://gitea.company.tld"
|
||||
token: "<gitea-token>"
|
||||
```
|
||||
|
||||
### Bring your own Secret
|
||||
|
||||
```yaml
|
||||
gitea-mirror:
|
||||
existingSecret: "gitea-mirror-secrets"
|
||||
github:
|
||||
username: "gitea-mirror"
|
||||
gitea:
|
||||
url: "https://gitea.company.tld"
|
||||
```
|
||||
|
||||
Where `gitea-mirror-secrets` contains keys `GITHUB_TOKEN`, `GITEA_TOKEN`, `ENCRYPTION_SECRET`.
|
||||
|
||||
---
|
||||
|
||||
## Development
|
||||
|
||||
Lint the chart:
|
||||
|
||||
```bash
|
||||
yamllint -c helm/gitea-mirror/.yamllint helm/gitea-mirror
|
||||
```
|
||||
|
||||
Tweak probes, resources, and scheduling as needed; see `values.yaml`.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
This chart is part of the `RayLabsHQ/gitea-mirror` repository. See the repository for licensing details.
|
||||
59
helm/gitea-mirror/templates/_helpers.tpl
Normal file
@@ -0,0 +1,59 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
|
||||
{{- define "gitea-mirror.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "gitea-mirror.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "gitea-mirror.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "gitea-mirror.labels" -}}
|
||||
helm.sh/chart: {{ include "gitea-mirror.chart" . }}
|
||||
app: {{ include "gitea-mirror.name" . }}
|
||||
{{ include "gitea-mirror.selectorLabels" . }}
|
||||
app.kubernetes.io/version: {{ .Values.image.tag | default .Chart.AppVersion | quote }}
|
||||
version: {{ .Values.image.tag | default .Chart.AppVersion | quote }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "gitea-mirror.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "gitea-mirror.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
ServiceAccount name
|
||||
*/}}
|
||||
{{- define "gitea-mirror.serviceAccountName" -}}
|
||||
{{ .Values.serviceAccount.name | default (include "gitea-mirror.fullname" .) }}
|
||||
{{- end -}}
|
||||
38
helm/gitea-mirror/templates/configmap.yaml
Normal file
@@ -0,0 +1,38 @@
|
||||
{{- $gm := index .Values "gitea-mirror" -}}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "gitea-mirror.fullname" . }}
|
||||
labels:
|
||||
{{- include "gitea-mirror.labels" . | nindent 4 }}
|
||||
data:
|
||||
NODE_ENV: {{ $gm.nodeEnv | quote }}
|
||||
# Core configuration
|
||||
DATABASE_URL: {{ $gm.core.databaseUrl | quote }}
|
||||
BETTER_AUTH_SECRET: {{ $gm.core.betterAuthSecret | quote }}
|
||||
BETTER_AUTH_URL: {{ $gm.core.betterAuthUrl | quote }}
|
||||
BETTER_AUTH_TRUSTED_ORIGINS: {{ $gm.core.betterAuthTrustedOrigins | quote }}
|
||||
# GitHub Config
|
||||
GITHUB_USERNAME: {{ $gm.github.username | quote }}
|
||||
GITHUB_TYPE: {{ $gm.github.type | quote }}
|
||||
PRIVATE_REPOSITORIES: {{ $gm.github.privateRepositories | quote }}
|
||||
MIRROR_STARRED: {{ $gm.github.mirrorStarred | quote }}
|
||||
SKIP_FORKS: {{ $gm.github.skipForks | quote }}
|
||||
SKIP_STARRED_ISSUES: {{ $gm.github.starredCodeOnly | quote }}
|
||||
# Gitea Config
|
||||
GITEA_URL: {{ $gm.gitea.url | quote }}
|
||||
GITEA_USERNAME: {{ $gm.gitea.username | quote }}
|
||||
GITEA_ORGANIZATION: {{ $gm.gitea.organization | quote }}
|
||||
GITEA_ORG_VISIBILITY: {{ $gm.gitea.visibility | quote }}
|
||||
# Mirror Options
|
||||
MIRROR_RELEASES: {{ $gm.mirror.releases | quote }}
|
||||
MIRROR_WIKI: {{ $gm.mirror.wiki | quote }}
|
||||
MIRROR_METADATA: {{ $gm.mirror.metadata | quote }}
|
||||
MIRROR_ISSUES: {{ $gm.mirror.issues | quote }}
|
||||
MIRROR_PULL_REQUESTS: {{ $gm.mirror.pullRequests | quote }}
|
||||
# Automation
|
||||
SCHEDULE_ENABLED: {{ $gm.automation.schedule_enabled| quote }}
|
||||
SCHEDULE_INTERVAL: {{ $gm.automation.schedule_interval | quote }}
|
||||
# Cleanup
|
||||
CLEANUP_ENABLED: {{ $gm.cleanup.enabled | quote }}
|
||||
CLEANUP_RETENTION_DAYS: {{ $gm.cleanup.retentionDays | quote }}
|
||||
143
helm/gitea-mirror/templates/deployment.yaml
Normal file
@@ -0,0 +1,143 @@
|
||||
{{- $gm := index .Values "gitea-mirror" -}}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "gitea-mirror.fullname" . }}
|
||||
{{- with .Values.deployment.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "gitea-mirror.labels" . | nindent 4 }}
|
||||
{{- if .Values.deployment.labels }}
|
||||
{{- toYaml .Values.deployment.labels | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: {{ .Values.deployment.strategy.type }}
|
||||
{{- if eq .Values.deployment.strategy.type "RollingUpdate" }}
|
||||
rollingUpdate:
|
||||
maxUnavailable: {{ .Values.deployment.strategy.rollingUpdate.maxUnavailable }}
|
||||
maxSurge: {{ .Values.deployment.strategy.rollingUpdate.maxSurge }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "gitea-mirror.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "gitea-mirror.labels" . | nindent 8 }}
|
||||
{{- if .Values.deployment.labels }}
|
||||
{{- toYaml .Values.deployment.labels | nindent 8 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if (or .Values.serviceAccount.create .Values.serviceAccount.name) }}
|
||||
serviceAccountName: {{ include "gitea-mirror.serviceAccountName" . }}
|
||||
{{- end }}
|
||||
{{- if .Values.priorityClassName }}
|
||||
priorityClassName: "{{ .Values.priorityClassName }}"
|
||||
{{- end }}
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.podSecurityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
terminationGracePeriodSeconds: {{ .Values.deployment.terminationGracePeriodSeconds }}
|
||||
containers:
|
||||
- name: gitea-mirror
|
||||
image: {{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default (printf "v%s" .Chart.AppVersion) }}
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{ include "gitea-mirror.fullname" . }}
|
||||
{{- if $gm.existingSecret }}
|
||||
- secretRef:
|
||||
name: {{ $gm.existingSecret }}
|
||||
{{- else }}
|
||||
- secretRef:
|
||||
name: {{ include "gitea-mirror.fullname" . }}
|
||||
{{- end }}
|
||||
env:
|
||||
- name: PORT
|
||||
value: "{{ .Values.deployment.port }}"
|
||||
{{- if .Values.deployment.env }}
|
||||
{{- toYaml .Values.deployment.env | nindent 12 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.deployment.port }}
|
||||
{{- if .Values.livenessProbe.enabled }}
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: "http"
|
||||
initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.livenessProbe.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.livenessProbe.failureThreshold }}
|
||||
successThreshold: {{ .Values.livenessProbe.successThreshold }}
|
||||
{{- end }}
|
||||
{{- if .Values.readinessProbe.enabled }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: "http"
|
||||
initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.readinessProbe.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.readinessProbe.failureThreshold }}
|
||||
successThreshold: {{ .Values.readinessProbe.successThreshold }}
|
||||
{{- end }}
|
||||
{{- if .Values.startupProbe.enabled }}
|
||||
startupProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: "http"
|
||||
initialDelaySeconds: {{ .Values.startupProbe.initialDelaySeconds }}
|
||||
periodSeconds: {{ .Values.startupProbe.periodSeconds }}
|
||||
timeoutSeconds: {{ .Values.startupProbe.timeoutSeconds }}
|
||||
failureThreshold: {{ .Values.startupProbe.failureThreshold }}
|
||||
successThreshold: {{ .Values.startupProbe.successThreshold }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /app/data
|
||||
{{- if .Values.extraVolumeMounts }}
|
||||
{{- toYaml .Values.extraVolumeMounts | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.deployment.resources }}
|
||||
resources:
|
||||
{{- toYaml .Values.deployment.resources | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.topologySpreadConstraints }}
|
||||
topologySpreadConstraints:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
{{- if .Values.persistence.enabled }}
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .Values.persistence.claimName }}
|
||||
{{- else if not .Values.persistence.enabled }}
|
||||
- name: data
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- if .Values.extraVolumes }}
|
||||
{{- toYaml .Values.extraVolumes | nindent 8 }}
|
||||
{{- end }}
|
||||
77
helm/gitea-mirror/templates/httproute.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
{{- if .Values.route.enabled }}
|
||||
{{- if .Values.route.forceHTTPS }}
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: {{ include "gitea-mirror.fullname" . }}-http
|
||||
labels:
|
||||
{{- include "gitea-mirror.labels" . | nindent 4 }}
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: {{ .Values.route.gateway }}
|
||||
sectionName: {{ .Values.route.http.gatewaySection }}
|
||||
namespace: {{ .Values.route.gatewayNamespace }}
|
||||
hostnames: {{ .Values.route.domain }}
|
||||
rules:
|
||||
- filters:
|
||||
- type: RequestRedirect
|
||||
requestRedirect:
|
||||
scheme: https
|
||||
statusCode: 301
|
||||
{{- with .Values.route.http.filters }}
|
||||
{{ toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: {{ include "gitea-mirror.fullname" . }}-http
|
||||
labels:
|
||||
{{- include "gitea-mirror.labels" . | nindent 4 }}
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: {{ .Values.route.gateway }}
|
||||
sectionName: {{ .Values.route.http.gatewaySection }}
|
||||
namespace: {{ .Values.route.gatewayNamespace }}
|
||||
hostnames: {{ .Values.route.domain }}
|
||||
rules:
|
||||
- matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /
|
||||
backendRefs:
|
||||
- name: {{ include "gitea-mirror.fullname" . }}
|
||||
port: {{ .Values.service.port }}
|
||||
{{- with .Values.route.http.filters }}
|
||||
filters:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
kind: HTTPRoute
|
||||
metadata:
|
||||
name: {{ include "gitea-mirror.fullname" . }}-https
|
||||
labels:
|
||||
{{- include "gitea-mirror.labels" . | nindent 4 }}
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: {{ .Values.route.gateway }}
|
||||
sectionName: {{ .Values.route.https.gatewaySection }}
|
||||
namespace: {{ .Values.route.gatewayNamespace }}
|
||||
hostnames: {{ .Values.route.domain }}
|
||||
rules:
|
||||
- matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /
|
||||
backendRefs:
|
||||
- name: {{ include "gitea-mirror.fullname" . }}
|
||||
port: {{ .Values.service.port }}
|
||||
{{- with .Values.route.https.filters }}
|
||||
filters:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
40
helm/gitea-mirror/templates/ingress.yaml
Normal file
@@ -0,0 +1,40 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ include "gitea-mirror.fullname" . }}
|
||||
labels:
|
||||
{{- include "gitea-mirror.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- . | toYaml | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.ingress.className }}
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ tpl . $ | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ tpl .host $ | quote }}
|
||||
http:
|
||||
paths:
|
||||
- path: {{ .path | default "/" }}
|
||||
pathType: {{ .pathType | default "Prefix" }}
|
||||
backend:
|
||||
service:
|
||||
name: {{ include "gitea-mirror.fullname" $ }}
|
||||
port:
|
||||
number: {{ $.Values.service.port }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
26
helm/gitea-mirror/templates/pvc.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
{{- if and .Values.persistence.enabled .Values.persistence.create }}
|
||||
{{- $gm := index .Values "gitea-mirror" -}}
|
||||
kind: PersistentVolumeClaim
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: {{ .Values.persistence.claimName }}
|
||||
labels:
|
||||
{{- include "gitea-mirror.labels" . | nindent 4 }}
|
||||
{{- with .Values.persistence.annotations }}
|
||||
annotations:
|
||||
{{ . | toYaml | indent 4}}
|
||||
{{- end }}
|
||||
spec:
|
||||
accessModes:
|
||||
{{- toYaml .Values.persistence.accessModes | nindent 4 }}
|
||||
{{- with .Values.persistence.storageClass }}
|
||||
storageClassName: {{ . }}
|
||||
{{- end }}
|
||||
volumeMode: Filesystem
|
||||
{{- with .Values.persistence.volumeName }}
|
||||
volumeName: {{ . }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.persistence.size }}
|
||||
{{- end }}
|
||||
14
helm/gitea-mirror/templates/secret.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
{{- $gm := index .Values "gitea-mirror" -}}
|
||||
{{- if (empty $gm.existingSecret) }}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ include "gitea-mirror.fullname" . }}
|
||||
labels:
|
||||
{{- include "gitea-mirror.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
stringData:
|
||||
GITHUB_TOKEN: {{ $gm.github.token | quote }}
|
||||
GITEA_TOKEN: {{ $gm.gitea.token | quote }}
|
||||
ENCRYPTION_SECRET: {{ $gm.core.encryptionSecret | quote }}
|
||||
{{- end }}
|
||||
34
helm/gitea-mirror/templates/service.yaml
Normal file
@@ -0,0 +1,34 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "gitea-mirror.fullname" . }}
|
||||
labels:
|
||||
{{- include "gitea-mirror.labels" . | nindent 4 }}
|
||||
{{- if .Values.service.labels }}
|
||||
{{- toYaml .Values.service.labels | nindent 4 }}
|
||||
{{- end }}
|
||||
annotations:
|
||||
{{- toYaml .Values.service.annotations | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
{{- if eq .Values.service.type "LoadBalancer" }}
|
||||
{{- if .Values.service.loadBalancerClass }}
|
||||
loadBalancerClass: {{ .Values.service.loadBalancerClass }}
|
||||
{{- end }}
|
||||
{{- if and .Values.service.loadBalancerIP }}
|
||||
loadBalancerIP: {{ .Values.service.loadBalancerIP }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .Values.service.externalTrafficPolicy }}
|
||||
externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy }}
|
||||
{{- end }}
|
||||
{{- if and .Values.service.clusterIP (eq .Values.service.type "ClusterIP") }}
|
||||
clusterIP: {{ .Values.service.clusterIP }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
protocol: TCP
|
||||
targetPort: http
|
||||
selector:
|
||||
{{- include "gitea-mirror.selectorLabels" . | nindent 4 }}
|
||||
17
helm/gitea-mirror/templates/serviceaccount.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "gitea-mirror.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "gitea-mirror.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.labels }}
|
||||
{{- . | toYaml | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- . | toYaml | nindent 4 }}
|
||||
{{- end }}
|
||||
automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }}
|
||||
{{- end }}
|
||||
|
||||
151
helm/gitea-mirror/values.yaml
Normal file
@@ -0,0 +1,151 @@
|
||||
image:
|
||||
registry: ghcr.io
|
||||
repository: raylabshq/gitea-mirror
|
||||
# Leave blank to use the Appversion tag
|
||||
tag: ""
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
imagePullSecrets: []
|
||||
podSecurityContext:
|
||||
runAsUser: 1001
|
||||
runAsGroup: 1001
|
||||
fsGroup: 1001
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
hosts:
|
||||
- host: mirror.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - mirror.example.com
|
||||
|
||||
route:
|
||||
enabled: false
|
||||
forceHTTPS: true
|
||||
domain: ["mirror.example.com"]
|
||||
gateway: ""
|
||||
gatewayNamespace: ""
|
||||
http:
|
||||
gatewaySection: ""
|
||||
filters: []
|
||||
https:
|
||||
gatewaySection: ""
|
||||
filters:
|
||||
- type: ResponseHeaderModifier
|
||||
responseHeaderModifier:
|
||||
add:
|
||||
- name: Strict-Transport-Security
|
||||
value: "max-age=31536000; includeSubDomains; preload"
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 4321
|
||||
clusterIP: None
|
||||
annotations: {}
|
||||
externalTrafficPolicy:
|
||||
labels: {}
|
||||
loadBalancerIP:
|
||||
loadBalancerClass:
|
||||
|
||||
deployment:
|
||||
port: 4321
|
||||
strategy:
|
||||
type: Recreate
|
||||
env: []
|
||||
terminationGracePeriodSeconds: 60
|
||||
labels: {}
|
||||
annotations: {}
|
||||
resources: {}
|
||||
|
||||
livenessProbe:
|
||||
enabled: true
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 6
|
||||
successThreshold: 1
|
||||
readinessProbe:
|
||||
enabled: true
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 6
|
||||
successThreshold: 1
|
||||
startupProbe:
|
||||
enabled: true
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 6
|
||||
successThreshold: 1
|
||||
|
||||
persistence:
|
||||
enabled: true
|
||||
create: true
|
||||
claimName: gitea-mirror-storage
|
||||
storageClass: ""
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
size: 1Gi
|
||||
|
||||
affinity: {}
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
topologySpreadConstraints: []
|
||||
extraVolumes: []
|
||||
extraVolumeMounts: []
|
||||
|
||||
serviceAccount:
|
||||
create: false
|
||||
name: ""
|
||||
annotations: {}
|
||||
labels: {}
|
||||
automountServiceAccountToken: false
|
||||
|
||||
gitea-mirror:
|
||||
existingSecret: ""
|
||||
nodeEnv: production
|
||||
core:
|
||||
databaseUrl: file:data/gitea-mirror.db
|
||||
encryptionSecret: ""
|
||||
betterAuthSecret: ""
|
||||
betterAuthUrl: "http://localhost:4321"
|
||||
betterAuthTrustedOrigins: "http://localhost:4321"
|
||||
|
||||
github:
|
||||
username: ""
|
||||
token: ""
|
||||
type: personal
|
||||
privateRepositories: true
|
||||
mirrorStarred: false
|
||||
skipForks: false
|
||||
starredCodeOnly: false
|
||||
|
||||
gitea:
|
||||
url: ""
|
||||
token: ""
|
||||
username: ""
|
||||
organization: "github-mirrors"
|
||||
visibility: "public"
|
||||
|
||||
mirror:
|
||||
releases: true
|
||||
wiki: true
|
||||
metadata: true
|
||||
issues: true
|
||||
pullRequests: true
|
||||
|
||||
automation:
|
||||
schedule_enabled: true
|
||||
schedule_interval: 3600
|
||||
|
||||
cleanup:
|
||||
enabled: true
|
||||
retentionDays: 30
|
||||
9087
package-lock.json
generated
92
package.json
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "gitea-mirror",
|
||||
"type": "module",
|
||||
"version": "3.0.0",
|
||||
"version": "3.8.7",
|
||||
"engines": {
|
||||
"bun": ">=1.2.9"
|
||||
},
|
||||
"scripts": {
|
||||
"setup": "bun install && bun run manage-db init",
|
||||
"dev": "bunx --bun astro dev --port 4567",
|
||||
"dev": "bunx --bun astro dev",
|
||||
"dev:clean": "bun run cleanup-db && bun run manage-db init && bunx --bun astro dev",
|
||||
"build": "bunx --bun astro build",
|
||||
"cleanup-db": "rm -f gitea-mirror.db data/gitea-mirror.db",
|
||||
@@ -22,10 +22,9 @@
|
||||
"db:pull": "bun drizzle-kit pull",
|
||||
"db:check": "bun drizzle-kit check",
|
||||
"db:studio": "bun drizzle-kit studio",
|
||||
"migrate:better-auth": "bun scripts/migrate-to-better-auth.ts",
|
||||
"migrate:encrypt-tokens": "bun scripts/migrate-tokens-encryption.ts",
|
||||
"startup-recovery": "bun scripts/startup-recovery.ts",
|
||||
"startup-recovery-force": "bun scripts/startup-recovery.ts --force",
|
||||
"startup-env-config": "bun scripts/startup-env-config.ts",
|
||||
"test-recovery": "bun scripts/test-recovery.ts",
|
||||
"test-recovery-cleanup": "bun scripts/test-recovery.ts --cleanup",
|
||||
"test-shutdown": "bun scripts/test-graceful-shutdown.ts",
|
||||
@@ -38,70 +37,79 @@
|
||||
"test:coverage": "bun test --coverage",
|
||||
"astro": "bunx --bun astro"
|
||||
},
|
||||
"overrides": {
|
||||
"@esbuild-kit/esm-loader": "npm:tsx@^4.20.5",
|
||||
"devalue": "^5.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/mdx": "^4.3.0",
|
||||
"@astrojs/node": "9.3.0",
|
||||
"@astrojs/react": "^4.3.0",
|
||||
"@astrojs/check": "^0.9.5",
|
||||
"@astrojs/mdx": "4.3.7",
|
||||
"@astrojs/node": "9.5.0",
|
||||
"@astrojs/react": "^4.4.0",
|
||||
"@better-auth/sso": "1.4.0-beta.12",
|
||||
"@octokit/plugin-throttling": "^11.0.2",
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.11",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.14",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-radio-group": "^1.3.7",
|
||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-radio-group": "^1.3.8",
|
||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@tailwindcss/vite": "^4.1.15",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"astro": "5.11.2",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"astro": "^5.14.8",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"better-auth": "^1.2.12",
|
||||
"buffer": "^6.0.3",
|
||||
"better-auth": "1.4.0-beta.12",
|
||||
"canvas-confetti": "^1.9.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"drizzle-orm": "^0.44.3",
|
||||
"dotenv": "^17.2.3",
|
||||
"drizzle-orm": "^0.44.6",
|
||||
"fuse.js": "^7.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lucide-react": "^0.525.0",
|
||||
"lucide-react": "^0.546.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"sonner": "^2.0.6",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tw-animate-css": "^1.3.5",
|
||||
"typescript": "^5.8.3",
|
||||
"uuid": "^11.1.0",
|
||||
"tailwindcss": "^4.1.15",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3",
|
||||
"uuid": "^13.0.0",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^4.0.5"
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/bun": "^1.2.18",
|
||||
"@types/bun": "^1.3.0",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"drizzle-kit": "^0.31.5",
|
||||
"jsdom": "^26.1.0",
|
||||
"tsx": "^4.20.3",
|
||||
"tsx": "^4.20.6",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"packageManager": "bun@1.2.18"
|
||||
"packageManager": "bun@1.2.23"
|
||||
}
|
||||
|
||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 KiB |
BIN
public/logo.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 13 KiB |
@@ -57,7 +57,7 @@ http://<container-ip>:4321
|
||||
|
||||
```bash
|
||||
git clone https://github.com/RayLabsHQ/gitea-mirror.git # if not already
|
||||
curl -fsSL https://raw.githubusercontent.com/arunavo4/gitea-mirror/main/scripts/gitea-mirror-lxc-local.sh -o gitea-mirror-lxc-local.sh
|
||||
curl -fsSL https://raw.githubusercontent.com/raylabshq/gitea-mirror:/main/scripts/gitea-mirror-lxc-local.sh -o gitea-mirror-lxc-local.sh
|
||||
chmod +x gitea-mirror-lxc-local.sh
|
||||
|
||||
sudo LOCAL_REPO_DIR=~/Development/gitea-mirror \
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
import { db } from "../src/lib/db";
|
||||
import { accounts } from "../src/lib/db/schema";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
console.log("🔄 Starting Better Auth migration...");
|
||||
|
||||
async function migrateToBetterAuth() {
|
||||
try {
|
||||
// Check if migration is needed
|
||||
const existingAccounts = await db.select().from(accounts).limit(1);
|
||||
if (existingAccounts.length > 0) {
|
||||
console.log("✓ Better Auth migration already completed");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have old users table with passwords
|
||||
// This query checks if password column exists in users table
|
||||
const hasPasswordColumn = await db.get<{ count: number }>(
|
||||
sql`SELECT COUNT(*) as count FROM pragma_table_info('users') WHERE name = 'password'`
|
||||
);
|
||||
|
||||
if (!hasPasswordColumn || hasPasswordColumn.count === 0) {
|
||||
console.log("ℹ️ Users table doesn't have password column - migration may have already been done");
|
||||
|
||||
// Check if we have any users without accounts
|
||||
const usersWithoutAccounts = await db.all<{ id: string; email: string }>(
|
||||
sql`SELECT u.id, u.email FROM users u LEFT JOIN accounts a ON u.id = a.user_id WHERE a.id IS NULL`
|
||||
);
|
||||
|
||||
if (usersWithoutAccounts.length === 0) {
|
||||
console.log("✓ All users have accounts - migration complete");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`⚠️ Found ${usersWithoutAccounts.length} users without accounts - they may need to reset passwords`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all users with password hashes using raw SQL since the schema doesn't have password
|
||||
const allUsersWithPasswords = await db.all<{ id: string; email: string; username: string; password: string }>(
|
||||
sql`SELECT id, email, username, password FROM users WHERE password IS NOT NULL`
|
||||
);
|
||||
|
||||
if (allUsersWithPasswords.length === 0) {
|
||||
console.log("ℹ️ No users with passwords to migrate");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📊 Found ${allUsersWithPasswords.length} users to migrate`);
|
||||
|
||||
// Migrate each user
|
||||
for (const user of allUsersWithPasswords) {
|
||||
try {
|
||||
// Create Better Auth account entry
|
||||
await db.insert(accounts).values({
|
||||
id: crypto.randomUUID(),
|
||||
userId: user.id,
|
||||
accountId: user.email, // Use email as account ID
|
||||
providerId: "credential", // Better Auth credential provider
|
||||
providerUserId: null,
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
expiresAt: null,
|
||||
password: user.password, // Move password hash to accounts table
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
});
|
||||
|
||||
console.log(`✓ Migrated user: ${user.email}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to migrate user ${user.email}:`, error);
|
||||
// Continue with other users even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
// Remove password column from users table if it exists
|
||||
console.log("🔄 Cleaning up old password column...");
|
||||
try {
|
||||
// SQLite doesn't support DROP COLUMN directly, so we need to recreate the table
|
||||
// For now, we'll just leave it as is since it's not harmful
|
||||
console.log("ℹ️ Password column left in users table for compatibility");
|
||||
} catch (error) {
|
||||
console.error("⚠️ Could not remove password column:", error);
|
||||
}
|
||||
|
||||
console.log("✅ Better Auth migration completed successfully");
|
||||
|
||||
// Verify migration
|
||||
const migratedAccounts = await db.select().from(accounts);
|
||||
console.log(`📊 Total accounts after migration: ${migratedAccounts.length}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error("❌ Better Auth migration failed:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run migration
|
||||
migrateToBetterAuth();
|
||||
@@ -1,87 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { db, users, accounts } from "../src/lib/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
/**
|
||||
* Migrate existing users to Better Auth schema
|
||||
*
|
||||
* This script:
|
||||
* 1. Moves existing password hashes from users table to accounts table
|
||||
* 2. Updates user data to match Better Auth requirements
|
||||
* 3. Creates credential accounts for existing users
|
||||
*/
|
||||
|
||||
async function migrateUsers() {
|
||||
console.log("🔄 Starting user migration to Better Auth...");
|
||||
|
||||
try {
|
||||
// Get all existing users
|
||||
const existingUsers = await db.select().from(users);
|
||||
|
||||
if (existingUsers.length === 0) {
|
||||
console.log("✅ No users to migrate");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found ${existingUsers.length} users to migrate`);
|
||||
|
||||
for (const user of existingUsers) {
|
||||
console.log(`\nMigrating user: ${user.username} (${user.email})`);
|
||||
|
||||
// Check if user already has a credential account
|
||||
const existingAccount = await db
|
||||
.select()
|
||||
.from(accounts)
|
||||
.where(
|
||||
eq(accounts.userId, user.id) &&
|
||||
eq(accounts.providerId, "credential")
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (existingAccount.length > 0) {
|
||||
console.log("✓ User already migrated");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create credential account with existing password hash
|
||||
const accountId = uuidv4();
|
||||
await db.insert(accounts).values({
|
||||
id: accountId,
|
||||
accountId: accountId,
|
||||
userId: user.id,
|
||||
providerId: "credential",
|
||||
providerUserId: user.email, // Use email as provider user ID
|
||||
// password: user.password, // Password is not in users table anymore
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: user.updatedAt,
|
||||
});
|
||||
|
||||
console.log("✓ Created credential account");
|
||||
|
||||
// Update user name field if it's null (Better Auth uses 'name' field)
|
||||
// Note: Better Auth expects a 'name' field, but we're using username
|
||||
// This is handled by our additional fields configuration
|
||||
}
|
||||
|
||||
console.log("\n✅ User migration completed successfully!");
|
||||
|
||||
// Summary
|
||||
const migratedAccounts = await db
|
||||
.select()
|
||||
.from(accounts)
|
||||
.where(eq(accounts.providerId, "credential"));
|
||||
|
||||
console.log(`\nMigration Summary:`);
|
||||
console.log(`- Total users: ${existingUsers.length}`);
|
||||
console.log(`- Migrated accounts: ${migratedAccounts.length}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error("❌ Migration failed:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run migration
|
||||
migrateUsers();
|
||||
@@ -1,135 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Migration script to encrypt existing GitHub and Gitea tokens in the database
|
||||
* Run with: bun run scripts/migrate-tokens-encryption.ts
|
||||
*/
|
||||
|
||||
import { db, configs } from "../src/lib/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { encrypt, isEncrypted, migrateToken } from "../src/lib/utils/encryption";
|
||||
|
||||
async function migrateTokens() {
|
||||
console.log("Starting token encryption migration...");
|
||||
|
||||
try {
|
||||
// Fetch all configs
|
||||
const allConfigs = await db.select().from(configs);
|
||||
|
||||
console.log(`Found ${allConfigs.length} configurations to check`);
|
||||
|
||||
let migratedCount = 0;
|
||||
let skippedCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const config of allConfigs) {
|
||||
try {
|
||||
let githubUpdated = false;
|
||||
let giteaUpdated = false;
|
||||
|
||||
// Parse configs
|
||||
const githubConfig = typeof config.githubConfig === "string"
|
||||
? JSON.parse(config.githubConfig)
|
||||
: config.githubConfig;
|
||||
|
||||
const giteaConfig = typeof config.giteaConfig === "string"
|
||||
? JSON.parse(config.giteaConfig)
|
||||
: config.giteaConfig;
|
||||
|
||||
// Check and migrate GitHub token
|
||||
if (githubConfig.token) {
|
||||
if (!isEncrypted(githubConfig.token)) {
|
||||
console.log(`Encrypting GitHub token for config ${config.id} (user: ${config.userId})`);
|
||||
githubConfig.token = encrypt(githubConfig.token);
|
||||
githubUpdated = true;
|
||||
} else {
|
||||
console.log(`GitHub token already encrypted for config ${config.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check and migrate Gitea token
|
||||
if (giteaConfig.token) {
|
||||
if (!isEncrypted(giteaConfig.token)) {
|
||||
console.log(`Encrypting Gitea token for config ${config.id} (user: ${config.userId})`);
|
||||
giteaConfig.token = encrypt(giteaConfig.token);
|
||||
giteaUpdated = true;
|
||||
} else {
|
||||
console.log(`Gitea token already encrypted for config ${config.id}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Update config if any tokens were migrated
|
||||
if (githubUpdated || giteaUpdated) {
|
||||
await db
|
||||
.update(configs)
|
||||
.set({
|
||||
githubConfig,
|
||||
giteaConfig,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(configs.id, config.id));
|
||||
|
||||
migratedCount++;
|
||||
console.log(`✓ Config ${config.id} updated successfully`);
|
||||
} else {
|
||||
skippedCount++;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
errorCount++;
|
||||
console.error(`✗ Error processing config ${config.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n=== Migration Summary ===");
|
||||
console.log(`Total configs: ${allConfigs.length}`);
|
||||
console.log(`Migrated: ${migratedCount}`);
|
||||
console.log(`Skipped (already encrypted): ${skippedCount}`);
|
||||
console.log(`Errors: ${errorCount}`);
|
||||
|
||||
if (errorCount > 0) {
|
||||
console.error("\n⚠️ Some configs failed to migrate. Please check the errors above.");
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("\n✅ Token encryption migration completed successfully!");
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Fatal error during migration:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify environment setup
|
||||
function verifyEnvironment() {
|
||||
const requiredEnvVars = ["ENCRYPTION_SECRET", "JWT_SECRET", "BETTER_AUTH_SECRET"];
|
||||
const availableSecrets = requiredEnvVars.filter(varName => process.env[varName]);
|
||||
|
||||
if (availableSecrets.length === 0) {
|
||||
console.error("❌ No encryption secret found!");
|
||||
console.error("Please set one of the following environment variables:");
|
||||
console.error(" - ENCRYPTION_SECRET (recommended)");
|
||||
console.error(" - JWT_SECRET");
|
||||
console.error(" - BETTER_AUTH_SECRET");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Using encryption secret from: ${availableSecrets[0]}`);
|
||||
}
|
||||
|
||||
// Main execution
|
||||
async function main() {
|
||||
console.log("=== Gitea Mirror Token Encryption Migration ===\n");
|
||||
|
||||
// Verify environment
|
||||
verifyEnvironment();
|
||||
|
||||
// Run migration
|
||||
await migrateTokens();
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Unexpected error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
180
scripts/setup-authentik-test.sh
Executable file
@@ -0,0 +1,180 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Setup script for testing Authentik SSO with Gitea Mirror
|
||||
# This script helps configure Authentik for testing SSO integration
|
||||
|
||||
set -e
|
||||
|
||||
echo "======================================"
|
||||
echo "Authentik SSO Test Environment Setup"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Check if docker and docker-compose are installed
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo -e "${RED}Docker is not installed. Please install Docker first.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
|
||||
echo -e "${RED}Docker Compose is not installed. Please install Docker Compose first.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Function to generate random secret
|
||||
generate_secret() {
|
||||
openssl rand -base64 32 | tr -d '\n' | tr -d '=' | tr -d '/' | tr -d '+'
|
||||
}
|
||||
|
||||
# Function to wait for service
|
||||
wait_for_service() {
|
||||
local service=$1
|
||||
local port=$2
|
||||
local max_attempts=30
|
||||
local attempt=1
|
||||
|
||||
echo -n "Waiting for $service to be ready"
|
||||
while ! nc -z localhost $port 2>/dev/null; do
|
||||
if [ $attempt -eq $max_attempts ]; then
|
||||
echo -e "\n${RED}Timeout waiting for $service${NC}"
|
||||
return 1
|
||||
fi
|
||||
echo -n "."
|
||||
sleep 2
|
||||
((attempt++))
|
||||
done
|
||||
echo -e " ${GREEN}Ready!${NC}"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Parse command line arguments
|
||||
ACTION=${1:-start}
|
||||
|
||||
case $ACTION in
|
||||
start)
|
||||
echo "Starting Authentik test environment..."
|
||||
echo ""
|
||||
|
||||
# Check if .env.authentik exists, if not create it
|
||||
if [ ! -f .env.authentik ]; then
|
||||
echo "Creating .env.authentik with secure defaults..."
|
||||
cat > .env.authentik << EOF
|
||||
# Authentik Configuration
|
||||
AUTHENTIK_SECRET_KEY=$(generate_secret)
|
||||
AUTHENTIK_DB_PASSWORD=$(generate_secret)
|
||||
AUTHENTIK_BOOTSTRAP_PASSWORD=admin-password
|
||||
AUTHENTIK_BOOTSTRAP_EMAIL=admin@example.com
|
||||
|
||||
# Gitea Mirror Configuration
|
||||
BETTER_AUTH_SECRET=$(generate_secret)
|
||||
BETTER_AUTH_URL=http://localhost:4321
|
||||
BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:4321,http://localhost:9000
|
||||
|
||||
# URLs for testing
|
||||
AUTHENTIK_URL=http://localhost:9000
|
||||
GITEA_MIRROR_URL=http://localhost:4321
|
||||
EOF
|
||||
echo -e "${GREEN}Created .env.authentik with secure secrets${NC}"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Load environment variables
|
||||
source .env.authentik
|
||||
|
||||
# Start Authentik services
|
||||
echo "Starting Authentik services..."
|
||||
docker-compose -f docker-compose.authentik.yml --env-file .env.authentik up -d
|
||||
|
||||
# Wait for Authentik to be ready
|
||||
echo ""
|
||||
wait_for_service "Authentik" 9000
|
||||
|
||||
# Wait a bit more for initialization
|
||||
echo "Waiting for Authentik to initialize..."
|
||||
sleep 10
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}✓ Authentik is running!${NC}"
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Authentik Access Information:"
|
||||
echo "======================================"
|
||||
echo "URL: http://localhost:9000"
|
||||
echo "Admin Username: akadmin"
|
||||
echo "Admin Password: admin-password"
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Next Steps:"
|
||||
echo "======================================"
|
||||
echo "1. Access Authentik at http://localhost:9000"
|
||||
echo "2. Login with akadmin / admin-password"
|
||||
echo "3. Create OAuth2 Provider for Gitea Mirror:"
|
||||
echo " - Name: gitea-mirror"
|
||||
echo " - Redirect URIs:"
|
||||
echo " http://localhost:4321/api/auth/callback/sso-provider"
|
||||
echo " - Scopes: openid, profile, email"
|
||||
echo ""
|
||||
echo "4. Create Application:"
|
||||
echo " - Name: Gitea Mirror"
|
||||
echo " - Slug: gitea-mirror"
|
||||
echo " - Provider: gitea-mirror (created above)"
|
||||
echo ""
|
||||
echo "5. Start Gitea Mirror with:"
|
||||
echo " bun run dev"
|
||||
echo ""
|
||||
echo "6. Configure SSO in Gitea Mirror:"
|
||||
echo " - Go to Settings → Authentication & SSO"
|
||||
echo " - Add provider with:"
|
||||
echo " - Issuer URL: http://localhost:9000/application/o/gitea-mirror/"
|
||||
echo " - Client ID: (from Authentik provider)"
|
||||
echo " - Client Secret: (from Authentik provider)"
|
||||
echo ""
|
||||
;;
|
||||
|
||||
stop)
|
||||
echo "Stopping Authentik test environment..."
|
||||
docker-compose -f docker-compose.authentik.yml down
|
||||
echo -e "${GREEN}✓ Authentik stopped${NC}"
|
||||
;;
|
||||
|
||||
clean)
|
||||
echo "Cleaning up Authentik test environment..."
|
||||
docker-compose -f docker-compose.authentik.yml down -v
|
||||
echo -e "${GREEN}✓ Authentik data cleaned${NC}"
|
||||
|
||||
read -p "Remove .env.authentik file? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
rm -f .env.authentik
|
||||
echo -e "${GREEN}✓ Configuration file removed${NC}"
|
||||
fi
|
||||
;;
|
||||
|
||||
logs)
|
||||
docker-compose -f docker-compose.authentik.yml logs -f
|
||||
;;
|
||||
|
||||
status)
|
||||
echo "Authentik Service Status:"
|
||||
echo "========================="
|
||||
docker-compose -f docker-compose.authentik.yml ps
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|clean|logs|status}"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " start - Start Authentik test environment"
|
||||
echo " stop - Stop Authentik services"
|
||||
echo " clean - Stop and remove all data"
|
||||
echo " logs - Show Authentik logs"
|
||||
echo " status - Show service status"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
52
scripts/startup-env-config.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Startup environment configuration script
|
||||
* This script loads configuration from environment variables before the application starts
|
||||
* It ensures that Docker environment variables are properly populated in the database
|
||||
*
|
||||
* Usage:
|
||||
* bun scripts/startup-env-config.ts
|
||||
*/
|
||||
|
||||
import { initializeConfigFromEnv } from "../src/lib/env-config-loader";
|
||||
|
||||
async function runEnvConfigInitialization() {
|
||||
console.log('=== Gitea Mirror Environment Configuration ===');
|
||||
console.log('Loading configuration from environment variables...');
|
||||
console.log('');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
await initializeConfigFromEnv();
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
console.log(`✅ Environment configuration loaded successfully in ${duration}ms`);
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
const endTime = Date.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
console.error(`❌ Failed to load environment configuration after ${duration}ms:`, error);
|
||||
console.error('Application will start anyway, but environment configuration was not loaded.');
|
||||
|
||||
// Exit with error code but allow startup to continue
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle process signals gracefully
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n⚠️ Configuration loading interrupted by SIGINT');
|
||||
process.exit(130);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('\n⚠️ Configuration loading interrupted by SIGTERM');
|
||||
process.exit(143);
|
||||
});
|
||||
|
||||
// Run the environment configuration initialization
|
||||
runEnvConfigInitialization();
|
||||
@@ -47,7 +47,6 @@ async function createTestJob(): Promise<string> {
|
||||
jobType: "mirror",
|
||||
totalItems: 10,
|
||||
itemIds: ['item-1', 'item-2', 'item-3', 'item-4', 'item-5'],
|
||||
completedItems: 2, // Simulate partial completion
|
||||
inProgress: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { Home, ArrowLeft, GitBranch, BookOpen, Settings, FileQuestion } from "lucide-react";
|
||||
|
||||
@@ -11,11 +11,12 @@ import { authClient } from '@/lib/auth-client';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { toast, Toaster } from 'sonner';
|
||||
import { showErrorToast } from '@/lib/utils';
|
||||
import { Loader2, Mail, Globe } from 'lucide-react';
|
||||
import { Loader2, Mail, Globe, Eye, EyeOff } from 'lucide-react';
|
||||
|
||||
|
||||
export function LoginForm() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [ssoEmail, setSsoEmail] = useState('');
|
||||
const { login } = useAuth();
|
||||
const { authMethods, isLoading: isLoadingMethods } = useAuthMethods();
|
||||
@@ -55,7 +56,7 @@ export function LoginForm() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSSOLogin(domain?: string) {
|
||||
async function handleSSOLogin(domain?: string, providerId?: string) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
if (!domain && !ssoEmail) {
|
||||
@@ -63,10 +64,13 @@ export function LoginForm() {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseURL = typeof window !== 'undefined' ? window.location.origin : 'http://localhost:4321';
|
||||
await authClient.signIn.sso({
|
||||
email: ssoEmail || undefined,
|
||||
domain: domain,
|
||||
callbackURL: '/',
|
||||
providerId: providerId,
|
||||
callbackURL: `${baseURL}/`,
|
||||
scopes: ['openid', 'email', 'profile'], // TODO: This is not being respected by the SSO plugin.
|
||||
});
|
||||
} catch (error) {
|
||||
showErrorToast(error, toast);
|
||||
@@ -81,14 +85,9 @@ export function LoginForm() {
|
||||
<CardHeader className="text-center">
|
||||
<div className="flex justify-center mb-4">
|
||||
<img
|
||||
src="/logo-light.svg"
|
||||
src="/logo.png"
|
||||
alt="Gitea Mirror Logo"
|
||||
className="h-10 w-10 dark:hidden"
|
||||
/>
|
||||
<img
|
||||
src="/logo-dark.svg"
|
||||
alt="Gitea Mirror Logo"
|
||||
className="h-10 w-10 hidden dark:block"
|
||||
className="h-8 w-10"
|
||||
/>
|
||||
</div>
|
||||
<CardTitle className="text-2xl">Gitea Mirror</CardTitle>
|
||||
@@ -141,15 +140,29 @@ export function LoginForm() {
|
||||
<label htmlFor="password" className="block text-sm font-medium mb-1">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
placeholder="Enter your password"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
required
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 pr-10 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
placeholder="Enter your password"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 flex items-center pr-3"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff className="h-4 w-4 text-muted-foreground hover:text-foreground transition-colors" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4 text-muted-foreground hover:text-foreground transition-colors" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -175,7 +188,7 @@ export function LoginForm() {
|
||||
key={provider.id}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => handleSSOLogin(provider.domain)}
|
||||
onClick={() => handleSSOLogin(provider.domain, provider.providerId)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Globe className="h-4 w-4 mr-2" />
|
||||
@@ -217,7 +230,7 @@ export function LoginForm() {
|
||||
<CardFooter>
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => handleSSOLogin()}
|
||||
onClick={() => handleSSOLogin(undefined, undefined)}
|
||||
disabled={isLoading || !ssoEmail}
|
||||
>
|
||||
{isLoading ? 'Redirecting...' : 'Continue with SSO'}
|
||||
|
||||
@@ -6,9 +6,12 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
|
||||
import { toast, Toaster } from 'sonner';
|
||||
import { showErrorToast } from '@/lib/utils';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { Eye, EyeOff } from 'lucide-react';
|
||||
|
||||
export function SignupForm() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const { register } = useAuth();
|
||||
|
||||
async function handleSignup(e: React.FormEvent<HTMLFormElement>) {
|
||||
@@ -54,14 +57,9 @@ export function SignupForm() {
|
||||
<CardHeader className="text-center">
|
||||
<div className="flex justify-center mb-4">
|
||||
<img
|
||||
src="/logo-light.svg"
|
||||
src="/logo.png"
|
||||
alt="Gitea Mirror Logo"
|
||||
className="h-10 w-10 dark:hidden"
|
||||
/>
|
||||
<img
|
||||
src="/logo-dark.svg"
|
||||
alt="Gitea Mirror Logo"
|
||||
className="h-10 w-10 hidden dark:block"
|
||||
className="h-8 w-10"
|
||||
/>
|
||||
</div>
|
||||
<CardTitle className="text-2xl">Create Admin Account</CardTitle>
|
||||
@@ -91,29 +89,57 @@ export function SignupForm() {
|
||||
<label htmlFor="password" className="block text-sm font-medium mb-1">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
placeholder="Create a password"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type={showPassword ? "text" : "password"}
|
||||
required
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 pr-10 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
placeholder="Create a password"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 flex items-center pr-3"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff className="h-4 w-4 text-muted-foreground hover:text-foreground transition-colors" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4 text-muted-foreground hover:text-foreground transition-colors" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-medium mb-1">
|
||||
Confirm Password
|
||||
</label>
|
||||
<input
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
required
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
placeholder="Confirm your password"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
type={showConfirmPassword ? "text" : "password"}
|
||||
required
|
||||
className="w-full rounded-md border border-input bg-background px-3 py-2 pr-10 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
placeholder="Confirm your password"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-y-0 right-0 flex items-center pr-3"
|
||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showConfirmPassword ? (
|
||||
<EyeOff className="h-4 w-4 text-muted-foreground hover:text-foreground transition-colors" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4 text-muted-foreground hover:text-foreground transition-colors" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -67,21 +67,21 @@ export function AdvancedOptionsForm({
|
||||
|
||||
<div className="flex items-center">
|
||||
<Checkbox
|
||||
id="skip-starred-issues"
|
||||
checked={config.skipStarredIssues}
|
||||
id="starred-code-only"
|
||||
checked={config.starredCodeOnly}
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange("skipStarredIssues", Boolean(checked))
|
||||
handleChange("starredCodeOnly", Boolean(checked))
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor="skip-starred-issues"
|
||||
htmlFor="starred-code-only"
|
||||
className="ml-2 text-sm select-none"
|
||||
>
|
||||
Don't fetch issues for starred repos
|
||||
Code-only mode for starred repos
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground ml-6">
|
||||
Skip mirroring issues and pull requests for starred repositories
|
||||
Mirror only source code for starred repositories, skipping all metadata (issues, PRs, labels, milestones, wiki, releases)
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Clock,
|
||||
Database,
|
||||
@@ -16,7 +17,8 @@ import {
|
||||
Calendar,
|
||||
Activity,
|
||||
Zap,
|
||||
Info
|
||||
Info,
|
||||
Archive,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -120,14 +122,14 @@ export function AutomationSettings({
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Automatic Mirroring Section */}
|
||||
<div className="space-y-4 p-4 border border-border rounded-lg bg-card/50">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium flex items-center gap-2">
|
||||
<RefreshCw className="h-4 w-4 text-primary" />
|
||||
Automatic Mirroring
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Automatic Syncing Section */}
|
||||
<div className="space-y-4 p-4 border border-border rounded-lg bg-card/50">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium flex items-center gap-2">
|
||||
<RefreshCw className="h-4 w-4 text-primary" />
|
||||
Automatic Syncing
|
||||
</h3>
|
||||
{isAutoSavingSchedule && (
|
||||
<Activity className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
@@ -139,6 +141,7 @@ export function AutomationSettings({
|
||||
<Checkbox
|
||||
id="enable-auto-mirror"
|
||||
checked={scheduleConfig.enabled}
|
||||
className="mt-1.25"
|
||||
onCheckedChange={(checked) =>
|
||||
onScheduleChange({ ...scheduleConfig, enabled: !!checked })
|
||||
}
|
||||
@@ -195,34 +198,40 @@ export function AutomationSettings({
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
Last sync
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
<span className="font-medium text-muted-foreground">
|
||||
{scheduleConfig.lastRun
|
||||
? formatDate(scheduleConfig.lastRun)
|
||||
: "Never"}
|
||||
</span>
|
||||
</div>
|
||||
{scheduleConfig.enabled && scheduleConfig.nextRun && (
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Calendar className="h-3.5 w-3.5" />
|
||||
Next sync
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{formatDate(scheduleConfig.nextRun)}
|
||||
</span>
|
||||
{scheduleConfig.enabled ? (
|
||||
scheduleConfig.nextRun && (
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Calendar className="h-3.5 w-3.5" />
|
||||
Next sync
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{formatDate(scheduleConfig.nextRun)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Enable automatic syncing to schedule periodic repository updates
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Database Cleanup Section */}
|
||||
<div className="space-y-4 p-4 border border-border rounded-lg bg-card/50">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium flex items-center gap-2">
|
||||
<Database className="h-4 w-4 text-primary" />
|
||||
Database Maintenance
|
||||
</h3>
|
||||
{/* Database Cleanup Section */}
|
||||
<div className="space-y-4 p-4 border border-border rounded-lg bg-card/50">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium flex items-center gap-2">
|
||||
<Database className="h-4 w-4 text-primary" />
|
||||
Database Maintenance
|
||||
</h3>
|
||||
{isAutoSavingCleanup && (
|
||||
<Activity className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
)}
|
||||
@@ -233,6 +242,7 @@ export function AutomationSettings({
|
||||
<Checkbox
|
||||
id="enable-auto-cleanup"
|
||||
checked={cleanupConfig.enabled}
|
||||
className="mt-1.25"
|
||||
onCheckedChange={(checked) =>
|
||||
onCleanupChange({ ...cleanupConfig, enabled: !!checked })
|
||||
}
|
||||
@@ -251,8 +261,8 @@ export function AutomationSettings({
|
||||
</div>
|
||||
|
||||
{cleanupConfig.enabled && (
|
||||
<div className="ml-6 space-y-3">
|
||||
<div>
|
||||
<div className="ml-6 space-y-5">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="retention-period" className="text-sm flex items-center gap-2">
|
||||
Data retention period
|
||||
<TooltipProvider>
|
||||
@@ -269,35 +279,36 @@ export function AutomationSettings({
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Label>
|
||||
<Select
|
||||
value={cleanupConfig.retentionDays.toString()}
|
||||
onValueChange={(value) =>
|
||||
onCleanupChange({
|
||||
...cleanupConfig,
|
||||
retentionDays: parseInt(value, 10),
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="retention-period" className="mt-1.5">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{retentionPeriods.map((option) => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value.toString()}
|
||||
>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{cleanupConfig.enabled && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
<div className="flex items-center gap-3 mt-1.5">
|
||||
<Select
|
||||
value={cleanupConfig.retentionDays.toString()}
|
||||
onValueChange={(value) =>
|
||||
onCleanupChange({
|
||||
...cleanupConfig,
|
||||
retentionDays: parseInt(value, 10),
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="retention-period" className="w-40">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{retentionPeriods.map((option) => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value.toString()}
|
||||
>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Cleanup runs {getCleanupFrequencyText(cleanupConfig.retentionDays)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -307,30 +318,129 @@ export function AutomationSettings({
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
Last cleanup
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
<span className="font-medium text-muted-foreground">
|
||||
{cleanupConfig.lastRun
|
||||
? formatDate(cleanupConfig.lastRun)
|
||||
: "Never"}
|
||||
</span>
|
||||
</div>
|
||||
{cleanupConfig.enabled && cleanupConfig.nextRun && (
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Calendar className="h-3.5 w-3.5" />
|
||||
Next cleanup
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{cleanupConfig.nextRun
|
||||
? formatDate(cleanupConfig.nextRun)
|
||||
: "Calculating..."}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{cleanupConfig.enabled ? (
|
||||
cleanupConfig.nextRun && (
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Calendar className="h-3.5 w-3.5" />
|
||||
Next cleanup
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{formatDate(cleanupConfig.nextRun)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Enable automatic cleanup to optimize database storage
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
|
||||
{/* Repository Cleanup Section */}
|
||||
<div className="space-y-4 p-4 border border-border rounded-lg bg-card/50 md:col-span-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium flex items-center gap-2">
|
||||
<Archive className="h-4 w-4 text-primary" />
|
||||
Repository Cleanup (orphaned mirrors)
|
||||
</h3>
|
||||
{isAutoSavingCleanup && (
|
||||
<Activity className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
<Checkbox
|
||||
id="cleanup-handle-orphans"
|
||||
checked={Boolean(cleanupConfig.deleteIfNotInGitHub)}
|
||||
className="mt-1.25"
|
||||
onCheckedChange={(checked) =>
|
||||
onCleanupChange({
|
||||
...cleanupConfig,
|
||||
deleteIfNotInGitHub: Boolean(checked),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<div className="space-y-0.5 flex-1">
|
||||
<Label
|
||||
htmlFor="cleanup-handle-orphans"
|
||||
className="text-sm font-normal cursor-pointer"
|
||||
>
|
||||
Handle orphaned repositories automatically
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Keep your Gitea backups when GitHub repos disappear. Archive is the safest option—it preserves data and disables automatic syncs.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{cleanupConfig.deleteIfNotInGitHub && (
|
||||
<div className="space-y-3 ml-6">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="cleanup-orphaned-action" className="text-sm font-medium">
|
||||
Action for orphaned repositories
|
||||
</Label>
|
||||
<Select
|
||||
value={cleanupConfig.orphanedRepoAction ?? "archive"}
|
||||
onValueChange={(value) =>
|
||||
onCleanupChange({
|
||||
...cleanupConfig,
|
||||
orphanedRepoAction: value as DatabaseCleanupConfig["orphanedRepoAction"],
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="cleanup-orphaned-action">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="archive">Archive (preserve data)</SelectItem>
|
||||
<SelectItem value="skip">Skip (leave as-is)</SelectItem>
|
||||
<SelectItem value="delete">Delete from Gitea</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Archive renames mirror backups with an <code>archived-</code> prefix and disables automatic syncs—use Manual Sync when you want to refresh.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label
|
||||
htmlFor="cleanup-dry-run"
|
||||
className="text-sm font-normal cursor-pointer"
|
||||
>
|
||||
Dry run (log only)
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground max-w-xl">
|
||||
When enabled, cleanup logs the planned action without modifying repositories.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="cleanup-dry-run"
|
||||
checked={Boolean(cleanupConfig.dryRun)}
|
||||
onCheckedChange={(checked) =>
|
||||
onCleanupChange({
|
||||
...cleanupConfig,
|
||||
dryRun: Boolean(checked),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,19 +46,25 @@ export function ConfigTabs() {
|
||||
token: '',
|
||||
organization: 'github-mirrors',
|
||||
visibility: 'public',
|
||||
starredReposOrg: 'github',
|
||||
starredReposOrg: 'starred',
|
||||
preserveOrgStructure: false,
|
||||
},
|
||||
scheduleConfig: {
|
||||
enabled: false,
|
||||
interval: 3600,
|
||||
enabled: false, // Don't set defaults here - will be loaded from API
|
||||
interval: 0, // Will be replaced with actual value from API
|
||||
},
|
||||
cleanupConfig: {
|
||||
enabled: false,
|
||||
retentionDays: 604800, // 7 days in seconds
|
||||
enabled: false, // Don't set defaults here - will be loaded from API
|
||||
retentionDays: 0, // Will be replaced with actual value from API
|
||||
deleteIfNotInGitHub: true,
|
||||
orphanedRepoAction: "archive",
|
||||
dryRun: false,
|
||||
deleteFromGitea: false,
|
||||
protectedRepos: [],
|
||||
},
|
||||
mirrorOptions: {
|
||||
mirrorReleases: false,
|
||||
mirrorLFS: false,
|
||||
mirrorMetadata: false,
|
||||
metadataComponents: {
|
||||
issues: false,
|
||||
@@ -70,7 +76,7 @@ export function ConfigTabs() {
|
||||
},
|
||||
advancedOptions: {
|
||||
skipForks: false,
|
||||
skipStarredIssues: false,
|
||||
starredCodeOnly: false,
|
||||
},
|
||||
});
|
||||
const { user } = useAuth();
|
||||
@@ -470,10 +476,14 @@ export function ConfigTabs() {
|
||||
response.giteaConfig || config.giteaConfig,
|
||||
scheduleConfig:
|
||||
response.scheduleConfig || config.scheduleConfig,
|
||||
cleanupConfig:
|
||||
response.cleanupConfig || config.cleanupConfig,
|
||||
mirrorOptions:
|
||||
response.mirrorOptions || config.mirrorOptions,
|
||||
cleanupConfig: {
|
||||
...config.cleanupConfig,
|
||||
...response.cleanupConfig, // Merge to preserve all fields
|
||||
},
|
||||
mirrorOptions: {
|
||||
...config.mirrorOptions,
|
||||
...response.mirrorOptions, // Merge to preserve all fields including new mirrorLFS
|
||||
},
|
||||
advancedOptions:
|
||||
response.advancedOptions || config.advancedOptions,
|
||||
});
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Checkbox } from "../ui/checkbox";
|
||||
import type { DatabaseCleanupConfig } from "@/types/config";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "../ui/select";
|
||||
import { RefreshCw, Database } from "lucide-react";
|
||||
|
||||
interface DatabaseCleanupConfigFormProps {
|
||||
config: DatabaseCleanupConfig;
|
||||
setConfig: React.Dispatch<React.SetStateAction<DatabaseCleanupConfig>>;
|
||||
onAutoSave?: (config: DatabaseCleanupConfig) => void;
|
||||
isAutoSaving?: boolean;
|
||||
}
|
||||
|
||||
|
||||
// Helper to calculate cleanup interval in hours (should match backend logic)
|
||||
function calculateCleanupInterval(retentionSeconds: number): number {
|
||||
const retentionDays = retentionSeconds / (24 * 60 * 60);
|
||||
if (retentionDays <= 1) {
|
||||
return 6;
|
||||
} else if (retentionDays <= 3) {
|
||||
return 12;
|
||||
} else if (retentionDays <= 7) {
|
||||
return 24;
|
||||
} else if (retentionDays <= 30) {
|
||||
return 48;
|
||||
} else {
|
||||
return 168;
|
||||
}
|
||||
}
|
||||
|
||||
export function DatabaseCleanupConfigForm({
|
||||
config,
|
||||
setConfig,
|
||||
onAutoSave,
|
||||
isAutoSaving = false,
|
||||
}: DatabaseCleanupConfigFormProps) {
|
||||
// Optimistically update nextRun when enabled or retention changes
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
||||
) => {
|
||||
const { name, value, type } = e.target;
|
||||
let newConfig = {
|
||||
...config,
|
||||
[name]: type === "checkbox" ? (e.target as HTMLInputElement).checked : value,
|
||||
};
|
||||
|
||||
// If enabling or changing retention, recalculate nextRun
|
||||
if (
|
||||
(name === "enabled" && (e.target as HTMLInputElement).checked) ||
|
||||
(name === "retentionDays" && config.enabled)
|
||||
) {
|
||||
const now = new Date();
|
||||
const retentionSeconds =
|
||||
name === "retentionDays"
|
||||
? Number(value)
|
||||
: Number(newConfig.retentionDays);
|
||||
const intervalHours = calculateCleanupInterval(retentionSeconds);
|
||||
const nextRun = new Date(now.getTime() + intervalHours * 60 * 60 * 1000);
|
||||
newConfig = {
|
||||
...newConfig,
|
||||
nextRun,
|
||||
};
|
||||
}
|
||||
// If disabling, clear nextRun
|
||||
if (name === "enabled" && !(e.target as HTMLInputElement).checked) {
|
||||
newConfig = {
|
||||
...newConfig,
|
||||
nextRun: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
setConfig(newConfig);
|
||||
if (onAutoSave) {
|
||||
onAutoSave(newConfig);
|
||||
}
|
||||
};
|
||||
|
||||
// Predefined retention periods (in seconds, like schedule intervals)
|
||||
const retentionOptions: { value: number; label: string }[] = [
|
||||
{ value: 86400, label: "1 day" }, // 24 * 60 * 60
|
||||
{ value: 259200, label: "3 days" }, // 3 * 24 * 60 * 60
|
||||
{ value: 604800, label: "7 days" }, // 7 * 24 * 60 * 60
|
||||
{ value: 1209600, label: "14 days" }, // 14 * 24 * 60 * 60
|
||||
{ value: 2592000, label: "30 days" }, // 30 * 24 * 60 * 60
|
||||
{ value: 5184000, label: "60 days" }, // 60 * 24 * 60 * 60
|
||||
{ value: 7776000, label: "90 days" }, // 90 * 24 * 60 * 60
|
||||
];
|
||||
|
||||
return (
|
||||
<Card className="self-start">
|
||||
<CardContent className="pt-6 relative">
|
||||
{isAutoSaving && (
|
||||
<div className="absolute top-4 right-4 flex items-center text-sm text-muted-foreground">
|
||||
<RefreshCw className="h-3 w-3 animate-spin mr-1" />
|
||||
<span className="text-xs">Auto-saving...</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="flex items-center">
|
||||
<Checkbox
|
||||
id="cleanup-enabled"
|
||||
name="enabled"
|
||||
checked={config.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange({
|
||||
target: {
|
||||
name: "enabled",
|
||||
type: "checkbox",
|
||||
checked: Boolean(checked),
|
||||
value: "",
|
||||
},
|
||||
} as React.ChangeEvent<HTMLInputElement>)
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor="cleanup-enabled"
|
||||
className="select-none ml-2 block text-sm font-medium"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="h-4 w-4" />
|
||||
Enable Automatic Database Cleanup
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{config.enabled && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Data Retention Period
|
||||
</label>
|
||||
|
||||
<Select
|
||||
name="retentionDays"
|
||||
value={String(config.retentionDays)}
|
||||
onValueChange={(value) =>
|
||||
handleChange({
|
||||
target: { name: "retentionDays", value },
|
||||
} as React.ChangeEvent<HTMLInputElement>)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full border border-input dark:bg-background dark:hover:bg-background">
|
||||
<SelectValue placeholder="Select retention period" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-background text-foreground border border-input shadow-sm">
|
||||
{retentionOptions.map((option) => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value.toString()}
|
||||
className="cursor-pointer text-sm px-3 py-2 hover:bg-accent focus:bg-accent focus:text-accent-foreground"
|
||||
>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Activities and events older than this period will be automatically deleted.
|
||||
</p>
|
||||
<div className="mt-2 p-2 bg-muted/50 rounded-md">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<strong>Cleanup Frequency:</strong> The cleanup process runs automatically at optimal intervals:
|
||||
shorter retention periods trigger more frequent cleanups, longer periods trigger less frequent cleanups.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-x-4">
|
||||
<div className="flex-1">
|
||||
<label className="block text-sm font-medium mb-1">Last Cleanup</label>
|
||||
<div className="text-sm">
|
||||
{config.lastRun ? formatDate(config.lastRun) : "Never"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{config.enabled && (
|
||||
<div className="flex-1">
|
||||
<label className="block text-sm font-medium mb-1">Next Cleanup</label>
|
||||
<div className="text-sm">
|
||||
{config.nextRun
|
||||
? formatDate(config.nextRun)
|
||||
: config.enabled
|
||||
? "Calculating..."
|
||||
: "Never"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -15,11 +15,11 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Info,
|
||||
GitBranch,
|
||||
Star,
|
||||
Lock,
|
||||
import {
|
||||
Info,
|
||||
GitBranch,
|
||||
Star,
|
||||
Lock,
|
||||
Archive,
|
||||
GitPullRequest,
|
||||
Tag,
|
||||
@@ -29,9 +29,18 @@ import {
|
||||
BookOpen,
|
||||
GitFork,
|
||||
ChevronDown,
|
||||
Funnel
|
||||
Funnel,
|
||||
HardDrive,
|
||||
FileCode2
|
||||
} from "lucide-react";
|
||||
import type { GitHubConfig, MirrorOptions, AdvancedOptions } from "@/types/config";
|
||||
import type { GitHubConfig, MirrorOptions, AdvancedOptions, DuplicateNameStrategy } from "@/types/config";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface GitHubMirrorSettingsProps {
|
||||
@@ -52,11 +61,11 @@ export function GitHubMirrorSettings({
|
||||
onAdvancedOptionsChange,
|
||||
}: GitHubMirrorSettingsProps) {
|
||||
|
||||
const handleGitHubChange = (field: keyof GitHubConfig, value: boolean) => {
|
||||
const handleGitHubChange = (field: keyof GitHubConfig, value: boolean | string) => {
|
||||
onGitHubConfigChange({ ...githubConfig, [field]: value });
|
||||
};
|
||||
|
||||
const handleMirrorChange = (field: keyof MirrorOptions, value: boolean) => {
|
||||
const handleMirrorChange = (field: keyof MirrorOptions, value: boolean | number) => {
|
||||
onMirrorOptionsChange({ ...mirrorOptions, [field]: value });
|
||||
};
|
||||
|
||||
@@ -80,10 +89,10 @@ export function GitHubMirrorSettings({
|
||||
// Calculate what content is included for starred repos
|
||||
const starredRepoContent = {
|
||||
code: true, // Always included
|
||||
releases: !advancedOptions.skipStarredIssues && mirrorOptions.mirrorReleases,
|
||||
issues: !advancedOptions.skipStarredIssues && mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.issues,
|
||||
pullRequests: !advancedOptions.skipStarredIssues && mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.pullRequests,
|
||||
wiki: !advancedOptions.skipStarredIssues && mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.wiki,
|
||||
releases: !advancedOptions.starredCodeOnly && mirrorOptions.mirrorReleases,
|
||||
issues: !advancedOptions.starredCodeOnly && mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.issues,
|
||||
pullRequests: !advancedOptions.starredCodeOnly && mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.pullRequests,
|
||||
wiki: !advancedOptions.starredCodeOnly && mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.wiki,
|
||||
};
|
||||
|
||||
const starredContentCount = Object.entries(starredRepoContent).filter(([key, value]) => key !== 'code' && value).length;
|
||||
@@ -159,7 +168,7 @@ export function GitHubMirrorSettings({
|
||||
className="h-8 text-xs font-normal min-w-[140px] md:min-w-[140px] justify-between"
|
||||
>
|
||||
<span>
|
||||
{advancedOptions.skipStarredIssues ? (
|
||||
{advancedOptions.starredCodeOnly ? (
|
||||
"Code only"
|
||||
) : starredContentCount === 0 ? (
|
||||
"Code only"
|
||||
@@ -197,8 +206,8 @@ export function GitHubMirrorSettings({
|
||||
<div className="flex items-center space-x-3 py-1 px-1 rounded hover:bg-accent">
|
||||
<Checkbox
|
||||
id="starred-lightweight"
|
||||
checked={advancedOptions.skipStarredIssues}
|
||||
onCheckedChange={(checked) => handleAdvancedChange('skipStarredIssues', !!checked)}
|
||||
checked={advancedOptions.starredCodeOnly}
|
||||
onCheckedChange={(checked) => handleAdvancedChange('starredCodeOnly', !!checked)}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="starred-lightweight"
|
||||
@@ -213,7 +222,7 @@ export function GitHubMirrorSettings({
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{!advancedOptions.skipStarredIssues && (
|
||||
{!advancedOptions.starredCodeOnly && (
|
||||
<>
|
||||
<Separator className="my-2" />
|
||||
<div className="space-y-2">
|
||||
@@ -277,6 +286,40 @@ export function GitHubMirrorSettings({
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Duplicate name handling for starred repos */}
|
||||
{githubConfig.mirrorStarred && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<Label className="text-xs font-medium text-muted-foreground">
|
||||
Duplicate name handling
|
||||
</Label>
|
||||
<div className="flex items-center gap-3">
|
||||
<FileCode2 className="h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm">Name collision strategy</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
How to handle repos with the same name from different owners
|
||||
</p>
|
||||
</div>
|
||||
<Select
|
||||
value={githubConfig.starredDuplicateStrategy || "suffix"}
|
||||
onValueChange={(value) => handleGitHubChange('starredDuplicateStrategy', value as DuplicateNameStrategy)}
|
||||
>
|
||||
<SelectTrigger className="w-[180px] h-8 text-xs">
|
||||
<SelectValue placeholder="Select strategy" />
|
||||
</SelectTrigger>
|
||||
<SelectContent align="end">
|
||||
<SelectItem value="suffix" className="text-xs">
|
||||
<span className="font-mono">repo-owner</span>
|
||||
</SelectItem>
|
||||
<SelectItem value="prefix" className="text-xs">
|
||||
<span className="font-mono">owner-repo</span>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -311,16 +354,62 @@ export function GitHubMirrorSettings({
|
||||
checked={mirrorOptions.mirrorReleases}
|
||||
onCheckedChange={(checked) => handleMirrorChange('mirrorReleases', !!checked)}
|
||||
/>
|
||||
<div className="space-y-0.5 flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<Label
|
||||
htmlFor="mirror-releases"
|
||||
className="text-sm font-normal cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
<Tag className="h-3.5 w-3.5" />
|
||||
Releases & Tags
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Include GitHub releases, tags, and associated assets
|
||||
</p>
|
||||
</div>
|
||||
{mirrorOptions.mirrorReleases && (
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
<label htmlFor="release-limit" className="text-xs text-muted-foreground">
|
||||
Latest
|
||||
</label>
|
||||
<input
|
||||
id="release-limit"
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
value={mirrorOptions.releaseLimit || 10}
|
||||
onChange={(e) => {
|
||||
const value = parseInt(e.target.value) || 10;
|
||||
const clampedValue = Math.min(100, Math.max(1, value));
|
||||
handleMirrorChange('releaseLimit', clampedValue);
|
||||
}}
|
||||
className="w-16 px-2 py-1 text-xs border border-input rounded bg-background text-foreground"
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">releases</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3">
|
||||
<Checkbox
|
||||
id="mirror-lfs"
|
||||
checked={mirrorOptions.mirrorLFS}
|
||||
onCheckedChange={(checked) => handleMirrorChange('mirrorLFS', !!checked)}
|
||||
/>
|
||||
<div className="space-y-0.5 flex-1">
|
||||
<Label
|
||||
htmlFor="mirror-releases"
|
||||
htmlFor="mirror-lfs"
|
||||
className="text-sm font-normal cursor-pointer flex items-center gap-2"
|
||||
>
|
||||
<Tag className="h-3.5 w-3.5" />
|
||||
Releases & Tags
|
||||
<HardDrive className="h-3.5 w-3.5" />
|
||||
Git LFS (Large File Storage)
|
||||
<Badge variant="secondary" className="ml-2 text-[10px] px-1.5 py-0">BETA</Badge>
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Include GitHub releases, tags, and associated assets
|
||||
Mirror Git LFS objects. Requires LFS to be enabled on your Gitea server and Git v2.1.2+
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -430,6 +519,31 @@ export function GitHubMirrorSettings({
|
||||
>
|
||||
<GitPullRequest className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
Pull Requests
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-3 w-3 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="max-w-sm">
|
||||
<div className="space-y-2">
|
||||
<p className="font-semibold">Pull Requests are mirrored as issues</p>
|
||||
<p className="text-xs">
|
||||
Due to Gitea API limitations, PRs cannot be created as actual pull requests.
|
||||
Instead, they are mirrored as issues with:
|
||||
</p>
|
||||
<ul className="text-xs space-y-1 ml-3">
|
||||
<li>• [PR #number] prefix in title</li>
|
||||
<li>• Full PR description and metadata</li>
|
||||
<li>• Commit history (up to 10 commits)</li>
|
||||
<li>• File changes summary</li>
|
||||
<li>• Diff preview (first 5 files)</li>
|
||||
<li>• Review comments preserved</li>
|
||||
<li>• Merge/close status tracking</li>
|
||||
</ul>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
@@ -524,4 +638,4 @@ export function GitHubMirrorSettings({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,11 +44,13 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving, g
|
||||
case "preserve":
|
||||
newConfig.preserveOrgStructure = true;
|
||||
newConfig.mirrorStrategy = "preserve";
|
||||
newConfig.personalReposOrg = undefined; // Clear personal repos org in preserve mode
|
||||
break;
|
||||
case "single-org":
|
||||
newConfig.preserveOrgStructure = false;
|
||||
newConfig.mirrorStrategy = "single-org";
|
||||
if (!newConfig.organization) {
|
||||
// Reset to default if coming from mixed mode where it was personal repos org
|
||||
if (config.mirrorStrategy === "mixed" || !newConfig.organization || newConfig.organization === "github-personal") {
|
||||
newConfig.organization = "github-mirrors";
|
||||
}
|
||||
break;
|
||||
@@ -60,8 +62,10 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving, g
|
||||
case "mixed":
|
||||
newConfig.preserveOrgStructure = false;
|
||||
newConfig.mirrorStrategy = "mixed";
|
||||
if (!newConfig.organization) {
|
||||
newConfig.organization = "github-mirrors";
|
||||
// In mixed mode, organization field represents personal repos org
|
||||
// Reset it to default if coming from single-org mode
|
||||
if (config.mirrorStrategy === "single-org" || !newConfig.organization || newConfig.organization === "github-mirrors") {
|
||||
newConfig.organization = "github-personal";
|
||||
}
|
||||
if (!newConfig.personalReposOrg) {
|
||||
newConfig.personalReposOrg = "github-personal";
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
import React from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "../ui/checkbox";
|
||||
import type { MirrorOptions } from "@/types/config";
|
||||
import { RefreshCw, Info } from "lucide-react";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||
|
||||
interface MirrorOptionsFormProps {
|
||||
config: MirrorOptions;
|
||||
setConfig: React.Dispatch<React.SetStateAction<MirrorOptions>>;
|
||||
onAutoSave?: (config: MirrorOptions) => Promise<void>;
|
||||
isAutoSaving?: boolean;
|
||||
}
|
||||
|
||||
export function MirrorOptionsForm({
|
||||
config,
|
||||
setConfig,
|
||||
onAutoSave,
|
||||
isAutoSaving = false,
|
||||
}: MirrorOptionsFormProps) {
|
||||
const handleChange = (name: string, checked: boolean) => {
|
||||
let newConfig = { ...config };
|
||||
|
||||
if (name === "mirrorMetadata") {
|
||||
newConfig.mirrorMetadata = checked;
|
||||
// If disabling metadata, also disable all components
|
||||
if (!checked) {
|
||||
newConfig.metadataComponents = {
|
||||
issues: false,
|
||||
pullRequests: false,
|
||||
labels: false,
|
||||
milestones: false,
|
||||
wiki: false,
|
||||
};
|
||||
}
|
||||
} else if (name.startsWith("metadataComponents.")) {
|
||||
const componentName = name.split(".")[1] as keyof typeof config.metadataComponents;
|
||||
newConfig.metadataComponents = {
|
||||
...config.metadataComponents,
|
||||
[componentName]: checked,
|
||||
};
|
||||
} else {
|
||||
newConfig = {
|
||||
...config,
|
||||
[name]: checked,
|
||||
};
|
||||
}
|
||||
|
||||
setConfig(newConfig);
|
||||
|
||||
// Auto-save
|
||||
if (onAutoSave) {
|
||||
onAutoSave(newConfig);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="self-start">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg font-semibold flex items-center justify-between">
|
||||
Mirror Options
|
||||
{isAutoSaving && (
|
||||
<div className="flex items-center text-sm text-muted-foreground">
|
||||
<RefreshCw className="h-3 w-3 animate-spin mr-1" />
|
||||
<span className="text-xs">Auto-saving...</span>
|
||||
</div>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Repository Content */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-sm font-medium text-foreground">Repository Content</h4>
|
||||
|
||||
<div className="flex items-center">
|
||||
<Checkbox
|
||||
id="mirror-releases"
|
||||
checked={config.mirrorReleases}
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange("mirrorReleases", Boolean(checked))
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor="mirror-releases"
|
||||
className="ml-2 text-sm select-none flex items-center"
|
||||
>
|
||||
Mirror releases
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="ml-1 cursor-pointer text-muted-foreground">
|
||||
<Info size={14} />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="max-w-xs text-xs">
|
||||
Include GitHub releases and tags in the mirror
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<Checkbox
|
||||
id="mirror-metadata"
|
||||
checked={config.mirrorMetadata}
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange("mirrorMetadata", Boolean(checked))
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor="mirror-metadata"
|
||||
className="ml-2 text-sm select-none flex items-center"
|
||||
>
|
||||
Mirror metadata
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="ml-1 cursor-pointer text-muted-foreground">
|
||||
<Info size={14} />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="max-w-xs text-xs">
|
||||
Include issues, pull requests, labels, milestones, and wiki
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Metadata Components */}
|
||||
{config.mirrorMetadata && (
|
||||
<div className="ml-6 space-y-3 border-l-2 border-muted pl-4">
|
||||
<h5 className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
||||
Metadata Components
|
||||
</h5>
|
||||
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
<div className="flex items-center">
|
||||
<Checkbox
|
||||
id="metadata-issues"
|
||||
checked={config.metadataComponents.issues}
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange("metadataComponents.issues", Boolean(checked))
|
||||
}
|
||||
disabled={!config.mirrorMetadata}
|
||||
/>
|
||||
<label
|
||||
htmlFor="metadata-issues"
|
||||
className="ml-2 text-sm select-none"
|
||||
>
|
||||
Issues
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<Checkbox
|
||||
id="metadata-pullRequests"
|
||||
checked={config.metadataComponents.pullRequests}
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange("metadataComponents.pullRequests", Boolean(checked))
|
||||
}
|
||||
disabled={!config.mirrorMetadata}
|
||||
/>
|
||||
<label
|
||||
htmlFor="metadata-pullRequests"
|
||||
className="ml-2 text-sm select-none"
|
||||
>
|
||||
Pull requests
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<Checkbox
|
||||
id="metadata-labels"
|
||||
checked={config.metadataComponents.labels}
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange("metadataComponents.labels", Boolean(checked))
|
||||
}
|
||||
disabled={!config.mirrorMetadata}
|
||||
/>
|
||||
<label
|
||||
htmlFor="metadata-labels"
|
||||
className="ml-2 text-sm select-none"
|
||||
>
|
||||
Labels
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<Checkbox
|
||||
id="metadata-milestones"
|
||||
checked={config.metadataComponents.milestones}
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange("metadataComponents.milestones", Boolean(checked))
|
||||
}
|
||||
disabled={!config.mirrorMetadata}
|
||||
/>
|
||||
<label
|
||||
htmlFor="metadata-milestones"
|
||||
className="ml-2 text-sm select-none"
|
||||
>
|
||||
Milestones
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<Checkbox
|
||||
id="metadata-wiki"
|
||||
checked={config.metadataComponents.wiki}
|
||||
onCheckedChange={(checked) =>
|
||||
handleChange("metadataComponents.wiki", Boolean(checked))
|
||||
}
|
||||
disabled={!config.mirrorMetadata}
|
||||
/>
|
||||
<label
|
||||
htmlFor="metadata-wiki"
|
||||
className="ml-2 text-sm select-none"
|
||||
>
|
||||
Wiki
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -104,7 +104,7 @@ export const OrganizationConfiguration: React.FC<OrganizationConfigurationProps>
|
||||
id="destinationOrg"
|
||||
value={destinationOrg || ""}
|
||||
onChange={(e) => onDestinationOrgChange(e.target.value)}
|
||||
placeholder="github-mirrors"
|
||||
placeholder={strategy === "mixed" ? "github-personal" : "github-mirrors"}
|
||||
className=""
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
@@ -114,32 +114,6 @@ export const OrganizationConfiguration: React.FC<OrganizationConfigurationProps>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
) : strategy === "preserve" ? (
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="personalReposOrg" className="text-sm font-normal flex items-center gap-2">
|
||||
Personal Repos Organization
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Override where your personal repositories are mirrored (leave empty to use your username)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Label>
|
||||
<Input
|
||||
id="personalReposOrg"
|
||||
value={personalReposOrg || ""}
|
||||
onChange={(e) => onPersonalReposOrgChange(e.target.value)}
|
||||
placeholder="my-personal-mirrors"
|
||||
className=""
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Override destination for your personal repos
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="hidden md:block" />
|
||||
)}
|
||||
|
||||
@@ -6,34 +6,58 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { apiRequest, showErrorToast } from '@/lib/utils';
|
||||
import { toast } from 'sonner';
|
||||
import { Plus, Trash2, ExternalLink, Loader2, AlertCircle, Copy, Shield, Info } from 'lucide-react';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Plus, Trash2, Loader2, AlertCircle, Shield, Edit2 } from 'lucide-react';
|
||||
import { Skeleton } from '../ui/skeleton';
|
||||
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';
|
||||
|
||||
function isTrustedIssuer(issuer: string, allowedHosts: string[]): boolean {
|
||||
try {
|
||||
const url = new URL(issuer);
|
||||
return allowedHosts.some(host => url.hostname === host || url.hostname.endsWith(`.${host}`));
|
||||
} catch {
|
||||
return false; // Return false if the URL is invalid
|
||||
}
|
||||
}
|
||||
interface SSOProvider {
|
||||
id: string;
|
||||
issuer: string;
|
||||
domain: string;
|
||||
providerId: string;
|
||||
organizationId?: string;
|
||||
oidcConfig: {
|
||||
oidcConfig?: {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
authorizationEndpoint: string;
|
||||
tokenEndpoint: string;
|
||||
jwksEndpoint: string;
|
||||
userInfoEndpoint: string;
|
||||
mapping: {
|
||||
id: string;
|
||||
email: string;
|
||||
emailVerified: string;
|
||||
name: string;
|
||||
image: string;
|
||||
};
|
||||
jwksEndpoint?: string;
|
||||
userInfoEndpoint?: string;
|
||||
discoveryEndpoint?: string;
|
||||
scopes?: string[];
|
||||
pkce?: boolean;
|
||||
};
|
||||
samlConfig?: {
|
||||
entryPoint: string;
|
||||
cert: string;
|
||||
callbackUrl?: string;
|
||||
audience?: string;
|
||||
wantAssertionsSigned?: boolean;
|
||||
signatureAlgorithm?: string;
|
||||
digestAlgorithm?: string;
|
||||
identifierFormat?: string;
|
||||
};
|
||||
mapping?: {
|
||||
id: string;
|
||||
email: string;
|
||||
emailVerified?: string;
|
||||
name?: string;
|
||||
image?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
};
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
@@ -43,20 +67,38 @@ export function SSOSettings() {
|
||||
const [providers, setProviders] = useState<SSOProvider[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [showProviderDialog, setShowProviderDialog] = useState(false);
|
||||
const [addingProvider, setAddingProvider] = useState(false);
|
||||
const [isDiscovering, setIsDiscovering] = useState(false);
|
||||
const [headerAuthEnabled, setHeaderAuthEnabled] = useState(false);
|
||||
const [editingProvider, setEditingProvider] = useState<SSOProvider | null>(null);
|
||||
|
||||
// Form states for new provider
|
||||
const [providerType, setProviderType] = useState<'oidc' | 'saml'>('oidc');
|
||||
const [providerForm, setProviderForm] = useState({
|
||||
// Common fields
|
||||
issuer: '',
|
||||
domain: '',
|
||||
providerId: '',
|
||||
organizationId: '',
|
||||
// OIDC fields
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
authorizationEndpoint: '',
|
||||
tokenEndpoint: '',
|
||||
jwksEndpoint: '',
|
||||
userInfoEndpoint: '',
|
||||
discoveryEndpoint: '',
|
||||
scopes: ['openid', 'email', 'profile'] as string[],
|
||||
pkce: true,
|
||||
// SAML fields
|
||||
entryPoint: '',
|
||||
cert: '',
|
||||
callbackUrl: '',
|
||||
audience: '',
|
||||
wantAssertionsSigned: true,
|
||||
signatureAlgorithm: 'sha256',
|
||||
digestAlgorithm: 'sha256',
|
||||
identifierFormat: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
|
||||
});
|
||||
|
||||
|
||||
@@ -69,11 +111,11 @@ export function SSOSettings() {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const [providersRes, headerAuthStatus] = await Promise.all([
|
||||
apiRequest<SSOProvider[]>('/sso/providers'),
|
||||
apiRequest<SSOProvider[] | { providers: SSOProvider[] }>('/sso/providers'),
|
||||
apiRequest<{ enabled: boolean }>('/auth/header-status').catch(() => ({ enabled: false }))
|
||||
]);
|
||||
|
||||
setProviders(providersRes);
|
||||
setProviders(Array.isArray(providersRes) ? providersRes : providersRes?.providers || []);
|
||||
setHeaderAuthEnabled(headerAuthStatus.enabled);
|
||||
} catch (error) {
|
||||
showErrorToast(error, toast);
|
||||
@@ -101,6 +143,7 @@ export function SSOSettings() {
|
||||
tokenEndpoint: discovered.tokenEndpoint || '',
|
||||
jwksEndpoint: discovered.jwksEndpoint || '',
|
||||
userInfoEndpoint: discovered.userInfoEndpoint || '',
|
||||
discoveryEndpoint: discovered.discoveryEndpoint || `${providerForm.issuer}/.well-known/openid-configuration`,
|
||||
domain: discovered.suggestedDomain || prev.domain,
|
||||
}));
|
||||
|
||||
@@ -113,40 +156,113 @@ export function SSOSettings() {
|
||||
};
|
||||
|
||||
const createProvider = async () => {
|
||||
setAddingProvider(true);
|
||||
try {
|
||||
const newProvider = await apiRequest<SSOProvider>('/sso/providers', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
...providerForm,
|
||||
mapping: {
|
||||
id: 'sub',
|
||||
email: 'email',
|
||||
emailVerified: 'email_verified',
|
||||
name: 'name',
|
||||
image: 'picture',
|
||||
},
|
||||
},
|
||||
});
|
||||
const requestData: any = {
|
||||
providerId: providerForm.providerId,
|
||||
issuer: providerForm.issuer,
|
||||
domain: providerForm.domain,
|
||||
organizationId: providerForm.organizationId || undefined,
|
||||
providerType,
|
||||
};
|
||||
|
||||
if (providerType === 'oidc') {
|
||||
requestData.clientId = providerForm.clientId;
|
||||
requestData.clientSecret = providerForm.clientSecret;
|
||||
requestData.authorizationEndpoint = providerForm.authorizationEndpoint;
|
||||
requestData.tokenEndpoint = providerForm.tokenEndpoint;
|
||||
requestData.jwksEndpoint = providerForm.jwksEndpoint;
|
||||
requestData.userInfoEndpoint = providerForm.userInfoEndpoint;
|
||||
requestData.discoveryEndpoint = providerForm.discoveryEndpoint;
|
||||
requestData.scopes = providerForm.scopes;
|
||||
requestData.pkce = providerForm.pkce;
|
||||
} 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.wantAssertionsSigned = providerForm.wantAssertionsSigned;
|
||||
requestData.signatureAlgorithm = providerForm.signatureAlgorithm;
|
||||
requestData.digestAlgorithm = providerForm.digestAlgorithm;
|
||||
requestData.identifierFormat = providerForm.identifierFormat;
|
||||
}
|
||||
|
||||
if (editingProvider) {
|
||||
// Update existing provider
|
||||
const updatedProvider = await apiRequest<SSOProvider>(`/sso/providers?id=${editingProvider.id}`, {
|
||||
method: 'PUT',
|
||||
data: requestData,
|
||||
});
|
||||
setProviders(providers.map(p => p.id === editingProvider.id ? updatedProvider : p));
|
||||
toast.success('SSO provider updated successfully');
|
||||
} else {
|
||||
// Create new provider
|
||||
const newProvider = await apiRequest<SSOProvider>('/sso/providers', {
|
||||
method: 'POST',
|
||||
data: requestData,
|
||||
});
|
||||
setProviders([...providers, newProvider]);
|
||||
toast.success('SSO provider created successfully');
|
||||
}
|
||||
|
||||
setProviders([...providers, newProvider]);
|
||||
setShowProviderDialog(false);
|
||||
setEditingProvider(null);
|
||||
setProviderForm({
|
||||
issuer: '',
|
||||
domain: '',
|
||||
providerId: '',
|
||||
organizationId: '',
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
authorizationEndpoint: '',
|
||||
tokenEndpoint: '',
|
||||
jwksEndpoint: '',
|
||||
userInfoEndpoint: '',
|
||||
discoveryEndpoint: '',
|
||||
scopes: ['openid', 'email', 'profile'] as string[],
|
||||
pkce: true,
|
||||
entryPoint: '',
|
||||
cert: '',
|
||||
callbackUrl: '',
|
||||
audience: '',
|
||||
wantAssertionsSigned: true,
|
||||
signatureAlgorithm: 'sha256',
|
||||
digestAlgorithm: 'sha256',
|
||||
identifierFormat: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
|
||||
});
|
||||
toast.success('SSO provider created successfully');
|
||||
} catch (error) {
|
||||
showErrorToast(error, toast);
|
||||
} finally {
|
||||
setAddingProvider(false);
|
||||
}
|
||||
};
|
||||
|
||||
const startEditProvider = (provider: SSOProvider) => {
|
||||
setEditingProvider(provider);
|
||||
setProviderType(provider.samlConfig ? 'saml' : 'oidc');
|
||||
|
||||
if (provider.oidcConfig) {
|
||||
setProviderForm({
|
||||
...providerForm,
|
||||
providerId: provider.providerId,
|
||||
issuer: provider.issuer,
|
||||
domain: provider.domain,
|
||||
organizationId: provider.organizationId || '',
|
||||
clientId: provider.oidcConfig.clientId || '',
|
||||
clientSecret: provider.oidcConfig.clientSecret || '',
|
||||
authorizationEndpoint: provider.oidcConfig.authorizationEndpoint || '',
|
||||
tokenEndpoint: provider.oidcConfig.tokenEndpoint || '',
|
||||
jwksEndpoint: provider.oidcConfig.jwksEndpoint || '',
|
||||
userInfoEndpoint: provider.oidcConfig.userInfoEndpoint || '',
|
||||
discoveryEndpoint: provider.oidcConfig.discoveryEndpoint || '',
|
||||
scopes: provider.oidcConfig.scopes || ['openid', 'email', 'profile'],
|
||||
pkce: provider.oidcConfig.pkce !== false,
|
||||
});
|
||||
}
|
||||
|
||||
setShowProviderDialog(true);
|
||||
};
|
||||
|
||||
const deleteProvider = async (id: string) => {
|
||||
try {
|
||||
await apiRequest(`/sso/providers?id=${id}`, { method: 'DELETE' });
|
||||
@@ -158,10 +274,6 @@ export function SSOSettings() {
|
||||
};
|
||||
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
toast.success('Copied to clipboard');
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -177,8 +289,8 @@ export function SSOSettings() {
|
||||
{/* Header with status indicators */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">Authentication & SSO</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<h2 className="text-2xl font-semibold">Authentication & SSO</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Configure how users authenticate with your application
|
||||
</p>
|
||||
</div>
|
||||
@@ -191,9 +303,9 @@ export function SSOSettings() {
|
||||
</div>
|
||||
|
||||
{/* Authentication Methods Overview */}
|
||||
<Card className="mb-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Active Authentication Methods</CardTitle>
|
||||
<CardTitle className="text-lg font-semibold">Active Authentication Methods</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
@@ -248,8 +360,8 @@ export function SSOSettings() {
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>External Identity Providers</CardTitle>
|
||||
<CardDescription>
|
||||
<CardTitle className="text-lg font-semibold">External Identity Providers</CardTitle>
|
||||
<CardDescription className="text-sm">
|
||||
Connect external OIDC/OAuth providers (Google, Azure AD, etc.) to allow users to sign in with their existing accounts
|
||||
</CardDescription>
|
||||
</div>
|
||||
@@ -260,106 +372,249 @@ export function SSOSettings() {
|
||||
Add Provider
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add SSO Provider</DialogTitle>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] md:max-h-[85vh] lg:max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader className="flex-shrink-0">
|
||||
<DialogTitle>{editingProvider ? 'Edit SSO Provider' : 'Add SSO Provider'}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure an external OIDC provider for user authentication
|
||||
{editingProvider
|
||||
? 'Update the configuration for this identity provider'
|
||||
: 'Configure an external identity provider for user authentication'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="issuer">Issuer URL</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="issuer"
|
||||
value={providerForm.issuer}
|
||||
onChange={e => setProviderForm(prev => ({ ...prev, issuer: e.target.value }))}
|
||||
placeholder="https://accounts.google.com"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={discoverOIDC}
|
||||
disabled={isDiscovering}
|
||||
>
|
||||
{isDiscovering ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Discover'}
|
||||
</Button>
|
||||
<div className="flex-1 overflow-y-auto px-1 -mx-1">
|
||||
<Tabs value={providerType} onValueChange={(value) => setProviderType(value as 'oidc' | 'saml')}>
|
||||
<TabsList className="grid w-full grid-cols-2 sticky top-0 z-10 bg-background">
|
||||
<TabsTrigger value="oidc">OIDC / OAuth2</TabsTrigger>
|
||||
<TabsTrigger value="saml">SAML 2.0</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Common Fields */}
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="providerId">Provider ID</Label>
|
||||
<Input
|
||||
id="providerId"
|
||||
value={providerForm.providerId}
|
||||
onChange={e => setProviderForm(prev => ({ ...prev, providerId: e.target.value }))}
|
||||
placeholder="google-sso"
|
||||
disabled={!!editingProvider}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="domain">Email Domain</Label>
|
||||
<Input
|
||||
id="domain"
|
||||
value={providerForm.domain}
|
||||
onChange={e => setProviderForm(prev => ({ ...prev, domain: e.target.value }))}
|
||||
placeholder="example.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="domain">Domain</Label>
|
||||
<Input
|
||||
id="domain"
|
||||
value={providerForm.domain}
|
||||
onChange={e => setProviderForm(prev => ({ ...prev, domain: e.target.value }))}
|
||||
placeholder="example.com"
|
||||
/>
|
||||
<Label htmlFor="issuer">Issuer URL</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="issuer"
|
||||
value={providerForm.issuer}
|
||||
onChange={e => setProviderForm(prev => ({ ...prev, issuer: e.target.value }))}
|
||||
placeholder={providerType === 'oidc' ? "https://accounts.google.com" : "https://idp.example.com"}
|
||||
/>
|
||||
{providerType === 'oidc' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={discoverOIDC}
|
||||
disabled={isDiscovering}
|
||||
>
|
||||
{isDiscovering ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Discover'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="providerId">Provider ID</Label>
|
||||
<Label htmlFor="organizationId">Organization ID (Optional)</Label>
|
||||
<Input
|
||||
id="providerId"
|
||||
value={providerForm.providerId}
|
||||
onChange={e => setProviderForm(prev => ({ ...prev, providerId: e.target.value }))}
|
||||
placeholder="google-sso"
|
||||
id="organizationId"
|
||||
value={providerForm.organizationId}
|
||||
onChange={e => setProviderForm(prev => ({ ...prev, organizationId: e.target.value }))}
|
||||
placeholder="org_123"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">Link this provider to an organization for automatic user provisioning</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<TabsContent value="oidc" className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="clientId">Client ID</Label>
|
||||
<Input
|
||||
id="clientId"
|
||||
value={providerForm.clientId}
|
||||
onChange={e => setProviderForm(prev => ({ ...prev, clientId: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="clientSecret">Client Secret</Label>
|
||||
<Input
|
||||
id="clientSecret"
|
||||
type="password"
|
||||
value={providerForm.clientSecret}
|
||||
onChange={e => setProviderForm(prev => ({ ...prev, clientSecret: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="clientId">Client ID</Label>
|
||||
<Label htmlFor="authEndpoint">Authorization Endpoint</Label>
|
||||
<Input
|
||||
id="clientId"
|
||||
value={providerForm.clientId}
|
||||
onChange={e => setProviderForm(prev => ({ ...prev, clientId: e.target.value }))}
|
||||
id="authEndpoint"
|
||||
value={providerForm.authorizationEndpoint}
|
||||
onChange={e => setProviderForm(prev => ({ ...prev, authorizationEndpoint: e.target.value }))}
|
||||
placeholder="https://accounts.google.com/o/oauth2/auth"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="clientSecret">Client Secret</Label>
|
||||
<Label htmlFor="tokenEndpoint">Token Endpoint</Label>
|
||||
<Input
|
||||
id="clientSecret"
|
||||
type="password"
|
||||
value={providerForm.clientSecret}
|
||||
onChange={e => setProviderForm(prev => ({ ...prev, clientSecret: e.target.value }))}
|
||||
id="tokenEndpoint"
|
||||
value={providerForm.tokenEndpoint}
|
||||
onChange={e => setProviderForm(prev => ({ ...prev, tokenEndpoint: e.target.value }))}
|
||||
placeholder="https://oauth2.googleapis.com/token"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="authEndpoint">Authorization Endpoint</Label>
|
||||
<Input
|
||||
id="authEndpoint"
|
||||
value={providerForm.authorizationEndpoint}
|
||||
onChange={e => setProviderForm(prev => ({ ...prev, authorizationEndpoint: e.target.value }))}
|
||||
placeholder="https://accounts.google.com/o/oauth2/auth"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="scopes">OAuth Scopes</Label>
|
||||
<MultiSelect
|
||||
options={[
|
||||
{ label: "OpenID", value: "openid" },
|
||||
{ label: "Email", value: "email" },
|
||||
{ label: "Profile", value: "profile" },
|
||||
{ label: "Offline Access", value: "offline_access" },
|
||||
]}
|
||||
selected={providerForm.scopes}
|
||||
onChange={(scopes) => setProviderForm(prev => ({ ...prev, scopes }))}
|
||||
placeholder="Select scopes..."
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Select the OAuth scopes to request from the provider
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tokenEndpoint">Token Endpoint</Label>
|
||||
<Input
|
||||
id="tokenEndpoint"
|
||||
value={providerForm.tokenEndpoint}
|
||||
onChange={e => setProviderForm(prev => ({ ...prev, tokenEndpoint: e.target.value }))}
|
||||
placeholder="https://oauth2.googleapis.com/token"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="pkce"
|
||||
checked={providerForm.pkce}
|
||||
onCheckedChange={(checked) => setProviderForm(prev => ({ ...prev, pkce: checked }))}
|
||||
/>
|
||||
<Label htmlFor="pkce">Enable PKCE</Label>
|
||||
</div>
|
||||
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Redirect URL: {window.location.origin}/api/auth/sso/callback/{providerForm.providerId || '{provider-id}'}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<Alert>
|
||||
<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>
|
||||
{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.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="saml" className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="entryPoint">SAML Entry Point</Label>
|
||||
<Input
|
||||
id="entryPoint"
|
||||
value={providerForm.entryPoint}
|
||||
onChange={e => setProviderForm(prev => ({ ...prev, entryPoint: e.target.value }))}
|
||||
placeholder="https://idp.example.com/sso"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="cert">X.509 Certificate</Label>
|
||||
<Textarea
|
||||
id="cert"
|
||||
value={providerForm.cert}
|
||||
onChange={e => setProviderForm(prev => ({ ...prev, cert: e.target.value }))}
|
||||
placeholder="-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
|
||||
rows={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="wantAssertionsSigned"
|
||||
checked={providerForm.wantAssertionsSigned}
|
||||
onCheckedChange={(checked) => setProviderForm(prev => ({ ...prev, wantAssertionsSigned: checked }))}
|
||||
/>
|
||||
<Label htmlFor="wantAssertionsSigned">Require Signed Assertions</Label>
|
||||
</div>
|
||||
|
||||
<Alert>
|
||||
<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>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowProviderDialog(false)}>
|
||||
<DialogFooter className="flex-shrink-0 pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setShowProviderDialog(false);
|
||||
setEditingProvider(null);
|
||||
// Reset form
|
||||
setProviderForm({
|
||||
issuer: '',
|
||||
domain: '',
|
||||
providerId: '',
|
||||
organizationId: '',
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
authorizationEndpoint: '',
|
||||
tokenEndpoint: '',
|
||||
jwksEndpoint: '',
|
||||
userInfoEndpoint: '',
|
||||
discoveryEndpoint: '',
|
||||
scopes: ['openid', 'email', 'profile'] as string[],
|
||||
pkce: true,
|
||||
entryPoint: '',
|
||||
cert: '',
|
||||
callbackUrl: '',
|
||||
audience: '',
|
||||
wantAssertionsSigned: true,
|
||||
signatureAlgorithm: 'sha256',
|
||||
digestAlgorithm: 'sha256',
|
||||
identifierFormat: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={createProvider}>Create Provider</Button>
|
||||
<Button onClick={createProvider} disabled={addingProvider}>
|
||||
{addingProvider ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
{editingProvider ? 'Updating...' : 'Creating...'}
|
||||
</>
|
||||
) : (
|
||||
editingProvider ? 'Update Provider' : 'Create Provider'
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -385,37 +640,83 @@ export function SSOSettings() {
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-3">
|
||||
{providers.map(provider => (
|
||||
<Card key={provider.id}>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-semibold">{provider.providerId}</h4>
|
||||
<p className="text-sm text-muted-foreground">{provider.domain}</p>
|
||||
<div key={provider.id} className="border rounded-lg p-4 hover:bg-muted/50 transition-colors">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<h4 className="font-semibold text-sm">{provider.providerId}</h4>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{provider.samlConfig ? 'SAML' : 'OIDC'}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-3">{provider.domain}</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<span className="text-muted-foreground min-w-[80px]">Issuer:</span>
|
||||
<span className="text-muted-foreground break-all">{provider.issuer}</span>
|
||||
</div>
|
||||
|
||||
{provider.oidcConfig && (
|
||||
<>
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<span className="text-muted-foreground min-w-[80px]">Client ID:</span>
|
||||
<span className="font-mono text-xs text-muted-foreground break-all">{provider.oidcConfig.clientId}</span>
|
||||
</div>
|
||||
|
||||
{provider.oidcConfig.scopes && provider.oidcConfig.scopes.length > 0 && (
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<span className="text-muted-foreground min-w-[80px]">Scopes:</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{provider.oidcConfig.scopes.map(scope => (
|
||||
<Badge key={scope} variant="secondary" className="text-xs">
|
||||
{scope}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{provider.samlConfig && (
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<span className="text-muted-foreground min-w-[80px]">Entry Point:</span>
|
||||
<span className="text-muted-foreground break-all">{provider.samlConfig.entryPoint}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{provider.organizationId && (
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<span className="text-muted-foreground min-w-[80px]">Organization:</span>
|
||||
<span className="text-muted-foreground">{provider.organizationId}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
onClick={() => startEditProvider(provider)}
|
||||
>
|
||||
<Edit2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
onClick={() => deleteProvider(provider.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="font-medium">Issuer</p>
|
||||
<p className="text-muted-foreground">{provider.issuer}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">Client ID</p>
|
||||
<p className="text-muted-foreground font-mono">{provider.oidcConfig.clientId}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import React from 'react';
|
||||
import { ScheduleConfigForm } from './ScheduleConfigForm';
|
||||
import { DatabaseCleanupConfigForm } from './DatabaseCleanupConfigForm';
|
||||
import { Separator } from '../ui/separator';
|
||||
import type { ScheduleConfig, DatabaseCleanupConfig } from '@/types/config';
|
||||
|
||||
interface ScheduleAndCleanupFormProps {
|
||||
scheduleConfig: ScheduleConfig;
|
||||
cleanupConfig: DatabaseCleanupConfig;
|
||||
setScheduleConfig: (update: ScheduleConfig | ((prev: ScheduleConfig) => ScheduleConfig)) => void;
|
||||
setCleanupConfig: (update: DatabaseCleanupConfig | ((prev: DatabaseCleanupConfig) => DatabaseCleanupConfig)) => void;
|
||||
onAutoSaveSchedule?: (config: ScheduleConfig) => Promise<void>;
|
||||
onAutoSaveCleanup?: (config: DatabaseCleanupConfig) => Promise<void>;
|
||||
isAutoSavingSchedule?: boolean;
|
||||
isAutoSavingCleanup?: boolean;
|
||||
}
|
||||
|
||||
export function ScheduleAndCleanupForm({
|
||||
scheduleConfig,
|
||||
cleanupConfig,
|
||||
setScheduleConfig,
|
||||
setCleanupConfig,
|
||||
onAutoSaveSchedule,
|
||||
onAutoSaveCleanup,
|
||||
isAutoSavingSchedule,
|
||||
isAutoSavingCleanup,
|
||||
}: ScheduleAndCleanupFormProps) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<ScheduleConfigForm
|
||||
config={scheduleConfig}
|
||||
setConfig={setScheduleConfig}
|
||||
onAutoSave={onAutoSaveSchedule}
|
||||
isAutoSaving={isAutoSavingSchedule}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
||||
<DatabaseCleanupConfigForm
|
||||
config={cleanupConfig}
|
||||
setConfig={setCleanupConfig}
|
||||
onAutoSave={onAutoSaveCleanup}
|
||||
isAutoSaving={isAutoSavingCleanup}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -83,7 +83,7 @@ export function ScheduleConfigForm({
|
||||
htmlFor="enabled"
|
||||
className="select-none ml-2 block text-sm font-medium"
|
||||
>
|
||||
Enable Automatic Mirroring
|
||||
Enable Automatic Syncing
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -93,7 +93,7 @@ export function ScheduleConfigForm({
|
||||
htmlFor="interval"
|
||||
className="block text-sm font-medium mb-1.5"
|
||||
>
|
||||
Mirroring Interval
|
||||
Sync Interval
|
||||
</label>
|
||||
|
||||
<Select
|
||||
@@ -122,7 +122,7 @@ export function ScheduleConfigForm({
|
||||
</Select>
|
||||
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
How often the mirroring process should run.
|
||||
How often the sync process should run.
|
||||
</p>
|
||||
<div className="mt-2 p-2 bg-muted/50 rounded-md">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
|
||||
@@ -9,6 +9,7 @@ import { apiRequest, showErrorToast } from "@/lib/utils";
|
||||
import type { DashboardApiResponse } from "@/types/dashboard";
|
||||
import { useSSE } from "@/hooks/useSEE";
|
||||
import { toast } from "sonner";
|
||||
import { useEffect as useEffectForToasts } from "react";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { useLiveRefresh } from "@/hooks/useLiveRefresh";
|
||||
@@ -16,6 +17,46 @@ import { usePageVisibility } from "@/hooks/usePageVisibility";
|
||||
import { useConfigStatus } from "@/hooks/useConfigStatus";
|
||||
import { useNavigation } from "@/components/layout/MainLayout";
|
||||
|
||||
// Helper function to format last sync time
|
||||
function formatLastSyncTime(date: Date | null): string {
|
||||
if (!date) return "Never";
|
||||
|
||||
const now = new Date();
|
||||
const syncDate = new Date(date);
|
||||
const diffMs = now.getTime() - syncDate.getTime();
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
// Show relative time for recent syncs
|
||||
if (diffMins < 1) return "Just now";
|
||||
if (diffMins < 60) return `${diffMins} min ago`;
|
||||
if (diffHours < 24) return `${diffHours} hr${diffHours === 1 ? '' : 's'} ago`;
|
||||
if (diffDays < 7) return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`;
|
||||
|
||||
// For older syncs, show week count
|
||||
const diffWeeks = Math.floor(diffDays / 7);
|
||||
if (diffWeeks < 4) return `${diffWeeks} week${diffWeeks === 1 ? '' : 's'} ago`;
|
||||
|
||||
// For even older, show month count
|
||||
const diffMonths = Math.floor(diffDays / 30);
|
||||
return `${diffMonths} month${diffMonths === 1 ? '' : 's'} ago`;
|
||||
}
|
||||
|
||||
// Helper function to format full timestamp
|
||||
function formatFullTimestamp(date: Date | null): string {
|
||||
if (!date) return "";
|
||||
|
||||
return new Date(date).toLocaleString("en-US", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
year: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: true
|
||||
}).replace(',', '');
|
||||
}
|
||||
|
||||
export function Dashboard() {
|
||||
const { user } = useAuth();
|
||||
const { registerRefreshCallback } = useLiveRefresh();
|
||||
@@ -65,6 +106,51 @@ export function Dashboard() {
|
||||
onMessage: handleNewMessage,
|
||||
});
|
||||
|
||||
// Setup rate limit event listener for toast notifications
|
||||
useEffectForToasts(() => {
|
||||
if (!user?.id) return;
|
||||
|
||||
const eventSource = new EventSource(`/api/events?userId=${user.id}`);
|
||||
|
||||
eventSource.addEventListener("rate-limit", (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
switch (data.type) {
|
||||
case "warning":
|
||||
// 80% threshold warning
|
||||
toast.warning("GitHub API Rate Limit Warning", {
|
||||
description: data.message,
|
||||
duration: 8000,
|
||||
});
|
||||
break;
|
||||
|
||||
case "exceeded":
|
||||
// 100% rate limit exceeded
|
||||
toast.error("GitHub API Rate Limit Exceeded", {
|
||||
description: data.message,
|
||||
duration: 10000,
|
||||
});
|
||||
break;
|
||||
|
||||
case "resumed":
|
||||
// Rate limit reset notification
|
||||
toast.success("Rate Limit Reset", {
|
||||
description: "API operations have resumed.",
|
||||
duration: 5000,
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing rate limit event:", error);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
eventSource.close();
|
||||
};
|
||||
}, [user?.id]);
|
||||
|
||||
// Extract fetchDashboardData as a stable callback
|
||||
const fetchDashboardData = useCallback(async (showToast = false) => {
|
||||
try {
|
||||
@@ -193,7 +279,7 @@ export function Dashboard() {
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-16 w-full" />
|
||||
<Skeleton key={i} className="h-14 w-full" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -206,7 +292,7 @@ export function Dashboard() {
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-16 w-full" />
|
||||
<Skeleton key={i} className="h-14 w-full" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -236,30 +322,19 @@ export function Dashboard() {
|
||||
/>
|
||||
<StatusCard
|
||||
title="Last Sync"
|
||||
value={
|
||||
lastSync
|
||||
? new Date(lastSync).toLocaleString("en-US", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
year: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: "N/A"
|
||||
}
|
||||
value={formatLastSyncTime(lastSync)}
|
||||
icon={<Clock className="h-4 w-4" />}
|
||||
description="Last successful sync"
|
||||
description={formatFullTimestamp(lastSync)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-6 items-start">
|
||||
<div className="w-full lg:w-1/2">
|
||||
<RepositoryList repositories={repositories} />
|
||||
<RepositoryList repositories={repositories.slice(0, 8)} />
|
||||
</div>
|
||||
|
||||
<div className="w-full lg:w-1/2">
|
||||
{/* the api already sends 10 activities only but slicing in case of realtime updates */}
|
||||
<RecentActivity activities={activities.slice(0, 10)} />
|
||||
<RecentActivity activities={activities.slice(0, 8)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import type { MirrorJob } from "@/lib/db/schema";
|
||||
import { formatDate, getStatusColor } from "@/lib/utils";
|
||||
import { Button } from "../ui/button";
|
||||
import { Activity, Clock } from "lucide-react";
|
||||
|
||||
interface RecentActivityProps {
|
||||
activities: MirrorJob[];
|
||||
@@ -16,32 +17,46 @@ export function RecentActivity({ activities }: RecentActivityProps) {
|
||||
<a href="/activity">View All</a>
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="max-h-[300px] sm:max-h-[400px] lg:max-h-[calc(100dvh-22.5rem)] overflow-y-auto">
|
||||
<div className="flex flex-col divide-y divide-border">
|
||||
{activities.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">No recent activity</p>
|
||||
) : (
|
||||
activities.map((activity, index) => (
|
||||
<div key={index} className="flex items-start gap-x-4 py-4">
|
||||
<div className="relative mt-1">
|
||||
<CardContent>
|
||||
{activities.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-6 text-center">
|
||||
<Clock className="h-10 w-10 text-muted-foreground mb-4" />
|
||||
<h3 className="text-lg font-medium">No recent activity</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1 mb-4">
|
||||
Activity will appear here when you start mirroring repositories.
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<a href="/activity">
|
||||
<Activity className="h-3.5 w-3.5 mr-1.5" />
|
||||
View History
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col divide-y divide-border">
|
||||
{activities.map((activity, index) => (
|
||||
<div key={index} className="flex items-center gap-x-3 py-3.5">
|
||||
<div className="relative flex-shrink-0">
|
||||
<div
|
||||
className={`h-2 w-2 rounded-full ${getStatusColor(
|
||||
activity.status
|
||||
)}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<p className="text-sm font-medium leading-none break-words">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium">
|
||||
{activity.message}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
{formatDate(activity.timestamp)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -47,14 +47,13 @@ export function RepositoryList({ repositories }: RepositoryListProps) {
|
||||
|
||||
return (
|
||||
<Card className="w-full">
|
||||
{/* calculating the max height based non the other elements and sizing styles */}
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle>Repositories</CardTitle>
|
||||
<Button variant="outline" asChild>
|
||||
<a href="/repositories">View All</a>
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="max-h-[300px] sm:max-h-[400px] lg:max-h-[calc(100dvh-22.5rem)] overflow-y-auto">
|
||||
<CardContent>
|
||||
{repositories.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-6 text-center">
|
||||
<GitFork className="h-10 w-10 text-muted-foreground mb-4" />
|
||||
@@ -71,89 +70,80 @@ export function RepositoryList({ repositories }: RepositoryListProps) {
|
||||
{repositories.map((repo, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-x-4 py-4"
|
||||
className="flex items-center gap-x-3 py-3.5"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center flex-wrap gap-2">
|
||||
<h4 className="text-sm font-medium break-all">{repo.name}</h4>
|
||||
{repo.isPrivate && (
|
||||
<span className="rounded-full bg-muted px-2 py-0.5 text-xs">
|
||||
Private
|
||||
</span>
|
||||
)}
|
||||
{repo.isForked && (
|
||||
<span className="rounded-full bg-muted px-2 py-0.5 text-xs">
|
||||
Fork
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{repo.owner}
|
||||
</span>
|
||||
{repo.organization && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
• {repo.organization}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 sm:ml-auto">
|
||||
<div className="relative flex-shrink-0">
|
||||
<div
|
||||
className={`h-2 w-2 rounded-full ${getStatusColor(
|
||||
repo.status
|
||||
)}`}
|
||||
/>
|
||||
<span className="text-xs capitalize w-[3rem] sm:w-auto">
|
||||
{/* setting the minimum width to 3rem corresponding to the largest status (mirrored) so that all are left alligned */}
|
||||
{repo.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h4 className="text-sm font-medium truncate">{repo.name}</h4>
|
||||
{repo.isPrivate && (
|
||||
<span className="rounded-full bg-muted px-2 py-0.5 text-[10px]">
|
||||
Private
|
||||
</span>
|
||||
)}
|
||||
{repo.isForked && (
|
||||
<span className="rounded-full bg-muted px-2 py-0.5 text-[10px]">
|
||||
Fork
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 mt-1 text-xs text-muted-foreground">
|
||||
<span className="truncate">{repo.owner}</span>
|
||||
{repo.organization && (
|
||||
<>
|
||||
<span>/</span>
|
||||
<span className="truncate">{repo.organization}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span className={`inline-flex items-center rounded-full px-2.5 py-1 text-[10px] font-medium mr-2
|
||||
${repo.status === 'imported' ? 'bg-yellow-500/10 text-yellow-600 dark:text-yellow-400' :
|
||||
repo.status === 'mirrored' || repo.status === 'synced' ? 'bg-green-500/10 text-green-600 dark:text-green-400' :
|
||||
repo.status === 'mirroring' || repo.status === 'syncing' ? 'bg-blue-500/10 text-blue-600 dark:text-blue-400' :
|
||||
repo.status === 'failed' ? 'bg-red-500/10 text-red-600 dark:text-red-400' :
|
||||
'bg-muted text-muted-foreground'}`}>
|
||||
{repo.status}
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{(() => {
|
||||
const giteaUrl = getGiteaRepoUrl(repo);
|
||||
const giteaEnabled = giteaUrl && ['mirrored', 'synced'].includes(repo.status);
|
||||
|
||||
// Determine tooltip based on status and configuration
|
||||
let tooltip: string;
|
||||
if (!giteaConfig?.url) {
|
||||
tooltip = "Gitea not configured";
|
||||
} else if (repo.status === 'imported') {
|
||||
tooltip = "Repository not yet mirrored to Gitea";
|
||||
} else if (repo.status === 'failed') {
|
||||
tooltip = "Repository mirroring failed";
|
||||
} else if (repo.status === 'mirroring') {
|
||||
tooltip = "Repository is being mirrored to Gitea";
|
||||
} else if (giteaUrl) {
|
||||
tooltip = "View on Gitea";
|
||||
} else {
|
||||
tooltip = "Gitea repository not available";
|
||||
}
|
||||
|
||||
return giteaUrl ? (
|
||||
<Button variant="ghost" size="icon" asChild>
|
||||
return giteaEnabled ? (
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" asChild>
|
||||
<a
|
||||
href={giteaUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={tooltip}
|
||||
title="View on Gitea"
|
||||
>
|
||||
<SiGitea className="h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="ghost" size="icon" disabled title={tooltip}>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" disabled title="Not mirrored yet">
|
||||
<SiGitea className="h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
})()}
|
||||
<Button variant="ghost" size="icon" asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8" asChild>
|
||||
<a
|
||||
href={repo.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="View on GitHub"
|
||||
>
|
||||
<SiGithub className="h-4 w-4" />
|
||||
</a>
|
||||
>
|
||||
<SiGithub className="h-4 w-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { toast } from "sonner";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useLiveRefresh } from "@/hooks/useLiveRefresh";
|
||||
import { useConfigStatus } from "@/hooks/useConfigStatus";
|
||||
import { Menu, LogOut } from "lucide-react";
|
||||
import { Menu, LogOut, PanelRightOpen, PanelRightClose } from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -19,9 +19,12 @@ interface HeaderProps {
|
||||
currentPage?: "dashboard" | "repositories" | "organizations" | "configuration" | "activity-log";
|
||||
onNavigate?: (page: string) => void;
|
||||
onMenuClick: () => void;
|
||||
onToggleCollapse?: () => void;
|
||||
isSidebarCollapsed?: boolean;
|
||||
isSidebarOpen?: boolean;
|
||||
}
|
||||
|
||||
export function Header({ currentPage, onNavigate, onMenuClick }: HeaderProps) {
|
||||
export function Header({ currentPage, onNavigate, onMenuClick, onToggleCollapse, isSidebarCollapsed, isSidebarOpen }: HeaderProps) {
|
||||
const { user, logout, isLoading } = useAuth();
|
||||
const { isLiveEnabled, toggleLive } = useLiveRefresh();
|
||||
const { isFullyConfigured, isLoading: configLoading } = useConfigStatus();
|
||||
@@ -63,18 +66,38 @@ export function Header({ currentPage, onNavigate, onMenuClick }: HeaderProps) {
|
||||
return (
|
||||
<header className="border-b bg-background">
|
||||
<div className="flex h-[4.5rem] items-center justify-between px-4 sm:px-6">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Hamburger Menu Button - Mobile Only */}
|
||||
<div className="flex items-center lg:gap-12 md:gap-6 gap-4">
|
||||
{/* Sidebar Toggle - Mobile uses slide-in, Medium uses collapse */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="lg"
|
||||
className="lg:hidden"
|
||||
size="icon"
|
||||
className="md:hidden h-10 w-10"
|
||||
onClick={onMenuClick}
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
{isSidebarOpen ? (
|
||||
<PanelRightOpen className="h-5 w-5" />
|
||||
) : (
|
||||
<PanelRightClose className="h-5 w-5" />
|
||||
)}
|
||||
<span className="sr-only">Toggle menu</span>
|
||||
</Button>
|
||||
|
||||
{/* Sidebar Collapse Toggle - Only on medium screens (768px - 1280px) */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="hidden md:flex xl:hidden h-10 w-10"
|
||||
onClick={onToggleCollapse}
|
||||
title={isSidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
||||
>
|
||||
{isSidebarCollapsed ? (
|
||||
<PanelRightClose className="h-5 w-5" />
|
||||
) : (
|
||||
<PanelRightOpen className="h-5 w-5" />
|
||||
)}
|
||||
<span className="sr-only">Toggle sidebar</span>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
if (currentPage !== 'dashboard') {
|
||||
@@ -85,14 +108,9 @@ export function Header({ currentPage, onNavigate, onMenuClick }: HeaderProps) {
|
||||
className="flex items-center gap-2 py-1 hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<img
|
||||
src="/logo-light.svg"
|
||||
src="/logo.png"
|
||||
alt="Gitea Mirror Logo"
|
||||
className="h-6 w-6 dark:hidden"
|
||||
/>
|
||||
<img
|
||||
src="/logo-dark.svg"
|
||||
alt="Gitea Mirror Logo"
|
||||
className="h-6 w-6 hidden dark:block"
|
||||
className="h-5 w-6"
|
||||
/>
|
||||
<span className="text-xl font-bold hidden sm:inline">Gitea Mirror</span>
|
||||
</button>
|
||||
|
||||