Compare commits
9 Commits
v3.8.11
...
security-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcc92b8f27 | ||
|
|
749ad4a694 | ||
|
|
0f752acae5 | ||
|
|
652bd220c2 | ||
|
|
9f2eaaf04e | ||
|
|
63d3f0e86c | ||
|
|
25e7d234ba | ||
|
|
7a3f734728 | ||
|
|
d59a07a8c5 |
496
Security-Audit.md
Normal file
@@ -0,0 +1,496 @@
|
||||
Date: 2025-11-07Application: Gitea Mirror v3.9.0Branch: bun-v1.3.1Scope: Full application security review
|
||||
|
||||
---
|
||||
Executive Summary
|
||||
|
||||
A comprehensive security audit was conducted across all components of the Gitea Mirror application, including authentication, authorization,
|
||||
encryption, input validation, API endpoints, database operations, UI components, and external integrations. The review identified 23 security
|
||||
findings across multiple severity levels.
|
||||
|
||||
Critical Findings: 2
|
||||
|
||||
High Severity: 9
|
||||
|
||||
Medium Severity: 10
|
||||
|
||||
Low Severity: 2
|
||||
|
||||
Overall Security Posture: The application demonstrates strong foundational security practices (AES-256-GCM encryption, Drizzle ORM parameterized
|
||||
queries, React auto-escaping) but has critical authorization flaws that require immediate remediation.
|
||||
|
||||
---
|
||||
CRITICAL SEVERITY VULNERABILITIES
|
||||
|
||||
Vuln 1: Broken Object Level Authorization (BOLA) - Multiple Endpoints
|
||||
|
||||
Severity: CRITICALConfidence: 1.0Category: Authorization Bypass
|
||||
|
||||
Affected Files:
|
||||
- src/pages/api/config/index.ts:18
|
||||
- src/pages/api/job/mirror-repo.ts:16
|
||||
- src/pages/api/github/repositories.ts:11
|
||||
- src/pages/api/dashboard/index.ts:9
|
||||
- src/pages/api/activities/index.ts:8
|
||||
- src/pages/api/sync/repository.ts:15
|
||||
- src/pages/api/sync/organization.ts:14
|
||||
- src/pages/api/job/mirror-org.ts:14
|
||||
- src/pages/api/job/retry-repo.ts:18
|
||||
- src/pages/api/job/sync-repo.ts:11
|
||||
- src/pages/api/job/schedule-sync-repo.ts:13
|
||||
|
||||
Description: Multiple API endpoints accept a userId parameter from client requests without validating that the authenticated user matches the
|
||||
requested userId. This allows any authenticated user to access and manipulate any other user's data.
|
||||
|
||||
Exploit Scenario:
|
||||
POST /api/config/index
|
||||
Content-Type: application/json
|
||||
Cookie: better-auth-session=attacker-session
|
||||
|
||||
{
|
||||
"userId": "victim-user-id",
|
||||
"giteaConfig": {
|
||||
"token": "attacker-controlled-token"
|
||||
}
|
||||
}
|
||||
|
||||
Result: Attacker modifies victim's configuration, steals encrypted tokens, triggers mirror operations, or accesses activity logs.
|
||||
|
||||
Recommendation:
|
||||
export const POST: APIRoute = async (context) => {
|
||||
// 1. Validate authentication
|
||||
const { user, response } = await requireAuth(context);
|
||||
if (response) return response;
|
||||
|
||||
// 2. Use authenticated userId from session, NOT from request body
|
||||
const authenticatedUserId = user!.id;
|
||||
|
||||
// 3. Don't accept userId from client
|
||||
const body = await request.json();
|
||||
const { githubConfig, giteaConfig } = body;
|
||||
|
||||
// 4. Use session userId for all operations
|
||||
const config = await db
|
||||
.select()
|
||||
.from(configs)
|
||||
.where(eq(configs.userId, authenticatedUserId));
|
||||
}
|
||||
|
||||
---
|
||||
Vuln 2: Header Authentication Spoofing - src/lib/auth-header.ts:48
|
||||
|
||||
Severity: CRITICALConfidence: 0.95Category: Authentication Bypass
|
||||
|
||||
Description: The header authentication mechanism trusts HTTP headers (X-Authentik-Username, X-Authentik-Email) without proper validation. If the
|
||||
application is accessible without a properly configured reverse proxy, attackers can send these headers directly to impersonate any user.
|
||||
|
||||
Exploit Scenario:
|
||||
GET /api/config/index?userId=admin
|
||||
Host: gitea-mirror.example.com
|
||||
X-Authentik-Username: admin
|
||||
X-Authentik-Email: admin@example.com
|
||||
|
||||
Result: Attacker gains admin access without authentication.
|
||||
|
||||
Recommendation:
|
||||
// 1. Add trusted proxy IP validation
|
||||
const TRUSTED_PROXY_IPS = process.env.TRUSTED_PROXY_IPS?.split(',') || [];
|
||||
|
||||
export function extractUserFromHeaders(headers: Headers, remoteIp: string): UserInfo | null {
|
||||
// Validate request comes from trusted reverse proxy
|
||||
if (TRUSTED_PROXY_IPS.length > 0 && !TRUSTED_PROXY_IPS.includes(remoteIp)) {
|
||||
console.warn(`Header auth rejected: untrusted IP ${remoteIp}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. Add shared secret validation
|
||||
const authSecret = headers.get('X-Auth-Secret');
|
||||
if (authSecret !== process.env.HEADER_AUTH_SECRET) {
|
||||
console.warn('Header auth rejected: invalid secret');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Continue with header extraction...
|
||||
}
|
||||
|
||||
Additional Requirements:
|
||||
- Document that header auth MUST only be used behind a reverse proxy
|
||||
- Reverse proxy MUST strip X-Authentik-* headers from untrusted requests
|
||||
- Direct access MUST be blocked via firewall rules
|
||||
|
||||
---
|
||||
HIGH SEVERITY VULNERABILITIES
|
||||
|
||||
Vuln 3: Missing Authentication on Critical Endpoints
|
||||
|
||||
Severity: HIGHConfidence: 1.0Category: Authentication Bypass
|
||||
|
||||
Affected Endpoints:
|
||||
- /api/github/test-connection - Tests GitHub connection with user's token
|
||||
- /api/gitea/test-connection - Tests Gitea connection with user's token
|
||||
- /api/rate-limit/index - Exposes rate limit info
|
||||
- /api/events/index - SSE endpoint for events
|
||||
- /api/activities/cleanup - Deletes activities
|
||||
|
||||
Description: Several sensitive endpoints lack authentication checks entirely.
|
||||
|
||||
Exploit Scenario:
|
||||
POST /api/github/test-connection
|
||||
{"token": "stolen-token", "userId": "victim"}
|
||||
|
||||
Recommendation: Add requireAuth to all sensitive endpoints.
|
||||
|
||||
---
|
||||
Vuln 4: Token Exposure Through Config API - src/pages/api/config/index.ts:220
|
||||
|
||||
Severity: HIGHConfidence: 0.9Category: Sensitive Data Exposure
|
||||
|
||||
Description: The GET /api/config/index endpoint decrypts and returns GitHub/Gitea API tokens in plaintext. Combined with BOLA, any user can retrieve
|
||||
any other user's tokens.
|
||||
|
||||
Exploit Scenario:
|
||||
1. Attacker authenticates as User A
|
||||
2. Calls /api/config/index?userId=admin
|
||||
3. Receives admin's decrypted GitHub Personal Access Token
|
||||
4. Uses stolen token to access admin's repositories
|
||||
|
||||
Recommendation:
|
||||
// Never send full tokens - use masked versions
|
||||
if (githubConfig.token) {
|
||||
const decrypted = decrypt(githubConfig.token);
|
||||
githubConfig.token = maskToken(decrypted); // "ghp_****...last4"
|
||||
githubConfig.tokenSet = true;
|
||||
}
|
||||
|
||||
---
|
||||
Vuln 5: Static Salt in Key Derivation - src/lib/utils/encryption.ts:21
|
||||
|
||||
Severity: HIGHConfidence: 0.95Category: Cryptographic Weakness
|
||||
|
||||
Description: The key derivation function uses a static, hardcoded salt that reduces protection against precomputation attacks.
|
||||
|
||||
Exploit Scenario: If ENCRYPTION_SECRET is weak or compromised, the static salt makes brute-force attacks more efficient. Attackers can build targeted
|
||||
rainbow tables.
|
||||
|
||||
Recommendation:
|
||||
// Generate and store unique salt per installation
|
||||
const SALT_FILE = '/app/data/.encryption_salt';
|
||||
let installationSalt: Buffer;
|
||||
|
||||
if (fs.existsSync(SALT_FILE)) {
|
||||
installationSalt = Buffer.from(fs.readFileSync(SALT_FILE, 'utf8'), 'hex');
|
||||
} else {
|
||||
installationSalt = crypto.randomBytes(32);
|
||||
fs.writeFileSync(SALT_FILE, installationSalt.toString('hex'));
|
||||
fs.chmodSync(SALT_FILE, 0o600);
|
||||
}
|
||||
|
||||
return crypto.pbkdf2Sync(secret, installationSalt, ITERATIONS, KEY_LENGTH, 'sha256');
|
||||
|
||||
---
|
||||
Vuln 6: Weak Default Secret - src/lib/config.ts:22
|
||||
|
||||
Severity: HIGHConfidence: 0.90Category: Hardcoded Credentials
|
||||
|
||||
Description: The application uses a hardcoded default for BETTER_AUTH_SECRET when not set.
|
||||
|
||||
Exploit Scenario: Non-Docker deployments may use the weak default. Attackers can forge authentication tokens using the known default secret.
|
||||
|
||||
Recommendation:
|
||||
BETTER_AUTH_SECRET: (() => {
|
||||
const secret = process.env.BETTER_AUTH_SECRET;
|
||||
const weakDefaults = ["your-secret-key-change-this-in-production"];
|
||||
|
||||
if (!secret || weakDefaults.includes(secret)) {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
throw new Error("BETTER_AUTH_SECRET required. Generate with: openssl rand -base64 32");
|
||||
}
|
||||
return "dev-secret-minimum-32-chars";
|
||||
}
|
||||
|
||||
if (secret.length < 32) {
|
||||
throw new Error("BETTER_AUTH_SECRET must be at least 32 characters");
|
||||
}
|
||||
|
||||
return secret;
|
||||
})()
|
||||
|
||||
---
|
||||
Vuln 7: Unencrypted OAuth Client Secrets - src/lib/db/schema.ts:569
|
||||
|
||||
Severity: HIGHConfidence: 0.92Category: Sensitive Data Exposure
|
||||
|
||||
Description: OAuth application client secrets and SSO provider credentials are stored as plaintext, inconsistent with encrypted GitHub/Gitea tokens.
|
||||
|
||||
Exploit Scenario: Database leak exposes OAuth client secrets, allowing attackers to impersonate the application to external OAuth providers.
|
||||
|
||||
Recommendation:
|
||||
// Encrypt before storage
|
||||
import { encrypt } from "@/lib/utils/encryption";
|
||||
|
||||
await db.insert(oauthApplications).values({
|
||||
clientSecret: encrypt(clientSecret),
|
||||
// ...
|
||||
});
|
||||
|
||||
// Add decryption helper
|
||||
export function decryptOAuthClientSecret(encrypted: string): string {
|
||||
return decrypt(encrypted);
|
||||
}
|
||||
|
||||
---
|
||||
Vuln 8: SSRF in OIDC Discovery - src/pages/api/sso/discover.ts:48
|
||||
|
||||
Severity: HIGHConfidence: 0.95Category: Server-Side Request Forgery
|
||||
|
||||
Description: The endpoint accepts user-provided issuer URL and fetches ${issuer}/.well-known/openid-configuration without validating against internal
|
||||
networks.
|
||||
|
||||
Exploit Scenario:
|
||||
POST /api/sso/discover
|
||||
{"issuer": "http://169.254.169.254/latest/meta-data"}
|
||||
|
||||
Result: Access to AWS metadata service, internal APIs, or port scanning.
|
||||
|
||||
Recommendation:
|
||||
// Block private IPs and localhost
|
||||
const ALLOWED_PROTOCOLS = ['https:'];
|
||||
const hostname = parsedIssuer.hostname.toLowerCase();
|
||||
|
||||
if (
|
||||
!ALLOWED_PROTOCOLS.includes(parsedIssuer.protocol) ||
|
||||
hostname === 'localhost' ||
|
||||
hostname.match(/^10\./) ||
|
||||
hostname.match(/^192\.168\./) ||
|
||||
hostname.match(/^172\.(1[6-9]|2[0-9]|3[0-1])\./) ||
|
||||
hostname.match(/^169\.254\./) ||
|
||||
hostname.match(/^127\./)
|
||||
) {
|
||||
return new Response(JSON.stringify({ error: "Invalid issuer" }), { status: 400 });
|
||||
}
|
||||
|
||||
---
|
||||
Vuln 9: SSRF in Gitea Connection Test - src/pages/api/gitea/test-connection.ts:29
|
||||
|
||||
Severity: HIGHConfidence: 0.95Category: SSRF / Input Validation
|
||||
|
||||
Description: Accepts user-provided url and makes HTTP request without validation.
|
||||
|
||||
Exploit Scenario: Same as Vuln 8 - internal network scanning, metadata service access.
|
||||
|
||||
Recommendation: Apply same URL validation as Vuln 8.
|
||||
|
||||
---
|
||||
Vuln 10: Command Injection in Docker Build - scripts/build-docker.sh:72
|
||||
|
||||
Severity: HIGHConfidence: 0.95Category: Command Injection
|
||||
|
||||
Description: Uses eval with dynamically constructed Docker command including environment variables.
|
||||
|
||||
Exploit Scenario:
|
||||
DOCKER_IMAGE='test; rm -rf /; #'
|
||||
./scripts/build-docker.sh
|
||||
|
||||
Recommendation:
|
||||
# Replace eval with direct execution
|
||||
if docker buildx build --platform linux/amd64,linux/arm64 \
|
||||
-t "$FULL_IMAGE_NAME" \
|
||||
${LOAD:+--load} \
|
||||
${PUSH:+--push} \
|
||||
.; then
|
||||
echo "Success"
|
||||
fi
|
||||
|
||||
---
|
||||
Vuln 11: Path Traversal in Gitea API - src/lib/gitea.ts:193
|
||||
|
||||
Severity: MEDIUM-HIGHConfidence: 0.85Category: Path Traversal / SSRF
|
||||
|
||||
Description: Repository owner/name interpolated into URLs without encoding.
|
||||
|
||||
Exploit Scenario:
|
||||
owner = "../../admin"
|
||||
repoName = "users/../../../config"
|
||||
// Results in: /api/v1/repos/../../admin/users/../../../config
|
||||
|
||||
Recommendation:
|
||||
const response = await fetch(
|
||||
`${config.url}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}`,
|
||||
// ...
|
||||
);
|
||||
|
||||
Apply to all URL constructions in gitea.ts and gitea-enhanced.ts.
|
||||
|
||||
---
|
||||
MEDIUM SEVERITY VULNERABILITIES
|
||||
|
||||
Vuln 12: Weak Session Configuration - src/lib/auth.ts:105
|
||||
|
||||
Severity: MEDIUMConfidence: 0.85
|
||||
|
||||
Description: 30-day session expiration is excessive; missing explicit security flags.
|
||||
|
||||
Recommendation:
|
||||
session: {
|
||||
expiresIn: 60 * 60 * 24 * 7, // 7 days max
|
||||
cookieOptions: {
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
httpOnly: true,
|
||||
},
|
||||
}
|
||||
|
||||
---
|
||||
Vuln 13: Missing CSRF Protection
|
||||
|
||||
Severity: MEDIUMConfidence: 0.80
|
||||
|
||||
Description: No visible CSRF token validation on state-changing operations.
|
||||
|
||||
Recommendation: Verify Better Auth CSRF protection is enabled; implement explicit tokens if needed.
|
||||
|
||||
---
|
||||
Vuln 14: Weak Random Number Generation - src/lib/utils.ts:12
|
||||
|
||||
Severity: MEDIUMConfidence: 0.85
|
||||
|
||||
Description: generateRandomString() uses Math.random() (not cryptographically secure) for OAuth client credentials.
|
||||
|
||||
Recommendation:
|
||||
// Use existing secure function
|
||||
const clientId = `client_${generateSecureToken(16)}`;
|
||||
const clientSecret = `secret_${generateSecureToken(24)}`;
|
||||
|
||||
---
|
||||
Vuln 15-18: Input Validation Issues
|
||||
|
||||
Severity: MEDIUMConfidence: 0.85
|
||||
|
||||
Affected:
|
||||
- src/pages/api/repositories/[id].ts:24 - destinationOrg not validated
|
||||
- src/pages/api/organizations/[id].ts:24 - destinationOrg not validated
|
||||
- src/pages/api/job/mirror-repo.ts:19 - repositoryIds array not validated
|
||||
- src/lib/helpers.ts:49 - Repository names in messages not sanitized
|
||||
|
||||
Recommendation: Use Zod schemas for all user inputs:
|
||||
const updateRepoSchema = z.object({
|
||||
destinationOrg: z.string()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.regex(/^[a-zA-Z0-9\-_\.]+$/)
|
||||
.nullable()
|
||||
.optional()
|
||||
});
|
||||
|
||||
---
|
||||
LOW SEVERITY VULNERABILITIES
|
||||
|
||||
Vuln 19: XSS via Astro set:html - src/pages/docs/quickstart.astro:102
|
||||
|
||||
Severity: LOWConfidence: 0.85
|
||||
|
||||
Description: Uses set:html with static data; risky pattern if copied with dynamic content.
|
||||
|
||||
Recommendation:
|
||||
<p class="text-sm" set:text={item.text}></p>
|
||||
|
||||
---
|
||||
Vuln 20: Open Redirect in OAuth - src/components/oauth/ConsentPage.tsx:118
|
||||
|
||||
Severity: LOWConfidence: 0.90
|
||||
|
||||
Description: OAuth redirect uses window.location.href without explicit protocol validation.
|
||||
|
||||
Recommendation:
|
||||
if (!['http:', 'https:'].includes(url.protocol)) {
|
||||
throw new Error('Invalid protocol');
|
||||
}
|
||||
window.location.assign(url.toString());
|
||||
|
||||
---
|
||||
POSITIVE SECURITY FINDINGS ✅
|
||||
|
||||
The codebase demonstrates excellent security practices:
|
||||
|
||||
1. SQL Injection Protection - Drizzle ORM with parameterized queries throughout
|
||||
2. Token Encryption - AES-256-GCM for GitHub/Gitea tokens
|
||||
3. XSS Protection - React auto-escaping, no dangerouslySetInnerHTML found
|
||||
4. Password Hashing - bcrypt via Better Auth
|
||||
5. Secure Random - crypto.randomBytes() in encryption functions
|
||||
6. Input Validation - Comprehensive Zod schemas
|
||||
7. Safe Path Operations - path.join() with proper base directories
|
||||
8. No Command Execution - TypeScript codebase doesn't use child_process
|
||||
9. Authentication Framework - Better Auth with session management
|
||||
10. Type Safety - TypeScript strict mode
|
||||
|
||||
---
|
||||
REMEDIATION PRIORITY
|
||||
|
||||
Phase 1: EMERGENCY (Deploy Today)
|
||||
|
||||
1. Fix BOLA vulnerabilities - Add authentication and userId validation (Vuln 1)
|
||||
2. Disable or secure header auth - Add IP whitelist validation (Vuln 2)
|
||||
3. Add authentication checks - Protect unprotected endpoints (Vuln 3)
|
||||
|
||||
Phase 2: URGENT (This Week)
|
||||
|
||||
1. Implement token masking - Don't return decrypted tokens (Vuln 4)
|
||||
2. Fix SSRF vulnerabilities - Add URL validation (Vuln 8, 9)
|
||||
3. Fix command injection - Remove eval from build script (Vuln 10)
|
||||
4. Add URL encoding - Path traversal fix (Vuln 11)
|
||||
|
||||
Phase 3: HIGH (This Sprint)
|
||||
|
||||
1. Fix KDF salt - Generate unique salt per installation (Vuln 5)
|
||||
2. Enforce strong secrets - Fail if weak defaults used (Vuln 6)
|
||||
3. Encrypt OAuth secrets - Match GitHub/Gitea token encryption (Vuln 7)
|
||||
4. Reduce session expiration - 7 days maximum (Vuln 12)
|
||||
5. Add CSRF protection - Explicit tokens (Vuln 13)
|
||||
|
||||
Phase 4: MEDIUM (Next Sprint)
|
||||
|
||||
1. Input validation - Zod schemas for all endpoints (Vuln 14-18)
|
||||
2. Fix weak RNG - Use crypto.randomBytes (Vuln 14)
|
||||
|
||||
Phase 5: LOW (Backlog)
|
||||
|
||||
1. Remove set:html - Use safer alternatives (Vuln 19)
|
||||
2. OAuth redirect validation - Add protocol check (Vuln 20)
|
||||
|
||||
---
|
||||
TESTING RECOMMENDATIONS
|
||||
|
||||
Proof of Concept Tests
|
||||
|
||||
# Test BOLA
|
||||
curl -X POST http://localhost:4321/api/config/index \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Cookie: better-auth-session=<attacker-session>" \
|
||||
-d '{"userId":"victim-id","githubConfig":{...}}'
|
||||
|
||||
# Test header spoofing
|
||||
curl http://localhost:4321/api/config/index?userId=admin \
|
||||
-H "X-Authentik-Username: admin" \
|
||||
-H "X-Authentik-Email: admin@example.com"
|
||||
|
||||
# Test SSRF
|
||||
curl -X POST http://localhost:4321/api/sso/discover \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"issuer":"http://169.254.169.254/latest/meta-data"}'
|
||||
|
||||
---
|
||||
SECURITY GRADE
|
||||
|
||||
Overall Security Grade: C+
|
||||
- Would be B+ after Phase 1-2 fixes
|
||||
- Would be A- after all fixes
|
||||
|
||||
Critical Issues: 2 (authorization bypass, auth spoofing)Risk Level: HIGH due to BOLA allowing complete account takeover
|
||||
|
||||
---
|
||||
CONCLUSION
|
||||
|
||||
The Gitea Mirror application has a strong security foundation with proper encryption, ORM usage, and XSS protection. However, critical authorization
|
||||
flaws in API endpoints allow authenticated users to access any other user's data. These must be fixed immediately before production deployment.
|
||||
|
||||
The development team has clearly prioritized security (encryption, input validation, type safety), and the identified issues are fixable with
|
||||
moderate effort. After remediation, this will be a secure, production-ready application.
|
||||
1
drizzle/0008_serious_thena.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE `repositories` ADD `metadata` text;
|
||||
2006
drizzle/meta/0008_snapshot.json
Normal file
@@ -57,6 +57,13 @@
|
||||
"when": 1761534391115,
|
||||
"tag": "0007_whole_hellion",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 8,
|
||||
"version": "6",
|
||||
"when": 1761802056073,
|
||||
"tag": "0008_serious_thena",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "gitea-mirror",
|
||||
"type": "module",
|
||||
"version": "3.8.11",
|
||||
"version": "3.9.0",
|
||||
"engines": {
|
||||
"bun": ">=1.2.9"
|
||||
},
|
||||
|
||||
BIN
public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
3
public/favicon.svg
Normal file
|
After Width: | Height: | Size: 216 KiB |
@@ -164,6 +164,7 @@ export const repositorySchema = z.object({
|
||||
lastMirrored: z.coerce.date().optional().nullable(),
|
||||
errorMessage: z.string().optional().nullable(),
|
||||
destinationOrg: z.string().optional().nullable(),
|
||||
metadata: z.string().optional().nullable(), // JSON string for metadata sync state
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
});
|
||||
@@ -376,6 +377,8 @@ export const repositories = sqliteTable("repositories", {
|
||||
|
||||
destinationOrg: text("destination_org"),
|
||||
|
||||
metadata: text("metadata"), // JSON string storing metadata sync state (issues, PRs, releases, etc.)
|
||||
|
||||
createdAt: integer("created_at", { mode: "timestamp" })
|
||||
.notNull()
|
||||
.default(sql`(unixepoch())`),
|
||||
|
||||
@@ -2005,6 +2005,12 @@ export async function mirrorGitHubReleasesToGitea({
|
||||
.slice(0, releaseLimit)
|
||||
.sort((a, b) => getReleaseTimestamp(a) - getReleaseTimestamp(b));
|
||||
|
||||
console.log(`[Releases] Processing ${releasesToProcess.length} releases in chronological order (oldest to newest)`);
|
||||
releasesToProcess.forEach((rel, idx) => {
|
||||
const date = new Date(rel.published_at || rel.created_at);
|
||||
console.log(`[Releases] ${idx + 1}. ${rel.tag_name} - Originally published: ${date.toISOString()}`);
|
||||
});
|
||||
|
||||
for (const release of releasesToProcess) {
|
||||
try {
|
||||
// Check if release already exists
|
||||
@@ -2015,8 +2021,14 @@ export async function mirrorGitHubReleasesToGitea({
|
||||
}
|
||||
).catch(() => null);
|
||||
|
||||
const releaseNote = release.body || "";
|
||||
|
||||
// Prepare release body with GitHub original date header
|
||||
const githubPublishedDate = release.published_at || release.created_at;
|
||||
const githubDateHeader = githubPublishedDate
|
||||
? `> 📅 **Originally published on GitHub:** ${new Date(githubPublishedDate).toUTCString()}\n\n`
|
||||
: '';
|
||||
const originalReleaseNote = release.body || "";
|
||||
const releaseNote = githubDateHeader + originalReleaseNote;
|
||||
|
||||
if (existingReleasesResponse) {
|
||||
// Update existing release if the changelog/body differs
|
||||
const existingRelease = existingReleasesResponse.data;
|
||||
@@ -2039,9 +2051,11 @@ export async function mirrorGitHubReleasesToGitea({
|
||||
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
|
||||
}
|
||||
);
|
||||
|
||||
if (releaseNote) {
|
||||
console.log(`[Releases] Updated changelog for ${release.tag_name} (${releaseNote.length} characters)`);
|
||||
|
||||
if (originalReleaseNote) {
|
||||
console.log(`[Releases] Updated changelog for ${release.tag_name} (${originalReleaseNote.length} characters + GitHub date header)`);
|
||||
} else {
|
||||
console.log(`[Releases] Updated release ${release.tag_name} with GitHub date header`);
|
||||
}
|
||||
mirroredCount++;
|
||||
} else {
|
||||
@@ -2051,9 +2065,11 @@ export async function mirrorGitHubReleasesToGitea({
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create new release with changelog/body content
|
||||
if (releaseNote) {
|
||||
console.log(`[Releases] Including changelog for ${release.tag_name} (${releaseNote.length} characters)`);
|
||||
// Create new release with changelog/body content (includes GitHub date header)
|
||||
if (originalReleaseNote) {
|
||||
console.log(`[Releases] Including changelog for ${release.tag_name} (${originalReleaseNote.length} characters + GitHub date header)`);
|
||||
} else {
|
||||
console.log(`[Releases] Creating release ${release.tag_name} with GitHub date header (no changelog)`);
|
||||
}
|
||||
|
||||
const createReleaseResponse = await httpPost(
|
||||
@@ -2121,8 +2137,14 @@ export async function mirrorGitHubReleasesToGitea({
|
||||
}
|
||||
|
||||
mirroredCount++;
|
||||
const noteInfo = releaseNote ? ` with ${releaseNote.length} character changelog` : " without changelog";
|
||||
const noteInfo = originalReleaseNote ? ` with ${originalReleaseNote.length} character changelog` : " without changelog";
|
||||
console.log(`[Releases] Successfully mirrored release: ${release.tag_name}${noteInfo}`);
|
||||
|
||||
// Add delay to ensure proper timestamp ordering in Gitea
|
||||
// Gitea sorts releases by created_unix DESC, and all releases created in quick succession
|
||||
// will have nearly identical timestamps. The 1-second delay ensures proper chronological order.
|
||||
console.log(`[Releases] Waiting 1 second to ensure proper timestamp ordering in Gitea...`);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
} catch (error) {
|
||||
console.error(`[Releases] Failed to mirror release ${release.tag_name}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
|
||||
BIN
www/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
3
www/public/favicon.svg
Normal file
|
After Width: | Height: | Size: 216 KiB |
|
Before Width: | Height: | Size: 338 KiB After Width: | Height: | Size: 330 KiB |
@@ -22,7 +22,7 @@ export function Installation() {
|
||||
steps: [
|
||||
{
|
||||
title: "Clone the repository",
|
||||
command: "git clone https://github.com/RayLabsHQ/gitea-mirror.git\ncd gitea-mirror",
|
||||
command: "git clone https://github.com/RayLabsHQ/gitea-mirror.git && cd gitea-mirror",
|
||||
id: "docker-clone"
|
||||
},
|
||||
{
|
||||
@@ -166,4 +166,4 @@ export function Installation() {
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||