diff --git a/src/components/GitHubTokenPermissions.tsx b/src/components/GitHubTokenPermissions.tsx new file mode 100644 index 000000000..dfa68586a --- /dev/null +++ b/src/components/GitHubTokenPermissions.tsx @@ -0,0 +1,145 @@ +import React, { useId, useRef, useState } from 'react'; +import { FileText, GitFork, Info, KeyRound, List, ShieldCheck, Star, Workflow } from 'lucide-react'; +import { useT } from '../i18n/useT'; +import { cn } from '../lib/utils'; + +const CODE_SPAN_RE = /`([^`]+)`/g; + +type TokenMode = 'fine-grained' | 'classic'; + +const MODE_TABS: Array<{ mode: TokenMode; labelKey: string }> = [ + { mode: 'fine-grained', labelKey: 'loginScreen.token-permissions-tab-finegrained' }, + { mode: 'classic', labelKey: 'loginScreen.token-permissions-tab-classic' }, +]; + +const FINE_GRAINED_ROWS = [ + { icon: Star, featureKey: 'loginScreen.token-permissions-feature-star', valueKey: 'loginScreen.token-permissions-fine-star' }, + { icon: FileText, featureKey: 'loginScreen.token-permissions-feature-gists', valueKey: 'loginScreen.token-permissions-fine-gists' }, + { icon: GitFork, featureKey: 'loginScreen.token-permissions-feature-fork-sync', valueKey: 'loginScreen.token-permissions-fine-fork-sync' }, + { icon: Workflow, featureKey: 'loginScreen.token-permissions-feature-workflows', valueKey: 'loginScreen.token-permissions-fine-workflows' }, +] as const; + +const CLASSIC_ROWS = [ + { icon: Star, featureKey: 'loginScreen.token-permissions-feature-star', valueKey: 'loginScreen.token-permissions-classic-star' }, + { icon: FileText, featureKey: 'loginScreen.token-permissions-feature-gists', valueKey: 'loginScreen.token-permissions-classic-gists' }, + { icon: List, featureKey: 'loginScreen.token-permissions-feature-star-lists', valueKey: 'loginScreen.token-permissions-classic-star-lists' }, + { icon: GitFork, featureKey: 'loginScreen.token-permissions-feature-fork-workflows', valueKey: 'loginScreen.token-permissions-classic-fork-workflows' }, +] as const; + +/** Renders a locale string, showing `backtick` spans as highlighted permission + * tokens (bold, tinted, dashed underline — no code styling). */ +function renderWithHighlights(text: string): React.ReactNode[] { + return text.split(CODE_SPAN_RE).map((part, index) => index % 2 === 1 + ? ( + + {part} + + ) + : {part}); +} + +/** Explains the permissions before a token is saved, without probing account-changing APIs. */ +export function GitHubTokenPermissions({ heading = true }: { heading?: boolean }) { + const t = useT('login'); + const baseId = useId(); + const [mode, setMode] = useState('fine-grained'); + const tabRefs = useRef>>({}); + + const tabId = (tabMode: TokenMode) => `${baseId}-tab-${tabMode}`; + const panelId = (tabMode: TokenMode) => `${baseId}-panel-${tabMode}`; + const rowsFor = (tabMode: TokenMode) => ( + tabMode === 'fine-grained' ? FINE_GRAINED_ROWS : CLASSIC_ROWS + ); + + /** WAI-ARIA tabs: Left/Right moves selection and focus (roving tabindex). */ + const handleTabKeyDown = (event: React.KeyboardEvent, tabMode: TokenMode) => { + if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return; + event.preventDefault(); + const offset = event.key === 'ArrowRight' ? 1 : -1; + const index = MODE_TABS.findIndex(({ mode: candidate }) => candidate === tabMode); + const { mode: nextMode } = MODE_TABS[(index + offset + MODE_TABS.length) % MODE_TABS.length]; + setMode(nextMode); + tabRefs.current[nextMode]?.focus(); + }; + + return ( +
+ {heading && ( +

+ + {t('loginScreen.token-permissions-title')} +

+ )} + +
+ {MODE_TABS.map(({ mode: tabMode, labelKey }) => { + const selected = mode === tabMode; + return ( + + ); + })} +
+ + {MODE_TABS.map(({ mode: tabMode }) => ( + + ))} + +
+

+ + {renderWithHighlights(t('loginScreen.token-permissions-check'))} +

+

+ + {renderWithHighlights(t('loginScreen.token-permissions-local-storage'))} +

+
+ + + {t('loginScreen.create-token-on-github')} + +
+ ); +} diff --git a/src/components/LoginScreen.test.tsx b/src/components/LoginScreen.test.tsx index f8a3daf28..5e05db4cf 100644 --- a/src/components/LoginScreen.test.tsx +++ b/src/components/LoginScreen.test.tsx @@ -69,6 +69,16 @@ const enterBackendMode = async () => { }; }; +describe('LoginScreen token guidance', () => { + it('explains Star permissions before sign-in', () => { + render(); + expect(screen.getByText('GitHub Token 权限')).toBeVisible(); + expect(screen.getByText('Starring')).toBeVisible(); + fireEvent.click(screen.getByRole('tab', { name: '经典 Token' })); + expect(screen.getByText('public_repo')).toBeVisible(); + }); +}); + describe('LoginScreen 后端登录', () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/components/LoginScreen.tsx b/src/components/LoginScreen.tsx index d9b86ea0a..8108e2597 100644 --- a/src/components/LoginScreen.tsx +++ b/src/components/LoginScreen.tsx @@ -1,6 +1,6 @@ import { useT } from "../i18n/useT"; import React, { useState, useCallback, useRef } from 'react'; -import { AlertCircle, ArrowLeft, ArrowRight, Database, Github, Key, Link, Moon, Sun } from 'lucide-react'; +import { AlertCircle, ArrowLeft, ArrowRight, Database, Github, Key, Link, Loader2, Moon, Sun } from 'lucide-react'; import { useAppStore } from '../store/useAppStore'; import { useShallow } from 'zustand/react/shallow'; import { useLoginActions } from '../features/lifecycle/hooks/useLoginActions'; @@ -15,6 +15,65 @@ import { Label } from './ui/label'; import { Tooltip, TooltipContent, TooltipTrigger } from './ui/tooltip'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select'; import { APP_LANGUAGES, type AppLanguage } from '../i18n/languages'; +import { GitHubTokenPermissions } from './GitHubTokenPermissions'; +import { cn } from '../lib/utils'; + +/** GitHub-contribution-heatmap style band across the top of the login card: + * deterministic pseudo-random cells with four intensity levels, fading + * towards the bottom. Sized for the widest card layout; clipped on narrow ones. */ +const HEATMAP_ROWS = 4; +const HEATMAP_COLS = 66; +const HEATMAP_PITCH = 13; +const HEATMAP_CELL = 10; + +function mulberry32(seed: number) { + return () => { + seed |= 0; + seed = (seed + 0x6D2B79F5) | 0; + let t = Math.imul(seed ^ (seed >>> 15), 1 | seed); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +const HEATMAP_CELLS = (() => { + const random = mulberry32(20261001); + const levelOpacity = [0, 0.07, 0.14, 0.24]; + const rowFade = [1, 0.7, 0.4, 0.15]; + const cells: Array<{ x: number; y: number; opacity: number }> = []; + for (let row = 0; row < HEATMAP_ROWS; row += 1) { + for (let col = 0; col < HEATMAP_COLS; col += 1) { + const roll = random(); + const level = roll < 0.32 ? 0 : roll < 0.62 ? 1 : roll < 0.86 ? 2 : 3; + const opacity = levelOpacity[level] * rowFade[row]; + if (opacity <= 0) continue; + cells.push({ x: col * HEATMAP_PITCH, y: row * HEATMAP_PITCH, opacity }); + } + } + return cells; +})(); + +function LoginCardPattern() { + return ( + + {HEATMAP_CELLS.map((cell, index) => ( + + ))} + + ); +} export const LoginScreen: React.FC = () => { const { authenticateWithGitHub, configuredBackendUrl, restoreBackendSession, setupBackendGitHubToken, syncBackendData, syncTokenToBackend } = useLoginActions(); @@ -271,12 +330,20 @@ export const LoginScreen: React.FC = () => { conflictResolveRef.current = null; }, []); + const showsTokenPermissions = loginMode === 'github' || backendStep === 'githubToken'; + return ( -
+
+
+
+
+
+
+
-
+
{ - setBackendUrl(e.target.value); - setError(''); - }} - onKeyDown={handleKeyPress} - disabled={isLoading} - className="pl-10" - /> + + {cachedRepoCount > 0 && ( +
+
+
+ {t('loginScreen.cachedrepocount-repositories-cached', { cachedRepoCount: cachedRepoCount })} +
+ {cachedLastSync &&

{t('loginScreen.last-sync')} {new Date(cachedLastSync).toLocaleString()}

} +
+ )} + +
+ {loginMode === 'backend' && backendStep === 'credentials' && ( +
+ +
+ + { + setBackendUrl(e.target.value); + setError(''); + }} + onKeyDown={handleKeyPress} + disabled={isLoading} + className="pl-10" + /> +
+
+ )} + +
+ +
+ + { + if (loginMode === 'github') { + setToken(e.target.value); + } else if (backendStep === 'githubToken') { + setBackendGithubToken(e.target.value); + } else { + setBackendApiKey(e.target.value); + } + setError(''); + }} + onKeyDown={handleKeyPress} + disabled={isLoading} + className="pl-10" + /> +
+ + {error && ( +
+ +

{error}

+
+ )} + +
- )} -
- -
- - { - if (loginMode === 'github') { - setToken(e.target.value); - } else if (backendStep === 'githubToken') { - setBackendGithubToken(e.target.value); - } else { - setBackendApiKey(e.target.value); - } - setError(''); - }} - onKeyDown={handleKeyPress} - disabled={isLoading} - className="pl-10" - /> +
- {error && ( -
- -

{error}

+ {showsTokenPermissions && ( +
+
)} - -
- - {loginMode === 'github' &&
-

{t('loginScreen.how-to-create-a-github-token')}

-
    -
  1. 1. {t('loginScreen.go-to-github-settings-developer-settings-persona')}
  2. -
  3. 2. {t('loginScreen.click-generate-new-token-classic')}
  4. -
  5. 3. {t('loginScreen.select-scopes')} repo、user {t('loginScreen.and')} gist
  6. -
  7. 4. {t('loginScreen.copy-the-generated-token-and-paste-it-above')}
  8. -
- -
} - -
diff --git a/src/components/settings/GeneralPanel.tsx b/src/components/settings/GeneralPanel.tsx index 922b42bce..39be21dff 100644 --- a/src/components/settings/GeneralPanel.tsx +++ b/src/components/settings/GeneralPanel.tsx @@ -1,7 +1,7 @@ import { TranslateFn } from '../../i18n/useT'; -import React from 'react'; -import { ExternalLink, Github, Globe, Key, Mail, Monitor, Package, Twitter } from 'lucide-react'; +import React, { useState } from 'react'; +import { ArrowRight, ExternalLink, Github, Globe, Key, Mail, Monitor, Package, Twitter } from 'lucide-react'; import { UpdateChecker } from '../UpdateChecker'; import { useAppStore } from '../../store/useAppStore'; import { useShallow } from 'zustand/react/shallow'; @@ -10,6 +10,7 @@ import { PROJECT_REPO_URL } from '../../constants/project'; import { APP_LANGUAGES, type AppLanguage } from '../../i18n/languages'; import { Button } from '../ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../ui/dialog'; import { Input } from '../ui/input'; import { Label } from '../ui/label'; import { RadioGroup, RadioGroupItem } from '../ui/radio-group'; @@ -17,6 +18,7 @@ import { Switch } from '../ui/switch'; import { ThemeSettingsCard } from './ThemeSettingsCard'; import { useDesktopActions } from '../../features/settings/hooks/useDesktopActions'; import { useGitHubTokenActions } from '../../features/settings/hooks/useGitHubTokenActions'; +import { GitHubTokenPermissions } from '../GitHubTokenPermissions'; interface GeneralPanelProps { t: TranslateFn; @@ -30,6 +32,7 @@ export const GeneralPanel: React.FC = ({ t }) => { }))); const desktop = useDesktopActions(); const githubToken = useGitHubTokenActions(); + const [guideOpen, setGuideOpen] = useState(false); return (
@@ -38,6 +41,22 @@ export const GeneralPanel: React.FC = ({ t }) => {

{t('generalPanel.general-settings')}

+ + +
+ + {t('generalPanel.check-for-updates')} +
+
+ +
+

{t('generalPanel.current-version-v-version', { version: version })}

+

{t('generalPanel.check-if-a-new-version-is-available')}

+
+ +
+
+ @@ -68,9 +87,22 @@ export const GeneralPanel: React.FC = ({ t }) => { + + + + + {t('generalPanel.token-permission-guide')} + + + + +
@@ -158,22 +190,6 @@ export const GeneralPanel: React.FC = ({ t }) => { )} - - -
- - {t('generalPanel.check-for-updates')} -
-
- -
-

{t('generalPanel.current-version-v-version', { version: version })}

-

{t('generalPanel.check-if-a-new-version-is-available')}

-
- -
-
-
diff --git a/src/features/repositories/hooks/useBatchStarImport.test.tsx b/src/features/repositories/hooks/useBatchStarImport.test.tsx index e3b6ec89b..625896bdf 100644 --- a/src/features/repositories/hooks/useBatchStarImport.test.tsx +++ b/src/features/repositories/hooks/useBatchStarImport.test.tsx @@ -1,6 +1,7 @@ import { act, renderHook } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useBatchStarImport } from './useBatchStarImport'; +import { GitHubTokenPermissionError } from '../../../services/githubApi'; const mocks = vi.hoisted(() => ({ token: 'token', @@ -64,6 +65,23 @@ describe('useBatchStarImport', () => { expect(mocks.starRepository).not.toHaveBeenCalled(); }); + it('explains a token permission failure during preview', async () => { + mocks.isRepositoryStarred.mockRejectedValueOnce(new GitHubTokenPermissionError()); + const { result } = renderHook(() => useBatchStarImport()); + await act(() => result.current.preview('https://github.com/owner/repo')); + expect(result.current.rows[0].status).toBe('unavailable'); + expect(result.current.rows[0].error).toMatch(/Starring|starring/i); + }); + + it('explains a token permission failure during starring', async () => { + mocks.starRepository.mockRejectedValueOnce(new GitHubTokenPermissionError()); + const { result } = renderHook(() => useBatchStarImport()); + await act(() => result.current.preview('https://github.com/owner/repo')); + await act(() => result.current.starSelected()); + expect(result.current.rows[0].status).toBe('failed'); + expect(result.current.rows[0].error).toMatch(/Starring|starring/i); + }); + it('requires selection for bare names and renamed repositories, and deduplicates canonical names', async () => { mocks.getRepositoryDetails.mockImplementation(async (_owner, name) => detail('new-owner', name)); const { result } = renderHook(() => useBatchStarImport()); diff --git a/src/features/repositories/hooks/useBatchStarImport.ts b/src/features/repositories/hooks/useBatchStarImport.ts index 6a9c3d2b2..de42f06a0 100644 --- a/src/features/repositories/hooks/useBatchStarImport.ts +++ b/src/features/repositories/hooks/useBatchStarImport.ts @@ -2,7 +2,8 @@ import { useCallback, useRef, useState } from 'react'; import { useAppStore } from '../../../store/useAppStore'; import { forceSyncToBackend } from '../../../services/autoSync'; import { translateBatch } from '../../../services/translateService'; -import type { GitHubRepoDetailRead } from '../../../services/githubApi'; +import { GitHubTokenPermissionError, type GitHubRepoDetailRead } from '../../../services/githubApi'; +import { makeT } from '../../../i18n/useT'; import { createGitHubApiService } from '../../../services/githubApiFactory'; import type { Repository } from '../../../types'; import type { ImportedRepositoryCandidate } from '../../../types/repositoryImport'; @@ -102,7 +103,9 @@ export function useBatchStarImport() { candidate, status: 'unavailable', selected: false, - error: error instanceof Error ? error.message : String(error), + error: error instanceof GitHubTokenPermissionError + ? makeT(language, 'repositories')('batchStar.token-permission-error') + : error instanceof Error ? error.message : String(error), }; } })); @@ -122,7 +125,7 @@ export function useBatchStarImport() { busyRef.current = false; setIsResolving(false); } - }, [githubToken, resetTranslations]); + }, [githubToken, language, resetTranslations]); /** Flips the selection of a starable row; other statuses cannot be toggled. */ const toggleRow = useCallback((index: number) => { @@ -240,7 +243,9 @@ export function useBatchStarImport() { )); } catch (error) { setRows(current => current.map((item, itemIndex) => itemIndex === index - ? { ...item, status: 'failed', selected: false, error: error instanceof Error ? error.message : String(error) } + ? { ...item, status: 'failed', selected: false, error: error instanceof GitHubTokenPermissionError + ? makeT(language, 'repositories')('batchStar.token-permission-error') + : error instanceof Error ? error.message : String(error) } : item )); } @@ -256,7 +261,7 @@ export function useBatchStarImport() { busyRef.current = false; setIsStarring(false); } - }, [addRepository, githubToken, rows]); + }, [addRepository, githubToken, language, rows]); return { rows, duplicateCount, inputError, isResolving, isStarring, syncError, diff --git a/src/locales/de/app.json b/src/locales/de/app.json index 743639810..cfd0d759e 100644 --- a/src/locales/de/app.json +++ b/src/locales/de/app.json @@ -934,7 +934,10 @@ "check-if-a-new-version-is-available": "Prüfen Sie, ob eine neue Version verfügbar ist", "contact-information": "Kontaktinformationen", "if-you-encounter-any-issues-or-have-suggestions": "Wenn Sie während der Nutzung auf Probleme stoßen oder Vorschläge haben, kontaktieren Sie mich bitte über die folgenden Methoden:", - "github": "GitHub" + "github": "GitHub", + "account-token-hint": "Angemeldet als {{login}}. Ein abgelaufenes Token lässt sich hier ohne Abmeldung erneuern.", + "token-hint": "Ein abgelaufenes Token lässt sich hier ohne Abmeldung erneuern.", + "token-permission-guide": "Token-Einrichtungshinweise" }, "includeKeysToggle": { "include-keys-in-backup-export": "Beziehen Sie beim Sichern/Exportieren Schlüssel mit ein", diff --git a/src/locales/de/login.json b/src/locales/de/login.json index 2abed9c37..16218a290 100644 --- a/src/locales/de/login.json +++ b/src/locales/de/login.json @@ -1,5 +1,7 @@ { "loginScreen": { + "token-permissions-title": "GitHub-Token-Berechtigungen", + "token-permissions-check": "Die Anmeldung prüft Ihre Identität, nicht alle Berechtigungen. Fehlende Rechte können erst bei der Nutzung einer Funktion auffallen.", "please-enter-the-backend-url-and-api-key": "Bitte geben Sie die Backend-URL und den API-Schlüssel ein", "invalid-backend-url-remote-backends-must-use-htt": "Ungültige Backend-Adresse: Das Remote-Backend erfordert HTTPS, nur localhost kann HTTP verwenden", "unable-to-connect-to-this-backend-url": "Es kann keine Verbindung zur Backend-URL hergestellt werden", @@ -26,12 +28,6 @@ "connect-to-github": "Stellen Sie eine Verbindung zu GitHub her", "connect-and-restore-data": "Daten verbinden und wiederherstellen", "save-and-continue": "Speichern und fortfahren", - "how-to-create-a-github-token": "So erstellen Sie ein GitHub-Token:", - "go-to-github-settings-developer-settings-persona": "Besuchen Sie GitHub-Einstellungen → Entwicklereinstellungen → Persönliche Zugriffstokens", - "click-generate-new-token-classic": "Klicken Sie auf „Neues Token generieren (klassisch)“", - "select-scopes": "Berechtigungsbereich auswählen:", - "and": "Und", - "copy-the-generated-token-and-paste-it-above": "Kopieren Sie das generierte Token und fügen Sie es oben ein", "create-token-on-github": "Erstellen Sie ein Token auf GitHub →", "already-have-backend-data": "Habe bereits Backend-Daten", "sign-in-with-github-token": "Melden Sie sich mit dem GitHub-Token an", @@ -41,6 +37,23 @@ "keep-local": "Bleiben Sie lokal", "please-enter-a-valid-github-token": "Bitte geben Sie ein gültiges GitHub-Token ein", "signed-in-but-failed-to-save-github-token-to-bac": "Sie haben sich angemeldet, aber das GitHub-Token konnte nicht im Backend gespeichert werden und Backend-Proxy-Funktionen wie README sind möglicherweise nicht verfügbar.", - "failed-to-authenticate-please-check-your-token": "Die Authentifizierung ist fehlgeschlagen. Bitte überprüfen Sie Ihr Token." + "failed-to-authenticate-please-check-your-token": "Die Authentifizierung ist fehlgeschlagen. Bitte überprüfen Sie Ihr Token.", + "token-permissions-tab-finegrained": "Fein abgestuft", + "token-permissions-tab-classic": "Klassisch", + "token-permissions-feature-star": "Stars verwalten", + "token-permissions-feature-gists": "Gists", + "token-permissions-feature-star-lists": "Star-Listen", + "token-permissions-feature-fork-sync": "Fork-Synchronisierung", + "token-permissions-feature-workflows": "Workflow-Starts", + "token-permissions-feature-fork-workflows": "Fork-Sync / Workflows", + "token-permissions-fine-star": "`Starring` Lesen/Schreiben, `Metadata` Lesen", + "token-permissions-fine-gists": "`Gists` Schreiben", + "token-permissions-fine-fork-sync": "`Contents` Schreiben", + "token-permissions-fine-workflows": "`Actions` Schreiben", + "token-permissions-classic-star": "`public_repo` (`repo` für private Repositorys)", + "token-permissions-classic-gists": "`gist`", + "token-permissions-classic-star-lists": "`user`", + "token-permissions-classic-fork-workflows": "`repo`", + "token-permissions-local-storage": "Ihr Token wird nur auf diesem Gerät und Ihrem eigenen Backend gespeichert – niemals auf den Servern der App." } } diff --git a/src/locales/de/repositories.json b/src/locales/de/repositories.json index 1d9ccb239..f9fe1948d 100644 --- a/src/locales/de/repositories.json +++ b/src/locales/de/repositories.json @@ -1,5 +1,6 @@ { "batchStar": { + "token-permission-error": "GitHub hat dieses Token abgewiesen (403). Zum Prüfen oder Setzen von Stars braucht ein fein abgestuftes Token Starring mit Lese-/Schreibzugriff und Metadata mit Lesezugriff; ein klassisches Token benötigt public_repo (für private Repositorys repo).", "title": "Sterne aus Links hinzufügen", "hint": "Füge beliebigen Text mit GitHub-Links ein, prüfe die Repositorys und vergib Sterne. Links zu anderen Websites werden ignoriert.", "history": "Einfügeverlauf", diff --git a/src/locales/en/app.json b/src/locales/en/app.json index 38197dff4..48416191d 100644 --- a/src/locales/en/app.json +++ b/src/locales/en/app.json @@ -934,7 +934,10 @@ "check-if-a-new-version-is-available": "Check if a new version is available", "contact-information": "Contact Information", "if-you-encounter-any-issues-or-have-suggestions": "If you encounter any issues or have suggestions while using the app, feel free to contact me through:", - "github": "GitHub" + "github": "GitHub", + "account-token-hint": "Signed in as {{login}}. Update an expired token here without logging out.", + "token-hint": "Update an expired token here without logging out.", + "token-permission-guide": "Token setup guide" }, "includeKeysToggle": { "include-keys-in-backup-export": "Include keys in backup/export", diff --git a/src/locales/en/login.json b/src/locales/en/login.json index 676fbd911..e6f86bd18 100644 --- a/src/locales/en/login.json +++ b/src/locales/en/login.json @@ -1,5 +1,7 @@ { "loginScreen": { + "token-permissions-title": "GitHub token permissions", + "token-permissions-check": "Signing in checks your identity, not every permission. Missing permissions may only appear when you use a feature.", "please-enter-the-backend-url-and-api-key": "Please enter the backend URL and API key", "invalid-backend-url-remote-backends-must-use-htt": "Invalid backend URL: remote backends must use HTTPS; only localhost may use HTTP", "unable-to-connect-to-this-backend-url": "Unable to connect to this backend URL", @@ -26,12 +28,6 @@ "connect-to-github": "Connect to GitHub", "connect-and-restore-data": "Connect and restore data", "save-and-continue": "Save and continue", - "how-to-create-a-github-token": "How to create a GitHub token:", - "go-to-github-settings-developer-settings-persona": "Go to GitHub Settings → Developer settings → Personal access tokens", - "click-generate-new-token-classic": "Click \"Generate new token (classic)\"", - "select-scopes": "Select scopes:", - "and": "and", - "copy-the-generated-token-and-paste-it-above": "Copy the generated token and paste it above", "create-token-on-github": "Create token on GitHub →", "already-have-backend-data": "Already have backend data", "sign-in-with-github-token": "Sign in with GitHub token", @@ -41,6 +37,23 @@ "keep-local": "Keep local", "please-enter-a-valid-github-token": "Please enter a valid GitHub token", "signed-in-but-failed-to-save-github-token-to-bac": "Signed in, but failed to save GitHub token to backend. README and other backend proxy features may be unavailable.", - "failed-to-authenticate-please-check-your-token": "Failed to authenticate. Please check your token." + "failed-to-authenticate-please-check-your-token": "Failed to authenticate. Please check your token.", + "token-permissions-tab-finegrained": "Fine-grained", + "token-permissions-tab-classic": "Classic", + "token-permissions-feature-star": "Managing Stars", + "token-permissions-feature-gists": "Gists", + "token-permissions-feature-star-lists": "Star Lists", + "token-permissions-feature-fork-sync": "Fork sync", + "token-permissions-feature-workflows": "Workflow dispatch", + "token-permissions-feature-fork-workflows": "Fork sync / workflows", + "token-permissions-fine-star": "`Starring` read & write, `Metadata` read", + "token-permissions-fine-gists": "`Gists` write", + "token-permissions-fine-fork-sync": "`Contents` write", + "token-permissions-fine-workflows": "`Actions` write", + "token-permissions-classic-star": "`public_repo` (`repo` for private repositories)", + "token-permissions-classic-gists": "`gist`", + "token-permissions-classic-star-lists": "`user`", + "token-permissions-classic-fork-workflows": "`repo`", + "token-permissions-local-storage": "Your token is stored only on this device and your own backend — never on the app's servers." } } diff --git a/src/locales/en/repositories.json b/src/locales/en/repositories.json index dc3a6f10b..e33d07c93 100644 --- a/src/locales/en/repositories.json +++ b/src/locales/en/repositories.json @@ -1,5 +1,6 @@ { "batchStar": { + "token-permission-error": "GitHub rejected this token (403). To check or add Stars, enable Starring read/write and repository Metadata read on a fine-grained token, or public_repo on a classic token (repo for private repositories).", "title": "Star from links", "hint": "Paste any text containing GitHub links, review the repositories, then Star them. Links to other websites are ignored.", "history": "Paste history", diff --git a/src/locales/es/app.json b/src/locales/es/app.json index 2c5efc850..1807d9bf3 100644 --- a/src/locales/es/app.json +++ b/src/locales/es/app.json @@ -934,7 +934,10 @@ "check-if-a-new-version-is-available": "Compruebe si hay una nueva versión disponible", "contact-information": "Información del contacto", "if-you-encounter-any-issues-or-have-suggestions": "Si encuentra algún problema o tiene sugerencias durante el uso, comuníquese conmigo a través de los siguientes métodos:", - "github": "GitHub" + "github": "GitHub", + "account-token-hint": "Sesión iniciada como {{login}}. Renueva aquí un token caducado sin cerrar la sesión.", + "token-hint": "Renueva aquí un token caducado sin cerrar la sesión.", + "token-permission-guide": "Guía de configuración del token" }, "includeKeysToggle": { "include-keys-in-backup-export": "Incluir claves al realizar copias de seguridad/exportar", diff --git a/src/locales/es/login.json b/src/locales/es/login.json index 677e75ff5..b2bca0b6a 100644 --- a/src/locales/es/login.json +++ b/src/locales/es/login.json @@ -1,5 +1,7 @@ { "loginScreen": { + "token-permissions-title": "Permisos del token de GitHub", + "token-permissions-check": "El inicio de sesión verifica tu identidad, no todos los permisos. La falta de permisos puede aparecer al usar una función.", "please-enter-the-backend-url-and-api-key": "Ingrese la URL de backend y la clave API", "invalid-backend-url-remote-backends-must-use-htt": "Dirección de backend no válida: el backend remoto requiere HTTPS, solo el host local puede usar HTTP", "unable-to-connect-to-this-backend-url": "No se puede conectar a la URL de backend", @@ -26,12 +28,6 @@ "connect-to-github": "Conéctate a GitHub", "connect-and-restore-data": "Conectar y restaurar datos", "save-and-continue": "Guardar y continuar", - "how-to-create-a-github-token": "Cómo crear un token de GitHub:", - "go-to-github-settings-developer-settings-persona": "Visite Configuración de GitHub → Configuración de desarrollador → Tokens de acceso personal", - "click-generate-new-token-classic": "Haga clic en \"Generar nuevo token (clásico)\"", - "select-scopes": "Seleccione el alcance del permiso:", - "and": "y", - "copy-the-generated-token-and-paste-it-above": "Copie el token generado y péguelo arriba", "create-token-on-github": "Crea un token en GitHub →", "already-have-backend-data": "Ya tengo datos de backend", "sign-in-with-github-token": "Inicie sesión usando el token de GitHub", @@ -41,6 +37,23 @@ "keep-local": "mantener local", "please-enter-a-valid-github-token": "Ingrese un token de GitHub válido", "signed-in-but-failed-to-save-github-token-to-bac": "Ha iniciado sesión, pero el token de GitHub no se pudo guardar en el backend y es posible que las funciones de proxy del backend, como README, no estén disponibles.", - "failed-to-authenticate-please-check-your-token": "La autenticación falló, verifique su token." + "failed-to-authenticate-please-check-your-token": "La autenticación falló, verifique su token.", + "token-permissions-tab-finegrained": "Específico", + "token-permissions-tab-classic": "Clásico", + "token-permissions-feature-star": "Gestionar estrellas", + "token-permissions-feature-gists": "Gists", + "token-permissions-feature-star-lists": "Listas de estrellas", + "token-permissions-feature-fork-sync": "Sincronizar forks", + "token-permissions-feature-workflows": "Ejecutar flujos", + "token-permissions-feature-fork-workflows": "Forks / flujos", + "token-permissions-fine-star": "`Starring` lectura y escritura, `Metadata` lectura", + "token-permissions-fine-gists": "`Gists` escritura", + "token-permissions-fine-fork-sync": "`Contents` escritura", + "token-permissions-fine-workflows": "`Actions` escritura", + "token-permissions-classic-star": "`public_repo` (`repo` para repositorios privados)", + "token-permissions-classic-gists": "`gist`", + "token-permissions-classic-star-lists": "`user`", + "token-permissions-classic-fork-workflows": "`repo`", + "token-permissions-local-storage": "Tu token se guarda solo en este dispositivo y en tu propio backend; nunca en los servidores de la app." } } diff --git a/src/locales/es/repositories.json b/src/locales/es/repositories.json index 4d4a5c5a1..1911f33ca 100644 --- a/src/locales/es/repositories.json +++ b/src/locales/es/repositories.json @@ -1,5 +1,6 @@ { "batchStar": { + "token-permission-error": "GitHub rechazó este token (403). Para consultar o añadir estrellas, activa Starring de lectura/escritura y Metadata de lectura en un token de permisos específicos, o public_repo en uno clásico (repo para repositorios privados).", "title": "Marcar desde enlaces", "hint": "Pega cualquier texto con enlaces de GitHub, revisa los repositorios y márcalos con Star. Se ignoran los enlaces a otros sitios.", "history": "Historial de texto", diff --git a/src/locales/fr/app.json b/src/locales/fr/app.json index ba7f97339..3f440ad4d 100644 --- a/src/locales/fr/app.json +++ b/src/locales/fr/app.json @@ -934,7 +934,10 @@ "check-if-a-new-version-is-available": "Vérifiez si une nouvelle version est disponible", "contact-information": "Coordonnées", "if-you-encounter-any-issues-or-have-suggestions": "Si vous rencontrez des problèmes ou avez des suggestions lors de l'utilisation, veuillez me contacter via les méthodes suivantes :", - "github": "GitHub" + "github": "GitHub", + "account-token-hint": "Connecté en tant que {{login}}. Renouvelez ici un token expiré sans vous déconnecter.", + "token-hint": "Renouvelez ici un token expiré sans vous déconnecter.", + "token-permission-guide": "Guide de configuration du token" }, "includeKeysToggle": { "include-keys-in-backup-export": "Inclure les clés lors de la sauvegarde/exportation", diff --git a/src/locales/fr/login.json b/src/locales/fr/login.json index e471acc3e..678ed9ff1 100644 --- a/src/locales/fr/login.json +++ b/src/locales/fr/login.json @@ -1,5 +1,7 @@ { "loginScreen": { + "token-permissions-title": "Autorisations du jeton GitHub", + "token-permissions-check": "La connexion vérifie votre identité, pas toutes les autorisations. Un manque de droits peut n'apparaître qu'à l'utilisation d'une fonction.", "please-enter-the-backend-url-and-api-key": "Veuillez saisir l'URL du backend et la clé API", "invalid-backend-url-remote-backends-must-use-htt": "Adresse du backend invalide : le backend distant nécessite HTTPS, seul l'hôte local peut utiliser HTTP", "unable-to-connect-to-this-backend-url": "Impossible de se connecter à l'URL du backend", @@ -26,12 +28,6 @@ "connect-to-github": "Connectez-vous à GitHub", "connect-and-restore-data": "Connectez-vous et restaurez les données", "save-and-continue": "Enregistrer et continuer", - "how-to-create-a-github-token": "Comment créer un jeton GitHub :", - "go-to-github-settings-developer-settings-persona": "Visitez Paramètres GitHub → Paramètres du développeur → Jetons d'accès personnels", - "click-generate-new-token-classic": "Cliquez sur \"Générer un nouveau token (classique)\"", - "select-scopes": "Sélectionnez la portée de l'autorisation :", - "and": "et", - "copy-the-generated-token-and-paste-it-above": "Copiez le jeton généré et collez-le ci-dessus", "create-token-on-github": "Créer un jeton sur GitHub →", "already-have-backend-data": "Vous disposez déjà de données back-end", "sign-in-with-github-token": "Connectez-vous en utilisant le jeton GitHub", @@ -41,6 +37,23 @@ "keep-local": "rester local", "please-enter-a-valid-github-token": "Veuillez saisir un jeton GitHub valide", "signed-in-but-failed-to-save-github-token-to-bac": "Vous êtes connecté, mais le jeton GitHub n'a pas pu être enregistré dans le backend et les fonctions de proxy backend telles que README peuvent ne pas être disponibles.", - "failed-to-authenticate-please-check-your-token": "L'authentification a échoué, veuillez vérifier votre jeton." + "failed-to-authenticate-please-check-your-token": "L'authentification a échoué, veuillez vérifier votre jeton.", + "token-permissions-tab-finegrained": "Granularité fine", + "token-permissions-tab-classic": "Classique", + "token-permissions-feature-star": "Gérer les étoiles", + "token-permissions-feature-gists": "Gists", + "token-permissions-feature-star-lists": "Listes d'étoiles", + "token-permissions-feature-fork-sync": "Synchronisation des forks", + "token-permissions-feature-workflows": "Lancement de workflows", + "token-permissions-feature-fork-workflows": "Forks / workflows", + "token-permissions-fine-star": "`Starring` lecture/écriture, `Metadata` lecture", + "token-permissions-fine-gists": "`Gists` écriture", + "token-permissions-fine-fork-sync": "`Contents` écriture", + "token-permissions-fine-workflows": "`Actions` écriture", + "token-permissions-classic-star": "`public_repo` (`repo` pour les dépôts privés)", + "token-permissions-classic-gists": "`gist`", + "token-permissions-classic-star-lists": "`user`", + "token-permissions-classic-fork-workflows": "`repo`", + "token-permissions-local-storage": "Votre jeton est stocké uniquement sur cet appareil et votre propre backend, jamais sur les serveurs de l'application." } } diff --git a/src/locales/fr/repositories.json b/src/locales/fr/repositories.json index 6e752331d..9d934b7b3 100644 --- a/src/locales/fr/repositories.json +++ b/src/locales/fr/repositories.json @@ -1,5 +1,6 @@ { "batchStar": { + "token-permission-error": "GitHub a refusé ce jeton (403). Pour consulter ou ajouter des étoiles, activez Starring en lecture/écriture et Metadata en lecture avec un jeton à granularité fine, ou public_repo avec un jeton classique (repo pour les dépôts privés).", "title": "Ajouter des étoiles depuis des liens", "hint": "Collez un texte contenant des liens GitHub, vérifiez les dépôts, puis ajoutez une étoile. Les liens vers les autres sites sont ignorés.", "history": "Historique des textes", diff --git a/src/locales/ja/app.json b/src/locales/ja/app.json index 63f2a4e36..8af873178 100644 --- a/src/locales/ja/app.json +++ b/src/locales/ja/app.json @@ -934,7 +934,10 @@ "check-if-a-new-version-is-available": "新しいバージョンが利用可能かどうかを確認する", "contact-information": "連絡先", "if-you-encounter-any-issues-or-have-suggestions": "使用中に問題が発生したり、提案がある場合は、次の方法でご連絡ください。", - "github": "GitHub" + "github": "GitHub", + "account-token-hint": "{{login}} としてサインイン中です。期限切れのトークンはログアウトせずにここで更新できます。", + "token-hint": "期限切れのトークンはログアウトせずにここで更新できます。", + "token-permission-guide": "トークン設定ガイド" }, "includeKeysToggle": { "include-keys-in-backup-export": "バックアップ/エクスポート時にキーを含める", diff --git a/src/locales/ja/login.json b/src/locales/ja/login.json index 9d3721008..76e7f79d8 100644 --- a/src/locales/ja/login.json +++ b/src/locales/ja/login.json @@ -1,5 +1,7 @@ { "loginScreen": { + "token-permissions-title": "GitHub トークンの権限", + "token-permissions-check": "ログインでは本人確認のみを行い、すべての権限は確認しません。権限不足は機能の使用時に判明する場合があります。", "please-enter-the-backend-url-and-api-key": "バックエンド URL と API キーを入力してください", "invalid-backend-url-remote-backends-must-use-htt": "無効なバックエンド アドレス: リモート バックエンドには HTTPS が必要です。HTTP を使用できるのはローカルホストのみです", "unable-to-connect-to-this-backend-url": "バックエンド URL に接続できません", @@ -26,12 +28,6 @@ "connect-to-github": "GitHub に接続する", "connect-and-restore-data": "データの接続と復元", "save-and-continue": "保存して続行", - "how-to-create-a-github-token": "GitHub トークンを作成する方法:", - "go-to-github-settings-developer-settings-persona": "GitHub 設定 → 開発者設定 → 個人アクセス トークンにアクセスします。", - "click-generate-new-token-classic": "「新しいトークンを生成(クラシック)」をクリックします。", - "select-scopes": "権限の範囲を選択します:", - "and": "そして", - "copy-the-generated-token-and-paste-it-above": "生成されたトークンをコピーして上に貼り付けます", "create-token-on-github": "GitHub でトークンを作成 →", "already-have-backend-data": "すでにバックエンドデータがある", "sign-in-with-github-token": "GitHub トークンを使用してログインする", @@ -41,6 +37,23 @@ "keep-local": "ローカルに保つ", "please-enter-a-valid-github-token": "有効な GitHub トークンを入力してください", "signed-in-but-failed-to-save-github-token-to-bac": "ログインしましたが、GitHub トークンをバックエンドに保存できず、README などのバックエンド プロキシ機能が利用できない可能性があります。", - "failed-to-authenticate-please-check-your-token": "認証に失敗しました。トークンを確認してください。" + "failed-to-authenticate-please-check-your-token": "認証に失敗しました。トークンを確認してください。", + "token-permissions-tab-finegrained": "Fine-grained", + "token-permissions-tab-classic": "クラシック", + "token-permissions-feature-star": "Star の管理", + "token-permissions-feature-gists": "Gist", + "token-permissions-feature-star-lists": "Star リスト", + "token-permissions-feature-fork-sync": "Fork 同期", + "token-permissions-feature-workflows": "ワークフロー実行", + "token-permissions-feature-fork-workflows": "Fork 同期 / ワークフロー", + "token-permissions-fine-star": "`Starring` 読み書き、`Metadata` 読み取り", + "token-permissions-fine-gists": "`Gists` 書き込み", + "token-permissions-fine-fork-sync": "`Contents` 書き込み", + "token-permissions-fine-workflows": "`Actions` 書き込み", + "token-permissions-classic-star": "`public_repo`(非公開リポジトリには `repo`)", + "token-permissions-classic-gists": "`gist`", + "token-permissions-classic-star-lists": "`user`", + "token-permissions-classic-fork-workflows": "`repo`", + "token-permissions-local-storage": "トークンはこのデバイスと自分のバックエンドにのみ保存され、アプリのサーバーには送信されません。" } } diff --git a/src/locales/ja/repositories.json b/src/locales/ja/repositories.json index 4260979cf..943877f92 100644 --- a/src/locales/ja/repositories.json +++ b/src/locales/ja/repositories.json @@ -1,5 +1,6 @@ { "batchStar": { + "token-permission-error": "GitHub がこのトークンを拒否しました(403)。Star の確認や追加には、Fine-grained トークンで Starring の読み書きと Metadata の読み取り、またはクラシックトークンで public_repo(非公開リポジトリは repo)が必要です。", "title": "リンクからスター", "hint": "GitHub リンクを含むテキストを貼り付け、リポジトリを確認してから Star を付けます。他のサイトのリンクは無視されます。", "history": "貼り付け履歴", diff --git a/src/locales/ko/app.json b/src/locales/ko/app.json index 4843982cc..5e5c29542 100644 --- a/src/locales/ko/app.json +++ b/src/locales/ko/app.json @@ -934,7 +934,10 @@ "check-if-a-new-version-is-available": "새 버전이 있는지 확인하세요.", "contact-information": "연락처 정보", "if-you-encounter-any-issues-or-have-suggestions": "사용 중 문제가 발생하거나 제안 사항이 있는 경우 다음 방법을 통해 저에게 연락해 주세요.", - "github": "GitHub" + "github": "GitHub", + "account-token-hint": "{{login}}(으)로 로그인했습니다. 만료된 토큰은 로그아웃하지 않고 여기서 갱신할 수 있습니다.", + "token-hint": "만료된 토큰은 로그아웃하지 않고 여기서 갱신할 수 있습니다.", + "token-permission-guide": "토큰 설정 가이드" }, "includeKeysToggle": { "include-keys-in-backup-export": "백업/내보내기 시 키 포함", diff --git a/src/locales/ko/login.json b/src/locales/ko/login.json index 3b54e1c8f..90b879c0d 100644 --- a/src/locales/ko/login.json +++ b/src/locales/ko/login.json @@ -1,5 +1,7 @@ { "loginScreen": { + "token-permissions-title": "GitHub 토큰 권한", + "token-permissions-check": "로그인은 신원만 확인하며 모든 권한을 검사하지 않습니다. 권한 부족은 해당 기능을 사용할 때 나타날 수 있습니다.", "please-enter-the-backend-url-and-api-key": "백엔드 URL과 API 키를 입력하세요.", "invalid-backend-url-remote-backends-must-use-htt": "잘못된 백엔드 주소: 원격 백엔드에는 HTTPS가 필요하며 로컬 호스트만 HTTP를 사용할 수 있습니다.", "unable-to-connect-to-this-backend-url": "백엔드 URL에 연결할 수 없습니다.", @@ -26,12 +28,6 @@ "connect-to-github": "GitHub에 연결", "connect-and-restore-data": "데이터 연결 및 복원", "save-and-continue": "저장하고 계속하세요", - "how-to-create-a-github-token": "GitHub 토큰을 만드는 방법:", - "go-to-github-settings-developer-settings-persona": "GitHub 설정 → 개발자 설정 → 개인 액세스 토큰을 방문하세요.", - "click-generate-new-token-classic": "\"새 토큰 생성(클래식)\"을 클릭하세요.", - "select-scopes": "권한 범위 선택:", - "and": "그리고", - "copy-the-generated-token-and-paste-it-above": "생성된 토큰을 복사하여 위에 붙여넣으세요.", "create-token-on-github": "GitHub에서 토큰 생성 →", "already-have-backend-data": "이미 백엔드 데이터가 있음", "sign-in-with-github-token": "GitHub 토큰을 사용하여 로그인", @@ -41,6 +37,23 @@ "keep-local": "지역을 유지하다", "please-enter-a-valid-github-token": "유효한 GitHub 토큰을 입력하세요.", "signed-in-but-failed-to-save-github-token-to-bac": "로그인했지만 GitHub 토큰이 백엔드에 저장되지 않아 README와 같은 백엔드 프록시 기능을 사용하지 못할 수 있습니다.", - "failed-to-authenticate-please-check-your-token": "인증에 실패했습니다. 토큰을 확인하세요." + "failed-to-authenticate-please-check-your-token": "인증에 실패했습니다. 토큰을 확인하세요.", + "token-permissions-tab-finegrained": "세분화", + "token-permissions-tab-classic": "클래식", + "token-permissions-feature-star": "Star 관리", + "token-permissions-feature-gists": "Gist", + "token-permissions-feature-star-lists": "Star 목록", + "token-permissions-feature-fork-sync": "Fork 동기화", + "token-permissions-feature-workflows": "워크플로 실행", + "token-permissions-feature-fork-workflows": "Fork 동기화 / 워크플로", + "token-permissions-fine-star": "`Starring` 읽기/쓰기, `Metadata` 읽기", + "token-permissions-fine-gists": "`Gists` 쓰기", + "token-permissions-fine-fork-sync": "`Contents` 쓰기", + "token-permissions-fine-workflows": "`Actions` 쓰기", + "token-permissions-classic-star": "`public_repo`(비공개 저장소는 `repo`)", + "token-permissions-classic-gists": "`gist`", + "token-permissions-classic-star-lists": "`user`", + "token-permissions-classic-fork-workflows": "`repo`", + "token-permissions-local-storage": "토큰은 이 기기와 직접 구성한 백엔드에만 저장되며 앱 서버로 전송되지 않습니다." } } diff --git a/src/locales/ko/repositories.json b/src/locales/ko/repositories.json index 6d12880ba..7129a7c22 100644 --- a/src/locales/ko/repositories.json +++ b/src/locales/ko/repositories.json @@ -1,5 +1,6 @@ { "batchStar": { + "token-permission-error": "GitHub가 이 토큰을 거부했습니다(403). Star 확인과 추가에는 세분화된 토큰의 Starring 읽기/쓰기 및 저장소 Metadata 읽기 권한이 필요합니다. 클래식 토큰은 public_repo(비공개 저장소는 repo)가 필요합니다.", "title": "링크로 별표 추가", "hint": "GitHub 링크가 포함된 텍스트를 붙여넣고 저장소를 확인한 뒤 Star를 추가하세요. 다른 사이트의 링크는 무시됩니다.", "history": "붙여넣기 기록", diff --git a/src/locales/pt-BR/app.json b/src/locales/pt-BR/app.json index fd4d31608..d8a44e90e 100644 --- a/src/locales/pt-BR/app.json +++ b/src/locales/pt-BR/app.json @@ -934,7 +934,10 @@ "check-if-a-new-version-is-available": "Verifique se uma nova versão está disponível", "contact-information": "Informações de contato", "if-you-encounter-any-issues-or-have-suggestions": "Se você encontrar algum problema ou tiver sugestões durante o uso, entre em contato comigo através dos seguintes métodos:", - "github": "GitHub" + "github": "GitHub", + "account-token-hint": "Conectado como {{login}}. Renove um token expirado aqui sem encerrar a sessão.", + "token-hint": "Renove um token expirado aqui sem encerrar a sessão.", + "token-permission-guide": "Guia de configuração do token" }, "includeKeysToggle": { "include-keys-in-backup-export": "Incluir chaves ao fazer backup/exportar", diff --git a/src/locales/pt-BR/login.json b/src/locales/pt-BR/login.json index 28dd5de6a..9ef1ee8f8 100644 --- a/src/locales/pt-BR/login.json +++ b/src/locales/pt-BR/login.json @@ -1,5 +1,7 @@ { "loginScreen": { + "token-permissions-title": "Permissões do token do GitHub", + "token-permissions-check": "O login verifica sua identidade, não todas as permissões. Uma permissão ausente pode aparecer só quando você usar o recurso.", "please-enter-the-backend-url-and-api-key": "Insira o URL de back-end e a chave de API", "invalid-backend-url-remote-backends-must-use-htt": "Endereço de back-end inválido: o back-end remoto requer HTTPS, apenas localhost pode usar HTTP", "unable-to-connect-to-this-backend-url": "Não foi possível conectar-se ao URL de back-end", @@ -26,12 +28,6 @@ "connect-to-github": "Conecte-se ao GitHub", "connect-and-restore-data": "Conecte e restaure dados", "save-and-continue": "Salvar e continuar", - "how-to-create-a-github-token": "Como criar um token GitHub:", - "go-to-github-settings-developer-settings-persona": "Visite Configurações do GitHub → Configurações do desenvolvedor → Tokens de acesso pessoal", - "click-generate-new-token-classic": "Clique em \"Gerar novo token (clássico)\"", - "select-scopes": "Selecione o escopo da permissão:", - "and": "e", - "copy-the-generated-token-and-paste-it-above": "Copie o token gerado e cole-o acima", "create-token-on-github": "Crie um token no GitHub →", "already-have-backend-data": "Já tem dados de back-end", "sign-in-with-github-token": "Faça login usando o token GitHub", @@ -41,6 +37,23 @@ "keep-local": "manter local", "please-enter-a-valid-github-token": "Insira um token GitHub válido", "signed-in-but-failed-to-save-github-token-to-bac": "Você fez login, mas o token GitHub não foi salvo no back-end e as funções de proxy de back-end, como README, podem não estar disponíveis.", - "failed-to-authenticate-please-check-your-token": "Falha na autenticação. Verifique seu token." + "failed-to-authenticate-please-check-your-token": "Falha na autenticação. Verifique seu token.", + "token-permissions-tab-finegrained": "Refinado", + "token-permissions-tab-classic": "Clássico", + "token-permissions-feature-star": "Gerenciar estrelas", + "token-permissions-feature-gists": "Gists", + "token-permissions-feature-star-lists": "Listas de estrelas", + "token-permissions-feature-fork-sync": "Sincronização de forks", + "token-permissions-feature-workflows": "Executar workflows", + "token-permissions-feature-fork-workflows": "Forks / workflows", + "token-permissions-fine-star": "`Starring` leitura e escrita, `Metadata` leitura", + "token-permissions-fine-gists": "`Gists` gravação", + "token-permissions-fine-fork-sync": "`Contents` gravação", + "token-permissions-fine-workflows": "`Actions` gravação", + "token-permissions-classic-star": "`public_repo` (`repo` para repositórios privados)", + "token-permissions-classic-gists": "`gist`", + "token-permissions-classic-star-lists": "`user`", + "token-permissions-classic-fork-workflows": "`repo`", + "token-permissions-local-storage": "Seu token é armazenado apenas neste dispositivo e no seu próprio backend, nunca nos servidores do app." } } diff --git a/src/locales/pt-BR/repositories.json b/src/locales/pt-BR/repositories.json index d87442218..9b8874f02 100644 --- a/src/locales/pt-BR/repositories.json +++ b/src/locales/pt-BR/repositories.json @@ -1,5 +1,6 @@ { "batchStar": { + "token-permission-error": "O GitHub recusou este token (403). Para consultar ou adicionar estrelas, ative Starring com leitura/escrita e Metadata com leitura em um token refinado, ou public_repo em um token clássico (repo para repositórios privados).", "title": "Favoritar por links", "hint": "Cole qualquer texto com links do GitHub, confira os repositórios e adicione uma estrela. Links de outros sites são ignorados.", "history": "Histórico de textos", diff --git a/src/locales/ru/app.json b/src/locales/ru/app.json index 8467efd37..ec316fdd2 100644 --- a/src/locales/ru/app.json +++ b/src/locales/ru/app.json @@ -934,7 +934,10 @@ "check-if-a-new-version-is-available": "Проверьте, доступна ли новая версия", "contact-information": "Контактная информация", "if-you-encounter-any-issues-or-have-suggestions": "Если у вас возникнут какие-либо проблемы или у вас есть предложения во время использования, свяжитесь со мной следующими способами:", - "github": "GitHub" + "github": "GitHub", + "account-token-hint": "Вы вошли как {{login}}. Просроченный токен можно обновить здесь без выхода из аккаунта.", + "token-hint": "Просроченный токен можно обновить здесь без выхода из аккаунта.", + "token-permission-guide": "Руководство по настройке токена" }, "includeKeysToggle": { "include-keys-in-backup-export": "Включить ключи при резервном копировании/экспорте", diff --git a/src/locales/ru/login.json b/src/locales/ru/login.json index e72a15090..1c09d34e7 100644 --- a/src/locales/ru/login.json +++ b/src/locales/ru/login.json @@ -1,5 +1,7 @@ { "loginScreen": { + "token-permissions-title": "Права токена GitHub", + "token-permissions-check": "Вход проверяет личность, но не все права. Нехватка прав может проявиться только при использовании функции.", "please-enter-the-backend-url-and-api-key": "Введите URL-адрес серверной части и ключ API.", "invalid-backend-url-remote-backends-must-use-htt": "Неверный адрес бэкэнда: удаленному бэкэнду требуется HTTPS, только локальный хост может использовать HTTP.", "unable-to-connect-to-this-backend-url": "Невозможно подключиться к внутреннему URL-адресу", @@ -26,12 +28,6 @@ "connect-to-github": "Подключиться к GitHub", "connect-and-restore-data": "Подключитесь и восстановите данные", "save-and-continue": "Сохранить и продолжить", - "how-to-create-a-github-token": "Как создать токен GitHub:", - "go-to-github-settings-developer-settings-persona": "Посетите «Настройки GitHub» → «Настройки разработчика» → «Токены личного доступа».", - "click-generate-new-token-classic": "Нажмите «Создать новый токен (классический)».", - "select-scopes": "Выберите область разрешений:", - "and": "и", - "copy-the-generated-token-and-paste-it-above": "Скопируйте сгенерированный токен и вставьте его выше.", "create-token-on-github": "Создать токен на GitHub →", "already-have-backend-data": "У вас уже есть серверные данные", "sign-in-with-github-token": "Войдите в систему, используя токен GitHub.", @@ -41,6 +37,23 @@ "keep-local": "сохранять локальность", "please-enter-a-valid-github-token": "Введите действительный токен GitHub.", "signed-in-but-failed-to-save-github-token-to-bac": "Вы вошли в систему, но токен GitHub не удалось сохранить на серверной стороне, а функции внутреннего прокси, такие как README, могут быть недоступны.", - "failed-to-authenticate-please-check-your-token": "Аутентификация не удалась, проверьте свой токен." + "failed-to-authenticate-please-check-your-token": "Аутентификация не удалась, проверьте свой токен.", + "token-permissions-tab-finegrained": "Fine-grained", + "token-permissions-tab-classic": "Классический", + "token-permissions-feature-star": "Управление звёздами", + "token-permissions-feature-gists": "Gists", + "token-permissions-feature-star-lists": "Списки звёзд", + "token-permissions-feature-fork-sync": "Синхронизация форков", + "token-permissions-feature-workflows": "Запуск workflow", + "token-permissions-feature-fork-workflows": "Форки / workflow", + "token-permissions-fine-star": "`Starring` чтение и запись, `Metadata` чтение", + "token-permissions-fine-gists": "`Gists` запись", + "token-permissions-fine-fork-sync": "`Contents` запись", + "token-permissions-fine-workflows": "`Actions` запись", + "token-permissions-classic-star": "`public_repo` (`repo` для приватных репозиториев)", + "token-permissions-classic-gists": "`gist`", + "token-permissions-classic-star-lists": "`user`", + "token-permissions-classic-fork-workflows": "`repo`", + "token-permissions-local-storage": "Токен хранится только на этом устройстве и вашем собственном бэкенде — никогда на серверах приложения." } } diff --git a/src/locales/ru/repositories.json b/src/locales/ru/repositories.json index 83a31d8e2..0d1394203 100644 --- a/src/locales/ru/repositories.json +++ b/src/locales/ru/repositories.json @@ -1,5 +1,6 @@ { "batchStar": { + "token-permission-error": "GitHub отклонил токен (403). Для проверки или добавления звёзд токену с точной настройкой нужны Starring на чтение/запись и Metadata на чтение; классическому — public_repo (для приватных репозиториев repo).", "title": "Добавить звёзды по ссылкам", "hint": "Вставьте любой текст со ссылками на GitHub, проверьте репозитории и добавьте звёзды. Ссылки на другие сайты игнорируются.", "history": "История вставок", diff --git a/src/locales/zh-TW/app.json b/src/locales/zh-TW/app.json index f5c4683f0..460fe0358 100644 --- a/src/locales/zh-TW/app.json +++ b/src/locales/zh-TW/app.json @@ -934,7 +934,10 @@ "check-if-a-new-version-is-available": "檢查是否有新版本可用", "contact-information": "聯絡方式", "if-you-encounter-any-issues-or-have-suggestions": "如果您在使用過程中遇到任何問題或有建議,歡迎透過以下方式與我聯絡:", - "github": "GitHub" + "github": "GitHub", + "account-token-hint": "目前帳號 {{login}}。Token 過期後可直接在此更換,不必登出。", + "token-hint": "Token 過期後可直接在此更換,不必登出。", + "token-permission-guide": "查看 Token 設定指引" }, "includeKeysToggle": { "include-keys-in-backup-export": "備份/匯出時包含金鑰", diff --git a/src/locales/zh-TW/login.json b/src/locales/zh-TW/login.json index 8a2ceefa7..00df782db 100644 --- a/src/locales/zh-TW/login.json +++ b/src/locales/zh-TW/login.json @@ -1,5 +1,7 @@ { "loginScreen": { + "token-permissions-title": "GitHub Token 權限", + "token-permissions-check": "登入只驗證身分,不會檢查所有功能權限;權限不足可能在使用功能時才出現。", "please-enter-the-backend-url-and-api-key": "請輸入後端 URL 和 API Key", "invalid-backend-url-remote-backends-must-use-htt": "後端位址無效:遠端後端需使用 HTTPS,僅 localhost 可使用 HTTP", "unable-to-connect-to-this-backend-url": "無法連線到該後端 URL", @@ -26,12 +28,6 @@ "connect-to-github": "連接到GitHub", "connect-and-restore-data": "連線並恢復數據", "save-and-continue": "保存並繼續", - "how-to-create-a-github-token": "如何建立GitHub token:", - "go-to-github-settings-developer-settings-persona": "訪問GitHub Settings → Developer settings → Personal access tokens", - "click-generate-new-token-classic": "點選\"Generate new token (classic)\"", - "select-scopes": "選擇權限範圍:", - "and": "和", - "copy-the-generated-token-and-paste-it-above": "複製生成的token並貼上到上方", "create-token-on-github": "在GitHub上建立token →", "already-have-backend-data": "已有後端數據", "sign-in-with-github-token": "使用 GitHub Token 登入", @@ -41,6 +37,23 @@ "keep-local": "保留本地", "please-enter-a-valid-github-token": "請輸入有效的GitHub token", "signed-in-but-failed-to-save-github-token-to-bac": "已登錄,但 GitHub Token 未能保存到後端,README 等後端代理功能可能無法使用。", - "failed-to-authenticate-please-check-your-token": "認證失敗,請檢查您的token。" + "failed-to-authenticate-please-check-your-token": "認證失敗,請檢查您的token。", + "token-permissions-tab-finegrained": "細粒度 Token", + "token-permissions-tab-classic": "經典 Token", + "token-permissions-feature-star": "為儲存庫新增 Star", + "token-permissions-feature-gists": "Gist", + "token-permissions-feature-star-lists": "Star 清單", + "token-permissions-feature-fork-sync": "Fork 儲存庫同步", + "token-permissions-feature-workflows": "觸發工作流程", + "token-permissions-feature-fork-workflows": "Fork 同步 / 觸發工作流程", + "token-permissions-fine-star": "`Starring` 讀寫、`Metadata` 讀取", + "token-permissions-fine-gists": "`Gists` 寫入", + "token-permissions-fine-fork-sync": "`Contents` 寫入", + "token-permissions-fine-workflows": "`Actions` 寫入", + "token-permissions-classic-star": "`public_repo`(私有儲存庫用 `repo`)", + "token-permissions-classic-gists": "`gist`", + "token-permissions-classic-star-lists": "`user`", + "token-permissions-classic-fork-workflows": "`repo`", + "token-permissions-local-storage": "Token 僅儲存在本機與你設定的後端,不會上傳到應用方伺服器。" } } diff --git a/src/locales/zh-TW/repositories.json b/src/locales/zh-TW/repositories.json index 63e773c0e..f2aaef8da 100644 --- a/src/locales/zh-TW/repositories.json +++ b/src/locales/zh-TW/repositories.json @@ -1,5 +1,6 @@ { "batchStar": { + "token-permission-error": "GitHub 拒絕了此 Token(403)。檢查或新增 Star 時,細緻權限 Token 需開啟 Starring 讀寫及儲存庫 Metadata 讀取;傳統 Token 需 public_repo(私有儲存庫用 repo)。", "title": "從連結批次 Star", "hint": "貼上任何包含 GitHub 連結的文字,確認儲存庫後再 Star。其他網站的連結會被忽略。", "history": "貼上歷史", diff --git a/src/locales/zh/app.json b/src/locales/zh/app.json index 39df6ee8b..e3685b517 100644 --- a/src/locales/zh/app.json +++ b/src/locales/zh/app.json @@ -934,7 +934,10 @@ "check-if-a-new-version-is-available": "检查是否有新版本可用", "contact-information": "联系方式", "if-you-encounter-any-issues-or-have-suggestions": "如果您在使用过程中遇到任何问题或有建议,欢迎通过以下方式联系我:", - "github": "GitHub" + "github": "GitHub", + "account-token-hint": "当前账号 {{login}}。过期后可在此直接更换 token,不必退出登录。", + "token-hint": "过期后可在此直接更换 token,不必退出登录。", + "token-permission-guide": "查看 Token 配置指引" }, "includeKeysToggle": { "include-keys-in-backup-export": "备份/导出时包含密钥", diff --git a/src/locales/zh/login.json b/src/locales/zh/login.json index f187801de..c4133ef9f 100644 --- a/src/locales/zh/login.json +++ b/src/locales/zh/login.json @@ -1,5 +1,7 @@ { "loginScreen": { + "token-permissions-title": "GitHub Token 权限", + "token-permissions-check": "登录只验证身份,不会检查所有功能权限;权限不足可能在使用相应功能时才出现。", "please-enter-the-backend-url-and-api-key": "请输入后端 URL 和 API Key", "invalid-backend-url-remote-backends-must-use-htt": "后端地址无效:远程后端需使用 HTTPS,仅 localhost 可使用 HTTP", "unable-to-connect-to-this-backend-url": "无法连接到该后端 URL", @@ -26,12 +28,6 @@ "connect-to-github": "连接到GitHub", "connect-and-restore-data": "连接并恢复数据", "save-and-continue": "保存并继续", - "how-to-create-a-github-token": "如何创建GitHub token:", - "go-to-github-settings-developer-settings-persona": "访问GitHub Settings → Developer settings → Personal access tokens", - "click-generate-new-token-classic": "点击\"Generate new token (classic)\"", - "select-scopes": "选择权限范围:", - "and": "和", - "copy-the-generated-token-and-paste-it-above": "复制生成的token并粘贴到上方", "create-token-on-github": "在GitHub上创建token →", "already-have-backend-data": "已有后端数据", "sign-in-with-github-token": "使用 GitHub Token 登录", @@ -41,6 +37,23 @@ "keep-local": "保留本地", "please-enter-a-valid-github-token": "请输入有效的GitHub token", "signed-in-but-failed-to-save-github-token-to-bac": "已登录,但 GitHub Token 未能保存到后端,README 等后端代理功能可能不可用。", - "failed-to-authenticate-please-check-your-token": "认证失败,请检查您的token。" + "failed-to-authenticate-please-check-your-token": "认证失败,请检查您的token。", + "token-permissions-tab-finegrained": "细粒度 Token", + "token-permissions-tab-classic": "经典 Token", + "token-permissions-feature-star": "为仓库添加 Star", + "token-permissions-feature-gists": "Gist", + "token-permissions-feature-star-lists": "Star 清单", + "token-permissions-feature-fork-sync": "Fork 储存库同步", + "token-permissions-feature-workflows": "触发工作流", + "token-permissions-feature-fork-workflows": "Fork 同步 / 触发工作流", + "token-permissions-fine-star": "`Starring` 读写、`Metadata` 读取", + "token-permissions-fine-gists": "`Gists` 写入", + "token-permissions-fine-fork-sync": "`Contents` 写入", + "token-permissions-fine-workflows": "`Actions` 写入", + "token-permissions-classic-star": "`public_repo`(私有仓库用 `repo`)", + "token-permissions-classic-gists": "`gist`", + "token-permissions-classic-star-lists": "`user`", + "token-permissions-classic-fork-workflows": "`repo`", + "token-permissions-local-storage": "Token 仅保存在本机与你配置的后端,不会上传到应用方服务器。" } } diff --git a/src/locales/zh/repositories.json b/src/locales/zh/repositories.json index 58b589cd4..793e18484 100644 --- a/src/locales/zh/repositories.json +++ b/src/locales/zh/repositories.json @@ -1,5 +1,6 @@ { "batchStar": { + "token-permission-error": "GitHub 拒绝了此 Token(403)。检查或添加 Star 时,细粒度 Token 需开启 Starring 读写及仓库 Metadata 读取;传统 Token 需 public_repo(私有仓库用 repo)。", "title": "从链接批量 Star", "hint": "粘贴任何包含 GitHub 链接的文字,确认仓库后再 Star。其他网站的链接会被忽略。", "history": "粘贴历史", diff --git a/src/services/githubApi.starStatus.test.ts b/src/services/githubApi.starStatus.test.ts index cc227eb07..6f82c56ca 100644 --- a/src/services/githubApi.starStatus.test.ts +++ b/src/services/githubApi.starStatus.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import { GitHubApiService } from './githubApi'; +import { GitHubApiService, GitHubTokenPermissionError } from './githubApi'; afterEach(() => vi.unstubAllGlobals()); @@ -16,6 +16,29 @@ describe('GitHubApiService.isRepositoryStarred', () => { await expect(new GitHubApiService('test-token').isRepositoryStarred('owner', 'repo')).rejects.toThrow('403'); }); + it('identifies a PAT permission failure without conflating it with rate limiting', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response( + JSON.stringify({ message: 'Resource not accessible by personal access token' }), + { status: 403, statusText: 'Forbidden', headers: { 'X-RateLimit-Remaining': '4999' } }, + ))); + await expect(new GitHubApiService('test-token').isRepositoryStarred('owner', 'repo')) + .rejects.toBeInstanceOf(GitHubTokenPermissionError); + }); + + it('does not reuse a previous zero rate limit for a later permission failure', async () => { + const fetchMock = vi.fn() + .mockResolvedValueOnce(new Response(null, { status: 204, headers: { 'X-RateLimit-Remaining': '0' } })) + .mockResolvedValueOnce(new Response( + JSON.stringify({ message: 'Resource not accessible by personal access token' }), + { status: 403, statusText: 'Forbidden' }, + )); + vi.stubGlobal('fetch', fetchMock); + const api = new GitHubApiService('test-token'); + expect(await api.isRepositoryStarred('owner', 'first')).toBe(true); + await expect(api.isRepositoryStarred('owner', 'second')) + .rejects.toBeInstanceOf(GitHubTokenPermissionError); + }); + it('uses the configured backend proxy', async () => { const fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 204 })); vi.stubGlobal('fetch', fetchMock); diff --git a/src/services/githubApi.ts b/src/services/githubApi.ts index 4215d5b99..ebf073fe4 100644 --- a/src/services/githubApi.ts +++ b/src/services/githubApi.ts @@ -257,6 +257,14 @@ const GITHUB_API_BASE = 'https://api.github.com'; // Sentinel message for the 401 thrown above; callers match on it to tell a // confirmed auth failure apart from network/rate-limit/5xx errors. export const GITHUB_TOKEN_INVALID_ERROR = 'GitHub token expired or invalid'; + +/** GitHub accepted the token, but it lacks permission for this request. */ +export class GitHubTokenPermissionError extends Error { + constructor() { + super('Resource not accessible by personal access token'); + this.name = 'GitHubTokenPermissionError'; + } +} // Sentinel for 401s produced by the backend proxy's own auth middleware (its // body carries code: 'UNAUTHORIZED'). Kept distinct from the GitHub sentinel // so callers can tell a stale backend API key from a dead GitHub token. @@ -488,13 +496,20 @@ export class GitHubApiService { } throw new Error(GITHUB_TOKEN_INVALID_ERROR); } - if (response.status === 403 && this.rateLimitRemaining === 0) { + if (response.status === 403 && remaining === '0') { const resetDate = this.rateLimitReset ? new Date(this.rateLimitReset * 1000).toLocaleString() : 'unknown'; logger.warn('githubApi', 'API request failed: rate limit exceeded', { method, endpoint, status: response.status, durationMs }); throw new Error(`GitHub API rate limit exceeded. Resets at ${resetDate}`); } + if (response.status === 403) { + const body = await response.clone().json().catch(() => null) as { message?: string } | null; + if (body?.message === 'Resource not accessible by personal access token') { + logger.warn('githubApi', 'API request failed: token permission', { method, endpoint, status: response.status, durationMs }); + throw new GitHubTokenPermissionError(); + } + } // 5xx 视为可重试的瞬时故障;其余 4xx 直接抛出(重试无意义)。 const isRetryable = response.status >= 500 && response.status <= 599;