From 3cc1c98d4e92d3eb513ce42e760d8a7e2a4ea6a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Jan 2026 14:18:13 +0000 Subject: [PATCH 01/19] refactor: improve architecture with custom hooks and better state management - Add useKeyPairs hook to centralize key pair state and localStorage logic - Add useToast hook with ToastProvider for non-blocking notifications - Add unified validation service with validateKeyPair function - Refactor ActivationKeyEditor to use useReducer for predictable state - Remove Monaco Editor key prop to fix unnecessary re-mounts - Replace browser alerts with toast notifications throughout - Extract EXAMPLE_JWT constant and EMPTY_KEY_PAIR constant - Use useCallback for memoized event handlers These changes improve: - DRY: Single source of truth for keyPairs across pages - Maintainability: Reducer pattern makes state changes traceable - Performance: Monaco no longer remounts on JWT changes - UX: Toast notifications instead of blocking alerts --- src/App.tsx | 5 +- src/components/pages/ActivationKeyEditor.tsx | 276 ++++++++++++------- src/components/pages/Keys.tsx | 201 ++++++-------- src/hooks/use-key-pairs.ts | 94 +++++++ src/hooks/use-toast.tsx | 164 +++++++++++ src/utils/validation.ts | 109 ++++++++ 6 files changed, 630 insertions(+), 219 deletions(-) create mode 100644 src/hooks/use-key-pairs.ts create mode 100644 src/hooks/use-toast.tsx create mode 100644 src/utils/validation.ts diff --git a/src/App.tsx b/src/App.tsx index 8306762..1efb304 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,8 @@ import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom'; import './App.css'; import { Key, ShieldCheck } from 'lucide-react'; -import { ThemeProvider } from './hooks/use-theme' +import { ThemeProvider } from './hooks/use-theme'; +import { ToastProvider } from './hooks/use-toast'; import { ThemeToggle } from './components/ui/theme-toggle'; // Import page components @@ -16,6 +17,7 @@ function App() { return ( +
+
); } diff --git a/src/components/pages/ActivationKeyEditor.tsx b/src/components/pages/ActivationKeyEditor.tsx index 3e54e0e..97c9550 100644 --- a/src/components/pages/ActivationKeyEditor.tsx +++ b/src/components/pages/ActivationKeyEditor.tsx @@ -1,155 +1,227 @@ -import { useState, useEffect } from 'react'; +import { useReducer, useEffect, useCallback } from 'react'; import { Card, CardContent } from '../ui/card'; import { Textarea } from '../ui/textarea'; import Editor from "@monaco-editor/react"; import { Button } from '../ui/button'; import { X, Check, Copy } from 'lucide-react'; -import { Algorithm, SUPPORTED_ALGORITHMS, KeyPair } from '../../types/activationKey'; +import { Algorithm, SUPPORTED_ALGORITHMS } from '../../types/activationKey'; import { decodeJWT, getJwtMetadata, signJWT, validateJWTSignature, ValidationResult } from '../../utils/activationKey'; import { ActivationKeyMetadataDisplay } from '../activationkey/ActivationKeyMetadata'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'; import { PageHeader } from '../ui/page-header'; import { ValidationStatus } from '../activationkey/validation-status'; -import './ActivationKeyEditor.css'; import { useTheme } from '../../hooks/use-theme'; +import { useKeyPairs } from '../../hooks/use-key-pairs'; +import { useToast } from '../../hooks/use-toast'; +import './ActivationKeyEditor.css'; + +// Example JWT for demonstration +const EXAMPLE_JWT = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJleHAiOjg4MDYxMjEyODAwLCJpc3MiOiJodHRwczovL2FwaS5iZXNodS50ZWNoIiwiaWF0IjoxNjYxMzU2MTAxLCJqdGkiOiJhbmFwaG9yYV9saWNfMjViMmFhYTgtMTQwMS00YjhmLThkMGYtNmMzMTdmOWJhNjcwIiwiYXVkIjoiQW5hcGhvcmEiLCJzdWIiOiIxMTExMTExMS0xMTExLTExMTEtMTExMS0xMTExMTExMSIsImxpY2Vuc29yIjp7Im5hbWUiOiJBbmFwaG9yYSIsImNvbnRhY3QiOlsic3VwcG9ydEByZWFkb25seXJlc3QuY29tIiwiZmluYW5jZUByZWFkb25seXJlc3QuY29tIl0sImlzc3VlciI6InN1cHBvcnRAcmVhZG9ubHlyZXN0LmNvbSJ9LCJsaWNlbnNlZSI6eyJuYW1lIjoiQW5vbnltb3VzIEZyZWUgVXNlciIsImJ1eWluZ19mb3IiOm51bGwsImJpbGxpbmdfZW1haWwiOiJ1bmtub3duQGFuYXBob3JhLmNvbSIsImFsdF9lbWFpbHMiOltdLCJhZGRyZXNzIjpbIlVua25vd24iXX0sImxpY2Vuc2UiOnsiY2x1c3Rlcl91dWlkIjoiKiIsImVkaXRpb24iOiJmcmVlIiwiZWRpdGlvbl9uYW1lIjoiRnJlZSIsImlzVHJpYWwiOmZhbHNlfX0.ATAB81zkWRxTdpSD_23tcFxba81OCrjdtcGlx_yXwa2VSvJAx7rWQYO2VM2N8zeknA01SzYpPP2o_FXzP3TCEo4iABRof2G1u0iD1AFf5Y0m_TYPs89acR5Fztb46wSBwsj4L1ONal0y8xHYfJC54SKwdXJV4XTJwIP2tBVcTl9QNAfn"; + +// State type for the editor +interface EditorState { + inputValue: string; + jwt: string; + editorValue: string; + algorithm: Algorithm; + expiryDate: Date | undefined; + selectedKeyId: string; + validation: ValidationResult | null; + showCopied: boolean; +} + +// Action types for the reducer +type EditorAction = + | { type: 'SET_INPUT'; payload: string } + | { type: 'SET_JWT'; payload: { jwt: string; editorValue: string; algorithm?: Algorithm; expiryDate?: Date } } + | { type: 'SET_EDITOR_VALUE'; payload: string } + | { type: 'SET_ALGORITHM'; payload: Algorithm } + | { type: 'SET_EXPIRY_DATE'; payload: Date | undefined } + | { type: 'SET_SELECTED_KEY'; payload: string } + | { type: 'SET_VALIDATION'; payload: ValidationResult | null } + | { type: 'SHOW_COPIED' } + | { type: 'HIDE_COPIED' } + | { type: 'CLEAR' }; + +const initialState: EditorState = { + inputValue: '', + jwt: '', + editorValue: '{}', + algorithm: 'ES512', + expiryDate: undefined, + selectedKeyId: '', + validation: null, + showCopied: false, +}; + +function editorReducer(state: EditorState, action: EditorAction): EditorState { + switch (action.type) { + case 'SET_INPUT': + return { ...state, inputValue: action.payload }; + case 'SET_JWT': + return { + ...state, + jwt: action.payload.jwt, + editorValue: action.payload.editorValue, + algorithm: action.payload.algorithm ?? state.algorithm, + expiryDate: action.payload.expiryDate ?? state.expiryDate, + }; + case 'SET_EDITOR_VALUE': + return { ...state, editorValue: action.payload }; + case 'SET_ALGORITHM': + return { ...state, algorithm: action.payload }; + case 'SET_EXPIRY_DATE': + return { ...state, expiryDate: action.payload }; + case 'SET_SELECTED_KEY': + return { ...state, selectedKeyId: action.payload }; + case 'SET_VALIDATION': + return { ...state, validation: action.payload }; + case 'SHOW_COPIED': + return { ...state, showCopied: true }; + case 'HIDE_COPIED': + return { ...state, showCopied: false }; + case 'CLEAR': + return { ...initialState, selectedKeyId: state.selectedKeyId }; + default: + return state; + } +} const ActivationKeyEditor = () => { const { theme } = useTheme(); - const [inputValue, setInputValue] = useState(''); - const [jwt, setJwt] = useState(''); - const [keyPairs, setKeyPairs] = useState([]); - const [selectedKeyId, setSelectedKeyId] = useState(''); - const [expiryDate, setExpiryDate] = useState(undefined); - const [showCopied, setShowCopied] = useState(false); - const [algorithm, setAlgorithm] = useState('ES512'); - const [editorValue, setEditorValue] = useState('{}'); - const [signatureValidation, setSignatureValidation] = useState(null); - - const exampleJwt = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJleHAiOjg4MDYxMjEyODAwLCJpc3MiOiJodHRwczovL2FwaS5iZXNodS50ZWNoIiwiaWF0IjoxNjYxMzU2MTAxLCJqdGkiOiJhbmFwaG9yYV9saWNfMjViMmFhYTgtMTQwMS00YjhmLThkMGYtNmMzMTdmOWJhNjcwIiwiYXVkIjoiQW5hcGhvcmEiLCJzdWIiOiIxMTExMTExMS0xMTExLTExMTEtMTExMS0xMTExMTExMSIsImxpY2Vuc29yIjp7Im5hbWUiOiJBbmFwaG9yYSIsImNvbnRhY3QiOlsic3VwcG9ydEByZWFkb25seXJlc3QuY29tIiwiZmluYW5jZUByZWFkb25seXJlc3QuY29tIl0sImlzc3VlciI6InN1cHBvcnRAcmVhZG9ubHlyZXN0LmNvbSJ9LCJsaWNlbnNlZSI6eyJuYW1lIjoiQW5vbnltb3VzIEZyZWUgVXNlciIsImJ1eWluZ19mb3IiOm51bGwsImJpbGxpbmdfZW1haWwiOiJ1bmtub3duQGFuYXBob3JhLmNvbSIsImFsdF9lbWFpbHMiOltdLCJhZGRyZXNzIjpbIlVua25vd24iXX0sImxpY2Vuc2UiOnsiY2x1c3Rlcl91dWlkIjoiKiIsImVkaXRpb24iOiJmcmVlIiwiZWRpdGlvbl9uYW1lIjoiRnJlZSIsImlzVHJpYWwiOmZhbHNlfX0.ATAB81zkWRxTdpSD_23tcFxba81OCrjdtcGlx_yXwa2VSvJAx7rWQYO2VM2N8zeknA01SzYpPP2o_FXzP3TCEo4iABRof2G1u0iD1AFf5Y0m_TYPs89acR5Fztb46wSBwsj4L1ONal0y8xHYfJC54SKwdXJV4XTJwIP2tBVcTl9QNAfn" + const { keyPairs, getKeyPairById } = useKeyPairs(); + const { error: showError } = useToast(); + const [state, dispatch] = useReducer(editorReducer, initialState); + + const { inputValue, jwt, editorValue, algorithm, expiryDate, selectedKeyId, validation, showCopied } = state; + + // Set initial selected key when keyPairs load useEffect(() => { - const savedKeys = localStorage.getItem('keyPairs'); - if (savedKeys) { - const keys = JSON.parse(savedKeys); - setKeyPairs(keys); - if (keys.length > 0 && !selectedKeyId) { - setSelectedKeyId(keys[0].id); - } + if (keyPairs.length > 0 && !selectedKeyId) { + dispatch({ type: 'SET_SELECTED_KEY', payload: keyPairs[0].id }); + } + }, [keyPairs, selectedKeyId]); + + // Validate JWT when selected key changes + useEffect(() => { + if (jwt && selectedKeyId) { + const selectedKey = getKeyPairById(selectedKeyId); + validateJWTSignature(jwt, selectedKey).then(result => { + dispatch({ type: 'SET_VALIDATION', payload: result }); + }); } - }, [selectedKeyId]); + }, [selectedKeyId, jwt, getKeyPairById]); - const handleInputChange = async (value: string) => { - setInputValue(value); + const handleInputChange = useCallback(async (value: string) => { + dispatch({ type: 'SET_INPUT', payload: value }); if (!value) { - clearJwt(); + dispatch({ type: 'CLEAR' }); return; } - if (!selectedKeyId && keyPairs.length > 0) { - setSelectedKeyId(keyPairs[0].id); - } - // Try to parse as JWT const metadata = getJwtMetadata(value); + let newAlgorithm: Algorithm | undefined; + let newExpiryDate: Date | undefined; + if (metadata) { if (metadata.algorithm && SUPPORTED_ALGORITHMS.includes(metadata.algorithm as Algorithm)) { - setAlgorithm(metadata.algorithm as Algorithm); + newAlgorithm = metadata.algorithm as Algorithm; } if (metadata.expiresAt) { - setExpiryDate(new Date(metadata.expiresAt)); + newExpiryDate = new Date(metadata.expiresAt); } } const decoded = await decodeJWT(value); + let payloadOnly = '{}'; try { const parsedDecoded = JSON.parse(decoded); - const payloadOnly = JSON.stringify(parsedDecoded.payload || {}, null, 2); - setEditorValue(payloadOnly); - } catch (e) { - setEditorValue('{}'); + payloadOnly = JSON.stringify(parsedDecoded.payload || {}, null, 2); + } catch { + // Keep default empty object } - setJwt(value); + dispatch({ + type: 'SET_JWT', + payload: { + jwt: value, + editorValue: payloadOnly, + algorithm: newAlgorithm, + expiryDate: newExpiryDate, + }, + }); + + // Validate signature + const selectedKey = getKeyPairById(selectedKeyId); try { - // Only validate against the selected key pair - const selectedKey = keyPairs.find(k => k.id === selectedKeyId); - const validation = await validateJWTSignature(value, selectedKey); - setSignatureValidation(validation); - } catch (error) { - setSignatureValidation({ - isValid: false, - error: "Invalid signature", + const validationResult = await validateJWTSignature(value, selectedKey); + dispatch({ type: 'SET_VALIDATION', payload: validationResult }); + } catch { + dispatch({ + type: 'SET_VALIDATION', + payload: { isValid: false, error: 'Invalid signature' }, }); } - }; + }, [selectedKeyId, getKeyPairById]); - // Also update validation when the selected key changes - useEffect(() => { - if (jwt && selectedKeyId) { - const selectedKey = keyPairs.find(k => k.id === selectedKeyId); - validateJWTSignature(jwt, selectedKey).then(setSignatureValidation); + const handleSign = useCallback(async () => { + const selectedKey = getKeyPairById(selectedKeyId); + if (!selectedKey) return; + + let payload; + try { + payload = JSON.parse(editorValue); + } catch { + showError('Invalid JSON payload'); + return; } - }, [selectedKeyId, jwt]); - const clearJwt = () => { - setInputValue(''); - setJwt(''); - setSignatureValidation(null); - if (keyPairs.length > 0) { - setSelectedKeyId(keyPairs[0].id); - } else { - setSelectedKeyId(''); + // Only override exp if a new expiry date is selected + if (expiryDate) { + payload.exp = Math.floor(expiryDate.getTime() / 1000); } - }; - const handleSign = async () => { try { - const selectedKey = keyPairs.find(k => k.id === selectedKeyId); - if (!selectedKey) return; - - let payload; - try { - // Parse the editor value directly as the payload - payload = JSON.parse(editorValue); - } catch (e) { - alert('Invalid JSON payload'); - return; - } - - // Only override exp if a new expiry date is selected - if (expiryDate) { - payload.exp = Math.floor(expiryDate.getTime() / 1000); - } - const newToken = await signJWT( payload, algorithm, selectedKey, - // Use the expiry date from the payload if no new date is selected expiryDate || (payload.exp ? new Date(payload.exp * 1000) : new Date()) ); - setJwt(newToken); const newDecoded = await decodeJWT(newToken); - // Extract just the payload object for the editor + let payloadOnly = '{}'; try { const parsedDecoded = JSON.parse(newDecoded); - const payloadOnly = JSON.stringify(parsedDecoded.payload || {}, null, 2); - setEditorValue(payloadOnly); - } catch (e) { - setEditorValue('{}'); + payloadOnly = JSON.stringify(parsedDecoded.payload || {}, null, 2); + } catch { + // Keep default } + + dispatch({ + type: 'SET_JWT', + payload: { jwt: newToken, editorValue: payloadOnly }, + }); } catch (error) { console.error('Signing error:', error); - alert('Failed to sign JWT'); + showError('Failed to sign JWT'); } - }; + }, [selectedKeyId, editorValue, expiryDate, algorithm, getKeyPairById, showError]); - const copyToClipboard = async (text: string) => { + const copyToClipboard = useCallback(async (text: string) => { await navigator.clipboard.writeText(text); - setShowCopied(true); - setTimeout(() => setShowCopied(false), 2000); - }; + dispatch({ type: 'SHOW_COPIED' }); + setTimeout(() => dispatch({ type: 'HIDE_COPIED' }), 2000); + }, []); + + const clearJwt = useCallback(() => { + dispatch({ type: 'CLEAR' }); + if (keyPairs.length > 0) { + dispatch({ type: 'SET_SELECTED_KEY', payload: keyPairs[0].id }); + } + }, [keyPairs]); return (
@@ -167,7 +239,7 @@ const ActivationKeyEditor = () => { className="ak-input" /> - + {showSec1Info && (

- If you have a SEC1 PEM key (starting with "BEGIN EC PRIVATE KEY"), + If you have a SEC1 PEM key (starting with "BEGIN EC PRIVATE KEY"), you can convert it to PKCS#8 format using this command:

@@ -245,8 +217,8 @@ const Keys = () => {
               
             
           
-          
                     {pair.name}
                   
- +
- )} - +
+ ); + })} +
+ + {/* Confirm Dialog */} + {confirmState && ( +
+
handleConfirm(false)} + /> +
+

{confirmState.message}

+
+ + +
+
+
+ )} + + ); +} + +export function useToast(): ToastContextValue { + const context = useContext(ToastContext); + if (!context) { + throw new Error('useToast must be used within a ToastProvider'); + } + return context; +} diff --git a/src/utils/validation.ts b/src/utils/validation.ts new file mode 100644 index 0000000..f106001 --- /dev/null +++ b/src/utils/validation.ts @@ -0,0 +1,109 @@ +import { KeyPair } from '../types/activationKey'; + +export interface KeyPairValidationErrors { + name?: string; + publicKey?: string; + privateKey?: string; +} + +/** + * Validates private key format + * Returns error message if invalid, null if valid + */ +export function validatePrivateKeyFormat(key: string): string | null { + const trimmed = key.trim(); + + if (!trimmed) { + return 'Private key is required'; + } + + if (trimmed.includes('-----BEGIN EC PRIVATE KEY-----')) { + return 'SEC1 private key format detected. Please convert to PKCS#8 format using:\nopenssl pkcs8 -topk8 -nocrypt -in sec1-key.pem -out pkcs8-key.pem'; + } + + if (!trimmed.includes('-----BEGIN PRIVATE KEY-----')) { + return "Invalid private key format. Key must be in PKCS#8 PEM format (starting with '-----BEGIN PRIVATE KEY-----')"; + } + + return null; +} + +/** + * Validates public key format + * Returns error message if invalid, null if valid + */ +export function validatePublicKeyFormat(key: string): string | null { + const trimmed = key.trim(); + + if (!trimmed) { + return 'Public key is required'; + } + + if (!trimmed.includes('-----BEGIN PUBLIC KEY-----')) { + return "Invalid public key format. Key must start with '-----BEGIN PUBLIC KEY-----'"; + } + + return null; +} + +/** + * Validates a key pair name + * Returns error message if invalid, null if valid + */ +export function validateKeyPairName(name: string): string | null { + const trimmed = name.trim(); + + if (!trimmed) { + return 'Name is required'; + } + + if (trimmed.length < 2) { + return 'Name must be at least 2 characters'; + } + + if (trimmed.length > 100) { + return 'Name must be less than 100 characters'; + } + + return null; +} + +/** + * Validates an entire key pair object + * Returns an object with field-specific errors, empty object if all valid + */ +export function validateKeyPair(keyPair: Partial): KeyPairValidationErrors { + const errors: KeyPairValidationErrors = {}; + + const nameError = validateKeyPairName(keyPair.name ?? ''); + if (nameError) { + errors.name = nameError; + } + + const publicKeyError = validatePublicKeyFormat(keyPair.publicKey ?? ''); + if (publicKeyError) { + errors.publicKey = publicKeyError; + } + + const privateKeyError = validatePrivateKeyFormat(keyPair.privateKey ?? ''); + if (privateKeyError) { + errors.privateKey = privateKeyError; + } + + return errors; +} + +/** + * Check if validation errors object has any errors + */ +export function hasValidationErrors(errors: KeyPairValidationErrors): boolean { + return Object.keys(errors).length > 0; +} + +/** + * Get first error message from validation errors (useful for simple alert display) + */ +export function getFirstError(errors: KeyPairValidationErrors): string | null { + const firstKey = Object.keys(errors)[0] as keyof KeyPairValidationErrors | undefined; + return firstKey ? errors[firstKey] ?? null : null; +} From fab88f6f6f7748b668959e884d7a58a69629fe42 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Jan 2026 14:19:03 +0000 Subject: [PATCH 02/19] chore: update package-lock.json --- package-lock.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package-lock.json b/package-lock.json index 4f4598a..d70aa12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,9 @@ "tailwindcss": "^3.4.1", "typescript": "^5.2.2", "vite": "^5.0.8" + }, + "engines": { + "node": ">=22.0.0" } }, "node_modules/@aashutoshrathi/word-wrap": { From e4986d99d0cfaad81d35031b5a74a4799f7215c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Jan 2026 14:32:31 +0000 Subject: [PATCH 03/19] ci: add GitHub Pages deployment with PR previews - Add pr-preview.yml workflow for PR preview deployments - Add deploy.yml workflow for main branch deployment - Update vite.config.ts to support dynamic base path via VITE_BASE_PATH PR previews will be available at: https://.github.io/ak-tools/pr-preview/pr-/ --- .github/workflows/deploy.yml | 52 ++++++++++++++++++++++++++++++++ .github/workflows/pr-preview.yml | 52 ++++++++++++++++++++++++++++++++ vite.config.ts | 2 ++ 3 files changed, 106 insertions(+) create mode 100644 .github/workflows/deploy.yml create mode 100644 .github/workflows/pr-preview.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..c40d3e5 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,52 @@ +name: Deploy to GitHub Pages + +on: + push: + branches: [main, master] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + env: + VITE_BASE_PATH: /ak-tools/ + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./dist + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml new file mode 100644 index 0000000..908df87 --- /dev/null +++ b/.github/workflows/pr-preview.yml @@ -0,0 +1,52 @@ +name: PR Preview + +on: + pull_request: + types: [opened, synchronize, reopened, closed] + +concurrency: preview-${{ github.ref }} + +jobs: + build-preview: + if: github.event.action != 'closed' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + env: + # Set base path for PR preview subdirectory + VITE_BASE_PATH: /ak-tools/pr-preview/pr-${{ github.event.number }}/ + + - name: Deploy Preview + uses: rossjrw/pr-preview-action@v1 + with: + source-dir: ./dist/ + preview-branch: gh-pages + umbrella-dir: pr-preview + action: deploy + + cleanup-preview: + if: github.event.action == 'closed' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Cleanup Preview + uses: rossjrw/pr-preview-action@v1 + with: + preview-branch: gh-pages + umbrella-dir: pr-preview + action: remove diff --git a/vite.config.ts b/vite.config.ts index be139b4..0dc6b4a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -4,6 +4,8 @@ import { defineConfig } from 'vite' export default defineConfig({ plugins: [react()], + // Use VITE_BASE_PATH for PR previews, default to '/' for local dev + base: process.env.VITE_BASE_PATH || '/', resolve: { alias: { '@': path.resolve(__dirname, './src'), From 1d3496ce256b540957541f45cdebbca19e808ec5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Jan 2026 14:39:44 +0000 Subject: [PATCH 04/19] feat: make Issued date editable via calendar picker - Add issuedDate and onIssuedChange props to ActivationKeyMetadataDisplay - Add SET_ISSUED_DATE action to editor reducer - Extract and display issuedAt from JWT metadata - Update payload.iat when signing with custom issued date --- .../activationkey/ActivationKeyMetadata.tsx | 34 +++++++++++++++---- src/components/pages/ActivationKeyEditor.tsx | 24 +++++++++++-- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/components/activationkey/ActivationKeyMetadata.tsx b/src/components/activationkey/ActivationKeyMetadata.tsx index acfc9c3..7a77712 100644 --- a/src/components/activationkey/ActivationKeyMetadata.tsx +++ b/src/components/activationkey/ActivationKeyMetadata.tsx @@ -11,12 +11,16 @@ interface ActivationKeyMetadataDisplayProps { metadata: ActivationKeyMetadata; expiryDate?: Date; onExpiryChange?: (date: Date | undefined) => void; + issuedDate?: Date; + onIssuedChange?: (date: Date | undefined) => void; } -export const ActivationKeyMetadataDisplay: React.FC = ({ - metadata, +export const ActivationKeyMetadataDisplay: React.FC = ({ + metadata, expiryDate, - onExpiryChange + onExpiryChange, + issuedDate, + onIssuedChange, }) => { return (
@@ -34,9 +38,27 @@ export const ActivationKeyMetadataDisplay: React.FC
Issued
-
- {formatRelativeTime(metadata.issuedAt)} -
+ {onIssuedChange ? ( + + + + + + + + + ) : ( +
+ {formatRelativeTime(metadata.issuedAt)} +
+ )}
diff --git a/src/components/pages/ActivationKeyEditor.tsx b/src/components/pages/ActivationKeyEditor.tsx index 97c9550..dad8941 100644 --- a/src/components/pages/ActivationKeyEditor.tsx +++ b/src/components/pages/ActivationKeyEditor.tsx @@ -25,6 +25,7 @@ interface EditorState { editorValue: string; algorithm: Algorithm; expiryDate: Date | undefined; + issuedDate: Date | undefined; selectedKeyId: string; validation: ValidationResult | null; showCopied: boolean; @@ -33,10 +34,11 @@ interface EditorState { // Action types for the reducer type EditorAction = | { type: 'SET_INPUT'; payload: string } - | { type: 'SET_JWT'; payload: { jwt: string; editorValue: string; algorithm?: Algorithm; expiryDate?: Date } } + | { type: 'SET_JWT'; payload: { jwt: string; editorValue: string; algorithm?: Algorithm; expiryDate?: Date; issuedDate?: Date } } | { type: 'SET_EDITOR_VALUE'; payload: string } | { type: 'SET_ALGORITHM'; payload: Algorithm } | { type: 'SET_EXPIRY_DATE'; payload: Date | undefined } + | { type: 'SET_ISSUED_DATE'; payload: Date | undefined } | { type: 'SET_SELECTED_KEY'; payload: string } | { type: 'SET_VALIDATION'; payload: ValidationResult | null } | { type: 'SHOW_COPIED' } @@ -49,6 +51,7 @@ const initialState: EditorState = { editorValue: '{}', algorithm: 'ES512', expiryDate: undefined, + issuedDate: undefined, selectedKeyId: '', validation: null, showCopied: false, @@ -65,6 +68,7 @@ function editorReducer(state: EditorState, action: EditorAction): EditorState { editorValue: action.payload.editorValue, algorithm: action.payload.algorithm ?? state.algorithm, expiryDate: action.payload.expiryDate ?? state.expiryDate, + issuedDate: action.payload.issuedDate ?? state.issuedDate, }; case 'SET_EDITOR_VALUE': return { ...state, editorValue: action.payload }; @@ -72,6 +76,8 @@ function editorReducer(state: EditorState, action: EditorAction): EditorState { return { ...state, algorithm: action.payload }; case 'SET_EXPIRY_DATE': return { ...state, expiryDate: action.payload }; + case 'SET_ISSUED_DATE': + return { ...state, issuedDate: action.payload }; case 'SET_SELECTED_KEY': return { ...state, selectedKeyId: action.payload }; case 'SET_VALIDATION': @@ -93,7 +99,7 @@ const ActivationKeyEditor = () => { const { error: showError } = useToast(); const [state, dispatch] = useReducer(editorReducer, initialState); - const { inputValue, jwt, editorValue, algorithm, expiryDate, selectedKeyId, validation, showCopied } = state; + const { inputValue, jwt, editorValue, algorithm, expiryDate, issuedDate, selectedKeyId, validation, showCopied } = state; // Set initial selected key when keyPairs load useEffect(() => { @@ -124,6 +130,7 @@ const ActivationKeyEditor = () => { const metadata = getJwtMetadata(value); let newAlgorithm: Algorithm | undefined; let newExpiryDate: Date | undefined; + let newIssuedDate: Date | undefined; if (metadata) { if (metadata.algorithm && SUPPORTED_ALGORITHMS.includes(metadata.algorithm as Algorithm)) { @@ -132,6 +139,9 @@ const ActivationKeyEditor = () => { if (metadata.expiresAt) { newExpiryDate = new Date(metadata.expiresAt); } + if (metadata.issuedAt) { + newIssuedDate = new Date(metadata.issuedAt); + } } const decoded = await decodeJWT(value); @@ -150,6 +160,7 @@ const ActivationKeyEditor = () => { editorValue: payloadOnly, algorithm: newAlgorithm, expiryDate: newExpiryDate, + issuedDate: newIssuedDate, }, }); @@ -183,6 +194,11 @@ const ActivationKeyEditor = () => { payload.exp = Math.floor(expiryDate.getTime() / 1000); } + // Only override iat if a new issued date is selected + if (issuedDate) { + payload.iat = Math.floor(issuedDate.getTime() / 1000); + } + try { const newToken = await signJWT( payload, @@ -208,7 +224,7 @@ const ActivationKeyEditor = () => { console.error('Signing error:', error); showError('Failed to sign JWT'); } - }, [selectedKeyId, editorValue, expiryDate, algorithm, getKeyPairById, showError]); + }, [selectedKeyId, editorValue, expiryDate, issuedDate, algorithm, getKeyPairById, showError]); const copyToClipboard = useCallback(async (text: string) => { await navigator.clipboard.writeText(text); @@ -316,6 +332,8 @@ const ActivationKeyEditor = () => { metadata={getJwtMetadata(jwt)!} expiryDate={expiryDate} onExpiryChange={(date) => dispatch({ type: 'SET_EXPIRY_DATE', payload: date })} + issuedDate={issuedDate} + onIssuedChange={(date) => dispatch({ type: 'SET_ISSUED_DATE', payload: date })} /> )} From e68435aab3e59818d36b236be772fe9aa11b542b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Jan 2026 14:52:46 +0000 Subject: [PATCH 05/19] feat: add JWT templates management - Add useTemplates hook for managing activation key templates - Create Templates page for adding/editing/deleting templates - Add Templates route to navigation (Editor, Templates, Keys) - Replace "Use example" with templates dropdown in editor - Include default Anaphora Free template - Templates stored in localStorage with CRUD operations --- src/App.tsx | 7 +- src/components/pages/ActivationKeyEditor.tsx | 62 +++- src/components/pages/Templates.tsx | 326 +++++++++++++++++++ src/hooks/use-templates.ts | 120 +++++++ 4 files changed, 502 insertions(+), 13 deletions(-) create mode 100644 src/components/pages/Templates.tsx create mode 100644 src/hooks/use-templates.ts diff --git a/src/App.tsx b/src/App.tsx index 1efb304..fb41ce0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,17 +1,19 @@ import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom'; import './App.css'; -import { Key, ShieldCheck } from 'lucide-react'; +import { Key, ShieldCheck, FileText } from 'lucide-react'; import { ThemeProvider } from './hooks/use-theme'; import { ToastProvider } from './hooks/use-toast'; import { ThemeToggle } from './components/ui/theme-toggle'; // Import page components import Keys from './components/pages/Keys'; +import Templates from './components/pages/Templates'; import ActivationKeyEditor from './components/pages/ActivationKeyEditor'; function App() { const menuItems = [ - { title: "Activation Key Editor", icon: ShieldCheck, path: "/" }, + { title: "Editor", icon: ShieldCheck, path: "/" }, + { title: "Templates", icon: FileText, path: "/templates" }, { title: "Keys", icon: Key, path: "/keys" }, ]; @@ -80,6 +82,7 @@ function App() {
} /> + } /> } />
diff --git a/src/components/pages/ActivationKeyEditor.tsx b/src/components/pages/ActivationKeyEditor.tsx index dad8941..1f82a41 100644 --- a/src/components/pages/ActivationKeyEditor.tsx +++ b/src/components/pages/ActivationKeyEditor.tsx @@ -1,23 +1,23 @@ import { useReducer, useEffect, useCallback } from 'react'; +import { Link } from 'react-router-dom'; import { Card, CardContent } from '../ui/card'; import { Textarea } from '../ui/textarea'; import Editor from "@monaco-editor/react"; import { Button } from '../ui/button'; -import { X, Check, Copy } from 'lucide-react'; +import { X, Check, Copy, FileText, Plus } from 'lucide-react'; import { Algorithm, SUPPORTED_ALGORITHMS } from '../../types/activationKey'; import { decodeJWT, getJwtMetadata, signJWT, validateJWTSignature, ValidationResult } from '../../utils/activationKey'; import { ActivationKeyMetadataDisplay } from '../activationkey/ActivationKeyMetadata'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'; +import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; import { PageHeader } from '../ui/page-header'; import { ValidationStatus } from '../activationkey/validation-status'; import { useTheme } from '../../hooks/use-theme'; import { useKeyPairs } from '../../hooks/use-key-pairs'; +import { useTemplates } from '../../hooks/use-templates'; import { useToast } from '../../hooks/use-toast'; import './ActivationKeyEditor.css'; -// Example JWT for demonstration -const EXAMPLE_JWT = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJleHAiOjg4MDYxMjEyODAwLCJpc3MiOiJodHRwczovL2FwaS5iZXNodS50ZWNoIiwiaWF0IjoxNjYxMzU2MTAxLCJqdGkiOiJhbmFwaG9yYV9saWNfMjViMmFhYTgtMTQwMS00YjhmLThkMGYtNmMzMTdmOWJhNjcwIiwiYXVkIjoiQW5hcGhvcmEiLCJzdWIiOiIxMTExMTExMS0xMTExLTExMTEtMTExMS0xMTExMTExMSIsImxpY2Vuc29yIjp7Im5hbWUiOiJBbmFwaG9yYSIsImNvbnRhY3QiOlsic3VwcG9ydEByZWFkb25seXJlc3QuY29tIiwiZmluYW5jZUByZWFkb25seXJlc3QuY29tIl0sImlzc3VlciI6InN1cHBvcnRAcmVhZG9ubHlyZXN0LmNvbSJ9LCJsaWNlbnNlZSI6eyJuYW1lIjoiQW5vbnltb3VzIEZyZWUgVXNlciIsImJ1eWluZ19mb3IiOm51bGwsImJpbGxpbmdfZW1haWwiOiJ1bmtub3duQGFuYXBob3JhLmNvbSIsImFsdF9lbWFpbHMiOltdLCJhZGRyZXNzIjpbIlVua25vd24iXX0sImxpY2Vuc2UiOnsiY2x1c3Rlcl91dWlkIjoiKiIsImVkaXRpb24iOiJmcmVlIiwiZWRpdGlvbl9uYW1lIjoiRnJlZSIsImlzVHJpYWwiOmZhbHNlfX0.ATAB81zkWRxTdpSD_23tcFxba81OCrjdtcGlx_yXwa2VSvJAx7rWQYO2VM2N8zeknA01SzYpPP2o_FXzP3TCEo4iABRof2G1u0iD1AFf5Y0m_TYPs89acR5Fztb46wSBwsj4L1ONal0y8xHYfJC54SKwdXJV4XTJwIP2tBVcTl9QNAfn"; - // State type for the editor interface EditorState { inputValue: string; @@ -96,6 +96,7 @@ function editorReducer(state: EditorState, action: EditorAction): EditorState { const ActivationKeyEditor = () => { const { theme } = useTheme(); const { keyPairs, getKeyPairById } = useKeyPairs(); + const { templates } = useTemplates(); const { error: showError } = useToast(); const [state, dispatch] = useReducer(editorReducer, initialState); @@ -247,19 +248,58 @@ const ActivationKeyEditor = () => { /> {!jwt ? ( -
+