Implement authentication system with access control UI
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
|
||||
import { TagDetail, Package, PaginatedResponse } from '../types';
|
||||
import { listTags, getDownloadUrl, getPackage } from '../api';
|
||||
import { useParams, useSearchParams, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { TagDetail, Package, PaginatedResponse, AccessLevel } from '../types';
|
||||
import { listTags, getDownloadUrl, getPackage, getMyProjectAccess, UnauthorizedError, ForbiddenError } from '../api';
|
||||
import { Breadcrumb } from '../components/Breadcrumb';
|
||||
import { Badge } from '../components/Badge';
|
||||
import { SearchInput } from '../components/SearchInput';
|
||||
@@ -10,6 +10,7 @@ import { FilterChip, FilterChipGroup } from '../components/FilterChip';
|
||||
import { DataTable } from '../components/DataTable';
|
||||
import { Pagination } from '../components/Pagination';
|
||||
import { DragDropUpload, UploadResult } from '../components/DragDropUpload';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import './Home.css';
|
||||
import './PackagePage.css';
|
||||
|
||||
@@ -56,15 +57,22 @@ function CopyButton({ text }: { text: string }) {
|
||||
function PackagePage() {
|
||||
const { projectName, packageName } = useParams<{ projectName: string; packageName: string }>();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { user } = useAuth();
|
||||
|
||||
const [pkg, setPkg] = useState<Package | null>(null);
|
||||
const [tagsData, setTagsData] = useState<PaginatedResponse<TagDetail> | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [accessDenied, setAccessDenied] = useState(false);
|
||||
const [uploadTag, setUploadTag] = useState('');
|
||||
const [uploadSuccess, setUploadSuccess] = useState<string | null>(null);
|
||||
const [artifactIdInput, setArtifactIdInput] = useState('');
|
||||
const [accessLevel, setAccessLevel] = useState<AccessLevel | null>(null);
|
||||
|
||||
// Derived permissions
|
||||
const canWrite = accessLevel === 'write' || accessLevel === 'admin';
|
||||
|
||||
// Get params from URL
|
||||
const page = parseInt(searchParams.get('page') || '1', 10);
|
||||
@@ -92,19 +100,32 @@ function PackagePage() {
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const [pkgData, tagsResult] = await Promise.all([
|
||||
setAccessDenied(false);
|
||||
const [pkgData, tagsResult, accessResult] = await Promise.all([
|
||||
getPackage(projectName, packageName),
|
||||
listTags(projectName, packageName, { page, search, sort, order }),
|
||||
getMyProjectAccess(projectName),
|
||||
]);
|
||||
setPkg(pkgData);
|
||||
setTagsData(tagsResult);
|
||||
setAccessLevel(accessResult.access_level);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
if (err instanceof UnauthorizedError) {
|
||||
navigate('/login', { state: { from: location.pathname } });
|
||||
return;
|
||||
}
|
||||
if (err instanceof ForbiddenError) {
|
||||
setAccessDenied(true);
|
||||
setError('You do not have access to this package');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setError(err instanceof Error ? err.message : 'Failed to load data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [projectName, packageName, page, search, sort, order]);
|
||||
}, [projectName, packageName, page, search, sort, order, navigate, location.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
@@ -226,6 +247,28 @@ function PackagePage() {
|
||||
return <div className="loading">Loading...</div>;
|
||||
}
|
||||
|
||||
if (accessDenied) {
|
||||
return (
|
||||
<div className="home">
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{ label: 'Projects', href: '/' },
|
||||
{ label: projectName!, href: `/project/${projectName}` },
|
||||
]}
|
||||
/>
|
||||
<div className="error-message" style={{ textAlign: 'center', padding: '48px 24px' }}>
|
||||
<h2>Access Denied</h2>
|
||||
<p>You do not have permission to view this package.</p>
|
||||
{!user && (
|
||||
<p style={{ marginTop: '16px' }}>
|
||||
<a href="/login" className="btn btn-primary">Sign in</a>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="home">
|
||||
<Breadcrumb
|
||||
@@ -286,28 +329,41 @@ function PackagePage() {
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
{uploadSuccess && <div className="success-message">{uploadSuccess}</div>}
|
||||
|
||||
<div className="upload-section card">
|
||||
<h3>Upload Artifact</h3>
|
||||
<div className="upload-form">
|
||||
<div className="form-group">
|
||||
<label htmlFor="upload-tag">Tag (optional)</label>
|
||||
<input
|
||||
id="upload-tag"
|
||||
type="text"
|
||||
value={uploadTag}
|
||||
onChange={(e) => setUploadTag(e.target.value)}
|
||||
placeholder="v1.0.0, latest, stable..."
|
||||
{user && (
|
||||
<div className="upload-section card">
|
||||
<h3>Upload Artifact</h3>
|
||||
{canWrite ? (
|
||||
<div className="upload-form">
|
||||
<div className="form-group">
|
||||
<label htmlFor="upload-tag">Tag (optional)</label>
|
||||
<input
|
||||
id="upload-tag"
|
||||
type="text"
|
||||
value={uploadTag}
|
||||
onChange={(e) => setUploadTag(e.target.value)}
|
||||
placeholder="v1.0.0, latest, stable..."
|
||||
/>
|
||||
</div>
|
||||
<DragDropUpload
|
||||
projectName={projectName!}
|
||||
packageName={packageName!}
|
||||
tag={uploadTag || undefined}
|
||||
onUploadComplete={handleUploadComplete}
|
||||
onUploadError={handleUploadError}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<DragDropUpload
|
||||
projectName={projectName!}
|
||||
packageName={packageName!}
|
||||
disabled={true}
|
||||
disabledReason="You have read-only access to this project and cannot upload artifacts."
|
||||
onUploadComplete={handleUploadComplete}
|
||||
onUploadError={handleUploadError}
|
||||
/>
|
||||
</div>
|
||||
<DragDropUpload
|
||||
projectName={projectName!}
|
||||
packageName={packageName!}
|
||||
tag={uploadTag || undefined}
|
||||
onUploadComplete={handleUploadComplete}
|
||||
onUploadError={handleUploadError}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="section-header">
|
||||
<h2>Tags / Versions</h2>
|
||||
|
||||
Reference in New Issue
Block a user