Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chore: Anonymous apollo key usage support #39

Merged
merged 3 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 72 additions & 64 deletions backend/src/routes/apolloApiKey.ts
Original file line number Diff line number Diff line change
@@ -1,83 +1,91 @@
import express, {Response} from 'express';
import express, {Request, Response} from 'express';
import {SessionRequest} from 'supertokens-node/framework/express';
import {ApolloApiKey} from '../models/apolloApiKey';
import {DI} from '../server';
import {logger} from '../utilities/logger';
import {ApolloApiKey} from '../models/apolloApiKey';
import {SessionRequest} from 'supertokens-node/framework/express';

const router = express.Router();

router.get('/apollo-api-key', async (req: SessionRequest, res: Response) => {
try {
const userId = req.session?.getUserId();
if (!userId) {
return res.status(401).json({error: 'Unauthorized'});
}
const handleRequest = async (
req: Request | SessionRequest,
defaultUserId = 'anonymous'
) => {
const userId = (req as SessionRequest).session?.getUserId() || defaultUserId;
return userId;
};

router.get(
'/apollo-api-key',
async (req: Request | SessionRequest, res: Response) => {
try {
const userId = await handleRequest(req);
const apiKey = await DI.apolloApiKeys.findOne({userId});

const apiKey = await DI.apolloApiKeys.findOne({userId});
if (apiKey) {
const decryptedKey = apiKey.getDecryptedKey();
const obfuscatedKey = `${decryptedKey.slice(0, 4)}****${decryptedKey.slice(-4)}`;
res.json({key: obfuscatedKey});
} else {
res.json({key: null});
if (apiKey) {
const decryptedKey = apiKey.getDecryptedKey();
const obfuscatedKey = `${decryptedKey.slice(0, 4)}****${decryptedKey.slice(-4)}`;
res.json({key: obfuscatedKey});
} else {
res.json({key: null});
}
} catch (error) {
logger.error('Failed to fetch API key', {error});
res.status(500).json({error: 'Failed to fetch API key'});
}
} catch (error) {
logger.error('Failed to fetch API key', {error});
res.status(500).json({error: 'Failed to fetch API key'});
}
});
);

router.post('/apollo-api-key', async (req: SessionRequest, res: Response) => {
try {
const userId = req.session?.getUserId();
if (!userId) {
return res.status(401).json({error: 'Unauthorized'});
}
router.post(
'/apollo-api-key',
async (req: Request | SessionRequest, res: Response) => {
try {
const userId = await handleRequest(req);
const {key} = req.body;

const {key} = req.body;
if (!key) {
return res.status(400).json({error: 'API key is required'});
}
if (!key) {
return res.status(400).json({error: 'API key is required'});
}

let apiKey = await DI.apolloApiKeys.findOne({userId});
if (apiKey) {
const newApiKey = new ApolloApiKey(key, userId);
apiKey.encryptedKey = newApiKey.encryptedKey;
apiKey.iv = newApiKey.iv;
apiKey.tag = newApiKey.tag;
} else {
apiKey = new ApolloApiKey(key, userId);
DI.em.persist(apiKey);
}
let apiKey = await DI.apolloApiKeys.findOne({userId});
if (apiKey) {
const newApiKey = new ApolloApiKey(key, userId);
apiKey.encryptedKey = newApiKey.encryptedKey;
apiKey.iv = newApiKey.iv;
apiKey.tag = newApiKey.tag;
} else {
apiKey = new ApolloApiKey(key, userId);
DI.em.persist(apiKey);
}

await DI.em.flush();
await DI.apolloClient.updateApiKey(key, userId);
res.status(200).json({message: 'API key saved successfully'});
} catch (error) {
logger.error('Failed to save API key', {error});
res.status(500).json({error: 'Failed to save API key'});
await DI.em.flush();
await DI.apolloClient.updateApiKey(key, userId);
res.status(200).json({message: 'API key saved successfully'});
} catch (error) {
logger.error('Failed to save API key', {error});
res.status(500).json({error: 'Failed to save API key'});
}
}
});
);

router.delete('/apollo-api-key', async (req: SessionRequest, res: Response) => {
try {
const userId = req.session?.getUserId();
if (!userId) {
return res.status(401).json({error: 'Unauthorized'});
}
router.delete(
'/apollo-api-key',
async (req: Request | SessionRequest, res: Response) => {
try {
const userId = await handleRequest(req);
const apiKey = await DI.apolloApiKeys.findOne({userId});

const apiKey = await DI.apolloApiKeys.findOne({userId});
if (!apiKey) {
return res.status(404).json({error: 'API key not found'});
}
if (!apiKey) {
return res.status(404).json({error: 'API key not found'});
}

await DI.em.remove(apiKey).flush();
await DI.apolloClient.updateApiKey('', userId);
res.status(200).json({message: 'API key deleted successfully'});
} catch (error) {
logger.error('Failed to delete API key', {error});
res.status(500).json({error: 'Failed to delete API key'});
await DI.em.remove(apiKey).flush();
await DI.apolloClient.updateApiKey('', userId);
res.status(200).json({message: 'API key deleted successfully'});
} catch (error) {
logger.error('Failed to delete API key', {error});
res.status(500).json({error: 'Failed to delete API key'});
}
}
});
);

export default router;
4 changes: 2 additions & 2 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 15 additions & 2 deletions frontend/src/components/ui/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ import {Tabs, TabsContent, TabsList, TabsTrigger} from './tabs';
import {Textarea} from './textarea';
import {Toaster} from './toaster';
import {toast} from './use-toast';
import {Badge} from './badge';
import {SettingsBadge} from './settings-badge';

const Home = () => {
const navigate = useNavigate();
Expand Down Expand Up @@ -117,6 +119,17 @@ const Home = () => {

const serverBaseUrl = getApiBaseUrl();

const [hasApolloKey, setHasApolloKey] = useState(true);

useEffect(() => {
fetch(`${serverBaseUrl}/api/apollo-api-key`)
.then((response) => response.json())
.then((data) => {
setHasApolloKey(!!data.key);
})
.catch(() => setHasApolloKey(false));
}, [serverBaseUrl]);

const handleSettingsClick = () => navigate('/settings');
const handleCreateSeedClick = () => {
populateSeedForm();
Expand Down Expand Up @@ -586,8 +599,8 @@ const Home = () => {
</div>

<div className="flex items-center gap-4">
<Settings
className="h-5 w-5 text-gray-500 cursor-pointer"
<SettingsBadge
hasApolloKey={hasApolloKey}
onClick={handleSettingsClick}
/>
<ConditionalLoginDropdown />
Expand Down
51 changes: 51 additions & 0 deletions frontend/src/components/ui/settings-badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {cn} from '../../lib/utils';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from './tooltip';
import {Settings} from 'lucide-react';

interface SettingsBadgeProps {
hasApolloKey: boolean;
onClick: () => void;
}

export function SettingsBadge({hasApolloKey, onClick}: SettingsBadgeProps) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="relative group cursor-pointer" onClick={onClick}>
<Settings className="h-5 w-5 text-gray-500 transition-colors group-hover:text-gray-900" />
{!hasApolloKey && (
<span
className={cn(
'absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full',
'bg-red-500/90 transition-all duration-300',
'group-hover:scale-125 group-hover:bg-red-600',
'animate-[pulse_3s_ease-in-out_infinite]'
)}
style={{
animation: 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
background: 'linear-gradient(45deg, #ef4444, #dc2626)',
}}
/>
)}
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-[300px]">
<p className="text-sm font-medium">
Connect to Apollo Studio
<span className="block text-xs font-normal text-muted-foreground mt-1">
Enter your user-scoped Apollo API key to instantly build custom
mock seeds against existing supergraph variants and schema
proposals
</span>
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
3 changes: 3 additions & 0 deletions frontend/src/components/ui/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from './card';
import {Input} from './input';
import {useToast} from './use-toast';
import {useNavigate} from 'react-router-dom';

interface ApiKey {
id: string;
Expand All @@ -28,6 +29,7 @@ export default function SettingsPage() {
const [isLoading, setIsLoading] = useState(false);
const {toast} = useToast();
const serverBaseUrl = getApiBaseUrl();
const navigate = useNavigate();

useEffect(() => {
fetchApiKey();
Expand Down Expand Up @@ -65,6 +67,7 @@ export default function SettingsPage() {
title: 'Apollo API Key Saved',
description: 'Your Apollo API key has been saved successfully.',
});
navigate('/');
}
});
};
Expand Down
6 changes: 5 additions & 1 deletion frontend/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,11 @@ module.exports = {
to: {
height: '0'
}
}
},
pulse: {
'0%, 100%': { opacity: '0.7', transform: 'scale(1)' },
'50%': { opacity: '0.3', transform: 'scale(0.95)' },
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
Expand Down