mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
fix: clone permissions for private repos (#190)
- reverted back to Flask development server for development - improved / suppressed git logging - fixed cloning for private repos by including PAT in clone job - add 'private repo' badge that replaces repository stats for private repos
This commit is contained in:
@@ -4,4 +4,4 @@ COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY . .
|
||||
# Use gunicorn with 10-minute timeout
|
||||
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--timeout", "600", "app.main:create_app()"]
|
||||
CMD ["python", "-m", "app.main"]
|
||||
@@ -15,12 +15,24 @@ class GitHubAuth:
|
||||
def get_authenticated_url(https_url):
|
||||
"""
|
||||
Convert an HTTPS URL to include authentication via PAT.
|
||||
Ensures the token is not duplicated in the URL.
|
||||
"""
|
||||
token = os.getenv("PROFILARR_PAT")
|
||||
if not token:
|
||||
raise ValueError(
|
||||
"PROFILARR_PAT is not set in environment variables")
|
||||
|
||||
|
||||
# Check if the URL already contains authentication
|
||||
if "@" in https_url:
|
||||
# Already has some form of authentication, remove it to add our token
|
||||
# This handles URLs that might have a token already
|
||||
protocol_part, rest = https_url.split("://", 1)
|
||||
if "@" in rest:
|
||||
# Remove any existing authentication
|
||||
_, server_part = rest.split("@", 1)
|
||||
https_url = f"{protocol_part}://{server_part}"
|
||||
|
||||
# Now add our token
|
||||
authenticated_url = https_url.replace("https://", f"https://{token}@")
|
||||
return authenticated_url
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import logging
|
||||
import yaml
|
||||
from git.exc import GitCommandError
|
||||
import git
|
||||
from ..auth.authenticate import GitHubAuth
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -19,16 +20,35 @@ def clone_repository(repo_url, repo_path):
|
||||
# Initial clone attempt
|
||||
logger.info(f"Starting clone operation for {repo_url}")
|
||||
try:
|
||||
# First try without authentication (for public repos)
|
||||
repo = git.Repo.clone_from(repo_url, temp_dir)
|
||||
logger.info("Repository clone successful")
|
||||
except GitCommandError as e:
|
||||
if "remote: Repository not found" in str(e):
|
||||
error_str = str(e)
|
||||
# If authentication error, try with token
|
||||
if "could not read Username" in error_str or "Authentication failed" in error_str:
|
||||
logger.info("Initial clone failed due to authentication. Trying with token...")
|
||||
try:
|
||||
# Verify token availability
|
||||
if not GitHubAuth.verify_token():
|
||||
logger.error("Private repository requires GitHub authentication. Please configure PAT.")
|
||||
return False, "This appears to be a private repository. Please configure PROFILARR_PAT environment variable."
|
||||
|
||||
# Get authenticated URL for private repositories
|
||||
authenticated_url = GitHubAuth.get_authenticated_url(repo_url)
|
||||
repo = git.Repo.clone_from(authenticated_url, temp_dir)
|
||||
logger.info("Repository clone with authentication successful")
|
||||
except GitCommandError as auth_e:
|
||||
logger.error(f"Clone with authentication failed: {str(auth_e)}")
|
||||
return False, f"Failed to clone repository: {str(auth_e)}"
|
||||
# If repository not found, create new one
|
||||
elif "remote: Repository not found" in error_str:
|
||||
logger.info("Creating new repository - remote not found")
|
||||
repo = git.Repo.init(temp_dir)
|
||||
repo.create_remote('origin', repo_url)
|
||||
else:
|
||||
logger.error(f"Clone failed: {str(e)}")
|
||||
return False, f"Failed to clone repository: {str(e)}"
|
||||
logger.error(f"Clone failed: {error_str}")
|
||||
return False, f"Failed to clone repository: {error_str}"
|
||||
|
||||
# Check if repo is empty
|
||||
try:
|
||||
|
||||
@@ -85,6 +85,11 @@ def setup_logging():
|
||||
'level': 'INFO',
|
||||
'handlers': ['console', 'file'],
|
||||
'propagate': False
|
||||
},
|
||||
'git': {
|
||||
'level': 'ERROR',
|
||||
'handlers': ['console', 'file'],
|
||||
'propagate': False
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,20 @@ const RepoStats = ({repoStats}) => {
|
||||
if (!repoStats) {
|
||||
return <Loader size={14} className='animate-spin' />;
|
||||
}
|
||||
|
||||
// For private repositories or when stats can't be loaded
|
||||
if (repoStats.isPrivate) {
|
||||
return (
|
||||
<div className='flex items-center space-x-1'>
|
||||
<span className='text-xs px-2 py-0.5 rounded-md bg-gray-700/70 border border-gray-600 text-gray-300 flex items-center'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3 mr-1" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
Private
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -21,9 +21,56 @@ const LinkRepo = ({isOpen, onClose, onSubmit}) => {
|
||||
);
|
||||
onSubmit();
|
||||
} catch (error) {
|
||||
Alert.error(
|
||||
'An unexpected error occurred while linking the repository.'
|
||||
);
|
||||
// Check for specific error cases
|
||||
if (error.response) {
|
||||
const { status, data } = error.response;
|
||||
|
||||
if (data && data.error) {
|
||||
// Authentication errors for private repos
|
||||
if (data.error.includes("could not read Username") ||
|
||||
data.error.includes("Authentication failed") ||
|
||||
data.error.includes("authentication") ||
|
||||
data.error.includes("PROFILARR_PAT")) {
|
||||
Alert.error(
|
||||
'Authentication failed. Please ensure you have configured a valid GitHub Personal Access Token in your .env file (PROFILARR_PAT).'
|
||||
);
|
||||
}
|
||||
// Repository not found
|
||||
else if (data.error.includes("not found") || status === 404) {
|
||||
Alert.error(
|
||||
'Repository not found. Please check the URL and ensure you have access to this repository.'
|
||||
);
|
||||
}
|
||||
// Permission issues (general 403)
|
||||
else if (data.error.includes("permission") ||
|
||||
data.error.includes("error: 403") ||
|
||||
status === 403) {
|
||||
Alert.error(
|
||||
'Permission denied. Your GitHub token may not have sufficient permissions to access this repository. Ensure it has "Contents: Read & write" permission.'
|
||||
);
|
||||
}
|
||||
// Write access issues - specifically check for this common error
|
||||
else if (data.error.includes("remote: Write access to repository not granted")) {
|
||||
Alert.error(
|
||||
'Your GitHub token does not have write access to this repository. Please update your token with the "Contents: Read & write" permission.'
|
||||
);
|
||||
}
|
||||
// Any other error - use a generic message rather than showing the raw error
|
||||
else {
|
||||
Alert.error(
|
||||
'Failed to link repository. Please check the URL and your GitHub token permissions.'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// HTTP error without specific message
|
||||
Alert.error(`Failed to link repository (Error ${status})`);
|
||||
}
|
||||
} else {
|
||||
// Network or other errors
|
||||
Alert.error(
|
||||
'Failed to connect to the server. Please check your network connection.'
|
||||
);
|
||||
}
|
||||
console.error('Error linking repository:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
||||
@@ -84,42 +84,94 @@ const RepoContainer = ({settings, setSettings, fetchGitStatus, status}) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const [avatarResponse, repoResponse] = await Promise.all([
|
||||
!isAvatarValid &&
|
||||
fetch(`https://api.github.com/users/${owner}`),
|
||||
!isStatsValid &&
|
||||
fetch(`https://api.github.com/repos/${owner}/${repo}`)
|
||||
]);
|
||||
|
||||
if (!isAvatarValid && avatarResponse.ok) {
|
||||
const userData = await avatarResponse.json();
|
||||
localStorage.setItem(avatarCacheKey, userData.avatar_url);
|
||||
localStorage.setItem(
|
||||
`${avatarCacheKey}-timestamp`,
|
||||
Date.now().toString()
|
||||
);
|
||||
setAvatarUrl(userData.avatar_url);
|
||||
// Try to fetch the avatar if needed
|
||||
if (!isAvatarValid) {
|
||||
try {
|
||||
const avatarResponse = await fetch(`https://api.github.com/users/${owner}`);
|
||||
|
||||
if (avatarResponse.ok) {
|
||||
const userData = await avatarResponse.json();
|
||||
localStorage.setItem(avatarCacheKey, userData.avatar_url);
|
||||
localStorage.setItem(
|
||||
`${avatarCacheKey}-timestamp`,
|
||||
Date.now().toString()
|
||||
);
|
||||
setAvatarUrl(userData.avatar_url);
|
||||
} else if (avatarResponse.status === 404) {
|
||||
// User not found - use first letter avatar
|
||||
setAvatarUrl(null);
|
||||
}
|
||||
} catch (avatarError) {
|
||||
console.warn('Could not fetch avatar:', avatarError);
|
||||
// Keep using cached avatar if available
|
||||
if (cachedAvatar) setAvatarUrl(cachedAvatar);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isStatsValid && repoResponse.ok) {
|
||||
const repoData = await repoResponse.json();
|
||||
const stats = {
|
||||
stars: repoData.stargazers_count,
|
||||
forks: repoData.forks_count,
|
||||
issues: repoData.open_issues_count
|
||||
};
|
||||
localStorage.setItem(statsCacheKey, JSON.stringify(stats));
|
||||
localStorage.setItem(
|
||||
`${statsCacheKey}-timestamp`,
|
||||
Date.now().toString()
|
||||
);
|
||||
setRepoStats(stats);
|
||||
|
||||
// Try to fetch repo stats if needed
|
||||
if (!isStatsValid) {
|
||||
try {
|
||||
const repoResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}`);
|
||||
|
||||
if (repoResponse.ok) {
|
||||
const repoData = await repoResponse.json();
|
||||
const stats = {
|
||||
stars: repoData.stargazers_count,
|
||||
forks: repoData.forks_count,
|
||||
issues: repoData.open_issues_count
|
||||
};
|
||||
localStorage.setItem(statsCacheKey, JSON.stringify(stats));
|
||||
localStorage.setItem(
|
||||
`${statsCacheKey}-timestamp`,
|
||||
Date.now().toString()
|
||||
);
|
||||
setRepoStats(stats);
|
||||
} else if (repoResponse.status === 404 || repoResponse.status === 403) {
|
||||
// Repository not found or private - set empty stats
|
||||
const privateRepoStats = {
|
||||
stars: '-',
|
||||
forks: '-',
|
||||
issues: '-',
|
||||
isPrivate: true
|
||||
};
|
||||
setRepoStats(privateRepoStats);
|
||||
localStorage.setItem(statsCacheKey, JSON.stringify(privateRepoStats));
|
||||
localStorage.setItem(
|
||||
`${statsCacheKey}-timestamp`,
|
||||
Date.now().toString()
|
||||
);
|
||||
}
|
||||
} catch (statsError) {
|
||||
console.warn('Could not fetch repo stats:', statsError);
|
||||
// Use cached stats or create private repo placeholder
|
||||
if (cachedStats) {
|
||||
setRepoStats(JSON.parse(cachedStats));
|
||||
} else {
|
||||
const privateRepoStats = {
|
||||
stars: '-',
|
||||
forks: '-',
|
||||
issues: '-',
|
||||
isPrivate: true
|
||||
};
|
||||
setRepoStats(privateRepoStats);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching GitHub data:', error);
|
||||
console.error('Error in fetchAvatarAndStats:', error);
|
||||
// Use cached values if available
|
||||
if (cachedAvatar && !isAvatarValid) setAvatarUrl(cachedAvatar);
|
||||
if (cachedStats && !isStatsValid)
|
||||
if (cachedStats && !isStatsValid) {
|
||||
setRepoStats(JSON.parse(cachedStats));
|
||||
} else {
|
||||
// Provide default stats for private repos
|
||||
setRepoStats({
|
||||
stars: '-',
|
||||
forks: '-',
|
||||
issues: '-',
|
||||
isPrivate: true
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user