Compare commits

...

9 Commits

Author SHA1 Message Date
Arunavo Ray
fcc92b8f27 added security audit 2025-11-08 07:49:33 +05:30
Arunavo Ray
749ad4a694 v3.9.0 2025-11-06 07:47:23 +05:30
ARUNAVO RAY
0f752acae5 Merge pull request #146 from RayLabsHQ/fix/issue-129-release-sort-order
fix: Ensure proper release ordering in Gitea mirrors (#129)
2025-11-06 07:44:12 +05:30
Arunavo Ray
652bd220c2 added missing favicon 2025-11-06 07:43:14 +05:30
Arunavo Ray
9f2eaaf04e fix: Ensure proper release ordering in Gitea mirrors (#129)
- Add 1-second delays between release creations to ensure distinct timestamps
- Prepend GitHub original publication date to release notes
- Improve logging to show chronological processing order
- Addresses Gitea API limitation where created_unix is always set to current time

Fixes #129
2025-11-05 20:57:33 +05:30
ARUNAVO RAY
63d3f0e86c Merge pull request #145 from z0xca/main
fix website install guide command
2025-11-05 11:53:22 +05:30
z0x
25e7d234ba Update clone command to use '&&' for chaining 2025-11-04 22:58:23 -05:00
ARUNAVO RAY
7a3f734728 Merge pull request #142 from RayLabsHQ/fix/issue-141-duplicate-issues-on-sync
fix: add metadata field to repositories table to prevent duplicate issues on sync
2025-10-31 08:51:34 +05:30
Arunavo Ray
d59a07a8c5 fix: add metadata field to repositories table to prevent duplicate issues on sync
Fixes #141

The repository metadata field was missing from the database schema, which
caused the metadata sync state (issues, PRs, releases, etc.) to not persist.
This resulted in duplicate issues being created every time a repository was
synced because the system couldn't track what had already been mirrored.

Changes:
- Added metadata text field to repositories table in schema
- Added metadata field to repositorySchema Zod validation
- Generated database migration 0008_serious_thena.sql

Root cause analysis:
1. Code tried to read/write repository.metadata to track mirrored components
2. The metadata field didn't exist in the database schema
3. On sync, metadataState.components.issues was always false
4. This triggered re-mirroring of all issues, creating duplicates

The fix ensures metadata state persists between mirrors and syncs, preventing
duplicate metadata (issues, PRs, releases) from being created in Gitea.
2025-10-30 10:58:48 +05:30
15 changed files with 2553 additions and 12 deletions

496
Security-Audit.md Normal file
View 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.

View File

@@ -0,0 +1 @@
ALTER TABLE `repositories` ADD `metadata` text;

File diff suppressed because it is too large Load Diff

View 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
}
]
}

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

3
public/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 216 KiB

View File

@@ -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())`),

View File

@@ -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)}`);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

3
www/public/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 KiB

After

Width:  |  Height:  |  Size: 330 KiB

View File

@@ -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>
);
}
}