mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-09 13:06:45 +03:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d2919717f | ||
|
|
1e06e2bd4b | ||
|
|
67080a7ce9 | ||
|
|
9d5db86bdf | ||
|
|
3458891511 | ||
|
|
d388f2e691 | ||
|
|
7bd862606b | ||
|
|
251baeb1aa | ||
|
|
e6a31512ac |
8
.claude/settings.local.json
Normal file
8
.claude/settings.local.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(docker build:*)"
|
||||||
|
],
|
||||||
|
"deny": []
|
||||||
|
}
|
||||||
|
}
|
||||||
102
CLAUDE.md
102
CLAUDE.md
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
DONT HALLUCIATE THINGS. IF YOU DONT KNOW LOOK AT THE CODE OR ASK FOR DOCS
|
||||||
|
|
||||||
## Project Overview
|
## 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 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.
|
||||||
@@ -40,7 +42,7 @@ bun run start # Start production server
|
|||||||
- **Frontend**: Astro (SSR) + React + Tailwind CSS v4 + Shadcn UI
|
- **Frontend**: Astro (SSR) + React + Tailwind CSS v4 + Shadcn UI
|
||||||
- **Backend**: Bun runtime + SQLite + Drizzle ORM
|
- **Backend**: Bun runtime + SQLite + Drizzle ORM
|
||||||
- **APIs**: GitHub (Octokit) and Gitea APIs
|
- **APIs**: GitHub (Octokit) and Gitea APIs
|
||||||
- **Auth**: JWT tokens with bcryptjs password hashing
|
- **Auth**: Better Auth with email/password, SSO, and OIDC provider support
|
||||||
|
|
||||||
### Project Structure
|
### Project Structure
|
||||||
- `/src/pages/api/` - API endpoints (Astro API routes)
|
- `/src/pages/api/` - API endpoints (Astro API routes)
|
||||||
@@ -68,10 +70,15 @@ export async function POST({ request }: APIContext) {
|
|||||||
|
|
||||||
3. **Real-time Updates**: Server-Sent Events (SSE) endpoint at `/api/events` for live dashboard updates
|
3. **Real-time Updates**: Server-Sent Events (SSE) endpoint at `/api/events` for live dashboard updates
|
||||||
|
|
||||||
4. **Authentication Flow**:
|
4. **Authentication System**:
|
||||||
|
- Built on Better Auth library
|
||||||
|
- Three authentication methods:
|
||||||
|
- Email & Password (traditional auth)
|
||||||
|
- SSO (authenticate via external OIDC providers)
|
||||||
|
- OIDC Provider (act as OIDC provider for other apps)
|
||||||
|
- Session-based authentication with secure cookies
|
||||||
- First user signup creates admin account
|
- First user signup creates admin account
|
||||||
- JWT tokens stored in cookies
|
- Protected routes use Better Auth session validation
|
||||||
- Protected routes check auth via `getUserFromCookie()`
|
|
||||||
|
|
||||||
5. **Mirror Process**:
|
5. **Mirror Process**:
|
||||||
- Discovers repos from GitHub (user/org)
|
- Discovers repos from GitHub (user/org)
|
||||||
@@ -79,11 +86,18 @@ export async function POST({ request }: APIContext) {
|
|||||||
- Tracks status in database
|
- Tracks status in database
|
||||||
- Supports scheduled automatic mirroring
|
- Supports scheduled automatic mirroring
|
||||||
|
|
||||||
6. **Mirror Strategies**: Three ways to organize repositories in Gitea:
|
6. **Mirror Strategies**: Four ways to organize repositories in Gitea:
|
||||||
- **preserve**: Maintains GitHub structure (default)
|
- **preserve**: Maintains GitHub structure (default)
|
||||||
|
- Organization repos → Same organization name in Gitea
|
||||||
|
- Personal repos → Under your Gitea username
|
||||||
- **single-org**: All repos go to one organization
|
- **single-org**: All repos go to one organization
|
||||||
|
- All repos → Single configured organization
|
||||||
- **flat-user**: All repos go under user account
|
- **flat-user**: All repos go under user account
|
||||||
- Starred repos always go to separate organization (starredReposOrg)
|
- All repos → Under your Gitea username
|
||||||
|
- **mixed**: Hybrid approach
|
||||||
|
- Organization repos → Preserve structure
|
||||||
|
- Personal repos → Single configured organization
|
||||||
|
- Starred repos always go to separate organization (starredReposOrg, default: "starred")
|
||||||
- Routing logic in `getGiteaRepoOwner()` function
|
- Routing logic in `getGiteaRepoOwner()` function
|
||||||
|
|
||||||
### Database Schema (SQLite)
|
### Database Schema (SQLite)
|
||||||
@@ -102,11 +116,18 @@ export async function POST({ request }: APIContext) {
|
|||||||
|
|
||||||
### Development Tips
|
### Development Tips
|
||||||
- Environment variables in `.env` (copy from `.env.example`)
|
- Environment variables in `.env` (copy from `.env.example`)
|
||||||
- JWT_SECRET auto-generated if not provided
|
- BETTER_AUTH_SECRET required for session signing
|
||||||
- Database auto-initializes on first run
|
- Database auto-initializes on first run
|
||||||
- Use `bun run dev:clean` for fresh database start
|
- Use `bun run dev:clean` for fresh database start
|
||||||
- Tailwind CSS v4 configured with Vite plugin
|
- Tailwind CSS v4 configured with Vite plugin
|
||||||
|
|
||||||
|
### Authentication Setup
|
||||||
|
- **Better Auth** handles all authentication
|
||||||
|
- Configuration in `/src/lib/auth.ts` (server) and `/src/lib/auth-client.ts` (client)
|
||||||
|
- Auth endpoints available at `/api/auth/*`
|
||||||
|
- SSO providers configured through the web UI
|
||||||
|
- OIDC provider functionality for external applications
|
||||||
|
|
||||||
### Common Tasks
|
### Common Tasks
|
||||||
|
|
||||||
**Adding a new API endpoint:**
|
**Adding a new API endpoint:**
|
||||||
@@ -125,6 +146,73 @@ export async function POST({ request }: APIContext) {
|
|||||||
2. Run `bun run init-db` to recreate database
|
2. Run `bun run init-db` to recreate database
|
||||||
3. Update related queries in `/src/lib/db/queries/`
|
3. Update related queries in `/src/lib/db/queries/`
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
### GitHub Configuration (UI Fields)
|
||||||
|
|
||||||
|
#### Basic Settings (`githubConfig`)
|
||||||
|
- **username**: GitHub username
|
||||||
|
- **token**: GitHub personal access token (requires repo and admin:org scopes)
|
||||||
|
- **privateRepositories**: Include private repositories
|
||||||
|
- **mirrorStarred**: Mirror starred repositories
|
||||||
|
|
||||||
|
### Gitea Configuration (UI Fields)
|
||||||
|
- **url**: Gitea instance URL
|
||||||
|
- **username**: Gitea username
|
||||||
|
- **token**: Gitea access token
|
||||||
|
- **organization**: Destination organization (for single-org/mixed strategies)
|
||||||
|
- **starredReposOrg**: Organization for starred repositories (default: "starred")
|
||||||
|
- **visibility**: Organization visibility - "public", "private", "limited"
|
||||||
|
- **mirrorStrategy**: Repository organization strategy (set via UI)
|
||||||
|
- **preserveOrgStructure**: Automatically set based on mirrorStrategy
|
||||||
|
|
||||||
|
### Schedule Configuration (`scheduleConfig`)
|
||||||
|
- **enabled**: Enable automatic mirroring (default: false)
|
||||||
|
- **interval**: Cron expression or seconds (default: "0 2 * * *" - 2 AM daily)
|
||||||
|
- **concurrent**: Allow concurrent mirror operations (default: false)
|
||||||
|
- **batchSize**: Number of repos to process in parallel (default: 10)
|
||||||
|
|
||||||
|
### Database Cleanup Configuration (`cleanupConfig`)
|
||||||
|
- **enabled**: Enable automatic cleanup (default: false)
|
||||||
|
- **retentionDays**: Days to keep events (stored as seconds internally)
|
||||||
|
|
||||||
|
### Mirror Options (UI Fields)
|
||||||
|
- **mirrorReleases**: Mirror GitHub releases to Gitea
|
||||||
|
- **mirrorMetadata**: Enable metadata mirroring (master toggle)
|
||||||
|
- **metadataComponents** (only available when mirrorMetadata is enabled):
|
||||||
|
- **issues**: Mirror issues
|
||||||
|
- **pullRequests**: Mirror pull requests
|
||||||
|
- **labels**: Mirror labels
|
||||||
|
- **milestones**: Mirror milestones
|
||||||
|
- **wiki**: Mirror wiki content
|
||||||
|
|
||||||
|
### Advanced Options (UI Fields)
|
||||||
|
- **skipForks**: Skip forked repositories (default: false)
|
||||||
|
- **skipStarredIssues**: Skip issues for starred repositories (default: false) - enables "Lightweight mode" for starred repos
|
||||||
|
|
||||||
|
### Authentication Configuration
|
||||||
|
|
||||||
|
#### SSO Provider Configuration
|
||||||
|
- **issuerUrl**: OIDC issuer URL (e.g., https://accounts.google.com)
|
||||||
|
- **domain**: Email domain for this provider
|
||||||
|
- **providerId**: Unique identifier for the provider
|
||||||
|
- **clientId**: OAuth client ID from provider
|
||||||
|
- **clientSecret**: OAuth client secret from provider
|
||||||
|
- **authorizationEndpoint**: OAuth authorization URL (auto-discovered if supported)
|
||||||
|
- **tokenEndpoint**: OAuth token exchange URL (auto-discovered if supported)
|
||||||
|
- **jwksEndpoint**: JSON Web Key Set URL (optional, auto-discovered)
|
||||||
|
- **userInfoEndpoint**: User information endpoint (optional, auto-discovered)
|
||||||
|
|
||||||
|
#### OIDC Provider Settings (for external apps)
|
||||||
|
- **allowedRedirectUris**: Comma-separated list of allowed redirect URIs
|
||||||
|
- **clientId**: Generated client ID for the application
|
||||||
|
- **clientSecret**: Generated client secret for the application
|
||||||
|
- **scopes**: Available scopes (openid, profile, email)
|
||||||
|
|
||||||
|
#### Environment Variables
|
||||||
|
- **BETTER_AUTH_SECRET**: Secret key for signing sessions (required)
|
||||||
|
- **BETTER_AUTH_URL**: Base URL for authentication (default: http://localhost:4321)
|
||||||
|
|
||||||
## Security Guidelines
|
## Security Guidelines
|
||||||
|
|
||||||
- **Confidentiality Guidelines**:
|
- **Confidentiality Guidelines**:
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> **Upgrading to v3?** Please read the [Migration Guide](MIGRATION_GUIDE.md) for breaking changes and upgrade instructions.
|
> **Upgrading to v3?** v3 requires a fresh start with a new data volume. Please read the [Upgrade Guide](UPGRADE.md) for instructions.
|
||||||
|
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
@@ -35,7 +35,7 @@ First user signup becomes admin. Configure GitHub and Gitea through the web inte
|
|||||||
- 🔁 Mirror public, private, and starred GitHub repos to Gitea
|
- 🔁 Mirror public, private, and starred GitHub repos to Gitea
|
||||||
- 🏢 Mirror entire organizations with flexible strategies
|
- 🏢 Mirror entire organizations with flexible strategies
|
||||||
- 🎯 Custom destination control for repos and organizations
|
- 🎯 Custom destination control for repos and organizations
|
||||||
- 🔐 Secure authentication with JWT tokens
|
- 🔐 Secure authentication with Better Auth (email/password, SSO, OIDC)
|
||||||
- 📊 Real-time dashboard with activity logs
|
- 📊 Real-time dashboard with activity logs
|
||||||
- ⏱️ Scheduled automatic mirroring
|
- ⏱️ Scheduled automatic mirroring
|
||||||
- 🐳 Dockerized with multi-arch support (AMD64/ARM64)
|
- 🐳 Dockerized with multi-arch support (AMD64/ARM64)
|
||||||
|
|||||||
74
UPGRADE.md
Normal file
74
UPGRADE.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# Upgrade Guide
|
||||||
|
|
||||||
|
## Upgrading to v3.0
|
||||||
|
|
||||||
|
> **⚠️ IMPORTANT**: v3.0 requires a fresh start. There is no automated migration from v2.x to v3.0.
|
||||||
|
|
||||||
|
### Why No Migration?
|
||||||
|
|
||||||
|
v3.0 introduces fundamental changes to the application architecture:
|
||||||
|
- **Authentication**: Switched from JWT to Better Auth
|
||||||
|
- **Database**: Now uses Drizzle ORM with proper migrations
|
||||||
|
- **Security**: All tokens are now encrypted
|
||||||
|
- **Features**: Added SSO support and OIDC provider functionality
|
||||||
|
|
||||||
|
Due to these extensive changes, we recommend starting fresh with v3.0 for the best experience.
|
||||||
|
|
||||||
|
### Upgrade Steps
|
||||||
|
|
||||||
|
1. **Stop your v2.x container**
|
||||||
|
```bash
|
||||||
|
docker stop gitea-mirror
|
||||||
|
docker rm gitea-mirror
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Backup your v2.x data (optional)**
|
||||||
|
```bash
|
||||||
|
# If you want to keep your v2 data for reference
|
||||||
|
docker run --rm -v gitea-mirror-data:/data -v $(pwd):/backup alpine tar czf /backup/gitea-mirror-v2-backup.tar.gz -C /data .
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Create a new volume for v3**
|
||||||
|
```bash
|
||||||
|
docker volume create gitea-mirror-v3-data
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Run v3 with the new volume**
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name gitea-mirror \
|
||||||
|
-p 4321:4321 \
|
||||||
|
-v gitea-mirror-v3-data:/app/data \
|
||||||
|
-e BETTER_AUTH_SECRET=your-secret-key \
|
||||||
|
-e ENCRYPTION_SECRET=your-encryption-key \
|
||||||
|
arunavo4/gitea-mirror:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Set up your configuration again**
|
||||||
|
- Navigate to http://localhost:4321
|
||||||
|
- Create a new admin account
|
||||||
|
- Re-enter your GitHub and Gitea credentials
|
||||||
|
- Configure your mirror settings
|
||||||
|
|
||||||
|
### What Happens to My Existing Mirrors?
|
||||||
|
|
||||||
|
Your existing mirrors in Gitea are **not affected**. The application will:
|
||||||
|
- Recognize existing repositories when you re-import
|
||||||
|
- Skip creating duplicates
|
||||||
|
- Resume normal mirror operations
|
||||||
|
|
||||||
|
### Environment Variable Changes
|
||||||
|
|
||||||
|
v3.0 uses different environment variables:
|
||||||
|
|
||||||
|
| v2.x | v3.0 | Notes |
|
||||||
|
|------|------|-------|
|
||||||
|
| `JWT_SECRET` | `BETTER_AUTH_SECRET` | Required for session management |
|
||||||
|
| - | `ENCRYPTION_SECRET` | New - required for token encryption |
|
||||||
|
|
||||||
|
### Need Help?
|
||||||
|
|
||||||
|
If you have questions about upgrading:
|
||||||
|
1. Check the [README](README.md) for v3 setup instructions
|
||||||
|
2. Review your v2 configuration before upgrading
|
||||||
|
3. Open an issue if you encounter problems
|
||||||
@@ -269,82 +269,7 @@ else
|
|||||||
bun scripts/manage-db.ts fix
|
bun scripts/manage-db.ts fix
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Run database migrations
|
echo "Database exists, checking integrity..."
|
||||||
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
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Extract version from package.json and set as environment variable
|
# Extract version from package.json and set as environment variable
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "gitea-mirror",
|
"name": "gitea-mirror",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "3.0.0",
|
"version": "3.0.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=1.2.9"
|
"bun": ">=1.2.9"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"setup": "bun install && bun run manage-db init",
|
"setup": "bun install && bun run manage-db init",
|
||||||
"dev": "bunx --bun astro dev --port 4567",
|
"dev": "bunx --bun astro dev --port 9876",
|
||||||
"dev:clean": "bun run cleanup-db && bun run manage-db init && bunx --bun astro dev",
|
"dev:clean": "bun run cleanup-db && bun run manage-db init && bunx --bun astro dev",
|
||||||
"build": "bunx --bun astro build",
|
"build": "bunx --bun astro build",
|
||||||
"cleanup-db": "rm -f gitea-mirror.db data/gitea-mirror.db",
|
"cleanup-db": "rm -f gitea-mirror.db data/gitea-mirror.db",
|
||||||
@@ -22,8 +22,6 @@
|
|||||||
"db:pull": "bun drizzle-kit pull",
|
"db:pull": "bun drizzle-kit pull",
|
||||||
"db:check": "bun drizzle-kit check",
|
"db:check": "bun drizzle-kit check",
|
||||||
"db:studio": "bun drizzle-kit studio",
|
"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": "bun scripts/startup-recovery.ts",
|
||||||
"startup-recovery-force": "bun scripts/startup-recovery.ts --force",
|
"startup-recovery-force": "bun scripts/startup-recovery.ts --force",
|
||||||
"test-recovery": "bun scripts/test-recovery.ts",
|
"test-recovery": "bun scripts/test-recovery.ts",
|
||||||
|
|||||||
@@ -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);
|
|
||||||
});
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||||
import { Home, ArrowLeft, GitBranch, BookOpen, Settings, FileQuestion } from "lucide-react";
|
import { Home, ArrowLeft, GitBranch, BookOpen, Settings, FileQuestion } from "lucide-react";
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function ConfigTabs() {
|
|||||||
token: '',
|
token: '',
|
||||||
organization: 'github-mirrors',
|
organization: 'github-mirrors',
|
||||||
visibility: 'public',
|
visibility: 'public',
|
||||||
starredReposOrg: 'github',
|
starredReposOrg: 'starred',
|
||||||
preserveOrgStructure: false,
|
preserveOrgStructure: false,
|
||||||
},
|
},
|
||||||
scheduleConfig: {
|
scheduleConfig: {
|
||||||
|
|||||||
@@ -44,11 +44,13 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving, g
|
|||||||
case "preserve":
|
case "preserve":
|
||||||
newConfig.preserveOrgStructure = true;
|
newConfig.preserveOrgStructure = true;
|
||||||
newConfig.mirrorStrategy = "preserve";
|
newConfig.mirrorStrategy = "preserve";
|
||||||
|
newConfig.personalReposOrg = undefined; // Clear personal repos org in preserve mode
|
||||||
break;
|
break;
|
||||||
case "single-org":
|
case "single-org":
|
||||||
newConfig.preserveOrgStructure = false;
|
newConfig.preserveOrgStructure = false;
|
||||||
newConfig.mirrorStrategy = "single-org";
|
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";
|
newConfig.organization = "github-mirrors";
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -60,8 +62,10 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving, g
|
|||||||
case "mixed":
|
case "mixed":
|
||||||
newConfig.preserveOrgStructure = false;
|
newConfig.preserveOrgStructure = false;
|
||||||
newConfig.mirrorStrategy = "mixed";
|
newConfig.mirrorStrategy = "mixed";
|
||||||
if (!newConfig.organization) {
|
// In mixed mode, organization field represents personal repos org
|
||||||
newConfig.organization = "github-mirrors";
|
// 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) {
|
if (!newConfig.personalReposOrg) {
|
||||||
newConfig.personalReposOrg = "github-personal";
|
newConfig.personalReposOrg = "github-personal";
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export const OrganizationConfiguration: React.FC<OrganizationConfigurationProps>
|
|||||||
id="destinationOrg"
|
id="destinationOrg"
|
||||||
value={destinationOrg || ""}
|
value={destinationOrg || ""}
|
||||||
onChange={(e) => onDestinationOrgChange(e.target.value)}
|
onChange={(e) => onDestinationOrgChange(e.target.value)}
|
||||||
placeholder="github-mirrors"
|
placeholder={strategy === "mixed" ? "github-personal" : "github-mirrors"}
|
||||||
className=""
|
className=""
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
@@ -114,32 +114,6 @@ export const OrganizationConfiguration: React.FC<OrganizationConfigurationProps>
|
|||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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" />
|
<div className="hidden md:block" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -281,6 +281,7 @@ export function OrganizationList({
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Skeleton className="h-4 w-20" />
|
<Skeleton className="h-4 w-20" />
|
||||||
<Skeleton className="h-4 w-20" />
|
<Skeleton className="h-4 w-20" />
|
||||||
|
<Skeleton className="h-4 w-20" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -300,6 +301,14 @@ export function OrganizationList({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{org.forkRepositoryCount !== undefined && org.forkRepositoryCount > 0 && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<div className="h-2.5 w-2.5 rounded-full bg-blue-500" />
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{org.forkRepositoryCount} {org.forkRepositoryCount === 1 ? "fork" : "forks"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ export function InlineDestinationEditor({
|
|||||||
if (repository.organization) {
|
if (repository.organization) {
|
||||||
return repository.organization;
|
return repository.organization;
|
||||||
}
|
}
|
||||||
// For personal repos, check if personalReposOrg is configured
|
// For personal repos, check if personalReposOrg is configured (but not in preserve mode)
|
||||||
if (!repository.organization && giteaConfig?.personalReposOrg) {
|
if (!repository.organization && giteaConfig?.personalReposOrg && strategy !== 'preserve') {
|
||||||
return giteaConfig.personalReposOrg;
|
return giteaConfig.personalReposOrg;
|
||||||
}
|
}
|
||||||
// Default to the gitea username or owner
|
// Default to the gitea username or owner
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
type RepositoryApiResponse,
|
type RepositoryApiResponse,
|
||||||
type RepoStatus,
|
type RepoStatus,
|
||||||
} from "@/types/Repository";
|
} from "@/types/Repository";
|
||||||
import { apiRequest, showErrorToast } from "@/lib/utils";
|
import { apiRequest, showErrorToast, getStatusColor } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -707,12 +707,7 @@ export default function Repository() {
|
|||||||
<SelectItem key={status} value={status}>
|
<SelectItem key={status} value={status}>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
{status !== "all" && (
|
{status !== "all" && (
|
||||||
<span className={`h-2 w-2 rounded-full ${
|
<span className={`h-2 w-2 rounded-full ${getStatusColor(status)}`} />
|
||||||
status === "synced" ? "bg-green-500" :
|
|
||||||
status === "failed" ? "bg-red-500" :
|
|
||||||
status === "syncing" ? "bg-blue-500" :
|
|
||||||
"bg-yellow-500"
|
|
||||||
}`} />
|
|
||||||
)}
|
)}
|
||||||
{status === "all"
|
{status === "all"
|
||||||
? "All statuses"
|
? "All statuses"
|
||||||
@@ -814,12 +809,7 @@ export default function Repository() {
|
|||||||
<SelectItem key={status} value={status}>
|
<SelectItem key={status} value={status}>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
{status !== "all" && (
|
{status !== "all" && (
|
||||||
<span className={`h-2 w-2 rounded-full ${
|
<span className={`h-2 w-2 rounded-full ${getStatusColor(status)}`} />
|
||||||
status === "synced" ? "bg-green-500" :
|
|
||||||
status === "failed" ? "bg-red-500" :
|
|
||||||
status === "syncing" ? "bg-blue-500" :
|
|
||||||
"bg-yellow-500"
|
|
||||||
}`} />
|
|
||||||
)}
|
)}
|
||||||
{status === "all"
|
{status === "all"
|
||||||
? "All statuses"
|
? "All statuses"
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
// Create the context value
|
// Create the context value
|
||||||
const contextValue = {
|
const contextValue = {
|
||||||
user: user as AuthUser | null,
|
user: user as AuthUser | null,
|
||||||
session,
|
session: session as Session | null,
|
||||||
isLoading: isLoading || betterAuthSession.isPending,
|
isLoading: isLoading || betterAuthSession.isPending,
|
||||||
error,
|
error,
|
||||||
login,
|
login,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createAuthClient } from "better-auth/react";
|
import { createAuthClient } from "better-auth/react";
|
||||||
import { oidcClient } from "better-auth/client/plugins";
|
import { oidcClient } from "better-auth/client/plugins";
|
||||||
import { ssoClient } from "better-auth/client/plugins";
|
import { ssoClient } from "better-auth/client/plugins";
|
||||||
|
import type { Session as BetterAuthSession, User as BetterAuthUser } from "better-auth";
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
export const authClient = createAuthClient({
|
||||||
// The base URL is optional when running on the same domain
|
// The base URL is optional when running on the same domain
|
||||||
@@ -23,6 +24,12 @@ export const {
|
|||||||
getSession
|
getSession
|
||||||
} = authClient;
|
} = authClient;
|
||||||
|
|
||||||
// Export types
|
// Export types - directly use the types from better-auth
|
||||||
export type Session = Awaited<ReturnType<typeof authClient.getSession>>["data"];
|
export type Session = BetterAuthSession & {
|
||||||
export type AuthUser = Session extends { user: infer U } ? U : never;
|
user: BetterAuthUser & {
|
||||||
|
username?: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type AuthUser = BetterAuthUser & {
|
||||||
|
username?: string | null;
|
||||||
|
};
|
||||||
@@ -24,7 +24,7 @@ export const githubConfigSchema = z.object({
|
|||||||
includePublic: z.boolean().default(true),
|
includePublic: z.boolean().default(true),
|
||||||
includeOrganizations: z.array(z.string()).default([]),
|
includeOrganizations: z.array(z.string()).default([]),
|
||||||
starredReposOrg: z.string().optional(),
|
starredReposOrg: z.string().optional(),
|
||||||
mirrorStrategy: z.enum(["preserve", "single-org", "flat-user"]).default("preserve"),
|
mirrorStrategy: z.enum(["preserve", "single-org", "flat-user", "mixed"]).default("preserve"),
|
||||||
defaultOrg: z.string().optional(),
|
defaultOrg: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,6 +47,13 @@ export const giteaConfigSchema = z.object({
|
|||||||
forkStrategy: z
|
forkStrategy: z
|
||||||
.enum(["skip", "reference", "full-copy"])
|
.enum(["skip", "reference", "full-copy"])
|
||||||
.default("reference"),
|
.default("reference"),
|
||||||
|
// Mirror options
|
||||||
|
mirrorReleases: z.boolean().default(false),
|
||||||
|
mirrorMetadata: z.boolean().default(false),
|
||||||
|
mirrorIssues: z.boolean().default(false),
|
||||||
|
mirrorPullRequests: z.boolean().default(false),
|
||||||
|
mirrorLabels: z.boolean().default(false),
|
||||||
|
mirrorMilestones: z.boolean().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const scheduleConfigSchema = z.object({
|
export const scheduleConfigSchema = z.object({
|
||||||
|
|||||||
@@ -312,13 +312,13 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
|
|||||||
skipStarredIssues: false
|
skipStarredIssues: false
|
||||||
},
|
},
|
||||||
giteaConfig: {
|
giteaConfig: {
|
||||||
username: "giteauser",
|
defaultOwner: "giteauser",
|
||||||
url: "https://gitea.example.com",
|
url: "https://gitea.example.com",
|
||||||
token: "gitea-token",
|
token: "gitea-token",
|
||||||
organization: "github-mirrors",
|
defaultOrg: "github-mirrors",
|
||||||
visibility: "public",
|
visibility: "public",
|
||||||
starredReposOrg: "starred",
|
starredReposOrg: "starred",
|
||||||
preserveOrgStructure: false,
|
preserveVisibility: false,
|
||||||
mirrorStrategy: "preserve"
|
mirrorStrategy: "preserve"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -354,19 +354,21 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
|
|||||||
expect(result).toBe("starred");
|
expect(result).toBe("starred");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("preserve strategy: personal repos use personalReposOrg override", () => {
|
test("starred repos default to 'starred' org when starredReposOrg is not configured", () => {
|
||||||
const configWithOverride = {
|
const repo = { ...baseRepo, isStarred: true };
|
||||||
|
const configWithoutStarredOrg = {
|
||||||
...baseConfig,
|
...baseConfig,
|
||||||
giteaConfig: {
|
giteaConfig: {
|
||||||
...baseConfig.giteaConfig!,
|
...baseConfig.giteaConfig,
|
||||||
personalReposOrg: "my-personal-mirrors"
|
starredReposOrg: undefined
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const repo = { ...baseRepo, organization: undefined };
|
const result = getGiteaRepoOwner({ config: configWithoutStarredOrg, repository: repo });
|
||||||
const result = getGiteaRepoOwner({ config: configWithOverride, repository: repo });
|
expect(result).toBe("starred");
|
||||||
expect(result).toBe("my-personal-mirrors");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Removed test for personalReposOrg as this field no longer exists
|
||||||
|
|
||||||
test("preserve strategy: personal repos fallback to username when no override", () => {
|
test("preserve strategy: personal repos fallback to username when no override", () => {
|
||||||
const repo = { ...baseRepo, organization: undefined };
|
const repo = { ...baseRepo, organization: undefined };
|
||||||
const result = getGiteaRepoOwner({ config: baseConfig, repository: repo });
|
const result = getGiteaRepoOwner({ config: baseConfig, repository: repo });
|
||||||
@@ -379,7 +381,7 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
|
|||||||
expect(result).toBe("myorg");
|
expect(result).toBe("myorg");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("mixed strategy: personal repos go to organization", () => {
|
test("single-org strategy: personal repos go to defaultOrg", () => {
|
||||||
const configWithMixed = {
|
const configWithMixed = {
|
||||||
...baseConfig,
|
...baseConfig,
|
||||||
giteaConfig: {
|
giteaConfig: {
|
||||||
@@ -393,7 +395,7 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
|
|||||||
expect(result).toBe("github-mirrors");
|
expect(result).toBe("github-mirrors");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("mixed strategy: org repos preserve their structure", () => {
|
test("single-org strategy: org repos also go to defaultOrg", () => {
|
||||||
const configWithMixed = {
|
const configWithMixed = {
|
||||||
...baseConfig,
|
...baseConfig,
|
||||||
giteaConfig: {
|
giteaConfig: {
|
||||||
@@ -407,18 +409,16 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
|
|||||||
expect(result).toBe("myorg");
|
expect(result).toBe("myorg");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("mixed strategy: fallback to username if no org configs", () => {
|
test("flat-user strategy: all repos go to defaultOwner", () => {
|
||||||
const configWithMixed = {
|
const configWithFlatUser = {
|
||||||
...baseConfig,
|
...baseConfig,
|
||||||
giteaConfig: {
|
giteaConfig: {
|
||||||
...baseConfig.giteaConfig!,
|
...baseConfig.giteaConfig!,
|
||||||
mirrorStrategy: "mixed" as const,
|
mirrorStrategy: "flat-user" as const
|
||||||
organization: undefined,
|
|
||||||
personalReposOrg: undefined
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const repo = { ...baseRepo, organization: undefined };
|
const repo = { ...baseRepo, organization: "myorg" };
|
||||||
const result = getGiteaRepoOwner({ config: configWithMixed, repository: repo });
|
const result = getGiteaRepoOwner({ config: configWithFlatUser, repository: repo });
|
||||||
expect(result).toBe("giteauser");
|
expect(result).toBe("giteauser");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const getGiteaRepoOwnerAsync = async ({
|
|||||||
throw new Error("GitHub or Gitea config is required.");
|
throw new Error("GitHub or Gitea config is required.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.giteaConfig.username) {
|
if (!config.giteaConfig.defaultOwner) {
|
||||||
throw new Error("Gitea username is required.");
|
throw new Error("Gitea username is required.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,8 +73,8 @@ export const getGiteaRepoOwnerAsync = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if repository is starred - starred repos always go to starredReposOrg (highest priority)
|
// Check if repository is starred - starred repos always go to starredReposOrg (highest priority)
|
||||||
if (repository.isStarred && config.giteaConfig.starredReposOrg) {
|
if (repository.isStarred) {
|
||||||
return config.giteaConfig.starredReposOrg;
|
return config.giteaConfig.starredReposOrg || "starred";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for repository-specific override (second highest priority)
|
// Check for repository-specific override (second highest priority)
|
||||||
@@ -96,11 +96,7 @@ export const getGiteaRepoOwnerAsync = async ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for personal repos override (when it's user's repo, not an organization)
|
// For personal repos (not organization repos), fall back to the default strategy
|
||||||
if (!repository.organization && config.giteaConfig.personalReposOrg) {
|
|
||||||
console.log(`Using personal repos override: ${config.giteaConfig.personalReposOrg}`);
|
|
||||||
return config.giteaConfig.personalReposOrg;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to existing strategy logic
|
// Fall back to existing strategy logic
|
||||||
return getGiteaRepoOwner({ config, repository });
|
return getGiteaRepoOwner({ config, repository });
|
||||||
@@ -117,13 +113,13 @@ export const getGiteaRepoOwner = ({
|
|||||||
throw new Error("GitHub or Gitea config is required.");
|
throw new Error("GitHub or Gitea config is required.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.giteaConfig.username) {
|
if (!config.giteaConfig.defaultOwner) {
|
||||||
throw new Error("Gitea username is required.");
|
throw new Error("Gitea username is required.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if repository is starred - starred repos always go to starredReposOrg
|
// Check if repository is starred - starred repos always go to starredReposOrg
|
||||||
if (repository.isStarred && config.giteaConfig.starredReposOrg) {
|
if (repository.isStarred) {
|
||||||
return config.giteaConfig.starredReposOrg;
|
return config.giteaConfig.starredReposOrg || "starred";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the mirror strategy - use preserveOrgStructure for backward compatibility
|
// Get the mirror strategy - use preserveOrgStructure for backward compatibility
|
||||||
@@ -137,7 +133,7 @@ export const getGiteaRepoOwner = ({
|
|||||||
return repository.organization;
|
return repository.organization;
|
||||||
}
|
}
|
||||||
// Use personal repos override if configured, otherwise use username
|
// Use personal repos override if configured, otherwise use username
|
||||||
return config.giteaConfig.personalReposOrg || config.giteaConfig.username;
|
return config.giteaConfig.defaultOwner;
|
||||||
|
|
||||||
case "single-org":
|
case "single-org":
|
||||||
// All non-starred repos go to the destination organization
|
// All non-starred repos go to the destination organization
|
||||||
@@ -145,11 +141,11 @@ export const getGiteaRepoOwner = ({
|
|||||||
return config.giteaConfig.organization;
|
return config.giteaConfig.organization;
|
||||||
}
|
}
|
||||||
// Fallback to username if no organization specified
|
// Fallback to username if no organization specified
|
||||||
return config.giteaConfig.username;
|
return config.giteaConfig.defaultOwner;
|
||||||
|
|
||||||
case "flat-user":
|
case "flat-user":
|
||||||
// All non-starred repos go under the user account
|
// All non-starred repos go under the user account
|
||||||
return config.giteaConfig.username;
|
return config.giteaConfig.defaultOwner;
|
||||||
|
|
||||||
case "mixed":
|
case "mixed":
|
||||||
// Mixed mode: personal repos to single org, organization repos preserve structure
|
// Mixed mode: personal repos to single org, organization repos preserve structure
|
||||||
@@ -162,11 +158,11 @@ export const getGiteaRepoOwner = ({
|
|||||||
return config.giteaConfig.organization;
|
return config.giteaConfig.organization;
|
||||||
}
|
}
|
||||||
// Fallback to username if no organization specified
|
// Fallback to username if no organization specified
|
||||||
return config.giteaConfig.username;
|
return config.giteaConfig.defaultOwner;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Default fallback
|
// Default fallback
|
||||||
return config.giteaConfig.username;
|
return config.giteaConfig.defaultOwner;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -268,10 +264,13 @@ export const mirrorGithubRepoToGitea = async ({
|
|||||||
throw new Error("github config and gitea config are required.");
|
throw new Error("github config and gitea config are required.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.giteaConfig.username) {
|
if (!config.giteaConfig.defaultOwner) {
|
||||||
throw new Error("Gitea username is required.");
|
throw new Error("Gitea username is required.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decrypt config tokens for API usage
|
||||||
|
const decryptedConfig = decryptConfigTokens(config as Config);
|
||||||
|
|
||||||
// Get the correct owner based on the strategy (with organization overrides)
|
// Get the correct owner based on the strategy (with organization overrides)
|
||||||
const repoOwner = await getGiteaRepoOwnerAsync({ config, repository });
|
const repoOwner = await getGiteaRepoOwnerAsync({ config, repository });
|
||||||
|
|
||||||
@@ -347,14 +346,14 @@ export const mirrorGithubRepoToGitea = async ({
|
|||||||
|
|
||||||
cloneAddress = repository.cloneUrl.replace(
|
cloneAddress = repository.cloneUrl.replace(
|
||||||
"https://",
|
"https://",
|
||||||
`https://${config.githubConfig.token}@`
|
`https://${decryptedConfig.githubConfig.token}@`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiUrl = `${config.giteaConfig.url}/api/v1/repos/migrate`;
|
const apiUrl = `${config.giteaConfig.url}/api/v1/repos/migrate`;
|
||||||
|
|
||||||
// Handle organization creation if needed for single-org or preserve strategies
|
// Handle organization creation if needed for single-org, preserve strategies, or starred repos
|
||||||
if (repoOwner !== config.giteaConfig.username && !repository.isStarred) {
|
if (repoOwner !== config.giteaConfig.defaultOwner) {
|
||||||
// Need to create the organization if it doesn't exist
|
// Need to create the organization if it doesn't exist
|
||||||
await getOrCreateGiteaOrg({
|
await getOrCreateGiteaOrg({
|
||||||
orgName: repoOwner,
|
orgName: repoOwner,
|
||||||
@@ -380,11 +379,13 @@ export const mirrorGithubRepoToGitea = async ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
//mirror releases
|
//mirror releases
|
||||||
await mirrorGitHubReleasesToGitea({
|
if (config.githubConfig?.mirrorReleases) {
|
||||||
config,
|
await mirrorGitHubReleasesToGitea({
|
||||||
octokit,
|
config,
|
||||||
repository,
|
octokit,
|
||||||
});
|
repository,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// clone issues
|
// clone issues
|
||||||
// Skip issues for starred repos if skipStarredIssues is enabled
|
// Skip issues for starred repos if skipStarredIssues is enabled
|
||||||
@@ -644,6 +645,9 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
|||||||
throw new Error("Gitea config is required.");
|
throw new Error("Gitea config is required.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decrypt config tokens for API usage
|
||||||
|
const decryptedConfig = decryptConfigTokens(config as Config);
|
||||||
|
|
||||||
const isExisting = await isRepoPresentInGitea({
|
const isExisting = await isRepoPresentInGitea({
|
||||||
config,
|
config,
|
||||||
owner: orgName,
|
owner: orgName,
|
||||||
@@ -698,7 +702,7 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
|||||||
|
|
||||||
cloneAddress = repository.cloneUrl.replace(
|
cloneAddress = repository.cloneUrl.replace(
|
||||||
"https://",
|
"https://",
|
||||||
`https://${config.githubConfig.token}@`
|
`https://${decryptedConfig.githubConfig.token}@`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -732,11 +736,13 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
|||||||
);
|
);
|
||||||
|
|
||||||
//mirror releases
|
//mirror releases
|
||||||
await mirrorGitHubReleasesToGitea({
|
if (config.githubConfig?.mirrorReleases) {
|
||||||
config,
|
await mirrorGitHubReleasesToGitea({
|
||||||
octokit,
|
config,
|
||||||
repository,
|
octokit,
|
||||||
});
|
repository,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Clone issues
|
// Clone issues
|
||||||
// Skip issues for starred repos if skipStarredIssues is enabled
|
// Skip issues for starred repos if skipStarredIssues is enabled
|
||||||
@@ -900,7 +906,7 @@ export async function mirrorGitHubOrgToGitea({
|
|||||||
// Determine the target organization based on strategy
|
// Determine the target organization based on strategy
|
||||||
if (mirrorStrategy === "single-org" && config.giteaConfig?.organization) {
|
if (mirrorStrategy === "single-org" && config.giteaConfig?.organization) {
|
||||||
// For single-org strategy, use the configured destination organization
|
// For single-org strategy, use the configured destination organization
|
||||||
targetOrgName = config.giteaConfig.organization;
|
targetOrgName = config.giteaConfig.defaultOrg || config.giteaConfig.defaultOwner;
|
||||||
giteaOrgId = await getOrCreateGiteaOrg({
|
giteaOrgId = await getOrCreateGiteaOrg({
|
||||||
orgId: organization.id,
|
orgId: organization.id,
|
||||||
orgName: targetOrgName,
|
orgName: targetOrgName,
|
||||||
@@ -919,7 +925,7 @@ export async function mirrorGitHubOrgToGitea({
|
|||||||
// For flat-user strategy, we shouldn't create organizations at all
|
// For flat-user strategy, we shouldn't create organizations at all
|
||||||
// Skip organization creation and let individual repos be handled by getGiteaRepoOwner
|
// Skip organization creation and let individual repos be handled by getGiteaRepoOwner
|
||||||
console.log(`Using flat-user strategy: repos will be placed under user account`);
|
console.log(`Using flat-user strategy: repos will be placed under user account`);
|
||||||
targetOrgName = config.giteaConfig?.username || "";
|
targetOrgName = config.giteaConfig?.defaultOwner || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
//query the db with the org name and get the repos
|
//query the db with the org name and get the repos
|
||||||
@@ -1076,7 +1082,7 @@ export const syncGiteaRepo = async ({
|
|||||||
!config.userId ||
|
!config.userId ||
|
||||||
!config.giteaConfig?.url ||
|
!config.giteaConfig?.url ||
|
||||||
!config.giteaConfig?.token ||
|
!config.giteaConfig?.token ||
|
||||||
!config.giteaConfig?.username
|
!config.giteaConfig?.defaultOwner
|
||||||
) {
|
) {
|
||||||
throw new Error("Gitea config is required.");
|
throw new Error("Gitea config is required.");
|
||||||
}
|
}
|
||||||
@@ -1125,7 +1131,7 @@ export const syncGiteaRepo = async ({
|
|||||||
const apiUrl = `${config.giteaConfig.url}/api/v1/repos/${actualOwner}/${repository.name}/mirror-sync`;
|
const apiUrl = `${config.giteaConfig.url}/api/v1/repos/${actualOwner}/${repository.name}/mirror-sync`;
|
||||||
|
|
||||||
const response = await httpPost(apiUrl, undefined, {
|
const response = await httpPost(apiUrl, undefined, {
|
||||||
Authorization: `token ${config.giteaConfig.token}`,
|
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mark repo as "synced" in DB
|
// Mark repo as "synced" in DB
|
||||||
@@ -1243,7 +1249,7 @@ export const mirrorGitRepoIssuesToGitea = async ({
|
|||||||
const giteaLabelsRes = await httpGet(
|
const giteaLabelsRes = await httpGet(
|
||||||
`${config.giteaConfig.url}/api/v1/repos/${giteaOwner}/${repository.name}/labels`,
|
`${config.giteaConfig.url}/api/v1/repos/${giteaOwner}/${repository.name}/labels`,
|
||||||
{
|
{
|
||||||
Authorization: `token ${config.giteaConfig.token}`,
|
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1399,7 +1405,7 @@ export async function mirrorGitHubReleasesToGitea({
|
|||||||
config: Partial<Config>;
|
config: Partial<Config>;
|
||||||
}) {
|
}) {
|
||||||
if (
|
if (
|
||||||
!config.giteaConfig?.username ||
|
!config.giteaConfig?.defaultOwner ||
|
||||||
!config.giteaConfig?.token ||
|
!config.giteaConfig?.token ||
|
||||||
!config.giteaConfig?.url
|
!config.giteaConfig?.url
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -52,13 +52,11 @@ export async function getGithubRepositories({
|
|||||||
{ per_page: 100 }
|
{ per_page: 100 }
|
||||||
);
|
);
|
||||||
|
|
||||||
const includePrivate = config.githubConfig?.privateRepositories ?? false;
|
|
||||||
const skipForks = config.githubConfig?.skipForks ?? false;
|
const skipForks = config.githubConfig?.skipForks ?? false;
|
||||||
|
|
||||||
const filteredRepos = repos.filter((repo) => {
|
const filteredRepos = repos.filter((repo) => {
|
||||||
const isPrivateAllowed = includePrivate || !repo.private;
|
|
||||||
const isForkAllowed = !skipForks || !repo.fork;
|
const isForkAllowed = !skipForks || !repo.fork;
|
||||||
return isPrivateAllowed && isForkAllowed;
|
return isForkAllowed;
|
||||||
});
|
});
|
||||||
|
|
||||||
return filteredRepos.map((repo) => ({
|
return filteredRepos.map((repo) => ({
|
||||||
|
|||||||
@@ -197,17 +197,17 @@ export async function apiRequest<T>(
|
|||||||
export const getStatusColor = (status: string): string => {
|
export const getStatusColor = (status: string): string => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "imported":
|
case "imported":
|
||||||
return "bg-blue-500"; // Info/primary-like
|
return "bg-yellow-500"; // Ready to mirror
|
||||||
case "mirroring":
|
case "mirroring":
|
||||||
return "bg-yellow-400"; // In progress
|
return "bg-amber-500"; // In progress
|
||||||
case "mirrored":
|
case "mirrored":
|
||||||
return "bg-emerald-500"; // Success
|
return "bg-green-500"; // Successfully mirrored
|
||||||
case "failed":
|
case "failed":
|
||||||
return "bg-rose-500"; // Error
|
return "bg-red-500"; // Error
|
||||||
case "syncing":
|
case "syncing":
|
||||||
return "bg-indigo-500"; // Sync in progress
|
return "bg-blue-500"; // Sync in progress
|
||||||
case "synced":
|
case "synced":
|
||||||
return "bg-teal-500"; // Sync complete
|
return "bg-emerald-500"; // Successfully synced
|
||||||
case "skipped":
|
case "skipped":
|
||||||
return "bg-gray-500"; // Skipped
|
return "bg-gray-500"; // Skipped
|
||||||
case "deleting":
|
case "deleting":
|
||||||
|
|||||||
@@ -9,35 +9,14 @@ import type {
|
|||||||
AdvancedOptions,
|
AdvancedOptions,
|
||||||
SaveConfigApiRequest
|
SaveConfigApiRequest
|
||||||
} from "@/types/config";
|
} from "@/types/config";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { githubConfigSchema, giteaConfigSchema, scheduleConfigSchema, cleanupConfigSchema } from "@/lib/db/schema";
|
||||||
|
|
||||||
interface DbGitHubConfig {
|
// Use the actual database schema types
|
||||||
username: string;
|
type DbGitHubConfig = z.infer<typeof githubConfigSchema>;
|
||||||
token?: string;
|
type DbGiteaConfig = z.infer<typeof giteaConfigSchema>;
|
||||||
skipForks: boolean;
|
type DbScheduleConfig = z.infer<typeof scheduleConfigSchema>;
|
||||||
privateRepositories: boolean;
|
type DbCleanupConfig = z.infer<typeof cleanupConfigSchema>;
|
||||||
mirrorIssues: boolean;
|
|
||||||
mirrorWiki: boolean;
|
|
||||||
mirrorStarred: boolean;
|
|
||||||
useSpecificUser: boolean;
|
|
||||||
singleRepo?: string;
|
|
||||||
includeOrgs: string[];
|
|
||||||
excludeOrgs: string[];
|
|
||||||
mirrorPublicOrgs: boolean;
|
|
||||||
publicOrgs: string[];
|
|
||||||
skipStarredIssues: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DbGiteaConfig {
|
|
||||||
username: string;
|
|
||||||
url: string;
|
|
||||||
token: string;
|
|
||||||
organization?: string;
|
|
||||||
visibility: "public" | "private" | "limited";
|
|
||||||
starredReposOrg: string;
|
|
||||||
preserveOrgStructure: boolean;
|
|
||||||
mirrorStrategy?: "preserve" | "single-org" | "flat-user" | "mixed";
|
|
||||||
personalReposOrg?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps UI config structure to database schema structure
|
* Maps UI config structure to database schema structure
|
||||||
@@ -48,32 +27,67 @@ export function mapUiToDbConfig(
|
|||||||
mirrorOptions: MirrorOptions,
|
mirrorOptions: MirrorOptions,
|
||||||
advancedOptions: AdvancedOptions
|
advancedOptions: AdvancedOptions
|
||||||
): { githubConfig: DbGitHubConfig; giteaConfig: DbGiteaConfig } {
|
): { githubConfig: DbGitHubConfig; giteaConfig: DbGiteaConfig } {
|
||||||
// Map GitHub config with fields from mirrorOptions and advancedOptions
|
// Map GitHub config to match database schema fields
|
||||||
const dbGithubConfig: DbGitHubConfig = {
|
const dbGithubConfig: DbGitHubConfig = {
|
||||||
username: githubConfig.username,
|
// Map username to owner field
|
||||||
token: githubConfig.token,
|
owner: githubConfig.username,
|
||||||
privateRepositories: githubConfig.privateRepositories,
|
type: "personal", // Default to personal, could be made configurable
|
||||||
mirrorStarred: githubConfig.mirrorStarred,
|
token: githubConfig.token || "",
|
||||||
|
|
||||||
// From mirrorOptions
|
// Map checkbox fields with proper names
|
||||||
mirrorIssues: mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.issues,
|
includeStarred: githubConfig.mirrorStarred,
|
||||||
mirrorWiki: mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.wiki,
|
includePrivate: githubConfig.privateRepositories,
|
||||||
|
includeForks: !advancedOptions.skipForks, // Note: UI has skipForks, DB has includeForks
|
||||||
|
includeArchived: false, // Not in UI yet, default to false
|
||||||
|
includePublic: true, // Not in UI yet, default to true
|
||||||
|
|
||||||
// From advancedOptions
|
// Organization related fields
|
||||||
skipForks: advancedOptions.skipForks,
|
includeOrganizations: [], // Not in UI yet
|
||||||
skipStarredIssues: advancedOptions.skipStarredIssues,
|
|
||||||
|
|
||||||
// Default values for fields not in UI
|
// Starred repos organization
|
||||||
useSpecificUser: false,
|
starredReposOrg: giteaConfig.starredReposOrg,
|
||||||
includeOrgs: [],
|
|
||||||
excludeOrgs: [],
|
// Mirror strategy
|
||||||
mirrorPublicOrgs: false,
|
mirrorStrategy: giteaConfig.mirrorStrategy || "preserve",
|
||||||
publicOrgs: [],
|
defaultOrg: giteaConfig.organization,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Gitea config remains mostly the same
|
// Map Gitea config to match database schema
|
||||||
const dbGiteaConfig: DbGiteaConfig = {
|
const dbGiteaConfig: DbGiteaConfig = {
|
||||||
...giteaConfig,
|
url: giteaConfig.url,
|
||||||
|
token: giteaConfig.token,
|
||||||
|
defaultOwner: giteaConfig.username, // Map username to defaultOwner
|
||||||
|
|
||||||
|
// Mirror interval and options
|
||||||
|
mirrorInterval: "8h", // Default value, could be made configurable
|
||||||
|
lfs: false, // Not in UI yet
|
||||||
|
wiki: mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.wiki,
|
||||||
|
|
||||||
|
// Visibility settings
|
||||||
|
visibility: giteaConfig.visibility || "default",
|
||||||
|
preserveVisibility: giteaConfig.preserveOrgStructure,
|
||||||
|
|
||||||
|
// Organization creation
|
||||||
|
createOrg: true, // Default to true
|
||||||
|
|
||||||
|
// Template settings (not in UI yet)
|
||||||
|
templateOwner: undefined,
|
||||||
|
templateRepo: undefined,
|
||||||
|
|
||||||
|
// Topics
|
||||||
|
addTopics: true, // Default to true
|
||||||
|
topicPrefix: undefined,
|
||||||
|
|
||||||
|
// Fork strategy
|
||||||
|
forkStrategy: advancedOptions.skipForks ? "skip" : "reference",
|
||||||
|
|
||||||
|
// Mirror options from UI
|
||||||
|
mirrorReleases: mirrorOptions.mirrorReleases,
|
||||||
|
mirrorMetadata: mirrorOptions.mirrorMetadata,
|
||||||
|
mirrorIssues: mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.issues,
|
||||||
|
mirrorPullRequests: mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.pullRequests,
|
||||||
|
mirrorLabels: mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.labels,
|
||||||
|
mirrorMilestones: mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.milestones,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -91,40 +105,44 @@ export function mapDbToUiConfig(dbConfig: any): {
|
|||||||
mirrorOptions: MirrorOptions;
|
mirrorOptions: MirrorOptions;
|
||||||
advancedOptions: AdvancedOptions;
|
advancedOptions: AdvancedOptions;
|
||||||
} {
|
} {
|
||||||
|
// Map from database GitHub config to UI fields
|
||||||
const githubConfig: GitHubConfig = {
|
const githubConfig: GitHubConfig = {
|
||||||
username: dbConfig.githubConfig?.username || "",
|
username: dbConfig.githubConfig?.owner || "", // Map owner to username
|
||||||
token: dbConfig.githubConfig?.token || "",
|
token: dbConfig.githubConfig?.token || "",
|
||||||
privateRepositories: dbConfig.githubConfig?.privateRepositories || false,
|
privateRepositories: dbConfig.githubConfig?.includePrivate || false, // Map includePrivate to privateRepositories
|
||||||
mirrorStarred: dbConfig.githubConfig?.mirrorStarred || false,
|
mirrorStarred: dbConfig.githubConfig?.includeStarred || false, // Map includeStarred to mirrorStarred
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Map from database Gitea config to UI fields
|
||||||
const giteaConfig: GiteaConfig = {
|
const giteaConfig: GiteaConfig = {
|
||||||
url: dbConfig.giteaConfig?.url || "",
|
url: dbConfig.giteaConfig?.url || "",
|
||||||
username: dbConfig.giteaConfig?.username || "",
|
username: dbConfig.giteaConfig?.defaultOwner || "", // Map defaultOwner to username
|
||||||
token: dbConfig.giteaConfig?.token || "",
|
token: dbConfig.giteaConfig?.token || "",
|
||||||
organization: dbConfig.giteaConfig?.organization || "github-mirrors",
|
organization: dbConfig.githubConfig?.defaultOrg || "github-mirrors", // Get from GitHub config
|
||||||
visibility: dbConfig.giteaConfig?.visibility || "public",
|
visibility: dbConfig.giteaConfig?.visibility === "default" ? "public" : dbConfig.giteaConfig?.visibility || "public",
|
||||||
starredReposOrg: dbConfig.giteaConfig?.starredReposOrg || "github",
|
starredReposOrg: dbConfig.githubConfig?.starredReposOrg || "starred", // Get from GitHub config
|
||||||
preserveOrgStructure: dbConfig.giteaConfig?.preserveOrgStructure || false,
|
preserveOrgStructure: dbConfig.giteaConfig?.preserveVisibility || false, // Map preserveVisibility
|
||||||
mirrorStrategy: dbConfig.giteaConfig?.mirrorStrategy,
|
mirrorStrategy: dbConfig.githubConfig?.mirrorStrategy || "preserve", // Get from GitHub config
|
||||||
personalReposOrg: dbConfig.giteaConfig?.personalReposOrg,
|
personalReposOrg: undefined, // Not stored in current schema
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Map mirror options from various database fields
|
||||||
const mirrorOptions: MirrorOptions = {
|
const mirrorOptions: MirrorOptions = {
|
||||||
mirrorReleases: false, // Not stored in DB yet
|
mirrorReleases: dbConfig.giteaConfig?.mirrorReleases || false,
|
||||||
mirrorMetadata: dbConfig.githubConfig?.mirrorIssues || dbConfig.githubConfig?.mirrorWiki || false,
|
mirrorMetadata: dbConfig.giteaConfig?.mirrorMetadata || false,
|
||||||
metadataComponents: {
|
metadataComponents: {
|
||||||
issues: dbConfig.githubConfig?.mirrorIssues || false,
|
issues: dbConfig.giteaConfig?.mirrorIssues || false,
|
||||||
pullRequests: false, // Not stored in DB yet
|
pullRequests: dbConfig.giteaConfig?.mirrorPullRequests || false,
|
||||||
labels: false, // Not stored in DB yet
|
labels: dbConfig.giteaConfig?.mirrorLabels || false,
|
||||||
milestones: false, // Not stored in DB yet
|
milestones: dbConfig.giteaConfig?.mirrorMilestones || false,
|
||||||
wiki: dbConfig.githubConfig?.mirrorWiki || false,
|
wiki: dbConfig.giteaConfig?.wiki || false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Map advanced options
|
||||||
const advancedOptions: AdvancedOptions = {
|
const advancedOptions: AdvancedOptions = {
|
||||||
skipForks: dbConfig.githubConfig?.skipForks || false,
|
skipForks: !(dbConfig.githubConfig?.includeForks ?? true), // Invert includeForks to get skipForks
|
||||||
skipStarredIssues: dbConfig.githubConfig?.skipStarredIssues || false,
|
skipStarredIssues: false, // Not stored in current schema
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -133,4 +151,73 @@ export function mapDbToUiConfig(dbConfig: any): {
|
|||||||
mirrorOptions,
|
mirrorOptions,
|
||||||
advancedOptions,
|
advancedOptions,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps UI schedule config to database schema
|
||||||
|
*/
|
||||||
|
export function mapUiScheduleToDb(uiSchedule: any): DbScheduleConfig {
|
||||||
|
return {
|
||||||
|
enabled: uiSchedule.enabled || false,
|
||||||
|
interval: uiSchedule.interval ? `0 */${Math.floor(uiSchedule.interval / 3600)} * * *` : "0 2 * * *", // Convert seconds to cron expression
|
||||||
|
concurrent: false,
|
||||||
|
batchSize: 10,
|
||||||
|
pauseBetweenBatches: 5000,
|
||||||
|
retryAttempts: 3,
|
||||||
|
retryDelay: 60000,
|
||||||
|
timeout: 3600000,
|
||||||
|
autoRetry: true,
|
||||||
|
cleanupBeforeMirror: false,
|
||||||
|
notifyOnFailure: true,
|
||||||
|
notifyOnSuccess: false,
|
||||||
|
logLevel: "info",
|
||||||
|
timezone: "UTC",
|
||||||
|
onlyMirrorUpdated: false,
|
||||||
|
updateInterval: 86400000,
|
||||||
|
skipRecentlyMirrored: true,
|
||||||
|
recentThreshold: 3600000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps database schedule config to UI format
|
||||||
|
*/
|
||||||
|
export function mapDbScheduleToUi(dbSchedule: DbScheduleConfig): any {
|
||||||
|
// Extract hours from cron expression if possible
|
||||||
|
let intervalSeconds = 3600; // Default 1 hour
|
||||||
|
const cronMatch = dbSchedule.interval.match(/0 \*\/(\d+) \* \* \*/);
|
||||||
|
if (cronMatch) {
|
||||||
|
intervalSeconds = parseInt(cronMatch[1]) * 3600;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled: dbSchedule.enabled,
|
||||||
|
interval: intervalSeconds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps UI cleanup config to database schema
|
||||||
|
*/
|
||||||
|
export function mapUiCleanupToDb(uiCleanup: any): DbCleanupConfig {
|
||||||
|
return {
|
||||||
|
enabled: uiCleanup.enabled || false,
|
||||||
|
deleteFromGitea: false,
|
||||||
|
deleteIfNotInGitHub: true,
|
||||||
|
protectedRepos: [],
|
||||||
|
dryRun: true,
|
||||||
|
orphanedRepoAction: "archive",
|
||||||
|
batchSize: 10,
|
||||||
|
pauseBetweenDeletes: 2000,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps database cleanup config to UI format
|
||||||
|
*/
|
||||||
|
export function mapDbCleanupToUi(dbCleanup: DbCleanupConfig): any {
|
||||||
|
return {
|
||||||
|
enabled: dbCleanup.enabled,
|
||||||
|
retentionDays: 604800, // 7 days in seconds (kept for compatibility)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,14 @@ import { v4 as uuidv4 } from "uuid";
|
|||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { calculateCleanupInterval } from "@/lib/cleanup-service";
|
import { calculateCleanupInterval } from "@/lib/cleanup-service";
|
||||||
import { createSecureErrorResponse } from "@/lib/utils";
|
import { createSecureErrorResponse } from "@/lib/utils";
|
||||||
import { mapUiToDbConfig, mapDbToUiConfig } from "@/lib/utils/config-mapper";
|
import {
|
||||||
|
mapUiToDbConfig,
|
||||||
|
mapDbToUiConfig,
|
||||||
|
mapUiScheduleToDb,
|
||||||
|
mapUiCleanupToDb,
|
||||||
|
mapDbScheduleToUi,
|
||||||
|
mapDbCleanupToUi
|
||||||
|
} from "@/lib/utils/config-mapper";
|
||||||
import { encrypt, decrypt, migrateToken } from "@/lib/utils/encryption";
|
import { encrypt, decrypt, migrateToken } from "@/lib/utils/encryption";
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request }) => {
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
@@ -78,62 +85,9 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
mappedGiteaConfig.token = encrypt(mappedGiteaConfig.token);
|
mappedGiteaConfig.token = encrypt(mappedGiteaConfig.token);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process schedule config - set/update nextRun if enabled, clear if disabled
|
// Map schedule and cleanup configs to database schema
|
||||||
const processedScheduleConfig = { ...scheduleConfig };
|
const processedScheduleConfig = mapUiScheduleToDb(scheduleConfig);
|
||||||
if (scheduleConfig.enabled) {
|
const processedCleanupConfig = mapUiCleanupToDb(cleanupConfig);
|
||||||
const now = new Date();
|
|
||||||
const interval = scheduleConfig.interval || 3600; // Default to 1 hour
|
|
||||||
|
|
||||||
// Check if we need to recalculate nextRun
|
|
||||||
// Recalculate if: no nextRun exists, or interval changed from existing config
|
|
||||||
let shouldRecalculate = !scheduleConfig.nextRun;
|
|
||||||
|
|
||||||
if (existingConfig && existingConfig.scheduleConfig) {
|
|
||||||
const existingScheduleConfig = existingConfig.scheduleConfig;
|
|
||||||
const existingInterval = existingScheduleConfig.interval || 3600;
|
|
||||||
|
|
||||||
// If interval changed, recalculate nextRun
|
|
||||||
if (interval !== existingInterval) {
|
|
||||||
shouldRecalculate = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldRecalculate) {
|
|
||||||
processedScheduleConfig.nextRun = new Date(now.getTime() + interval * 1000);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Clear nextRun when disabled
|
|
||||||
processedScheduleConfig.nextRun = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process cleanup config - set/update nextRun if enabled, clear if disabled
|
|
||||||
const processedCleanupConfig = { ...cleanupConfig };
|
|
||||||
if (cleanupConfig.enabled) {
|
|
||||||
const now = new Date();
|
|
||||||
const retentionSeconds = cleanupConfig.retentionDays || 604800; // Default 7 days in seconds
|
|
||||||
const cleanupIntervalHours = calculateCleanupInterval(retentionSeconds);
|
|
||||||
|
|
||||||
// Check if we need to recalculate nextRun
|
|
||||||
// Recalculate if: no nextRun exists, or retention period changed from existing config
|
|
||||||
let shouldRecalculate = !cleanupConfig.nextRun;
|
|
||||||
|
|
||||||
if (existingConfig && existingConfig.cleanupConfig) {
|
|
||||||
const existingCleanupConfig = existingConfig.cleanupConfig;
|
|
||||||
const existingRetentionSeconds = existingCleanupConfig.retentionDays || 604800;
|
|
||||||
|
|
||||||
// If retention period changed, recalculate nextRun
|
|
||||||
if (retentionSeconds !== existingRetentionSeconds) {
|
|
||||||
shouldRecalculate = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldRecalculate) {
|
|
||||||
processedCleanupConfig.nextRun = new Date(now.getTime() + cleanupIntervalHours * 60 * 60 * 1000);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Clear nextRun when disabled
|
|
||||||
processedCleanupConfig.nextRun = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingConfig) {
|
if (existingConfig) {
|
||||||
// Update path
|
// Update path
|
||||||
@@ -234,28 +188,34 @@ export const GET: APIRoute = async ({ request }) => {
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (config.length === 0) {
|
if (config.length === 0) {
|
||||||
// Return a default empty configuration with UI structure
|
// Return a default empty configuration with database structure
|
||||||
const defaultDbConfig = {
|
const defaultDbConfig = {
|
||||||
githubConfig: {
|
githubConfig: {
|
||||||
username: "",
|
owner: "",
|
||||||
|
type: "personal",
|
||||||
token: "",
|
token: "",
|
||||||
skipForks: false,
|
includeStarred: false,
|
||||||
privateRepositories: false,
|
includeForks: true,
|
||||||
mirrorIssues: false,
|
includeArchived: false,
|
||||||
mirrorWiki: false,
|
includePrivate: false,
|
||||||
mirrorStarred: false,
|
includePublic: true,
|
||||||
useSpecificUser: false,
|
includeOrganizations: [],
|
||||||
preserveOrgStructure: false,
|
starredReposOrg: "starred",
|
||||||
skipStarredIssues: false,
|
mirrorStrategy: "preserve",
|
||||||
|
defaultOrg: "github-mirrors",
|
||||||
},
|
},
|
||||||
giteaConfig: {
|
giteaConfig: {
|
||||||
url: "",
|
url: "",
|
||||||
token: "",
|
token: "",
|
||||||
username: "",
|
defaultOwner: "",
|
||||||
organization: "github-mirrors",
|
mirrorInterval: "8h",
|
||||||
|
lfs: false,
|
||||||
|
wiki: false,
|
||||||
visibility: "public",
|
visibility: "public",
|
||||||
starredReposOrg: "github",
|
createOrg: true,
|
||||||
preserveOrgStructure: false,
|
addTopics: true,
|
||||||
|
preserveVisibility: false,
|
||||||
|
forkStrategy: "reference",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -319,9 +279,23 @@ export const GET: APIRoute = async ({ request }) => {
|
|||||||
|
|
||||||
const uiConfig = mapDbToUiConfig(decryptedConfig);
|
const uiConfig = mapDbToUiConfig(decryptedConfig);
|
||||||
|
|
||||||
|
// Map schedule and cleanup configs to UI format
|
||||||
|
const uiScheduleConfig = mapDbScheduleToUi(dbConfig.scheduleConfig);
|
||||||
|
const uiCleanupConfig = mapDbCleanupToUi(dbConfig.cleanupConfig);
|
||||||
|
|
||||||
return new Response(JSON.stringify({
|
return new Response(JSON.stringify({
|
||||||
...dbConfig,
|
...dbConfig,
|
||||||
...uiConfig,
|
...uiConfig,
|
||||||
|
scheduleConfig: {
|
||||||
|
...uiScheduleConfig,
|
||||||
|
lastRun: dbConfig.scheduleConfig.lastRun,
|
||||||
|
nextRun: dbConfig.scheduleConfig.nextRun,
|
||||||
|
},
|
||||||
|
cleanupConfig: {
|
||||||
|
...uiCleanupConfig,
|
||||||
|
lastRun: dbConfig.cleanupConfig.lastRun,
|
||||||
|
nextRun: dbConfig.cleanupConfig.nextRun,
|
||||||
|
},
|
||||||
}), {
|
}), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@@ -330,9 +304,22 @@ export const GET: APIRoute = async ({ request }) => {
|
|||||||
console.error("Failed to decrypt tokens:", error);
|
console.error("Failed to decrypt tokens:", error);
|
||||||
// Return config without decrypting tokens if there's an error
|
// Return config without decrypting tokens if there's an error
|
||||||
const uiConfig = mapDbToUiConfig(dbConfig);
|
const uiConfig = mapDbToUiConfig(dbConfig);
|
||||||
|
const uiScheduleConfig = mapDbScheduleToUi(dbConfig.scheduleConfig);
|
||||||
|
const uiCleanupConfig = mapDbCleanupToUi(dbConfig.cleanupConfig);
|
||||||
|
|
||||||
return new Response(JSON.stringify({
|
return new Response(JSON.stringify({
|
||||||
...dbConfig,
|
...dbConfig,
|
||||||
...uiConfig,
|
...uiConfig,
|
||||||
|
scheduleConfig: {
|
||||||
|
...uiScheduleConfig,
|
||||||
|
lastRun: dbConfig.scheduleConfig.lastRun,
|
||||||
|
nextRun: dbConfig.scheduleConfig.nextRun,
|
||||||
|
},
|
||||||
|
cleanupConfig: {
|
||||||
|
...uiCleanupConfig,
|
||||||
|
lastRun: dbConfig.cleanupConfig.lastRun,
|
||||||
|
nextRun: dbConfig.cleanupConfig.nextRun,
|
||||||
|
},
|
||||||
}), {
|
}), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
|||||||
@@ -66,54 +66,39 @@ export const GET: APIRoute = async ({ request }) => {
|
|||||||
baseConditions.push(eq(repositories.isStarred, false));
|
baseConditions.push(eq(repositories.isStarred, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get total count with all user config filters applied
|
// Get actual total count (without user config filters)
|
||||||
const totalConditions = [...baseConditions];
|
|
||||||
if (githubConfig.skipForks) {
|
|
||||||
totalConditions.push(eq(repositories.isForked, false));
|
|
||||||
}
|
|
||||||
if (!githubConfig.privateRepositories) {
|
|
||||||
totalConditions.push(eq(repositories.isPrivate, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
const [totalCount] = await db
|
const [totalCount] = await db
|
||||||
.select({ count: count() })
|
.select({ count: count() })
|
||||||
.from(repositories)
|
.from(repositories)
|
||||||
.where(and(...totalConditions));
|
.where(and(...baseConditions));
|
||||||
|
|
||||||
// Get public count
|
|
||||||
const publicConditions = [...baseConditions, eq(repositories.isPrivate, false)];
|
|
||||||
if (githubConfig.skipForks) {
|
|
||||||
publicConditions.push(eq(repositories.isForked, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Get public count (actual count, not filtered)
|
||||||
const [publicCount] = await db
|
const [publicCount] = await db
|
||||||
.select({ count: count() })
|
.select({ count: count() })
|
||||||
.from(repositories)
|
.from(repositories)
|
||||||
.where(and(...publicConditions));
|
.where(and(...baseConditions, eq(repositories.isPrivate, false)));
|
||||||
|
|
||||||
// Get private count (only if private repos are enabled in config)
|
// Get private count (always show actual count regardless of config)
|
||||||
const [privateCount] = githubConfig.privateRepositories ? await db
|
const [privateCount] = await db
|
||||||
.select({ count: count() })
|
.select({ count: count() })
|
||||||
.from(repositories)
|
.from(repositories)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
...baseConditions,
|
...baseConditions,
|
||||||
eq(repositories.isPrivate, true),
|
eq(repositories.isPrivate, true)
|
||||||
...(githubConfig.skipForks ? [eq(repositories.isForked, false)] : [])
|
|
||||||
)
|
)
|
||||||
) : [{ count: 0 }];
|
);
|
||||||
|
|
||||||
// Get fork count (only if forks are enabled in config)
|
// Get fork count (always show actual count regardless of config)
|
||||||
const [forkCount] = !githubConfig.skipForks ? await db
|
const [forkCount] = await db
|
||||||
.select({ count: count() })
|
.select({ count: count() })
|
||||||
.from(repositories)
|
.from(repositories)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
...baseConditions,
|
...baseConditions,
|
||||||
eq(repositories.isForked, true),
|
eq(repositories.isForked, true)
|
||||||
...(!githubConfig.privateRepositories ? [eq(repositories.isPrivate, false)] : [])
|
|
||||||
)
|
)
|
||||||
) : [{ count: 0 }];
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...org,
|
...org,
|
||||||
|
|||||||
@@ -45,17 +45,10 @@ export const GET: APIRoute = async ({ request }) => {
|
|||||||
// Build query conditions based on config
|
// Build query conditions based on config
|
||||||
const conditions = [eq(repositories.userId, userId)];
|
const conditions = [eq(repositories.userId, userId)];
|
||||||
|
|
||||||
if (!githubConfig.mirrorStarred) {
|
// Note: We show ALL repositories in the list
|
||||||
conditions.push(eq(repositories.isStarred, false));
|
// The mirrorStarred and privateRepositories flags only control what gets mirrored,
|
||||||
}
|
// not what's displayed in the repository list
|
||||||
|
// Only skipForks is used for filtering the display since forked repos are often noise
|
||||||
if (githubConfig.skipForks) {
|
|
||||||
conditions.push(eq(repositories.isForked, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!githubConfig.privateRepositories) {
|
|
||||||
conditions.push(eq(repositories.isPrivate, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawRepositories = await db
|
const rawRepositories = await db
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
(config.githubConfig?.preserveOrgStructure ? "preserve" : "flat-user");
|
(config.githubConfig?.preserveOrgStructure ? "preserve" : "flat-user");
|
||||||
|
|
||||||
const shouldUseOrgMirror =
|
const shouldUseOrgMirror =
|
||||||
owner !== config.giteaConfig?.username || // Different owner means org
|
owner !== config.giteaConfig?.defaultOwner || // Different owner means org
|
||||||
mirrorStrategy === "single-org" || // Single-org strategy always uses org
|
mirrorStrategy === "single-org" || // Single-org strategy always uses org
|
||||||
repoData.isStarred; // Starred repos always go to org
|
repoData.isStarred; // Starred repos always go to org
|
||||||
|
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
(config.githubConfig?.preserveOrgStructure ? "preserve" : "flat-user");
|
(config.githubConfig?.preserveOrgStructure ? "preserve" : "flat-user");
|
||||||
|
|
||||||
const shouldUseOrgMirror =
|
const shouldUseOrgMirror =
|
||||||
owner !== config.giteaConfig?.username || // Different owner means org
|
owner !== config.giteaConfig?.defaultOwner || // Different owner means org
|
||||||
mirrorStrategy === "single-org" || // Single-org strategy always uses org
|
mirrorStrategy === "single-org" || // Single-org strategy always uses org
|
||||||
repoData.isStarred; // Starred repos always go to org
|
repoData.isStarred; // Starred repos always go to org
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import type {
|
|||||||
} from "@/types/organizations";
|
} from "@/types/organizations";
|
||||||
import type { RepositoryVisibility, RepoStatus } from "@/types/Repository";
|
import type { RepositoryVisibility, RepoStatus } from "@/types/Repository";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { decryptConfigTokens } from "@/lib/utils/config-encryption";
|
||||||
|
import { createGitHubClient } from "@/lib/github";
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request }) => {
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
@@ -44,32 +46,67 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
const [config] = await db
|
const [config] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(configs)
|
.from(configs)
|
||||||
.where(eq(configs.userId, userId))
|
.where(and(eq(configs.userId, userId), eq(configs.isActive, true)))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return jsonResponse({
|
return jsonResponse({
|
||||||
data: { error: "No configuration found for this user" },
|
data: { error: "No active configuration found for this user" },
|
||||||
status: 404,
|
status: 404,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const configId = config.id;
|
const configId = config.id;
|
||||||
|
|
||||||
const octokit = new Octokit();
|
// Decrypt the config to get tokens
|
||||||
|
const decryptedConfig = decryptConfigTokens(config);
|
||||||
|
|
||||||
|
// Check if we have a GitHub token
|
||||||
|
if (!decryptedConfig.githubConfig?.token) {
|
||||||
|
return jsonResponse({
|
||||||
|
data: { error: "GitHub token not configured" },
|
||||||
|
status: 401,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create authenticated Octokit instance
|
||||||
|
const octokit = createGitHubClient(decryptedConfig.githubConfig.token);
|
||||||
|
|
||||||
// Fetch org metadata
|
// Fetch org metadata
|
||||||
const { data: orgData } = await octokit.orgs.get({ org });
|
const { data: orgData } = await octokit.orgs.get({ org });
|
||||||
|
|
||||||
// Fetch public repos using Octokit paginator
|
// Fetch repos based on config settings
|
||||||
|
const allRepos = [];
|
||||||
|
|
||||||
|
// Fetch all repos (public, private, and member) to show in UI
|
||||||
const publicRepos = await octokit.paginate(octokit.repos.listForOrg, {
|
const publicRepos = await octokit.paginate(octokit.repos.listForOrg, {
|
||||||
org,
|
org,
|
||||||
type: "public",
|
type: "public",
|
||||||
per_page: 100,
|
per_page: 100,
|
||||||
});
|
});
|
||||||
|
allRepos.push(...publicRepos);
|
||||||
|
|
||||||
|
// Always fetch private repos to show them in the UI
|
||||||
|
const privateRepos = await octokit.paginate(octokit.repos.listForOrg, {
|
||||||
|
org,
|
||||||
|
type: "private",
|
||||||
|
per_page: 100,
|
||||||
|
});
|
||||||
|
allRepos.push(...privateRepos);
|
||||||
|
|
||||||
|
// Also fetch member repos (includes private repos the user has access to)
|
||||||
|
const memberRepos = await octokit.paginate(octokit.repos.listForOrg, {
|
||||||
|
org,
|
||||||
|
type: "member",
|
||||||
|
per_page: 100,
|
||||||
|
});
|
||||||
|
// Filter out duplicates
|
||||||
|
const existingIds = new Set(allRepos.map(r => r.id));
|
||||||
|
const uniqueMemberRepos = memberRepos.filter(r => !existingIds.has(r.id));
|
||||||
|
allRepos.push(...uniqueMemberRepos);
|
||||||
|
|
||||||
// Insert repositories
|
// Insert repositories
|
||||||
const repoRecords = publicRepos.map((repo) => ({
|
const repoRecords = allRepos.map((repo) => ({
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
userId,
|
userId,
|
||||||
configId,
|
configId,
|
||||||
@@ -110,7 +147,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|||||||
membershipRole: role,
|
membershipRole: role,
|
||||||
isIncluded: false,
|
isIncluded: false,
|
||||||
status: "imported" as RepoStatus,
|
status: "imported" as RepoStatus,
|
||||||
repositoryCount: publicRepos.length,
|
repositoryCount: allRepos.length,
|
||||||
createdAt: orgData.created_at ? new Date(orgData.created_at) : new Date(),
|
createdAt: orgData.created_at ? new Date(orgData.created_at) : new Date(),
|
||||||
updatedAt: orgData.updated_at ? new Date(orgData.updated_at) : new Date(),
|
updatedAt: orgData.updated_at ? new Date(orgData.updated_at) : new Date(),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user