mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-08 04:26:44 +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.
|
||||
|
||||
DONT HALLUCIATE THINGS. IF YOU DONT KNOW LOOK AT THE CODE OR ASK FOR DOCS
|
||||
|
||||
## 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.
|
||||
@@ -40,7 +42,7 @@ bun run start # Start production server
|
||||
- **Frontend**: Astro (SSR) + React + Tailwind CSS v4 + Shadcn UI
|
||||
- **Backend**: Bun runtime + SQLite + Drizzle ORM
|
||||
- **APIs**: GitHub (Octokit) and Gitea APIs
|
||||
- **Auth**: JWT tokens with bcryptjs password hashing
|
||||
- **Auth**: Better Auth with email/password, SSO, and OIDC provider support
|
||||
|
||||
### Project Structure
|
||||
- `/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
|
||||
|
||||
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
|
||||
- JWT tokens stored in cookies
|
||||
- Protected routes check auth via `getUserFromCookie()`
|
||||
- Protected routes use Better Auth session validation
|
||||
|
||||
5. **Mirror Process**:
|
||||
- Discovers repos from GitHub (user/org)
|
||||
@@ -79,11 +86,18 @@ export async function POST({ request }: APIContext) {
|
||||
- Tracks status in database
|
||||
- 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)
|
||||
- Organization repos → Same organization name in Gitea
|
||||
- Personal repos → Under your Gitea username
|
||||
- **single-org**: All repos go to one organization
|
||||
- All repos → Single configured organization
|
||||
- **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
|
||||
|
||||
### Database Schema (SQLite)
|
||||
@@ -102,11 +116,18 @@ export async function POST({ request }: APIContext) {
|
||||
|
||||
### Development Tips
|
||||
- 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
|
||||
- Use `bun run dev:clean` for fresh database start
|
||||
- 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
|
||||
|
||||
**Adding a new API endpoint:**
|
||||
@@ -125,6 +146,73 @@ export async function POST({ request }: APIContext) {
|
||||
2. Run `bun run init-db` to recreate database
|
||||
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
|
||||
|
||||
- **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>
|
||||
|
||||
> [!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
|
||||
@@ -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 entire organizations with flexible strategies
|
||||
- 🎯 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
|
||||
- ⏱️ Scheduled automatic mirroring
|
||||
- 🐳 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
|
||||
fi
|
||||
|
||||
# Run database migrations
|
||||
echo "Running database migrations..."
|
||||
|
||||
# Update mirror_jobs table with new columns for resilience
|
||||
if [ -f "dist/scripts/update-mirror-jobs-table.js" ]; then
|
||||
echo "Updating mirror_jobs table..."
|
||||
bun dist/scripts/update-mirror-jobs-table.js
|
||||
elif [ -f "scripts/update-mirror-jobs-table.ts" ]; then
|
||||
echo "Updating mirror_jobs table using TypeScript script..."
|
||||
bun scripts/update-mirror-jobs-table.ts
|
||||
else
|
||||
echo "Warning: Could not find mirror_jobs table update script."
|
||||
fi
|
||||
|
||||
# Run v3 migrations if needed
|
||||
echo "Checking for v3 migrations..."
|
||||
|
||||
# Check if we need to run Better Auth migration (check if accounts table exists)
|
||||
if ! sqlite3 /app/data/gitea-mirror.db "SELECT name FROM sqlite_master WHERE type='table' AND name='accounts';" | grep -q accounts; then
|
||||
echo "🔄 v3 Migration: Creating Better Auth tables..."
|
||||
# Create Better Auth tables
|
||||
sqlite3 /app/data/gitea-mirror.db <<EOF
|
||||
CREATE TABLE IF NOT EXISTS accounts (
|
||||
id TEXT PRIMARY KEY,
|
||||
userId TEXT NOT NULL,
|
||||
accountId TEXT NOT NULL,
|
||||
providerId TEXT NOT NULL,
|
||||
accessToken TEXT,
|
||||
refreshToken TEXT,
|
||||
expiresAt INTEGER,
|
||||
password TEXT,
|
||||
createdAt INTEGER NOT NULL,
|
||||
updatedAt INTEGER NOT NULL,
|
||||
FOREIGN KEY (userId) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
userId TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
expiresAt INTEGER NOT NULL,
|
||||
createdAt INTEGER NOT NULL,
|
||||
updatedAt INTEGER NOT NULL,
|
||||
FOREIGN KEY (userId) REFERENCES users(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS verification_tokens (
|
||||
id TEXT PRIMARY KEY,
|
||||
identifier TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
expires INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_accounts_userId ON accounts(userId);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token);
|
||||
CREATE INDEX IF NOT EXISTS idx_verification_identifier_token ON verification_tokens(identifier, token);
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Run Better Auth user migration
|
||||
if [ -f "dist/scripts/migrate-better-auth.js" ]; then
|
||||
echo "🔄 v3 Migration: Migrating users to Better Auth..."
|
||||
bun dist/scripts/migrate-better-auth.js
|
||||
elif [ -f "scripts/migrate-better-auth.ts" ]; then
|
||||
echo "🔄 v3 Migration: Migrating users to Better Auth..."
|
||||
bun scripts/migrate-better-auth.ts
|
||||
fi
|
||||
|
||||
# Run token encryption migration
|
||||
if [ -f "dist/scripts/migrate-tokens-encryption.js" ]; then
|
||||
echo "🔄 v3 Migration: Encrypting stored tokens..."
|
||||
bun dist/scripts/migrate-tokens-encryption.js
|
||||
elif [ -f "scripts/migrate-tokens-encryption.ts" ]; then
|
||||
echo "🔄 v3 Migration: Encrypting stored tokens..."
|
||||
bun scripts/migrate-tokens-encryption.ts
|
||||
fi
|
||||
echo "Database exists, checking integrity..."
|
||||
fi
|
||||
|
||||
# Extract version from package.json and set as environment variable
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "gitea-mirror",
|
||||
"type": "module",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.1",
|
||||
"engines": {
|
||||
"bun": ">=1.2.9"
|
||||
},
|
||||
"scripts": {
|
||||
"setup": "bun install && bun run manage-db init",
|
||||
"dev": "bunx --bun astro dev --port 4567",
|
||||
"dev": "bunx --bun astro dev --port 9876",
|
||||
"dev:clean": "bun run cleanup-db && bun run manage-db init && bunx --bun astro dev",
|
||||
"build": "bunx --bun astro build",
|
||||
"cleanup-db": "rm -f gitea-mirror.db data/gitea-mirror.db",
|
||||
@@ -22,8 +22,6 @@
|
||||
"db:pull": "bun drizzle-kit pull",
|
||||
"db:check": "bun drizzle-kit check",
|
||||
"db:studio": "bun drizzle-kit studio",
|
||||
"migrate:better-auth": "bun scripts/migrate-to-better-auth.ts",
|
||||
"migrate:encrypt-tokens": "bun scripts/migrate-tokens-encryption.ts",
|
||||
"startup-recovery": "bun scripts/startup-recovery.ts",
|
||||
"startup-recovery-force": "bun scripts/startup-recovery.ts --force",
|
||||
"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 { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { Home, ArrowLeft, GitBranch, BookOpen, Settings, FileQuestion } from "lucide-react";
|
||||
|
||||
@@ -46,7 +46,7 @@ export function ConfigTabs() {
|
||||
token: '',
|
||||
organization: 'github-mirrors',
|
||||
visibility: 'public',
|
||||
starredReposOrg: 'github',
|
||||
starredReposOrg: 'starred',
|
||||
preserveOrgStructure: false,
|
||||
},
|
||||
scheduleConfig: {
|
||||
|
||||
@@ -44,11 +44,13 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving, g
|
||||
case "preserve":
|
||||
newConfig.preserveOrgStructure = true;
|
||||
newConfig.mirrorStrategy = "preserve";
|
||||
newConfig.personalReposOrg = undefined; // Clear personal repos org in preserve mode
|
||||
break;
|
||||
case "single-org":
|
||||
newConfig.preserveOrgStructure = false;
|
||||
newConfig.mirrorStrategy = "single-org";
|
||||
if (!newConfig.organization) {
|
||||
// Reset to default if coming from mixed mode where it was personal repos org
|
||||
if (config.mirrorStrategy === "mixed" || !newConfig.organization || newConfig.organization === "github-personal") {
|
||||
newConfig.organization = "github-mirrors";
|
||||
}
|
||||
break;
|
||||
@@ -60,8 +62,10 @@ export function GiteaConfigForm({ config, setConfig, onAutoSave, isAutoSaving, g
|
||||
case "mixed":
|
||||
newConfig.preserveOrgStructure = false;
|
||||
newConfig.mirrorStrategy = "mixed";
|
||||
if (!newConfig.organization) {
|
||||
newConfig.organization = "github-mirrors";
|
||||
// In mixed mode, organization field represents personal repos org
|
||||
// Reset it to default if coming from single-org mode
|
||||
if (config.mirrorStrategy === "single-org" || !newConfig.organization || newConfig.organization === "github-mirrors") {
|
||||
newConfig.organization = "github-personal";
|
||||
}
|
||||
if (!newConfig.personalReposOrg) {
|
||||
newConfig.personalReposOrg = "github-personal";
|
||||
|
||||
@@ -104,7 +104,7 @@ export const OrganizationConfiguration: React.FC<OrganizationConfigurationProps>
|
||||
id="destinationOrg"
|
||||
value={destinationOrg || ""}
|
||||
onChange={(e) => onDestinationOrgChange(e.target.value)}
|
||||
placeholder="github-mirrors"
|
||||
placeholder={strategy === "mixed" ? "github-personal" : "github-mirrors"}
|
||||
className=""
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
@@ -114,32 +114,6 @@ export const OrganizationConfiguration: React.FC<OrganizationConfigurationProps>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
) : strategy === "preserve" ? (
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="personalReposOrg" className="text-sm font-normal flex items-center gap-2">
|
||||
Personal Repos Organization
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Info className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Override where your personal repositories are mirrored (leave empty to use your username)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Label>
|
||||
<Input
|
||||
id="personalReposOrg"
|
||||
value={personalReposOrg || ""}
|
||||
onChange={(e) => onPersonalReposOrgChange(e.target.value)}
|
||||
placeholder="my-personal-mirrors"
|
||||
className=""
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Override destination for your personal repos
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="hidden md:block" />
|
||||
)}
|
||||
|
||||
@@ -281,6 +281,7 @@ export function OrganizationList({
|
||||
<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" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -300,6 +301,14 @@ export function OrganizationList({
|
||||
</span>
|
||||
</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>
|
||||
|
||||
@@ -48,8 +48,8 @@ export function InlineDestinationEditor({
|
||||
if (repository.organization) {
|
||||
return repository.organization;
|
||||
}
|
||||
// For personal repos, check if personalReposOrg is configured
|
||||
if (!repository.organization && giteaConfig?.personalReposOrg) {
|
||||
// For personal repos, check if personalReposOrg is configured (but not in preserve mode)
|
||||
if (!repository.organization && giteaConfig?.personalReposOrg && strategy !== 'preserve') {
|
||||
return giteaConfig.personalReposOrg;
|
||||
}
|
||||
// Default to the gitea username or owner
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
type RepositoryApiResponse,
|
||||
type RepoStatus,
|
||||
} from "@/types/Repository";
|
||||
import { apiRequest, showErrorToast } from "@/lib/utils";
|
||||
import { apiRequest, showErrorToast, getStatusColor } from "@/lib/utils";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -707,12 +707,7 @@ export default function Repository() {
|
||||
<SelectItem key={status} value={status}>
|
||||
<span className="flex items-center gap-2">
|
||||
{status !== "all" && (
|
||||
<span className={`h-2 w-2 rounded-full ${
|
||||
status === "synced" ? "bg-green-500" :
|
||||
status === "failed" ? "bg-red-500" :
|
||||
status === "syncing" ? "bg-blue-500" :
|
||||
"bg-yellow-500"
|
||||
}`} />
|
||||
<span className={`h-2 w-2 rounded-full ${getStatusColor(status)}`} />
|
||||
)}
|
||||
{status === "all"
|
||||
? "All statuses"
|
||||
@@ -814,12 +809,7 @@ export default function Repository() {
|
||||
<SelectItem key={status} value={status}>
|
||||
<span className="flex items-center gap-2">
|
||||
{status !== "all" && (
|
||||
<span className={`h-2 w-2 rounded-full ${
|
||||
status === "synced" ? "bg-green-500" :
|
||||
status === "failed" ? "bg-red-500" :
|
||||
status === "syncing" ? "bg-blue-500" :
|
||||
"bg-yellow-500"
|
||||
}`} />
|
||||
<span className={`h-2 w-2 rounded-full ${getStatusColor(status)}`} />
|
||||
)}
|
||||
{status === "all"
|
||||
? "All statuses"
|
||||
|
||||
@@ -114,7 +114,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
// Create the context value
|
||||
const contextValue = {
|
||||
user: user as AuthUser | null,
|
||||
session,
|
||||
session: session as Session | null,
|
||||
isLoading: isLoading || betterAuthSession.isPending,
|
||||
error,
|
||||
login,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createAuthClient } from "better-auth/react";
|
||||
import { oidcClient } 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({
|
||||
// The base URL is optional when running on the same domain
|
||||
@@ -23,6 +24,12 @@ export const {
|
||||
getSession
|
||||
} = authClient;
|
||||
|
||||
// Export types
|
||||
export type Session = Awaited<ReturnType<typeof authClient.getSession>>["data"];
|
||||
export type AuthUser = Session extends { user: infer U } ? U : never;
|
||||
// Export types - directly use the types from better-auth
|
||||
export type Session = BetterAuthSession & {
|
||||
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),
|
||||
includeOrganizations: z.array(z.string()).default([]),
|
||||
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(),
|
||||
});
|
||||
|
||||
@@ -47,6 +47,13 @@ export const giteaConfigSchema = z.object({
|
||||
forkStrategy: z
|
||||
.enum(["skip", "reference", "full-copy"])
|
||||
.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({
|
||||
|
||||
@@ -312,13 +312,13 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
|
||||
skipStarredIssues: false
|
||||
},
|
||||
giteaConfig: {
|
||||
username: "giteauser",
|
||||
defaultOwner: "giteauser",
|
||||
url: "https://gitea.example.com",
|
||||
token: "gitea-token",
|
||||
organization: "github-mirrors",
|
||||
defaultOrg: "github-mirrors",
|
||||
visibility: "public",
|
||||
starredReposOrg: "starred",
|
||||
preserveOrgStructure: false,
|
||||
preserveVisibility: false,
|
||||
mirrorStrategy: "preserve"
|
||||
}
|
||||
};
|
||||
@@ -354,19 +354,21 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
|
||||
expect(result).toBe("starred");
|
||||
});
|
||||
|
||||
test("preserve strategy: personal repos use personalReposOrg override", () => {
|
||||
const configWithOverride = {
|
||||
test("starred repos default to 'starred' org when starredReposOrg is not configured", () => {
|
||||
const repo = { ...baseRepo, isStarred: true };
|
||||
const configWithoutStarredOrg = {
|
||||
...baseConfig,
|
||||
giteaConfig: {
|
||||
...baseConfig.giteaConfig!,
|
||||
personalReposOrg: "my-personal-mirrors"
|
||||
...baseConfig.giteaConfig,
|
||||
starredReposOrg: undefined
|
||||
}
|
||||
};
|
||||
const repo = { ...baseRepo, organization: undefined };
|
||||
const result = getGiteaRepoOwner({ config: configWithOverride, repository: repo });
|
||||
expect(result).toBe("my-personal-mirrors");
|
||||
const result = getGiteaRepoOwner({ config: configWithoutStarredOrg, repository: repo });
|
||||
expect(result).toBe("starred");
|
||||
});
|
||||
|
||||
// Removed test for personalReposOrg as this field no longer exists
|
||||
|
||||
test("preserve strategy: personal repos fallback to username when no override", () => {
|
||||
const repo = { ...baseRepo, organization: undefined };
|
||||
const result = getGiteaRepoOwner({ config: baseConfig, repository: repo });
|
||||
@@ -379,7 +381,7 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
|
||||
expect(result).toBe("myorg");
|
||||
});
|
||||
|
||||
test("mixed strategy: personal repos go to organization", () => {
|
||||
test("single-org strategy: personal repos go to defaultOrg", () => {
|
||||
const configWithMixed = {
|
||||
...baseConfig,
|
||||
giteaConfig: {
|
||||
@@ -393,7 +395,7 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
|
||||
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 = {
|
||||
...baseConfig,
|
||||
giteaConfig: {
|
||||
@@ -407,18 +409,16 @@ describe("getGiteaRepoOwner - Organization Override Tests", () => {
|
||||
expect(result).toBe("myorg");
|
||||
});
|
||||
|
||||
test("mixed strategy: fallback to username if no org configs", () => {
|
||||
const configWithMixed = {
|
||||
test("flat-user strategy: all repos go to defaultOwner", () => {
|
||||
const configWithFlatUser = {
|
||||
...baseConfig,
|
||||
giteaConfig: {
|
||||
...baseConfig.giteaConfig!,
|
||||
mirrorStrategy: "mixed" as const,
|
||||
organization: undefined,
|
||||
personalReposOrg: undefined
|
||||
mirrorStrategy: "flat-user" as const
|
||||
}
|
||||
};
|
||||
const repo = { ...baseRepo, organization: undefined };
|
||||
const result = getGiteaRepoOwner({ config: configWithMixed, repository: repo });
|
||||
const repo = { ...baseRepo, organization: "myorg" };
|
||||
const result = getGiteaRepoOwner({ config: configWithFlatUser, repository: repo });
|
||||
expect(result).toBe("giteauser");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,7 +64,7 @@ export const getGiteaRepoOwnerAsync = async ({
|
||||
throw new Error("GitHub or Gitea config is required.");
|
||||
}
|
||||
|
||||
if (!config.giteaConfig.username) {
|
||||
if (!config.giteaConfig.defaultOwner) {
|
||||
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)
|
||||
if (repository.isStarred && config.giteaConfig.starredReposOrg) {
|
||||
return config.giteaConfig.starredReposOrg;
|
||||
if (repository.isStarred) {
|
||||
return config.giteaConfig.starredReposOrg || "starred";
|
||||
}
|
||||
|
||||
// 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)
|
||||
if (!repository.organization && config.giteaConfig.personalReposOrg) {
|
||||
console.log(`Using personal repos override: ${config.giteaConfig.personalReposOrg}`);
|
||||
return config.giteaConfig.personalReposOrg;
|
||||
}
|
||||
// For personal repos (not organization repos), fall back to the default strategy
|
||||
|
||||
// Fall back to existing strategy logic
|
||||
return getGiteaRepoOwner({ config, repository });
|
||||
@@ -117,13 +113,13 @@ export const getGiteaRepoOwner = ({
|
||||
throw new Error("GitHub or Gitea config is required.");
|
||||
}
|
||||
|
||||
if (!config.giteaConfig.username) {
|
||||
if (!config.giteaConfig.defaultOwner) {
|
||||
throw new Error("Gitea username is required.");
|
||||
}
|
||||
|
||||
// Check if repository is starred - starred repos always go to starredReposOrg
|
||||
if (repository.isStarred && config.giteaConfig.starredReposOrg) {
|
||||
return config.giteaConfig.starredReposOrg;
|
||||
if (repository.isStarred) {
|
||||
return config.giteaConfig.starredReposOrg || "starred";
|
||||
}
|
||||
|
||||
// Get the mirror strategy - use preserveOrgStructure for backward compatibility
|
||||
@@ -137,7 +133,7 @@ export const getGiteaRepoOwner = ({
|
||||
return repository.organization;
|
||||
}
|
||||
// Use personal repos override if configured, otherwise use username
|
||||
return config.giteaConfig.personalReposOrg || config.giteaConfig.username;
|
||||
return config.giteaConfig.defaultOwner;
|
||||
|
||||
case "single-org":
|
||||
// All non-starred repos go to the destination organization
|
||||
@@ -145,11 +141,11 @@ export const getGiteaRepoOwner = ({
|
||||
return config.giteaConfig.organization;
|
||||
}
|
||||
// Fallback to username if no organization specified
|
||||
return config.giteaConfig.username;
|
||||
return config.giteaConfig.defaultOwner;
|
||||
|
||||
case "flat-user":
|
||||
// All non-starred repos go under the user account
|
||||
return config.giteaConfig.username;
|
||||
return config.giteaConfig.defaultOwner;
|
||||
|
||||
case "mixed":
|
||||
// Mixed mode: personal repos to single org, organization repos preserve structure
|
||||
@@ -162,11 +158,11 @@ export const getGiteaRepoOwner = ({
|
||||
return config.giteaConfig.organization;
|
||||
}
|
||||
// Fallback to username if no organization specified
|
||||
return config.giteaConfig.username;
|
||||
return config.giteaConfig.defaultOwner;
|
||||
|
||||
default:
|
||||
// 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.");
|
||||
}
|
||||
|
||||
if (!config.giteaConfig.username) {
|
||||
if (!config.giteaConfig.defaultOwner) {
|
||||
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)
|
||||
const repoOwner = await getGiteaRepoOwnerAsync({ config, repository });
|
||||
|
||||
@@ -347,14 +346,14 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
|
||||
cloneAddress = repository.cloneUrl.replace(
|
||||
"https://",
|
||||
`https://${config.githubConfig.token}@`
|
||||
`https://${decryptedConfig.githubConfig.token}@`
|
||||
);
|
||||
}
|
||||
|
||||
const apiUrl = `${config.giteaConfig.url}/api/v1/repos/migrate`;
|
||||
|
||||
// Handle organization creation if needed for single-org or preserve strategies
|
||||
if (repoOwner !== config.giteaConfig.username && !repository.isStarred) {
|
||||
// Handle organization creation if needed for single-org, preserve strategies, or starred repos
|
||||
if (repoOwner !== config.giteaConfig.defaultOwner) {
|
||||
// Need to create the organization if it doesn't exist
|
||||
await getOrCreateGiteaOrg({
|
||||
orgName: repoOwner,
|
||||
@@ -380,11 +379,13 @@ export const mirrorGithubRepoToGitea = async ({
|
||||
);
|
||||
|
||||
//mirror releases
|
||||
await mirrorGitHubReleasesToGitea({
|
||||
config,
|
||||
octokit,
|
||||
repository,
|
||||
});
|
||||
if (config.githubConfig?.mirrorReleases) {
|
||||
await mirrorGitHubReleasesToGitea({
|
||||
config,
|
||||
octokit,
|
||||
repository,
|
||||
});
|
||||
}
|
||||
|
||||
// clone issues
|
||||
// Skip issues for starred repos if skipStarredIssues is enabled
|
||||
@@ -644,6 +645,9 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
throw new Error("Gitea config is required.");
|
||||
}
|
||||
|
||||
// Decrypt config tokens for API usage
|
||||
const decryptedConfig = decryptConfigTokens(config as Config);
|
||||
|
||||
const isExisting = await isRepoPresentInGitea({
|
||||
config,
|
||||
owner: orgName,
|
||||
@@ -698,7 +702,7 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
|
||||
cloneAddress = repository.cloneUrl.replace(
|
||||
"https://",
|
||||
`https://${config.githubConfig.token}@`
|
||||
`https://${decryptedConfig.githubConfig.token}@`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -732,11 +736,13 @@ export async function mirrorGitHubRepoToGiteaOrg({
|
||||
);
|
||||
|
||||
//mirror releases
|
||||
await mirrorGitHubReleasesToGitea({
|
||||
config,
|
||||
octokit,
|
||||
repository,
|
||||
});
|
||||
if (config.githubConfig?.mirrorReleases) {
|
||||
await mirrorGitHubReleasesToGitea({
|
||||
config,
|
||||
octokit,
|
||||
repository,
|
||||
});
|
||||
}
|
||||
|
||||
// Clone issues
|
||||
// Skip issues for starred repos if skipStarredIssues is enabled
|
||||
@@ -900,7 +906,7 @@ export async function mirrorGitHubOrgToGitea({
|
||||
// Determine the target organization based on strategy
|
||||
if (mirrorStrategy === "single-org" && config.giteaConfig?.organization) {
|
||||
// For single-org strategy, use the configured destination organization
|
||||
targetOrgName = config.giteaConfig.organization;
|
||||
targetOrgName = config.giteaConfig.defaultOrg || config.giteaConfig.defaultOwner;
|
||||
giteaOrgId = await getOrCreateGiteaOrg({
|
||||
orgId: organization.id,
|
||||
orgName: targetOrgName,
|
||||
@@ -919,7 +925,7 @@ export async function mirrorGitHubOrgToGitea({
|
||||
// For flat-user strategy, we shouldn't create organizations at all
|
||||
// Skip organization creation and let individual repos be handled by getGiteaRepoOwner
|
||||
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
|
||||
@@ -1076,7 +1082,7 @@ export const syncGiteaRepo = async ({
|
||||
!config.userId ||
|
||||
!config.giteaConfig?.url ||
|
||||
!config.giteaConfig?.token ||
|
||||
!config.giteaConfig?.username
|
||||
!config.giteaConfig?.defaultOwner
|
||||
) {
|
||||
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 response = await httpPost(apiUrl, undefined, {
|
||||
Authorization: `token ${config.giteaConfig.token}`,
|
||||
Authorization: `token ${decryptedConfig.giteaConfig.token}`,
|
||||
});
|
||||
|
||||
// Mark repo as "synced" in DB
|
||||
@@ -1243,7 +1249,7 @@ export const mirrorGitRepoIssuesToGitea = async ({
|
||||
const giteaLabelsRes = await httpGet(
|
||||
`${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>;
|
||||
}) {
|
||||
if (
|
||||
!config.giteaConfig?.username ||
|
||||
!config.giteaConfig?.defaultOwner ||
|
||||
!config.giteaConfig?.token ||
|
||||
!config.giteaConfig?.url
|
||||
) {
|
||||
|
||||
@@ -52,13 +52,11 @@ export async function getGithubRepositories({
|
||||
{ per_page: 100 }
|
||||
);
|
||||
|
||||
const includePrivate = config.githubConfig?.privateRepositories ?? false;
|
||||
const skipForks = config.githubConfig?.skipForks ?? false;
|
||||
|
||||
const filteredRepos = repos.filter((repo) => {
|
||||
const isPrivateAllowed = includePrivate || !repo.private;
|
||||
const isForkAllowed = !skipForks || !repo.fork;
|
||||
return isPrivateAllowed && isForkAllowed;
|
||||
return isForkAllowed;
|
||||
});
|
||||
|
||||
return filteredRepos.map((repo) => ({
|
||||
|
||||
@@ -197,17 +197,17 @@ export async function apiRequest<T>(
|
||||
export const getStatusColor = (status: string): string => {
|
||||
switch (status) {
|
||||
case "imported":
|
||||
return "bg-blue-500"; // Info/primary-like
|
||||
return "bg-yellow-500"; // Ready to mirror
|
||||
case "mirroring":
|
||||
return "bg-yellow-400"; // In progress
|
||||
return "bg-amber-500"; // In progress
|
||||
case "mirrored":
|
||||
return "bg-emerald-500"; // Success
|
||||
return "bg-green-500"; // Successfully mirrored
|
||||
case "failed":
|
||||
return "bg-rose-500"; // Error
|
||||
return "bg-red-500"; // Error
|
||||
case "syncing":
|
||||
return "bg-indigo-500"; // Sync in progress
|
||||
return "bg-blue-500"; // Sync in progress
|
||||
case "synced":
|
||||
return "bg-teal-500"; // Sync complete
|
||||
return "bg-emerald-500"; // Successfully synced
|
||||
case "skipped":
|
||||
return "bg-gray-500"; // Skipped
|
||||
case "deleting":
|
||||
|
||||
@@ -9,35 +9,14 @@ import type {
|
||||
AdvancedOptions,
|
||||
SaveConfigApiRequest
|
||||
} from "@/types/config";
|
||||
import { z } from "zod";
|
||||
import { githubConfigSchema, giteaConfigSchema, scheduleConfigSchema, cleanupConfigSchema } from "@/lib/db/schema";
|
||||
|
||||
interface DbGitHubConfig {
|
||||
username: string;
|
||||
token?: string;
|
||||
skipForks: boolean;
|
||||
privateRepositories: boolean;
|
||||
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;
|
||||
}
|
||||
// Use the actual database schema types
|
||||
type DbGitHubConfig = z.infer<typeof githubConfigSchema>;
|
||||
type DbGiteaConfig = z.infer<typeof giteaConfigSchema>;
|
||||
type DbScheduleConfig = z.infer<typeof scheduleConfigSchema>;
|
||||
type DbCleanupConfig = z.infer<typeof cleanupConfigSchema>;
|
||||
|
||||
/**
|
||||
* Maps UI config structure to database schema structure
|
||||
@@ -48,32 +27,67 @@ export function mapUiToDbConfig(
|
||||
mirrorOptions: MirrorOptions,
|
||||
advancedOptions: AdvancedOptions
|
||||
): { githubConfig: DbGitHubConfig; giteaConfig: DbGiteaConfig } {
|
||||
// Map GitHub config with fields from mirrorOptions and advancedOptions
|
||||
// Map GitHub config to match database schema fields
|
||||
const dbGithubConfig: DbGitHubConfig = {
|
||||
username: githubConfig.username,
|
||||
token: githubConfig.token,
|
||||
privateRepositories: githubConfig.privateRepositories,
|
||||
mirrorStarred: githubConfig.mirrorStarred,
|
||||
// Map username to owner field
|
||||
owner: githubConfig.username,
|
||||
type: "personal", // Default to personal, could be made configurable
|
||||
token: githubConfig.token || "",
|
||||
|
||||
// From mirrorOptions
|
||||
mirrorIssues: mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.issues,
|
||||
mirrorWiki: mirrorOptions.mirrorMetadata && mirrorOptions.metadataComponents.wiki,
|
||||
// Map checkbox fields with proper names
|
||||
includeStarred: githubConfig.mirrorStarred,
|
||||
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
|
||||
skipForks: advancedOptions.skipForks,
|
||||
skipStarredIssues: advancedOptions.skipStarredIssues,
|
||||
// Organization related fields
|
||||
includeOrganizations: [], // Not in UI yet
|
||||
|
||||
// Default values for fields not in UI
|
||||
useSpecificUser: false,
|
||||
includeOrgs: [],
|
||||
excludeOrgs: [],
|
||||
mirrorPublicOrgs: false,
|
||||
publicOrgs: [],
|
||||
// Starred repos organization
|
||||
starredReposOrg: giteaConfig.starredReposOrg,
|
||||
|
||||
// Mirror strategy
|
||||
mirrorStrategy: giteaConfig.mirrorStrategy || "preserve",
|
||||
defaultOrg: giteaConfig.organization,
|
||||
};
|
||||
|
||||
// Gitea config remains mostly the same
|
||||
// Map Gitea config to match database schema
|
||||
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 {
|
||||
@@ -91,40 +105,44 @@ export function mapDbToUiConfig(dbConfig: any): {
|
||||
mirrorOptions: MirrorOptions;
|
||||
advancedOptions: AdvancedOptions;
|
||||
} {
|
||||
// Map from database GitHub config to UI fields
|
||||
const githubConfig: GitHubConfig = {
|
||||
username: dbConfig.githubConfig?.username || "",
|
||||
username: dbConfig.githubConfig?.owner || "", // Map owner to username
|
||||
token: dbConfig.githubConfig?.token || "",
|
||||
privateRepositories: dbConfig.githubConfig?.privateRepositories || false,
|
||||
mirrorStarred: dbConfig.githubConfig?.mirrorStarred || false,
|
||||
privateRepositories: dbConfig.githubConfig?.includePrivate || false, // Map includePrivate to privateRepositories
|
||||
mirrorStarred: dbConfig.githubConfig?.includeStarred || false, // Map includeStarred to mirrorStarred
|
||||
};
|
||||
|
||||
// Map from database Gitea config to UI fields
|
||||
const giteaConfig: GiteaConfig = {
|
||||
url: dbConfig.giteaConfig?.url || "",
|
||||
username: dbConfig.giteaConfig?.username || "",
|
||||
username: dbConfig.giteaConfig?.defaultOwner || "", // Map defaultOwner to username
|
||||
token: dbConfig.giteaConfig?.token || "",
|
||||
organization: dbConfig.giteaConfig?.organization || "github-mirrors",
|
||||
visibility: dbConfig.giteaConfig?.visibility || "public",
|
||||
starredReposOrg: dbConfig.giteaConfig?.starredReposOrg || "github",
|
||||
preserveOrgStructure: dbConfig.giteaConfig?.preserveOrgStructure || false,
|
||||
mirrorStrategy: dbConfig.giteaConfig?.mirrorStrategy,
|
||||
personalReposOrg: dbConfig.giteaConfig?.personalReposOrg,
|
||||
organization: dbConfig.githubConfig?.defaultOrg || "github-mirrors", // Get from GitHub config
|
||||
visibility: dbConfig.giteaConfig?.visibility === "default" ? "public" : dbConfig.giteaConfig?.visibility || "public",
|
||||
starredReposOrg: dbConfig.githubConfig?.starredReposOrg || "starred", // Get from GitHub config
|
||||
preserveOrgStructure: dbConfig.giteaConfig?.preserveVisibility || false, // Map preserveVisibility
|
||||
mirrorStrategy: dbConfig.githubConfig?.mirrorStrategy || "preserve", // Get from GitHub config
|
||||
personalReposOrg: undefined, // Not stored in current schema
|
||||
};
|
||||
|
||||
// Map mirror options from various database fields
|
||||
const mirrorOptions: MirrorOptions = {
|
||||
mirrorReleases: false, // Not stored in DB yet
|
||||
mirrorMetadata: dbConfig.githubConfig?.mirrorIssues || dbConfig.githubConfig?.mirrorWiki || false,
|
||||
mirrorReleases: dbConfig.giteaConfig?.mirrorReleases || false,
|
||||
mirrorMetadata: dbConfig.giteaConfig?.mirrorMetadata || false,
|
||||
metadataComponents: {
|
||||
issues: dbConfig.githubConfig?.mirrorIssues || false,
|
||||
pullRequests: false, // Not stored in DB yet
|
||||
labels: false, // Not stored in DB yet
|
||||
milestones: false, // Not stored in DB yet
|
||||
wiki: dbConfig.githubConfig?.mirrorWiki || false,
|
||||
issues: dbConfig.giteaConfig?.mirrorIssues || false,
|
||||
pullRequests: dbConfig.giteaConfig?.mirrorPullRequests || false,
|
||||
labels: dbConfig.giteaConfig?.mirrorLabels || false,
|
||||
milestones: dbConfig.giteaConfig?.mirrorMilestones || false,
|
||||
wiki: dbConfig.giteaConfig?.wiki || false,
|
||||
},
|
||||
};
|
||||
|
||||
// Map advanced options
|
||||
const advancedOptions: AdvancedOptions = {
|
||||
skipForks: dbConfig.githubConfig?.skipForks || false,
|
||||
skipStarredIssues: dbConfig.githubConfig?.skipStarredIssues || false,
|
||||
skipForks: !(dbConfig.githubConfig?.includeForks ?? true), // Invert includeForks to get skipForks
|
||||
skipStarredIssues: false, // Not stored in current schema
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -133,4 +151,73 @@ export function mapDbToUiConfig(dbConfig: any): {
|
||||
mirrorOptions,
|
||||
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 { calculateCleanupInterval } from "@/lib/cleanup-service";
|
||||
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";
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
@@ -78,62 +85,9 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
mappedGiteaConfig.token = encrypt(mappedGiteaConfig.token);
|
||||
}
|
||||
|
||||
// Process schedule config - set/update nextRun if enabled, clear if disabled
|
||||
const processedScheduleConfig = { ...scheduleConfig };
|
||||
if (scheduleConfig.enabled) {
|
||||
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;
|
||||
}
|
||||
// Map schedule and cleanup configs to database schema
|
||||
const processedScheduleConfig = mapUiScheduleToDb(scheduleConfig);
|
||||
const processedCleanupConfig = mapUiCleanupToDb(cleanupConfig);
|
||||
|
||||
if (existingConfig) {
|
||||
// Update path
|
||||
@@ -234,28 +188,34 @@ export const GET: APIRoute = async ({ request }) => {
|
||||
.limit(1);
|
||||
|
||||
if (config.length === 0) {
|
||||
// Return a default empty configuration with UI structure
|
||||
// Return a default empty configuration with database structure
|
||||
const defaultDbConfig = {
|
||||
githubConfig: {
|
||||
username: "",
|
||||
owner: "",
|
||||
type: "personal",
|
||||
token: "",
|
||||
skipForks: false,
|
||||
privateRepositories: false,
|
||||
mirrorIssues: false,
|
||||
mirrorWiki: false,
|
||||
mirrorStarred: false,
|
||||
useSpecificUser: false,
|
||||
preserveOrgStructure: false,
|
||||
skipStarredIssues: false,
|
||||
includeStarred: false,
|
||||
includeForks: true,
|
||||
includeArchived: false,
|
||||
includePrivate: false,
|
||||
includePublic: true,
|
||||
includeOrganizations: [],
|
||||
starredReposOrg: "starred",
|
||||
mirrorStrategy: "preserve",
|
||||
defaultOrg: "github-mirrors",
|
||||
},
|
||||
giteaConfig: {
|
||||
url: "",
|
||||
token: "",
|
||||
username: "",
|
||||
organization: "github-mirrors",
|
||||
defaultOwner: "",
|
||||
mirrorInterval: "8h",
|
||||
lfs: false,
|
||||
wiki: false,
|
||||
visibility: "public",
|
||||
starredReposOrg: "github",
|
||||
preserveOrgStructure: false,
|
||||
createOrg: true,
|
||||
addTopics: true,
|
||||
preserveVisibility: false,
|
||||
forkStrategy: "reference",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -319,9 +279,23 @@ export const GET: APIRoute = async ({ request }) => {
|
||||
|
||||
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({
|
||||
...dbConfig,
|
||||
...uiConfig,
|
||||
scheduleConfig: {
|
||||
...uiScheduleConfig,
|
||||
lastRun: dbConfig.scheduleConfig.lastRun,
|
||||
nextRun: dbConfig.scheduleConfig.nextRun,
|
||||
},
|
||||
cleanupConfig: {
|
||||
...uiCleanupConfig,
|
||||
lastRun: dbConfig.cleanupConfig.lastRun,
|
||||
nextRun: dbConfig.cleanupConfig.nextRun,
|
||||
},
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -330,9 +304,22 @@ export const GET: APIRoute = async ({ request }) => {
|
||||
console.error("Failed to decrypt tokens:", error);
|
||||
// Return config without decrypting tokens if there's an error
|
||||
const uiConfig = mapDbToUiConfig(dbConfig);
|
||||
const uiScheduleConfig = mapDbScheduleToUi(dbConfig.scheduleConfig);
|
||||
const uiCleanupConfig = mapDbCleanupToUi(dbConfig.cleanupConfig);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
...dbConfig,
|
||||
...uiConfig,
|
||||
scheduleConfig: {
|
||||
...uiScheduleConfig,
|
||||
lastRun: dbConfig.scheduleConfig.lastRun,
|
||||
nextRun: dbConfig.scheduleConfig.nextRun,
|
||||
},
|
||||
cleanupConfig: {
|
||||
...uiCleanupConfig,
|
||||
lastRun: dbConfig.cleanupConfig.lastRun,
|
||||
nextRun: dbConfig.cleanupConfig.nextRun,
|
||||
},
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
||||
@@ -66,54 +66,39 @@ export const GET: APIRoute = async ({ request }) => {
|
||||
baseConditions.push(eq(repositories.isStarred, false));
|
||||
}
|
||||
|
||||
// Get total count with all user config filters applied
|
||||
const totalConditions = [...baseConditions];
|
||||
if (githubConfig.skipForks) {
|
||||
totalConditions.push(eq(repositories.isForked, false));
|
||||
}
|
||||
if (!githubConfig.privateRepositories) {
|
||||
totalConditions.push(eq(repositories.isPrivate, false));
|
||||
}
|
||||
|
||||
// Get actual total count (without user config filters)
|
||||
const [totalCount] = await db
|
||||
.select({ count: count() })
|
||||
.from(repositories)
|
||||
.where(and(...totalConditions));
|
||||
|
||||
// Get public count
|
||||
const publicConditions = [...baseConditions, eq(repositories.isPrivate, false)];
|
||||
if (githubConfig.skipForks) {
|
||||
publicConditions.push(eq(repositories.isForked, false));
|
||||
}
|
||||
.where(and(...baseConditions));
|
||||
|
||||
// Get public count (actual count, not filtered)
|
||||
const [publicCount] = await db
|
||||
.select({ count: count() })
|
||||
.from(repositories)
|
||||
.where(and(...publicConditions));
|
||||
.where(and(...baseConditions, eq(repositories.isPrivate, false)));
|
||||
|
||||
// Get private count (only if private repos are enabled in config)
|
||||
const [privateCount] = githubConfig.privateRepositories ? await db
|
||||
// Get private count (always show actual count regardless of config)
|
||||
const [privateCount] = await db
|
||||
.select({ count: count() })
|
||||
.from(repositories)
|
||||
.where(
|
||||
and(
|
||||
...baseConditions,
|
||||
eq(repositories.isPrivate, true),
|
||||
...(githubConfig.skipForks ? [eq(repositories.isForked, false)] : [])
|
||||
eq(repositories.isPrivate, true)
|
||||
)
|
||||
) : [{ count: 0 }];
|
||||
);
|
||||
|
||||
// Get fork count (only if forks are enabled in config)
|
||||
const [forkCount] = !githubConfig.skipForks ? await db
|
||||
// Get fork count (always show actual count regardless of config)
|
||||
const [forkCount] = await db
|
||||
.select({ count: count() })
|
||||
.from(repositories)
|
||||
.where(
|
||||
and(
|
||||
...baseConditions,
|
||||
eq(repositories.isForked, true),
|
||||
...(!githubConfig.privateRepositories ? [eq(repositories.isPrivate, false)] : [])
|
||||
eq(repositories.isForked, true)
|
||||
)
|
||||
) : [{ count: 0 }];
|
||||
);
|
||||
|
||||
return {
|
||||
...org,
|
||||
|
||||
@@ -45,17 +45,10 @@ export const GET: APIRoute = async ({ request }) => {
|
||||
// Build query conditions based on config
|
||||
const conditions = [eq(repositories.userId, userId)];
|
||||
|
||||
if (!githubConfig.mirrorStarred) {
|
||||
conditions.push(eq(repositories.isStarred, false));
|
||||
}
|
||||
|
||||
if (githubConfig.skipForks) {
|
||||
conditions.push(eq(repositories.isForked, false));
|
||||
}
|
||||
|
||||
if (!githubConfig.privateRepositories) {
|
||||
conditions.push(eq(repositories.isPrivate, false));
|
||||
}
|
||||
// Note: We show ALL repositories in the list
|
||||
// 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
|
||||
|
||||
const rawRepositories = await db
|
||||
.select()
|
||||
|
||||
@@ -113,7 +113,7 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
(config.githubConfig?.preserveOrgStructure ? "preserve" : "flat-user");
|
||||
|
||||
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
|
||||
repoData.isStarred; // Starred repos always go to org
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
(config.githubConfig?.preserveOrgStructure ? "preserve" : "flat-user");
|
||||
|
||||
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
|
||||
repoData.isStarred; // Starred repos always go to org
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import type {
|
||||
} from "@/types/organizations";
|
||||
import type { RepositoryVisibility, RepoStatus } from "@/types/Repository";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { decryptConfigTokens } from "@/lib/utils/config-encryption";
|
||||
import { createGitHubClient } from "@/lib/github";
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
@@ -44,32 +46,67 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
const [config] = await db
|
||||
.select()
|
||||
.from(configs)
|
||||
.where(eq(configs.userId, userId))
|
||||
.where(and(eq(configs.userId, userId), eq(configs.isActive, true)))
|
||||
.limit(1);
|
||||
|
||||
if (!config) {
|
||||
return jsonResponse({
|
||||
data: { error: "No configuration found for this user" },
|
||||
data: { error: "No active configuration found for this user" },
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
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, {
|
||||
org,
|
||||
type: "public",
|
||||
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
|
||||
const repoRecords = publicRepos.map((repo) => ({
|
||||
const repoRecords = allRepos.map((repo) => ({
|
||||
id: uuidv4(),
|
||||
userId,
|
||||
configId,
|
||||
@@ -110,7 +147,7 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
membershipRole: role,
|
||||
isIncluded: false,
|
||||
status: "imported" as RepoStatus,
|
||||
repositoryCount: publicRepos.length,
|
||||
repositoryCount: allRepos.length,
|
||||
createdAt: orgData.created_at ? new Date(orgData.created_at) : new Date(),
|
||||
updatedAt: orgData.updated_at ? new Date(orgData.updated_at) : new Date(),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user