Skip to content

Commit

Permalink
chore: update version to 1.1.2 and enhance GitHub token validation
Browse files Browse the repository at this point in the history
- Bumped the version number from 1.1.1 to 1.1.2 in manifest.json to reflect the latest release.
- Removed outdated notice from README.md regarding v1.1.0 issues.
- Implemented GitHub token validation in GitHubSettings and App components, providing real-time feedback on token validity.
- Enhanced GitHubService to validate both token and associated user or organization, improving error handling and user experience during setup.
  • Loading branch information
mamertofabian committed Dec 26, 2024
1 parent badf7f8 commit 5e60f3d
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 21 deletions.
6 changes: 0 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,6 @@

A Chrome extension that automatically captures ZIP file downloads from bolt.new, extracts them, and pushes the contents to a specified GitHub repository. Built with Svelte, TypeScript, and TailwindCSS.

> **⚠️ Important Notice for v1.1.0 Users**
>
> The GitHub button is currently not working in v1.1.0 due to Bolt's recent addition of an Export dropdown button, which affects the Download button functionality used by this extension.
>
> ✨ A fix is already implemented in v1.1.1 and is currently under review in the Chrome Web Store. The update should be published within a few days.
## 📦 Installation Options

### Stable Version (Chrome Web Store)
Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Bolt to GitHub",
"version": "1.1.1",
"version": "1.1.2",
"description": "Automatically process your Bolt project zip files and upload them to your GitHub repository.",
"icons": {
"16": "assets/icons/icon16.png",
Expand Down
98 changes: 85 additions & 13 deletions src/lib/components/GitHubSettings.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
ExternalLink,
ChevronUp,
ChevronDown,
Check,
X,
} from "lucide-svelte";
import { onMount } from 'svelte';
import { GitHubService } from "../../services/GitHubService";
export let githubToken: string;
export let repoOwner: string;
Expand All @@ -30,6 +33,10 @@
"https://github.com/settings/tokens/new?scopes=repo&description=Bolt%20to%20GitHub";
let showNewUserGuide = true;
let isValidatingToken = false;
let isTokenValid: boolean | null = null;
let tokenValidationTimeout: number;
let validationError: string | null = null;
onMount(() => {
chrome.storage.local.get(['showNewUserGuide'], (result) => {
Expand All @@ -42,6 +49,52 @@
chrome.storage.local.set({ showNewUserGuide });
}
async function validateSettings() {
if (!githubToken) {
isTokenValid = null;
validationError = null;
return;
}
try {
isValidatingToken = true;
validationError = null;
const githubService = new GitHubService(githubToken);
const result = await githubService.validateTokenAndUser(repoOwner);
isTokenValid = result.isValid;
validationError = result.error || null;
} catch (error) {
console.error('Error validating settings:', error);
isTokenValid = false;
validationError = 'Validation failed';
} finally {
isValidatingToken = false;
}
}
function handleTokenInput() {
onInput();
isTokenValid = null;
validationError = null;
// Clear existing timeout
if (tokenValidationTimeout) {
clearTimeout(tokenValidationTimeout);
}
// Debounce validation to avoid too many API calls
tokenValidationTimeout = setTimeout(() => {
validateSettings();
}, 500) as unknown as number;
}
function handleOwnerInput() {
onInput();
if (githubToken) {
handleTokenInput(); // This will trigger validation of both token and username
}
}
$: if (projectId && projectSettings[projectId]) {
repoName = projectSettings[projectId].repoName;
branch = projectSettings[projectId].branch;
Expand Down Expand Up @@ -131,17 +184,32 @@
<div class="space-y-2">
<Label for="githubToken" class="text-slate-200">
GitHub Token
<span class="text-sm text-slate-400 ml-2">(Required for uploading)</span
>
<span class="text-sm text-slate-400 ml-2">(Required for uploading)</span>
</Label>
<Input
type="password"
id="githubToken"
bind:value={githubToken}
on:input={onInput}
placeholder="ghp_***********************************"
class="bg-slate-800 border-slate-700 text-slate-200 placeholder:text-slate-500"
/>
<div class="relative">
<Input
type="password"
id="githubToken"
bind:value={githubToken}
on:input={handleTokenInput}
placeholder="ghp_***********************************"
class="bg-slate-800 border-slate-700 text-slate-200 placeholder:text-slate-500 pr-10"
/>
{#if githubToken}
<div class="absolute right-3 top-1/2 -translate-y-1/2">
{#if isValidatingToken}
<div class="animate-spin h-4 w-4 border-2 border-slate-400 border-t-transparent rounded-full" />
{:else if isTokenValid === true}
<Check class="h-4 w-4 text-green-500" />
{:else if isTokenValid === false}
<X class="h-4 w-4 text-red-500" />
{/if}
</div>
{/if}
</div>
{#if validationError}
<p class="text-sm text-red-400 mt-1">{validationError}</p>
{/if}
</div>

<div class="space-y-2">
Expand All @@ -153,7 +221,7 @@
type="text"
id="repoOwner"
bind:value={repoOwner}
on:input={onInput}
on:input={handleOwnerInput}
placeholder="username or organization"
class="bg-slate-800 border-slate-700 text-slate-200 placeholder:text-slate-500"
/>
Expand Down Expand Up @@ -198,9 +266,13 @@
<Button
type="submit"
class="w-full bg-blue-600 hover:bg-blue-700 text-white"
disabled={!isSettingsValid || buttonDisabled}
disabled={!isSettingsValid || buttonDisabled || isValidatingToken || isTokenValid === false}
>
{status ? status : "Save Settings"}
{#if isValidatingToken}
Validating...
{:else}
{status ? status : "Save Settings"}
{/if}
</Button>
</form>
</div>
48 changes: 47 additions & 1 deletion src/popup/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import Footer from "$lib/components/Footer.svelte";
import type { GitHubSettingsInterface } from "$lib/types";
import ProjectsList from "$lib/components/ProjectsList.svelte";
import { GitHubService } from "../services/GitHubService";
let githubToken: string = "";
let repoOwner = "";
Expand All @@ -34,6 +35,33 @@
let parsedProjectId: string | null = null;
const version = chrome.runtime.getManifest().version;
let hasStatus = false;
let isValidatingToken = false;
let isTokenValid: boolean | null = null;
let validationError: string | null = null;
async function validateGitHubToken(token: string, username: string): Promise<boolean> {
if (!token) {
isTokenValid = false;
validationError = 'GitHub token is required';
return false;
}
try {
isValidatingToken = true;
const githubService = new GitHubService(token);
const result = await githubService.validateTokenAndUser(username);
isTokenValid = result.isValid;
validationError = result.error || null;
return result.isValid;
} catch (error) {
console.error('Error validating settings:', error);
isTokenValid = false;
validationError = 'Validation failed';
return false;
} finally {
isValidatingToken = false;
}
}
onMount(async () => {
// Add dark mode to the document
Expand All @@ -49,6 +77,11 @@
repoOwner = githubSettings.repoOwner || "";
projectSettings = githubSettings.projectSettings || {};
// Validate existing token and username if they exist
if (githubToken && repoOwner) {
await validateGitHubToken(githubToken, repoOwner);
}
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
console.log(`📄 App: ${tabs[0]?.url}`);
if (tabs[0]?.url) {
Expand Down Expand Up @@ -89,11 +122,24 @@
});
function checkSettingsValidity() {
isSettingsValid = Boolean(githubToken && repoOwner && repoName && branch);
// Only consider settings valid if we have all required fields AND the validation passed
isSettingsValid = Boolean(githubToken && repoOwner && repoName && branch) && !isValidatingToken && isTokenValid === true;
}
async function saveSettings() {
try {
// Validate token and username before saving
const isValid = await validateGitHubToken(githubToken, repoOwner);
if (!isValid) {
status = validationError || "Validation failed";
hasStatus = true;
setTimeout(() => {
status = "";
hasStatus = false;
}, 3000);
return;
}
const settings = {
githubToken: githubToken || "",
repoOwner: repoOwner || "",
Expand Down
52 changes: 52 additions & 0 deletions src/services/GitHubService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,58 @@ export class GitHubService {
this.token = token;
}

async validateToken(): Promise<boolean> {
try {
await this.request('GET', '/user');
return true;
} catch (error) {
console.error('Token validation failed:', error);
return false;
}
}

async validateTokenAndUser(username: string): Promise<{ isValid: boolean; error?: string }> {
try {
// First validate token and get authenticated user
try {
const authUser = await this.request('GET', '/user');
if (!authUser.login) {
return { isValid: false, error: 'Invalid GitHub token' };
}

// If username matches authenticated user, we're good
if (authUser.login.toLowerCase() === username.toLowerCase()) {
return { isValid: true };
}

// If username doesn't match, check if it's an organization the user has access to
try {
// Check if the target is an organization
const targetUser = await this.request('GET', `/users/${username}`);
if (targetUser.type === 'Organization') {
// Check if user has access to the organization
const orgs = await this.request('GET', '/user/orgs');
const hasOrgAccess = orgs.some((org: any) => org.login.toLowerCase() === username.toLowerCase());
if (hasOrgAccess) {
return { isValid: true };
}
return { isValid: false, error: 'Token does not have access to this organization' };
}

// If target is a user but not the authenticated user, token can't act as them
return { isValid: false, error: 'Token can only be used with your GitHub username or organizations you have access to' };
} catch (error) {
return { isValid: false, error: 'Invalid GitHub username or organization' };
}
} catch (error) {
return { isValid: false, error: 'Invalid GitHub token' };
}
} catch (error) {
console.error('Validation failed:', error);
return { isValid: false, error: 'Validation failed' };
}
}

async request(method: string, endpoint: string, body?: any, options: RequestInit = {}) {
const url = `${this.baseUrl}${endpoint}`;

Expand Down

0 comments on commit 5e60f3d

Please sign in to comment.