From d1e10d46d41d12e739437d263264952ee1058bca Mon Sep 17 00:00:00 2001 From: itzzavdhesh Date: Sat, 30 May 2026 19:55:54 +0530 Subject: [PATCH 1/7] feat(shared): add unified DeepLinkResolver with polymorphic fallback chain --- apps/backend/src/routes/follow.ts | 18 +- apps/mobile/src/screens/DevCardViewScreen.tsx | 229 +++++++++--------- apps/mobile/src/screens/SettingsScreen.tsx | 67 +++-- .../shared/src/__tests__/deepLinks.test.ts | 101 ++++++++ packages/shared/src/deepLinks.ts | 102 ++++++++ packages/shared/src/index.ts | 3 +- packages/shared/src/platforms.ts | 57 +++++ 7 files changed, 423 insertions(+), 154 deletions(-) create mode 100644 packages/shared/src/__tests__/deepLinks.test.ts create mode 100644 packages/shared/src/deepLinks.ts diff --git a/apps/backend/src/routes/follow.ts b/apps/backend/src/routes/follow.ts index a152fc55..90209e78 100644 --- a/apps/backend/src/routes/follow.ts +++ b/apps/backend/src/routes/follow.ts @@ -1,7 +1,7 @@ import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; import { decrypt } from '../utils/encryption.js'; import { getErrorMessage } from '../utils/error.util.js'; -import { getPlatform, getProfileUrl, getWebViewUrl } from '@devcard/shared'; +import { getPlatform, getProfileUrl, getWebViewUrl, resolveDeepLink } from '@devcard/shared'; import { followLogSchema } from '../validations/follow.validation.js'; export async function followRoutes(app: FastifyInstance) { @@ -35,14 +35,16 @@ export async function followRoutes(app: FastifyInstance) { }, }); - // Use WebView follow strategy if configured for the platform (e.g. LinkedIn, Twitter/X) + // Use WebView follow strategy if resolved for the platform (e.g. LinkedIn, Twitter/X) const platformDef = getPlatform(platform); - if (platformDef?.followStrategy === 'webview') { - const url = getWebViewUrl(platform, targetUsername) || getProfileUrl(platform, targetUsername); - return reply.send({ - strategy: 'webview', - url, - }); + if (platformDef) { + const resolved = resolveDeepLink(platform, targetUsername, { isMobile: false }); + if (resolved.strategy === 'webview') { + return reply.send({ + strategy: 'webview', + url: resolved.url, + }); + } } if (!oauthToken) { diff --git a/apps/mobile/src/screens/DevCardViewScreen.tsx b/apps/mobile/src/screens/DevCardViewScreen.tsx index ced4d38f..7f12ae11 100644 --- a/apps/mobile/src/screens/DevCardViewScreen.tsx +++ b/apps/mobile/src/screens/DevCardViewScreen.tsx @@ -5,7 +5,6 @@ import { StyleSheet, ScrollView, TouchableOpacity, - Image, Linking, Clipboard, StatusBar, @@ -16,8 +15,10 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS, SHADOWS } from '../theme/tokens'; import { Skeleton } from '../components/Skeleton'; import { EmptyState } from '../components/EmptyState'; -import { PLATFORMS, getProfileUrl, getWebViewUrl } from '@devcard/shared'; -import { API_BASE_URL } from '../config'; +import Avatar from '../components/Avatar'; +import { PLATFORMS, getProfileUrl, getWebViewUrl, resolveDeepLink } from '@devcard/shared'; +import type { ResolvedLink } from '@devcard/shared'; +import { get, post, del } from '../services/api'; import { useAuth } from '../context/AuthContext'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { RouteProp } from '@react-navigation/native'; @@ -99,20 +100,13 @@ export default function DevCardViewScreen({ navigation, route }: Props) { const fetchProfile = useCallback(async () => { try { - const headers: Record = {}; - if (token) { - headers['Authorization'] = `Bearer ${token}`; - } - const res = await fetch(`${API_BASE_URL}/api/u/${username}`, { headers }); - if (res.ok) { - const data = await res.json(); + const data = await get(`/api/u/${username}`, token); + if (data) { setProfile(data); const initialFollowStates: FollowState = {}; if (data.links) { data.links.forEach((link: any) => { - if (link.followed) { - initialFollowStates[link.id] = 'success'; - } + if (link.followed) initialFollowStates[link.id] = 'success'; }); } setFollowStates(initialFollowStates); @@ -152,27 +146,17 @@ export default function DevCardViewScreen({ navigation, route }: Props) { case 'webview': setFollowStates(prev => ({ ...prev, [link.id]: 'loading' })); try { - const res = await fetch( - `${API_BASE_URL}/api/follow/${link.platform}/${link.username}`, - { - method: 'POST', - headers: { Authorization: `Bearer ${token}` }, - } - ); + const data = await post(`/api/follow/${link.platform}/${link.username}`, undefined, token); setFollowStates(prev => ({ ...prev, [link.id]: 'idle' })); - if (res.ok) { - const data = await res.json(); - if (data.strategy === 'webview') { - handleWebViewConnect(link, data.url); - } else { - setFollowStates(prev => ({ ...prev, [link.id]: 'success' })); - } + if (data?.strategy === 'webview') { + handleWebViewConnect(link, data.url); } else { - handleWebViewConnect(link); + setFollowStates(prev => ({ ...prev, [link.id]: 'success' })); } } catch { setFollowStates(prev => ({ ...prev, [link.id]: 'idle' })); - handleWebViewConnect(link); + const resolved = resolveDeepLink(link.platform, link.username, { isMobile: true }); + await executeResolvedLinkChain(resolved, link); } break; @@ -183,14 +167,47 @@ export default function DevCardViewScreen({ navigation, route }: Props) { break; case 'link': - default: - const url = link.url || getProfileUrl(link.platform, link.username); - if (url) { - Linking.openURL(url).catch(() => - Alert.alert('Error', 'Could not open link') - ); - } + default: { + const resolved = resolveDeepLink(link.platform, link.username, { isMobile: true }); + await executeResolvedLinkChain(resolved, link); break; + } + } + }; + + const executeResolvedLinkChain = async (resolved: ResolvedLink, link: PlatformLink) => { + const tryOpenLink = async (node: ResolvedLink): Promise => { + switch (node.strategy) { + case 'native-deeplink': + case 'universal-link': + case 'web-url': + if (node.url) { + try { + const supported = await Linking.canOpenURL(node.url); + if (supported) { + await Linking.openURL(node.url); + return true; + } + } catch (err) { + console.warn(`Failed to open URL ${node.url}:`, err); + } + } + break; + + case 'webview': + handleWebViewConnect(link, node.url); + return true; + } + + if (node.fallback) { + return await tryOpenLink(node.fallback); + } + return false; + }; + + const success = await tryOpenLink(resolved); + if (!success) { + Alert.alert('Error', 'Could not open connection link'); } }; @@ -198,54 +215,26 @@ export default function DevCardViewScreen({ navigation, route }: Props) { const handleApiFollow = async (link: PlatformLink) => { setFollowStates(prev => ({ ...prev, [link.id]: 'loading' })); try { - const res = await fetch( - `${API_BASE_URL}/api/follow/${link.platform}/${link.username}`, - { - method: 'POST', - headers: { Authorization: `Bearer ${token}` }, - } - ); - if (res.ok) { - setFollowStates(prev => ({ ...prev, [link.id]: 'success' })); + await post(`/api/follow/${link.platform}/${link.username}`, undefined, token); + setFollowStates(prev => ({ ...prev, [link.id]: 'success' })); + } catch (err: any) { + const msg = (err && err.message) || ''; + if (msg.includes('requiresAuth')) { + setFollowStates(prev => ({ ...prev, [link.id]: 'idle' })); + const resolved = resolveDeepLink(link.platform, link.username, { isMobile: true }); + await executeResolvedLinkChain(resolved, link); } else { - const data = await res.json(); - if (data.requiresAuth) { - // Reset loading BEFORE opening fallback so button doesn't get stuck - setFollowStates(prev => ({ ...prev, [link.id]: 'idle' })); - // For platforms without a webview URL (e.g. GitHub), open in system browser - const webViewUrl = getWebViewUrl(link.platform, link.username); - if (webViewUrl) { - handleWebViewConnect(link); - } else { - // Open GitHub / other API-only platforms in the default browser - const profileUrl = link.url || getProfileUrl(link.platform, link.username); - if (profileUrl) { - Linking.openURL(profileUrl).catch(() => - Alert.alert('Error', `Could not open ${link.platform} profile`) - ); - } - } - } else { - setFollowStates(prev => ({ ...prev, [link.id]: 'error' })); - } + setFollowStates(prev => ({ ...prev, [link.id]: 'error' })); } - } catch { - setFollowStates(prev => ({ ...prev, [link.id]: 'error' })); } }; // Reset a "Done" tile — clears follow log from backend and resets local state const handleResetFollowState = async (link: PlatformLink) => { try { - await fetch( - `${API_BASE_URL}/api/follow/${link.platform}/${link.username}/log`, - { - method: 'DELETE', - headers: { Authorization: `Bearer ${token}` }, - } - ); + await del(`/api/follow/${link.platform}/${link.username}/log`, undefined, token); } catch { - // Ignore network errors — still reset local state + // ignore } setFollowStates(prev => ({ ...prev, [link.id]: 'idle' })); }; @@ -299,14 +288,14 @@ export default function DevCardViewScreen({ navigation, route }: Props) { {/* Header Skeleton */} - + - + @@ -318,12 +307,12 @@ export default function DevCardViewScreen({ navigation, route }: Props) { {/* Tiles Skeleton */} - + {[1, 2, 3].map(i => ( - - + + @@ -339,9 +328,13 @@ export default function DevCardViewScreen({ navigation, route }: Props) { return ( - 😕 + 😕 User not found - navigation.goBack()}> + navigation.goBack()} + accessibilityLabel="Go back to the previous screen" + accessibilityRole="button" + > Go Back @@ -354,8 +347,14 @@ export default function DevCardViewScreen({ navigation, route }: Props) { {/* Close Button */} - navigation.goBack()}> - + navigation.goBack()} + accessibilityLabel="Close" + accessibilityRole="button" + accessibilityHint="Returns to the previous screen" + > + @@ -379,15 +378,7 @@ export default function DevCardViewScreen({ navigation, route }: Props) { {/* Middle: avatar + name/role */} - {profile.avatarUrl ? ( - - ) : ( - - - {profile.displayName.charAt(0).toUpperCase()} - - - )} + {profile.displayName} @@ -432,6 +423,26 @@ export default function DevCardViewScreen({ navigation, route }: Props) { const state = followStates[link.id] || 'idle'; const btnColor = getButtonColor(link, state); const isDone = state === 'success'; + const tileIconDynamic = { + backgroundColor: isDone + ? 'rgba(34,197,94,0.12)' + : (platform?.color || COLORS.primary) + '22', + borderColor: isDone + ? COLORS.success + : (platform?.color || COLORS.primary) + '66', + }; + const actionLabel = getButtonLabel(link); + // Build a clear, human-readable label for screen readers + const a11yLabel = isDone + ? `${platform?.name || link.platform} — connected as ${link.username}` + : `${actionLabel} ${platform?.name || link.platform} — ${link.username}`; + const a11yHint = isDone + ? 'Long press to reset connection status' + : platform?.followStrategy === 'webview' + ? 'Opens an in-app browser to connect' + : platform?.followStrategy === 'copy' + ? 'Copies the username to your clipboard' + : 'Opens the profile in your browser'; return ( + disabled={state === 'loading'} + accessibilityLabel={a11yLabel} + accessibilityRole="button" + accessibilityHint={a11yHint} + accessibilityState={{ disabled: state === 'loading', selected: isDone }} + > {/* Icon */} - + {isDone ? ( ) : ( @@ -548,7 +553,7 @@ const styles = StyleSheet.create({ }, brandRow: { flexDirection: 'row', alignItems: 'center', gap: 7 }, miniChip: { width: 28, height: 18, borderRadius: 4, opacity: 0.7 }, - brandText: { color: 'rgba(255,255,255,0.45)', fontSize: 9, fontWeight: '800', letterSpacing: 2.5 }, + brandText: { color: 'rgba(255,255,255,0.45)', fontSize: FONT_SIZE.nano + 1, fontWeight: '800', letterSpacing: 2.5 }, cardMid: { flexDirection: 'row', alignItems: 'center', gap: SPACING.md }, avatarRing: { borderRadius: 38, @@ -565,18 +570,18 @@ const styles = StyleSheet.create({ profileRole: { fontSize: 11, color: 'rgba(255,255,255,0.55)', fontWeight: '500', lineHeight: 15, }, - pronouns: { fontSize: 10, color: COLORS.textMuted, fontStyle: 'italic' }, + pronouns: { fontSize: FONT_SIZE.micro, color: COLORS.textMuted, fontStyle: 'italic' }, cardBottom: { gap: SPACING.xs }, cardDivider: { height: 1, backgroundColor: 'rgba(255,255,255,0.06)', marginBottom: 2, }, - bioText: { fontSize: 10.5, color: 'rgba(255,255,255,0.38)', lineHeight: 15 }, + bioText: { fontSize: FONT_SIZE.micro + 0.5, color: 'rgba(255,255,255,0.38)', lineHeight: 15 }, cardBadge: { alignSelf: 'flex-start', paddingHorizontal: 8, paddingVertical: 3, borderRadius: 4, borderWidth: 1, }, - badgeText: { fontSize: 8, fontWeight: '900', letterSpacing: 1.5 }, + badgeText: { fontSize: FONT_SIZE.nano, fontWeight: '900', letterSpacing: 1.5 }, // ─── Tiles ─── tilesSection: { gap: SPACING.sm }, @@ -626,6 +631,10 @@ const styles = StyleSheet.create({ borderWidth: 1, borderColor: COLORS.border, }, + skelMb8: { marginBottom: 8 }, + skelMb12: { marginBottom: 12 }, + skelMb6: { marginBottom: 6 }, + tileInfoMl16: { marginLeft: 16 }, // ─── Error / Footer ─── errorState: { flex: 1, alignItems: 'center', justifyContent: 'center' }, diff --git a/apps/mobile/src/screens/SettingsScreen.tsx b/apps/mobile/src/screens/SettingsScreen.tsx index 58f952f3..96287ec4 100644 --- a/apps/mobile/src/screens/SettingsScreen.tsx +++ b/apps/mobile/src/screens/SettingsScreen.tsx @@ -8,15 +8,14 @@ import { TextInput, Alert, StatusBar, - Image, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useNavigation } from '@react-navigation/native'; import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS } from '../theme/tokens'; +import Avatar from '../components/Avatar'; import { useAuth } from '../context/AuthContext'; import { API_BASE_URL } from '../config'; - -import { useNavigation } from '@react-navigation/native'; +import { put } from '../services/api'; export default function SettingsScreen() { const navigation = useNavigation(); @@ -31,26 +30,17 @@ export default function SettingsScreen() { const handleSave = async () => { setSaving(true); try { - const res = await fetch(`${API_BASE_URL}/api/profiles/me`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ - displayName: displayName.trim(), - bio: bio.trim() || null, - pronouns: pronouns.trim() || null, - role: role.trim() || null, - company: company.trim() || null, - }), - }); - if (res.ok) { - await refreshUser(); - Alert.alert('Success', 'Profile updated!'); - } else { - Alert.alert('Error', 'Failed to update profile'); - } + const payload = { + displayName: displayName.trim() || undefined, + bio: bio.trim() || null, + pronouns: pronouns.trim() || null, + role: role.trim() || null, + company: company.trim() || null, + }; + + await put('/api/profiles/me', payload, token); + await refreshUser(); + Alert.alert('Success', 'Profile updated!'); } catch { Alert.alert('Error', 'Something went wrong'); } finally { @@ -74,15 +64,7 @@ export default function SettingsScreen() { {/* Avatar */} - {user?.avatarUrl ? ( - - ) : ( - - - {(user?.displayName || 'D').charAt(0).toUpperCase()} - - - )} + @{user?.username} @@ -98,7 +80,11 @@ export default function SettingsScreen() { + disabled={saving} + accessibilityLabel={saving ? 'Saving profile changes' : 'Save profile changes'} + accessibilityRole="button" + accessibilityState={{ disabled: saving, busy: saving }} + > {saving ? 'Saving...' : 'Save Changes'} @@ -109,7 +95,11 @@ export default function SettingsScreen() { Integrations (navigation as any).navigate('ConnectPlatforms')}> + onPress={() => (navigation as any).navigate('ConnectPlatforms')} + accessibilityLabel="Connected Platforms" + accessibilityRole="button" + accessibilityHint="Opens the screen to manage your connected developer platforms" + > 🔌 Connected Platforms @@ -118,7 +108,12 @@ export default function SettingsScreen() { - + Log Out @@ -156,6 +151,8 @@ function FormField({ placeholderTextColor={COLORS.textMuted} multiline={multiline} numberOfLines={multiline ? 3 : 1} + accessibilityLabel={label} + accessibilityHint={placeholder ? `Example: ${placeholder}` : undefined} /> ); diff --git a/packages/shared/src/__tests__/deepLinks.test.ts b/packages/shared/src/__tests__/deepLinks.test.ts new file mode 100644 index 00000000..7a7a4014 --- /dev/null +++ b/packages/shared/src/__tests__/deepLinks.test.ts @@ -0,0 +1,101 @@ +import { describe, it, expect } from 'vitest'; +import { resolveDeepLink } from '../deepLinks'; + +describe('Deep Link Resolver', () => { + describe('LinkedIn resolution (supports all strategies)', () => { + it('returns native deep link as first strategy when on mobile and hasApp is true', () => { + const link = resolveDeepLink('linkedin', 'john-doe', { isMobile: true, hasApp: true }); + expect(link.strategy).toBe('native-deeplink'); + expect(link.url).toBe('linkedin://profile?id=john-doe'); + + // Fallback chain verification + expect(link.fallback).toBeDefined(); + expect(link.fallback!.strategy).toBe('universal-link'); + expect(link.fallback!.url).toBe('https://www.linkedin.com/in/john-doe'); + + expect(link.fallback!.fallback).toBeDefined(); + expect(link.fallback!.fallback!.strategy).toBe('webview'); + expect(link.fallback!.fallback!.url).toBe('https://www.linkedin.com/in/john-doe'); + + expect(link.fallback!.fallback!.fallback).toBeDefined(); + expect(link.fallback!.fallback!.fallback!.strategy).toBe('web-url'); + expect(link.fallback!.fallback!.fallback!.url).toBe('https://www.linkedin.com/in/john-doe'); + }); + + it('omits native deep link when hasApp is false', () => { + const link = resolveDeepLink('linkedin', 'john-doe', { isMobile: true, hasApp: false }); + expect(link.strategy).toBe('universal-link'); + expect(link.url).toBe('https://www.linkedin.com/in/john-doe'); + + expect(link.fallback).toBeDefined(); + expect(link.fallback!.strategy).toBe('webview'); + }); + + it('includes native deep link as first strategy when hasApp is undefined (default guess)', () => { + const link = resolveDeepLink('linkedin', 'john-doe', { isMobile: true }); + expect(link.strategy).toBe('native-deeplink'); + }); + + it('resolves desktop context with webview as first choice', () => { + const link = resolveDeepLink('linkedin', 'john-doe', { isMobile: false }); + expect(link.strategy).toBe('webview'); + expect(link.url).toBe('https://www.linkedin.com/in/john-doe'); + + expect(link.fallback).toBeDefined(); + expect(link.fallback!.strategy).toBe('web-url'); + expect(link.fallback!.url).toBe('https://www.linkedin.com/in/john-doe'); + }); + }); + + describe('Twitter / X resolution', () => { + it('returns native deep link and replaces username in pattern', () => { + const link = resolveDeepLink('twitter', 'elonmusk', { isMobile: true, hasApp: true }); + expect(link.strategy).toBe('native-deeplink'); + expect(link.url).toBe('twitter://user?screen_name=elonmusk'); + + expect(link.fallback!.strategy).toBe('universal-link'); + expect(link.fallback!.url).toBe('https://x.com/elonmusk'); + }); + }); + + describe('Telegram resolution (custom native protocols)', () => { + it('resolves tg:// scheme correctly', () => { + const link = resolveDeepLink('telegram', 'durov', { isMobile: true }); + expect(link.strategy).toBe('native-deeplink'); + expect(link.url).toBe('tg://resolve?domain=durov'); + + expect(link.fallback!.strategy).toBe('universal-link'); + expect(link.fallback!.url).toBe('https://t.me/durov'); + }); + }); + + describe('GitHub resolution (standard web profile, no deep link/webview)', () => { + it('resolves directly to web-url fallback', () => { + const link = resolveDeepLink('github', 'octocat', { isMobile: true }); + expect(link.strategy).toBe('web-url'); + expect(link.url).toBe('https://github.com/octocat'); + expect(link.fallback).toBeUndefined(); + }); + + it('resolves directly to web-url on desktop', () => { + const link = resolveDeepLink('github', 'octocat', { isMobile: false }); + expect(link.strategy).toBe('web-url'); + expect(link.url).toBe('https://github.com/octocat'); + expect(link.fallback).toBeUndefined(); + }); + }); + + describe('Full URL platforms (portfolio / custom)', () => { + it('uses the username string directly as the URL without pattern formatting', () => { + const link = resolveDeepLink('portfolio', 'https://john.dev', { isMobile: true }); + expect(link.strategy).toBe('web-url'); + expect(link.url).toBe('https://john.dev'); + }); + }); + + describe('Unknown platforms', () => { + it('throws error for unregistered platform ID', () => { + expect(() => resolveDeepLink('myspace', 'user')).toThrowError('Unknown platform'); + }); + }); +}); diff --git a/packages/shared/src/deepLinks.ts b/packages/shared/src/deepLinks.ts new file mode 100644 index 00000000..7c5c64da --- /dev/null +++ b/packages/shared/src/deepLinks.ts @@ -0,0 +1,102 @@ +import { getPlatform } from './platforms'; + +export type LinkStrategy = 'native-deeplink' | 'universal-link' | 'web-url' | 'webview'; + +export type ResolvedLink = { + strategy: LinkStrategy; + url: string; + fallback?: ResolvedLink; +}; + +export function resolveDeepLink( + platformId: string, + username: string, + context: { hasApp?: boolean; isMobile?: boolean } = {} +): ResolvedLink { + const platform = getPlatform(platformId); + if (!platform) { + throw new Error(`Unknown platform: ${platformId}`); + } + + const { isMobile = false, hasApp } = context; + + // Helper to replace {username} in patterns + const buildUrl = (pattern: string | null): string => { + if (!pattern) return ''; + if (!pattern.includes('{username}')) { + return pattern === '{username}' ? username : pattern; + } + return pattern.replace(/{username}/g, username); + }; + + const chain: ResolvedLink[] = []; + + if (isMobile) { + // 1. Native Deep Link + if (platform.nativeScheme && platform.deepLinkPattern) { + const nativeUrl = buildUrl(platform.deepLinkPattern); + if (hasApp === true || hasApp === undefined) { + chain.push({ + strategy: 'native-deeplink', + url: nativeUrl, + }); + } + } + + // 2. Universal Link + if (platform.universalLink) { + chain.push({ + strategy: 'universal-link', + url: buildUrl(platform.universalLink), + }); + } + + // 3. WebView Fallback + if (platform.webViewFallback && platform.webViewUrlPattern) { + chain.push({ + strategy: 'webview', + url: buildUrl(platform.webViewUrlPattern), + }); + } + + // 4. Web URL Fallback + if (platform.urlPattern) { + chain.push({ + strategy: 'web-url', + url: buildUrl(platform.urlPattern), + }); + } + } else { + // Desktop context + // 1. WebView + if (platform.webViewFallback && platform.webViewUrlPattern) { + chain.push({ + strategy: 'webview', + url: buildUrl(platform.webViewUrlPattern), + }); + } + + // 2. Web URL + if (platform.urlPattern) { + chain.push({ + strategy: 'web-url', + url: buildUrl(platform.urlPattern), + }); + } + } + + if (chain.length === 0) { + // Fallback just in case + return { + strategy: 'web-url', + url: username, + }; + } + + // Link the chain together + for (let i = 0; i < chain.length - 1; i++) { + chain[i].fallback = chain[i + 1]; + } + + return chain[0]; +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 409d3e76..1c29b433 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,3 +1,4 @@ export * from './platforms'; export * from './types'; -export * from './cards'; \ No newline at end of file +export * from './cards'; +export * from './deepLinks'; \ No newline at end of file diff --git a/packages/shared/src/platforms.ts b/packages/shared/src/platforms.ts index 81c81ab4..916fc87e 100644 --- a/packages/shared/src/platforms.ts +++ b/packages/shared/src/platforms.ts @@ -29,6 +29,12 @@ export interface PlatformDef { usesFullUrl: boolean; /** Regex pattern to validate usernames */ validationRegex?: RegExp; + /** Native protocol scheme (e.g. 'linkedin://') */ + nativeScheme: string | null; + /** Universal Link URL pattern */ + universalLink: string | null; + /** True if platform requires webview fallback for follow actions */ + webViewFallback: boolean; } // ─── Platform Registry ─── @@ -47,6 +53,9 @@ export const PLATFORMS: Record = { usernamePlaceholder: 'e.g. octocat', usesFullUrl: false, validationRegex: /^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}$/, + nativeScheme: null, + universalLink: null, + webViewFallback: false, }, linkedin: { id: 'linkedin', @@ -61,6 +70,9 @@ export const PLATFORMS: Record = { usernamePlaceholder: 'e.g. johndoe', usesFullUrl: false, validationRegex: /^[a-zA-Z0-9-]{3,100}$/, + nativeScheme: 'linkedin://', + universalLink: 'https://www.linkedin.com/in/{username}', + webViewFallback: true, }, twitter: { id: 'twitter', @@ -75,6 +87,9 @@ export const PLATFORMS: Record = { usernamePlaceholder: 'e.g. elonmusk', usesFullUrl: false, validationRegex: /^[A-Za-z0-9_]{1,15}$/, + nativeScheme: 'twitter://', + universalLink: 'https://x.com/{username}', + webViewFallback: true, }, gitlab: { id: 'gitlab', @@ -88,6 +103,9 @@ export const PLATFORMS: Record = { oauthScopes: [], usernamePlaceholder: 'e.g. gitlab-user', usesFullUrl: false, + nativeScheme: null, + universalLink: 'https://gitlab.com/{username}', + webViewFallback: false, }, devfolio: { id: 'devfolio', @@ -101,6 +119,9 @@ export const PLATFORMS: Record = { oauthScopes: [], usernamePlaceholder: 'e.g. hacker123', usesFullUrl: false, + nativeScheme: null, + universalLink: 'https://devfolio.co/@{username}', + webViewFallback: false, }, npm: { id: 'npm', @@ -114,6 +135,9 @@ export const PLATFORMS: Record = { oauthScopes: [], usernamePlaceholder: 'e.g. sindresorhus', usesFullUrl: false, + nativeScheme: null, + universalLink: 'https://www.npmjs.com/~{username}', + webViewFallback: false, }, devto: { id: 'devto', @@ -127,6 +151,9 @@ export const PLATFORMS: Record = { oauthScopes: [], usernamePlaceholder: 'e.g. ben', usesFullUrl: false, + nativeScheme: null, + universalLink: 'https://dev.to/{username}', + webViewFallback: false, }, hashnode: { id: 'hashnode', @@ -140,6 +167,9 @@ export const PLATFORMS: Record = { oauthScopes: [], usernamePlaceholder: 'e.g. writer', usesFullUrl: false, + nativeScheme: null, + universalLink: 'https://hashnode.com/@{username}', + webViewFallback: false, }, medium: { id: 'medium', @@ -153,6 +183,9 @@ export const PLATFORMS: Record = { oauthScopes: [], usernamePlaceholder: 'e.g. writer', usesFullUrl: false, + nativeScheme: null, + universalLink: 'https://medium.com/@{username}', + webViewFallback: false, }, leetcode: { id: 'leetcode', @@ -166,6 +199,9 @@ export const PLATFORMS: Record = { oauthScopes: [], usernamePlaceholder: 'e.g. coder', usesFullUrl: false, + nativeScheme: null, + universalLink: 'https://leetcode.com/u/{username}', + webViewFallback: false, }, hackerrank: { id: 'hackerrank', @@ -179,6 +215,9 @@ export const PLATFORMS: Record = { oauthScopes: [], usernamePlaceholder: 'e.g. hacker', usesFullUrl: false, + nativeScheme: null, + universalLink: 'https://www.hackerrank.com/profile/{username}', + webViewFallback: false, }, stackoverflow: { id: 'stackoverflow', @@ -192,6 +231,9 @@ export const PLATFORMS: Record = { oauthScopes: [], usernamePlaceholder: 'e.g. 1234/username', usesFullUrl: false, + nativeScheme: null, + universalLink: 'https://stackoverflow.com/users/{username}', + webViewFallback: false, }, discord: { id: 'discord', @@ -205,6 +247,9 @@ export const PLATFORMS: Record = { oauthScopes: [], usernamePlaceholder: 'e.g. user#1234', usesFullUrl: false, + nativeScheme: null, + universalLink: null, + webViewFallback: false, }, telegram: { id: 'telegram', @@ -218,6 +263,9 @@ export const PLATFORMS: Record = { oauthScopes: [], usernamePlaceholder: 'e.g. durov', usesFullUrl: false, + nativeScheme: 'tg://', + universalLink: 'https://t.me/{username}', + webViewFallback: false, }, email: { id: 'email', @@ -231,6 +279,9 @@ export const PLATFORMS: Record = { oauthScopes: [], usernamePlaceholder: 'e.g. hello@example.com', usesFullUrl: true, + nativeScheme: 'mailto:', + universalLink: null, + webViewFallback: false, }, portfolio: { id: 'portfolio', @@ -244,6 +295,9 @@ export const PLATFORMS: Record = { oauthScopes: [], usernamePlaceholder: 'e.g. https://mysite.dev', usesFullUrl: true, + nativeScheme: null, + universalLink: null, + webViewFallback: false, }, custom: { id: 'custom', @@ -257,6 +311,9 @@ export const PLATFORMS: Record = { oauthScopes: [], usernamePlaceholder: 'e.g. https://example.com/profile', usesFullUrl: true, + nativeScheme: null, + universalLink: null, + webViewFallback: false, }, }; From a3c5c29315e09ff64814547074a608334bc0de2e Mon Sep 17 00:00:00 2001 From: itzzavdhesh Date: Sat, 6 Jun 2026 12:23:20 +0530 Subject: [PATCH 2/7] updates --- apps/backend/src/__tests__/analytics.test.ts | 11 +++++----- apps/backend/src/__tests__/app.test.ts | 5 +++-- apps/backend/src/__tests__/event.test.ts | 8 +++++--- apps/backend/src/__tests__/follow.test.ts | 2 +- .../backend/src/__tests__/oauth-scope.test.ts | 4 +++- apps/backend/src/__tests__/public.test.ts | 10 ++++++---- apps/backend/src/__tests__/team.test.ts | 7 ++++--- apps/backend/src/app.ts | 4 ++-- apps/backend/src/env.ts | 3 ++- apps/backend/src/plugins/prisma.ts | 3 ++- apps/backend/src/plugins/redis.ts | 1 + apps/backend/src/routes/analytics.ts | 4 ++-- apps/backend/src/routes/auth.ts | 5 +++-- apps/backend/src/routes/cards.ts | 16 +++++++-------- apps/backend/src/routes/connect.ts | 8 +++++--- apps/backend/src/routes/event.ts | 15 +++++++------- apps/backend/src/routes/follow.ts | 8 +++++--- apps/backend/src/routes/nfc.ts | 3 ++- apps/backend/src/routes/profiles.ts | 20 ++++++++++--------- apps/backend/src/routes/public.ts | 16 ++++++++------- apps/backend/src/routes/team.ts | 4 ++-- apps/backend/src/services/authService.ts | 2 +- apps/backend/src/services/cardService.ts | 14 ++++++------- apps/backend/src/services/profileService.ts | 16 ++++++++------- apps/backend/src/services/publicService.ts | 9 +++++---- apps/backend/src/utils/encryption.ts | 2 +- apps/backend/src/utils/error.util.ts | 3 ++- apps/backend/src/utils/slug.ts | 4 ++-- apps/backend/src/utils/validators.ts | 2 +- apps/web/package.json | 2 +- 30 files changed, 119 insertions(+), 92 deletions(-) diff --git a/apps/backend/src/__tests__/analytics.test.ts b/apps/backend/src/__tests__/analytics.test.ts index 4f0d07ae..e6f6b607 100644 --- a/apps/backend/src/__tests__/analytics.test.ts +++ b/apps/backend/src/__tests__/analytics.test.ts @@ -1,3 +1,6 @@ +import Fastify, { + type FastifyInstance, +} from 'fastify'; import { describe, it, @@ -7,13 +10,11 @@ import { vi, } from 'vitest'; -import Fastify, { - type FastifyInstance, -} from 'fastify'; + +import { analyticsRoutes } from '../routes/analytics'; import type { PrismaClient } from '@prisma/client'; -import { analyticsRoutes } from '../routes/analytics'; // ─── Shared mock data ──────────────────────────────────────────────────────── @@ -34,7 +35,7 @@ const prismaMock = { // ─── App factory ───────────────────────────────────────────────────────────── -let mockJwtVerify = vi.fn(); +const mockJwtVerify = vi.fn(); async function buildApp(): Promise { const app = Fastify({ diff --git a/apps/backend/src/__tests__/app.test.ts b/apps/backend/src/__tests__/app.test.ts index 648d98a6..fcd598bf 100644 --- a/apps/backend/src/__tests__/app.test.ts +++ b/apps/backend/src/__tests__/app.test.ts @@ -1,8 +1,9 @@ -process.env.NODE_ENV = 'test'; - import { describe, it, expect } from 'vitest'; + import { buildApp } from '../app'; +process.env.NODE_ENV = 'test'; + describe('GET /health', () => { it('should return status ok', async () => { const app = await buildApp(); diff --git a/apps/backend/src/__tests__/event.test.ts b/apps/backend/src/__tests__/event.test.ts index 44806af1..06b3fe9d 100644 --- a/apps/backend/src/__tests__/event.test.ts +++ b/apps/backend/src/__tests__/event.test.ts @@ -1,8 +1,10 @@ +import Fastify, { type FastifyInstance } from 'fastify'; import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; -import Fastify, { FastifyInstance } from 'fastify'; -import { PrismaClient } from '@prisma/client'; + import { eventRoutes } from '../routes/event'; +import type { PrismaClient } from '@prisma/client'; + // ─── Shared mock data ──────────────────────────────────────────────────────── const MOCK_USER_ID = 'user-uuid-001'; @@ -64,7 +66,7 @@ const prismaMock = { // // This mirrors the real app setup without touching a real DB or real JWT keys. -let mockJwtVerify = vi.fn(); +const mockJwtVerify = vi.fn(); async function buildApp(): Promise { const app = Fastify({ logger: false }); diff --git a/apps/backend/src/__tests__/follow.test.ts b/apps/backend/src/__tests__/follow.test.ts index 41830018..d0a44008 100644 --- a/apps/backend/src/__tests__/follow.test.ts +++ b/apps/backend/src/__tests__/follow.test.ts @@ -1,4 +1,4 @@ -import Fastify, { FastifyInstance } from 'fastify'; +import Fastify, { type FastifyInstance } from 'fastify'; import { describe, expect, it, vi, beforeAll, beforeEach, afterAll } from 'vitest'; import { followRoutes } from '../routes/follow.js'; diff --git a/apps/backend/src/__tests__/oauth-scope.test.ts b/apps/backend/src/__tests__/oauth-scope.test.ts index 0985dfa7..18dfc746 100644 --- a/apps/backend/src/__tests__/oauth-scope.test.ts +++ b/apps/backend/src/__tests__/oauth-scope.test.ts @@ -11,10 +11,12 @@ * flow so the two records are independent and can never overwrite each other. */ -import { describe, it, expect, beforeEach, vi } from 'vitest'; import Fastify from 'fastify'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; + import { connectRoutes } from '../routes/connect.js'; import { followRoutes } from '../routes/follow.js'; + import type { PrismaClient } from '@prisma/client'; // ── Mocks ───────────────────────────────────────────────────────────────────── diff --git a/apps/backend/src/__tests__/public.test.ts b/apps/backend/src/__tests__/public.test.ts index a767b25d..8e825782 100644 --- a/apps/backend/src/__tests__/public.test.ts +++ b/apps/backend/src/__tests__/public.test.ts @@ -1,9 +1,13 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import Fastify from 'fastify'; import jwt from '@fastify/jwt'; +import Fastify from 'fastify'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; + import { publicRoutes } from '../routes/public.js'; +import { generateQRBuffer, generateQRSvg } from '../utils/qr.js'; + import type { PrismaClient } from '@prisma/client'; + // ── Mock QR utilities ───────────────────────────────────────────────────────── // Prevents real QR rasterisation (and any native canvas/image deps) from running // during unit tests. The stubs return minimal valid values that satisfy the @@ -13,8 +17,6 @@ vi.mock('../utils/qr.js', () => ({ generateQRSvg: vi.fn().mockResolvedValue('fake'), })); -import { generateQRBuffer, generateQRSvg } from '../utils/qr.js'; - const mockUser = { id: 'user-123', username: 'testuser', diff --git a/apps/backend/src/__tests__/team.test.ts b/apps/backend/src/__tests__/team.test.ts index 350298a1..7904a311 100644 --- a/apps/backend/src/__tests__/team.test.ts +++ b/apps/backend/src/__tests__/team.test.ts @@ -1,6 +1,7 @@ +import { type PrismaClient, TeamRole } from '@prisma/client'; +import Fastify, { type FastifyInstance } from 'fastify'; import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; -import Fastify, { FastifyInstance } from 'fastify'; -import { PrismaClient, TeamRole } from '@prisma/client'; + import { teamRoutes } from '../routes/team'; // ─── Shared mock data ───────────────────────────────────────────────────────── @@ -92,7 +93,7 @@ const prismaMock = { // ─── App factory ────────────────────────────────────────────────────────────── -let mockJwtVerify = vi.fn(); +const mockJwtVerify = vi.fn(); async function buildApp(): Promise { const app = Fastify({ logger: false }); diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index 06b87205..82cdb4b7 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -21,8 +21,8 @@ import { followRoutes } from './routes/follow.js'; import { nfcRoutes } from './routes/nfc.js'; import { profileRoutes } from './routes/profiles.js'; import { publicRoutes } from './routes/public.js'; -import { validateEnv } from './utils/validateEnv.js'; import { teamRoutes } from './routes/team.js'; +import { validateEnv } from './utils/validateEnv.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -92,7 +92,7 @@ export async function buildApp():Promise { try { // Ensure the verified payload is assigned to `request.user` like the original plugin. const payload = await request.jwtVerify(); - if (payload) request.user = payload; + if (payload) {request.user = payload;} } catch (error) { reply.status(401).send({ error: 'Unauthorized' }); } diff --git a/apps/backend/src/env.ts b/apps/backend/src/env.ts index 7d841d9c..eb4ff4be 100644 --- a/apps/backend/src/env.ts +++ b/apps/backend/src/env.ts @@ -1,6 +1,7 @@ -import process from 'node:process'; import path from 'node:path'; +import process from 'node:process'; import { fileURLToPath } from 'node:url'; + import dotenv from 'dotenv'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/apps/backend/src/plugins/prisma.ts b/apps/backend/src/plugins/prisma.ts index f6ebede8..ec2d74aa 100644 --- a/apps/backend/src/plugins/prisma.ts +++ b/apps/backend/src/plugins/prisma.ts @@ -1,5 +1,6 @@ -import fp from 'fastify-plugin'; import { PrismaClient } from '@prisma/client'; +import fp from 'fastify-plugin'; + import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; declare module 'fastify' { diff --git a/apps/backend/src/plugins/redis.ts b/apps/backend/src/plugins/redis.ts index 864b112f..25b53552 100644 --- a/apps/backend/src/plugins/redis.ts +++ b/apps/backend/src/plugins/redis.ts @@ -1,5 +1,6 @@ import fp from 'fastify-plugin'; import Redis from 'ioredis'; + import type { FastifyInstance } from 'fastify'; declare module 'fastify' { diff --git a/apps/backend/src/routes/analytics.ts b/apps/backend/src/routes/analytics.ts index a975424f..6f16176c 100644 --- a/apps/backend/src/routes/analytics.ts +++ b/apps/backend/src/routes/analytics.ts @@ -11,7 +11,7 @@ export async function analyticsRoutes( app.get( '/overview', { - // eslint-disable-next-line @typescript-eslint/unbound-method + preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }], }, async ( @@ -96,7 +96,7 @@ export async function analyticsRoutes( }>( '/views', { - // eslint-disable-next-line @typescript-eslint/unbound-method + preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }], }, async ( diff --git a/apps/backend/src/routes/auth.ts b/apps/backend/src/routes/auth.ts index c14949e1..dadf5424 100644 --- a/apps/backend/src/routes/auth.ts +++ b/apps/backend/src/routes/auth.ts @@ -1,6 +1,7 @@ -import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { encrypt } from '../utils/encryption.js'; import { buildOAuthState, getMobileRedirectUri } from '../services/authService.js'; +import { encrypt } from '../utils/encryption.js'; + +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; const GITHUB_AUTH_URL = 'https://github.com/login/oauth/authorize'; const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token'; diff --git a/apps/backend/src/routes/cards.ts b/apps/backend/src/routes/cards.ts index 32fe835c..09dd6aae 100644 --- a/apps/backend/src/routes/cards.ts +++ b/apps/backend/src/routes/cards.ts @@ -1,6 +1,6 @@ +import * as cardService from '../services/cardService' import { handleDbError } from '../utils/error.util.js'; import { createCardSchema, updateCardSchema } from '../utils/validators.js'; -import * as cardService from '../services/cardService' import type { Card } from '@devcard/shared'; import type { Prisma } from '@prisma/client'; @@ -82,7 +82,7 @@ export async function cardRoutes(app: FastifyInstance): Promise { const card = await cardService.createCard(app, userId, parsed.data) return reply.status(201).send(card) } catch (error: any) { - if (error?.code === 'OWNERSHIP') return reply.status(403).send({ error: 'One or more links do not belong to your account' }) + if (error?.code === 'OWNERSHIP') {return reply.status(403).send({ error: 'One or more links do not belong to your account' })} return handleDbError(error, request, reply) } }); @@ -95,12 +95,12 @@ export async function cardRoutes(app: FastifyInstance): Promise { try { const parsed = updateCardSchema.safeParse(request.body) - if (!parsed.success) return reply.status(400).send({ error: 'Validation failed', details: parsed.error.flatten() }) + if (!parsed.success) {return reply.status(400).send({ error: 'Validation failed', details: parsed.error.flatten() })} const updated = await cardService.updateCard(app, userId, id, parsed.data) - if (!updated) return reply.status(404).send({ error: 'Card not found' }) + if (!updated) {return reply.status(404).send({ error: 'Card not found' })} return updated } catch (error: any) { - if (error?.code === 'OWNERSHIP') return reply.status(403).send({ error: 'One or more links do not belong to your account' }) + if (error?.code === 'OWNERSHIP') {return reply.status(403).send({ error: 'One or more links do not belong to your account' })} return handleDbError(error, request, reply) } }); @@ -113,8 +113,8 @@ export async function cardRoutes(app: FastifyInstance): Promise { try { const res = await cardService.deleteCard(app, userId, id) - if (res && (res as any).code === 'NOT_FOUND') return reply.status(404).send({ error: 'Card not found' }) - if (res && (res as any).code === 'LAST_CARD') return reply.status(400).send({ error: 'Cannot delete the last remaining card. A user must have at least one card.' }) + if (res && (res as any).code === 'NOT_FOUND') {return reply.status(404).send({ error: 'Card not found' })} + if (res && (res as any).code === 'LAST_CARD') {return reply.status(400).send({ error: 'Cannot delete the last remaining card. A user must have at least one card.' })} return reply.status(204).send() } catch (error) { return handleDbError(error, request, reply) @@ -129,7 +129,7 @@ export async function cardRoutes(app: FastifyInstance): Promise { try { const resp = await cardService.setDefaultCard(app, userId, id) - if (!resp) return reply.status(404).send({ error: 'Card not found' }) + if (!resp) {return reply.status(404).send({ error: 'Card not found' })} return resp } catch (error) { return handleDbError(error, request, reply) diff --git a/apps/backend/src/routes/connect.ts b/apps/backend/src/routes/connect.ts index bb04194d..1e7632f6 100644 --- a/apps/backend/src/routes/connect.ts +++ b/apps/backend/src/routes/connect.ts @@ -1,7 +1,9 @@ -import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { randomBytes } from 'crypto'; +import { randomBytes } from 'node:crypto'; + import { encrypt } from '../utils/encryption.js'; +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; + const GITHUB_AUTH_URL = 'https://github.com/login/oauth/authorize'; const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token'; @@ -102,7 +104,7 @@ export async function connectRoutes(app: FastifyInstance) { } // Consume the nonce -- one-time use only (if redis configured) - if (app.redis) await app.redis.del(`oauth:nonce:${decodedState.nonce}`); + if (app.redis) {await app.redis.del(`oauth:nonce:${decodedState.nonce}`);} const userId = decodedState.userId; diff --git a/apps/backend/src/routes/event.ts b/apps/backend/src/routes/event.ts index 4d4ee2d9..3acbaea9 100644 --- a/apps/backend/src/routes/event.ts +++ b/apps/backend/src/routes/event.ts @@ -1,7 +1,8 @@ -import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import {generateUniqueSlug} from '../utils/slug' import { createEventSchema, joinEventSchema} from '../validations/event.validation'; -import {generateUniqueSlug} from '../utils/slug' +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; + type EventDetails = { @@ -80,8 +81,8 @@ export async function eventRoutes(app:FastifyInstance) { const {name, description, startDate, endDate, isPublic ,location} = parsed.data - let finalSlug = await generateUniqueSlug(name, async(slug) => { - const existing = await app.prisma.event.findUnique({where: {slug : slug}}) + const finalSlug = await generateUniqueSlug(name, async(slug) => { + const existing = await app.prisma.event.findUnique({where: {slug}}) return !!existing }) @@ -95,7 +96,7 @@ export async function eventRoutes(app:FastifyInstance) { name, description, slug: finalSlug, - location: location, + location, startDate: startDateObj, endDate: endDateObj, isPublic: isPublic ?? true, @@ -171,7 +172,7 @@ export async function eventRoutes(app:FastifyInstance) { await app.prisma.eventAttendee.create({ data: { eventId: event.id, - userId: userId, + userId, joinedAt: new Date() } }) @@ -205,7 +206,7 @@ export async function eventRoutes(app:FastifyInstance) { await app.prisma.eventAttendee.delete({ where: { userId_eventId: { - userId: userId, + userId, eventId: event.id } } diff --git a/apps/backend/src/routes/follow.ts b/apps/backend/src/routes/follow.ts index 90209e78..7a1fded2 100644 --- a/apps/backend/src/routes/follow.ts +++ b/apps/backend/src/routes/follow.ts @@ -1,15 +1,17 @@ -import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { getPlatform, getProfileUrl, getWebViewUrl, resolveDeepLink } from '@devcard/shared'; + import { decrypt } from '../utils/encryption.js'; import { getErrorMessage } from '../utils/error.util.js'; -import { getPlatform, getProfileUrl, getWebViewUrl, resolveDeepLink } from '@devcard/shared'; import { followLogSchema } from '../validations/follow.validation.js'; +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; + export async function followRoutes(app: FastifyInstance) { app.addHook('preHandler', async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } - try { const payload = await request.jwtVerify(); if (payload) (request as any).user = payload; } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } + try { const payload = await request.jwtVerify(); if (payload) {(request as any).user = payload;} } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }); // ─── Follow via API (Layer 1) ─── diff --git a/apps/backend/src/routes/nfc.ts b/apps/backend/src/routes/nfc.ts index 5cf13f0c..84f80a0d 100644 --- a/apps/backend/src/routes/nfc.ts +++ b/apps/backend/src/routes/nfc.ts @@ -1,6 +1,7 @@ -import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; import { z } from 'zod'; +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; + type NfcPayloadResponse = { type: 'URI'; payload: string; diff --git a/apps/backend/src/routes/profiles.ts b/apps/backend/src/routes/profiles.ts index 81026c74..f369ef6e 100644 --- a/apps/backend/src/routes/profiles.ts +++ b/apps/backend/src/routes/profiles.ts @@ -1,8 +1,10 @@ -import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; import { getProfileUrl } from '@devcard/shared'; -import { updateProfileSchema, createLinkSchema, reorderLinksSchema } from '../utils/validators.js'; -import { getErrorMessage } from '../utils/error.util.js'; + import * as profileService from '../services/profileService' +import { getErrorMessage } from '../utils/error.util.js'; +import { updateProfileSchema, createLinkSchema, reorderLinksSchema } from '../utils/validators.js'; + +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; // ── Response types ──────────────────────────────────────────────────────────── // Declared explicitly so the API contract is visible without tracing through @@ -45,7 +47,7 @@ export async function profileRoutes(app: FastifyInstance) { app.get('/me', async (request: FastifyRequest, reply: FastifyReply) => { const userId = (request.user as any).id; const user = await profileService.getOwnProfile(app, userId) - if (!user) return reply.status(404).send({ error: 'User not found' }) + if (!user) {return reply.status(404).send({ error: 'User not found' })} return user }); @@ -80,7 +82,7 @@ export async function profileRoutes(app: FastifyInstance) { const response = await profileService.updateProfile(app, userId, parsed.data) return response } catch (err: any) { - if (err?.code === 'P2002') return reply.status(409).send({ error: 'Username already taken' }) + if (err?.code === 'P2002') {return reply.status(409).send({ error: 'Username already taken' })} app.log.error({ err }, 'DB error in PUT /profiles/me') return reply.status(500).send({ error: 'Internal server error' }) } @@ -112,10 +114,10 @@ export async function profileRoutes(app: FastifyInstance) { const { id } = request.params; const parsedReq = createLinkSchema.safeParse(request.body) - if (!parsedReq.success) return reply.status(400).send({ error: 'Validation failed', details: parsedReq.error.flatten() }) + if (!parsedReq.success) {return reply.status(400).send({ error: 'Validation failed', details: parsedReq.error.flatten() })} try { const updated = await profileService.updatePlatformLink(app, userId, id, parsedReq.data) - if (!updated) return reply.status(404).send({ error: 'Link not found' }) + if (!updated) {return reply.status(404).send({ error: 'Link not found' })} return updated } catch (err: any) { app.log.error({ err }, 'Failed to update platform link') @@ -131,7 +133,7 @@ export async function profileRoutes(app: FastifyInstance) { try { const deleted = await profileService.deletePlatformLink(app, userId, id) - if (!deleted) return reply.status(404).send({ error: 'Link not found' }) + if (!deleted) {return reply.status(404).send({ error: 'Link not found' })} return reply.status(204).send() } catch (err: any) { app.log.error({ err }, 'Failed to delete platform link') @@ -144,7 +146,7 @@ export async function profileRoutes(app: FastifyInstance) { app.put('/me/links/reorder', async (request: FastifyRequest, reply: FastifyReply) => { const userId = (request.user as any).id; const parsedReq = reorderLinksSchema.safeParse(request.body) - if (!parsedReq.success) return reply.status(400).send({ error: 'Validation failed', details: parsedReq.error.flatten() }) + if (!parsedReq.success) {return reply.status(400).send({ error: 'Validation failed', details: parsedReq.error.flatten() })} try { const resp = await profileService.reorderLinks(app, userId, parsedReq.data.links) return resp diff --git a/apps/backend/src/routes/public.ts b/apps/backend/src/routes/public.ts index 27f544d8..d6ac5ca2 100644 --- a/apps/backend/src/routes/public.ts +++ b/apps/backend/src/routes/public.ts @@ -1,8 +1,10 @@ -import type { FastifyContextConfig, FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import * as publicService from '../services/publicService' +import { getErrorMessage } from '../utils/error.util.js'; import { generateQRBuffer, generateQRSvg } from '../utils/qr.js'; + import type { PlatformLink } from '@devcard/shared'; -import { getErrorMessage } from '../utils/error.util.js'; -import * as publicService from '../services/publicService' +import type { FastifyContextConfig, FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; + // ── QR size bounds ──────────────────────────────────────────────────────────── @@ -122,7 +124,7 @@ export async function publicRoutes(app: FastifyInstance) { try { const result = await publicService.getPublicProfile(app, username, viewerId, request) - if (!result) return reply.status(404).send({ error: 'User not found' }) + if (!result) {return reply.status(404).send({ error: 'User not found' })} reply.header('X-Cache', result.cached ? 'HIT' : 'MISS').header('Cache-Control', CACHE_CONTROL_HEADER) return result.data } catch (err: any) { @@ -150,7 +152,7 @@ export async function publicRoutes(app: FastifyInstance) { try { const card = await publicService.getCardById(app, cardId) - if (!card) return reply.status(404).send({ error: 'Card not found' }) + if (!card) {return reply.status(404).send({ error: 'Card not found' })} const response = { id: card.id, title: card.title, owner: { username: card.user.username, displayName: card.user.displayName, bio: card.user.bio, avatarUrl: card.user.avatarUrl, accentColor: card.user.accentColor }, links: card.cardLinks.map((cl: any) => ({ id: cl.platformLink.id, platform: cl.platformLink.platform, username: cl.platformLink.username, url: cl.platformLink.url })) } return response } catch (err: any) { @@ -188,7 +190,7 @@ export async function publicRoutes(app: FastifyInstance) { try { const result = await publicService.getUserCard(app, username, cardId, viewerId, request) - if (result.notFound) return reply.status(404).send({ error: 'User or card not found' }) + if (result.notFound) {return reply.status(404).send({ error: 'User or card not found' })} return result.data } catch (err: any) { app.log.error({ err }, 'Failed to fetch user card') @@ -213,7 +215,7 @@ export async function publicRoutes(app: FastifyInstance) { try { const result = await publicService.getPublicProfile(app, username, null, request) - if (!result) return reply.status(404).send({ error: 'User not found' }) + if (!result) {return reply.status(404).send({ error: 'User not found' })} const snapshot = result.data const expiresIn = 600 const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString() diff --git a/apps/backend/src/routes/team.ts b/apps/backend/src/routes/team.ts index af177e52..d974a1ea 100644 --- a/apps/backend/src/routes/team.ts +++ b/apps/backend/src/routes/team.ts @@ -29,7 +29,7 @@ export async function teamRoutes(app:FastifyInstance){ const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } - try { const payload = await request.jwtVerify(); if (payload) (request as any).user = payload; } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } + try { const payload = await request.jwtVerify(); if (payload) {(request as any).user = payload;} } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request:FastifyRequest<{ Body: {name: string, description? : string, avatarUrl?: string } }>, reply: FastifyReply) => { @@ -161,7 +161,7 @@ export async function teamRoutes(app:FastifyInstance){ const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } - try { const payload = await request.jwtVerify(); if (payload) (request as any).user = payload; } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } + try { const payload = await request.jwtVerify(); if (payload) {(request as any).user = payload;} } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request: FastifyRequest<{Params: {slug:string}, Body:{username:string}}>, reply: FastifyReply) => { const paramsSlug = request.params.slug; const userId = (request.user as any).id; diff --git a/apps/backend/src/services/authService.ts b/apps/backend/src/services/authService.ts index 9af718c5..c9b839bb 100644 --- a/apps/backend/src/services/authService.ts +++ b/apps/backend/src/services/authService.ts @@ -1,4 +1,4 @@ -import { randomBytes } from 'crypto'; +import { randomBytes } from 'node:crypto'; export function generateState(): string { return randomBytes(32).toString('hex'); diff --git a/apps/backend/src/services/cardService.ts b/apps/backend/src/services/cardService.ts index a9721783..216a98b8 100644 --- a/apps/backend/src/services/cardService.ts +++ b/apps/backend/src/services/cardService.ts @@ -1,5 +1,5 @@ -import type { FastifyInstance } from 'fastify' import type { Prisma } from '@prisma/client' +import type { FastifyInstance } from 'fastify' export async function listCards(app: FastifyInstance, userId: string) { const cards = await app.prisma.card.findMany({ @@ -15,7 +15,7 @@ export async function listCards(app: FastifyInstance, userId: string) { export async function createCard(app: FastifyInstance, userId: string, body: { title: string; linkIds: string[] }) { if (body.linkIds.length > 0) { const ownedLinks = await app.prisma.platformLink.findMany({ where: { id: { in: body.linkIds }, userId }, select: { id: true } }) - if (ownedLinks.length !== body.linkIds.length) throw Object.assign(new Error('Link ownership mismatch'), { code: 'OWNERSHIP' }) + if (ownedLinks.length !== body.linkIds.length) {throw Object.assign(new Error('Link ownership mismatch'), { code: 'OWNERSHIP' })} } const cardCount = await app.prisma.card.count({ where: { userId } }) @@ -35,7 +35,7 @@ export async function createCard(app: FastifyInstance, userId: string, body: { t export async function updateCard(app: FastifyInstance, userId: string, id: string, body: { title?: string; linkIds?: string[] }) { const existing = await app.prisma.card.findFirst({ where: { id, userId } }) - if (!existing) return null + if (!existing) {return null} if (body.title) { await app.prisma.card.update({ where: { id }, data: { title: body.title } }) @@ -44,7 +44,7 @@ export async function updateCard(app: FastifyInstance, userId: string, id: strin if (body.linkIds) { if (body.linkIds.length > 0) { const ownedLinks = await app.prisma.platformLink.findMany({ where: { id: { in: body.linkIds }, userId }, select: { id: true } }) - if (ownedLinks.length !== body.linkIds.length) throw Object.assign(new Error('Link ownership mismatch'), { code: 'OWNERSHIP' }) + if (ownedLinks.length !== body.linkIds.length) {throw Object.assign(new Error('Link ownership mismatch'), { code: 'OWNERSHIP' })} } const linkIds = body.linkIds @@ -63,10 +63,10 @@ export async function updateCard(app: FastifyInstance, userId: string, id: strin export async function deleteCard(app: FastifyInstance, userId: string, id: string) { return await app.prisma.$transaction(async (tx: Prisma.TransactionClient) => { const existing = await tx.card.findFirst({ where: { id, userId } }) - if (!existing) return Object.assign(new Error('NotFound'), { code: 'NOT_FOUND' }) + if (!existing) {return Object.assign(new Error('NotFound'), { code: 'NOT_FOUND' })} const userCardCount = await tx.card.count({ where: { userId } }) - if (userCardCount <= 1) return Object.assign(new Error('Cannot delete last card'), { code: 'LAST_CARD' }) + if (userCardCount <= 1) {return Object.assign(new Error('Cannot delete last card'), { code: 'LAST_CARD' })} if (existing.isDefault) { const oldestRemainingCard = await tx.card.findFirst({ where: { userId, id: { not: id } }, orderBy: { createdAt: 'asc' } }) @@ -82,7 +82,7 @@ export async function deleteCard(app: FastifyInstance, userId: string, id: strin export async function setDefaultCard(app: FastifyInstance, userId: string, id: string) { const existing = await app.prisma.card.findFirst({ where: { id, userId } }) - if (!existing) return null + if (!existing) {return null} await app.prisma.$transaction(async (tx: Prisma.TransactionClient) => { await tx.card.updateMany({ where: { userId }, data: { isDefault: false } }) diff --git a/apps/backend/src/services/profileService.ts b/apps/backend/src/services/profileService.ts index dc97b2a4..e3e20618 100644 --- a/apps/backend/src/services/profileService.ts +++ b/apps/backend/src/services/profileService.ts @@ -1,8 +1,10 @@ -import type { FastifyInstance } from 'fastify' import { getProfileUrl } from '@devcard/shared' -import type { PlatformLink } from '@devcard/shared' + import { getErrorMessage } from '../utils/error.util.js' +import type { PlatformLink } from '@devcard/shared' +import type { FastifyInstance } from 'fastify' + export async function getOwnProfile(app: FastifyInstance, userId: string) { const user = await app.prisma.user.findUnique({ where: { id: userId }, @@ -12,7 +14,7 @@ export async function getOwnProfile(app: FastifyInstance, userId: string) { }, }) - if (!user) return null + if (!user) {return null} const { provider, providerId, ...profileData } = user as any return { ...profileData, defaultCardId: user.cards[0]?.id || null } @@ -24,7 +26,7 @@ export async function updateProfile(app: FastifyInstance, userId: string, data: const existing = await app.prisma.user.findFirst({ where: { username: data.username, NOT: { id: userId } }, }) - if (existing) throw Object.assign(new Error('Username taken'), { code: 'P2002' }) + if (existing) {throw Object.assign(new Error('Username taken'), { code: 'P2002' })} } const currentUser = await app.prisma.user.findUnique({ where: { id: userId }, select: { username: true } }) @@ -42,7 +44,7 @@ export async function updateProfile(app: FastifyInstance, userId: string, data: return response } catch (err: any) { - if (err?.code === 'P2002') throw err + if (err?.code === 'P2002') {throw err} app.log.error({ err }, 'DB error in updateProfile') throw err } @@ -56,14 +58,14 @@ export async function createPlatformLink(app: FastifyInstance, userId: string, l export async function updatePlatformLink(app: FastifyInstance, userId: string, id: string, linkData: any) { const existing = await app.prisma.platformLink.findFirst({ where: { id, userId } }) - if (!existing) return null + if (!existing) {return null} const url = linkData.url || getProfileUrl(linkData.platform, linkData.username) return app.prisma.platformLink.update({ where: { id }, data: { platform: linkData.platform, username: linkData.username, url } }) } export async function deletePlatformLink(app: FastifyInstance, userId: string, id: string) { const existing = await app.prisma.platformLink.findFirst({ where: { id, userId } }) - if (!existing) return false + if (!existing) {return false} await app.prisma.platformLink.delete({ where: { id } }) return true } diff --git a/apps/backend/src/services/publicService.ts b/apps/backend/src/services/publicService.ts index 758ab78f..8b8a0ecf 100644 --- a/apps/backend/src/services/publicService.ts +++ b/apps/backend/src/services/publicService.ts @@ -1,6 +1,7 @@ -import type { FastifyInstance } from 'fastify' import { getErrorMessage } from '../utils/error.util.js' +import type { FastifyInstance } from 'fastify' + const PROFILE_CACHE_TTL = 300 const CACHE_CONTROL_HEADER = 'public, max-age=300, stale-while-revalidate=60' @@ -23,7 +24,7 @@ export async function getPublicProfile(app: FastifyInstance, username: string, v } const user = await app.prisma.user.findUnique({ where: { username }, include: { platformLinks: { orderBy: { displayOrder: 'asc' } } } }) - if (!user) return null + if (!user) {return null} if (viewerId && viewerId !== user.id) { app.prisma.cardView.create({ data: { ownerId: user.id, cardId: null, viewerId, viewerIp: request.ip || null, viewerAgent: request.headers['user-agent'] || null, source: request.query?.source || 'link' } }).catch((error: unknown) => app.log.error(`Failed to log view: ${getErrorMessage(error)}`)) @@ -54,9 +55,9 @@ export async function getCardById(app: FastifyInstance, cardId: string) { export async function getUserCard(app: FastifyInstance, username: string, cardId: string, viewerId: string | null, request: any) { const user = await app.prisma.user.findUnique({ where: { username } }) - if (!user) return { notFound: true } + if (!user) {return { notFound: true }} const card = await app.prisma.card.findFirst({ where: { id: cardId, userId: user.id }, include: { cardLinks: { include: { platformLink: true }, orderBy: { displayOrder: 'asc' } } } }) - if (!card) return { notFound: true } + if (!card) {return { notFound: true }} if (viewerId && viewerId !== user.id) { app.prisma.cardView.create({ data: { ownerId: user.id, cardId: card.id, viewerId, viewerIp: request.ip || null, viewerAgent: request.headers['user-agent'] || null, source: request.query?.source || 'qr' } }).catch((error: unknown) => app.log.error(`Failed to log view: ${getErrorMessage(error)}`)) diff --git a/apps/backend/src/utils/encryption.ts b/apps/backend/src/utils/encryption.ts index b9105992..adfb3172 100644 --- a/apps/backend/src/utils/encryption.ts +++ b/apps/backend/src/utils/encryption.ts @@ -1,4 +1,4 @@ -import crypto from 'crypto'; +import crypto from 'node:crypto'; const ALGORITHM = 'aes-256-gcm'; const IV_LENGTH = 16; diff --git a/apps/backend/src/utils/error.util.ts b/apps/backend/src/utils/error.util.ts index fef1b98b..d9885d09 100644 --- a/apps/backend/src/utils/error.util.ts +++ b/apps/backend/src/utils/error.util.ts @@ -1,6 +1,7 @@ -import type { FastifyReply, FastifyRequest } from 'fastify'; import { Prisma } from '@prisma/client'; +import type { FastifyReply, FastifyRequest } from 'fastify'; + export function getErrorMessage(err: unknown): string { return err instanceof Error ? err.message : String(err); } diff --git a/apps/backend/src/utils/slug.ts b/apps/backend/src/utils/slug.ts index 24b772f3..4f0d0fcd 100644 --- a/apps/backend/src/utils/slug.ts +++ b/apps/backend/src/utils/slug.ts @@ -10,9 +10,9 @@ export async function generateUniqueSlug(name: string, while(true){ const exists = await slugExists(finalSlug) - if(!exists) break; + if(!exists) {break;} - const randomSuffix = Math.random().toString(36).substring(2,6); + const randomSuffix = Math.random().toString(36).slice(2,6); finalSlug = `${cleanSlug}-${randomSuffix}` } return finalSlug; diff --git a/apps/backend/src/utils/validators.ts b/apps/backend/src/utils/validators.ts index bd41bef2..d2f11579 100644 --- a/apps/backend/src/utils/validators.ts +++ b/apps/backend/src/utils/validators.ts @@ -1,5 +1,5 @@ -import { z } from 'zod'; import { getPlatform } from '@devcard/shared'; +import { z } from 'zod'; export const updateProfileSchema = z.object({ displayName: z.string().min(1).max(100).optional(), diff --git a/apps/web/package.json b/apps/web/package.json index 8df03ce6..50615247 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -28,4 +28,4 @@ "typescript-eslint": "^8.59.2", "vite": "^8.0.12" } -} \ No newline at end of file +} From b0314c145327ca231518e8c84323802a3f83f079 Mon Sep 17 00:00:00 2001 From: itzzavdhesh Date: Sat, 6 Jun 2026 12:33:39 +0530 Subject: [PATCH 3/7] fix(tests): align event and team mocks/expectations with route changes --- apps/backend/src/__tests__/event.test.ts | 26 ++++++++++++++++++++---- apps/backend/src/__tests__/team.test.ts | 6 ++++-- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/apps/backend/src/__tests__/event.test.ts b/apps/backend/src/__tests__/event.test.ts index 06b3fe9d..0d81474c 100644 --- a/apps/backend/src/__tests__/event.test.ts +++ b/apps/backend/src/__tests__/event.test.ts @@ -76,8 +76,10 @@ async function buildApp(): Promise { // Decorate jwtVerify on the request prototype so request.jwtVerify() resolves // to whatever the current test wants. - app.decorateRequest('jwtVerify', function () { - return mockJwtVerify(); + app.decorateRequest('jwtVerify', async function (this: any) { + const payload = await mockJwtVerify(); + this.user = payload; + return payload; }); // Register with the same prefix used in production (app.ts) so that @@ -254,6 +256,10 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, _count: { attendees: 42 }, + organizer: { + username: 'johndoe', + displayName: 'John Doe', + }, }); const res = await app.inject({ @@ -266,8 +272,9 @@ describe('Events API', () => { expect(body.slug).toBe('devcard-conf-2025'); expect(body.attendeesCount).toBe(42); expect(body.location).toBe('San Francisco, CA'); - // organizerId is exposed (public info) - expect(body.organizerId).toBe(MOCK_USER_ID); + // organizer public fields are exposed + expect(body.organizerUsername).toBe('johndoe'); + expect(body.organizerDisplayName).toBe('John Doe'); }); it('404 — returns 404 for unknown slug', async () => { @@ -288,6 +295,10 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, _count: { attendees: 0 }, + organizer: { + username: 'johndoe', + displayName: 'John Doe', + }, }); const res = await app.inject({ @@ -497,6 +508,7 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, attendees: attendeeRows, + _count: { attendees: 2 }, }); const res = await app.inject({ @@ -525,6 +537,7 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, attendees: [makeAttendeeRow(MOCK_OTHER_USER_PROFILE)], + _count: { attendees: 1 }, }); const res = await app.inject({ @@ -547,6 +560,7 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, attendees: [], + _count: { attendees: 0 }, }); const res = await app.inject({ @@ -563,6 +577,7 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, attendees: [], + _count: { attendees: 0 }, }); const res = await app.inject({ @@ -579,6 +594,7 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, attendees: [], + _count: { attendees: 0 }, }); const res = await app.inject({ @@ -596,6 +612,7 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, attendees: [makeAttendeeRow(MOCK_USER_PROFILE)], + _count: { attendees: 1 }, }); const res = await app.inject({ @@ -634,6 +651,7 @@ describe('Events API', () => { prismaMock.event.findUnique.mockResolvedValue({ ...MOCK_EVENT, attendees: [], + _count: { attendees: 0 }, }); await app.inject({ diff --git a/apps/backend/src/__tests__/team.test.ts b/apps/backend/src/__tests__/team.test.ts index 7904a311..aedfb2a7 100644 --- a/apps/backend/src/__tests__/team.test.ts +++ b/apps/backend/src/__tests__/team.test.ts @@ -100,8 +100,10 @@ async function buildApp(): Promise { app.decorate('prisma', prismaMock as unknown as PrismaClient); - app.decorateRequest('jwtVerify', function () { - return mockJwtVerify(); + app.decorateRequest('jwtVerify', async function (this: any) { + const payload = await mockJwtVerify(); + this.user = payload; + return payload; }); await app.register(teamRoutes); From abef26850fbdc8baf730cd626c3a0d20c22462fd Mon Sep 17 00:00:00 2001 From: itzzavdhesh Date: Sat, 6 Jun 2026 13:30:39 +0530 Subject: [PATCH 4/7] fix(ci): resolve compilation, shadowing, unused variable and ESLint failures --- .../backend/src/__tests__/oauth-scope.test.ts | 4 +- apps/backend/src/app.ts | 3 +- apps/backend/src/env.ts | 1 - apps/backend/src/plugins/redis.ts | 2 +- apps/backend/src/routes/analytics.ts | 4 +- apps/backend/src/routes/auth.ts | 2 +- apps/backend/src/routes/cards.ts | 29 +--- apps/backend/src/routes/connect.ts | 20 ++- apps/backend/src/routes/event.ts | 10 +- apps/backend/src/routes/follow.ts | 6 +- apps/backend/src/routes/nfc.ts | 4 +- apps/backend/src/routes/profiles.ts | 20 +-- apps/backend/src/routes/public.ts | 67 +-------- apps/backend/src/routes/team.ts | 18 +-- apps/backend/src/services/profileService.ts | 3 +- apps/backend/src/services/publicService.ts | 1 - apps/mobile/src/screens/EventsScreen.tsx | 2 +- apps/mobile/src/screens/ScanScreen.tsx | 2 +- apps/mobile/src/screens/SettingsScreen.tsx | 130 +++++++++++------- apps/mobile/src/screens/TeamDetailScreen.tsx | 1 - apps/mobile/src/screens/TeamsScreen.tsx | 4 +- apps/web/src/lib/theme.tsx | 1 + apps/web/src/pages/CardPage.tsx | 1 + apps/web/src/pages/ProfilePage.tsx | 2 + 24 files changed, 131 insertions(+), 206 deletions(-) diff --git a/apps/backend/src/__tests__/oauth-scope.test.ts b/apps/backend/src/__tests__/oauth-scope.test.ts index 18dfc746..d814e449 100644 --- a/apps/backend/src/__tests__/oauth-scope.test.ts +++ b/apps/backend/src/__tests__/oauth-scope.test.ts @@ -47,7 +47,7 @@ function makeConnectState(userId: string): string { function buildConnectApp(mockPrisma: Partial) { const app = Fastify({ logger: false }); app.decorate('prisma', mockPrisma as PrismaClient); - app.decorate('authenticate', async (req: any) => { req.user = { id: USER_ID }; }); + app.decorate('authenticate', async (request: any) => { request.user = { id: USER_ID }; }); app.register(connectRoutes, { prefix: '/api/connect' }); return app.ready().then(() => app); } @@ -57,7 +57,7 @@ function buildConnectApp(mockPrisma: Partial) { function buildFollowApp(mockPrisma: Partial) { const app = Fastify({ logger: false }); app.decorate('prisma', mockPrisma as PrismaClient); - app.decorate('authenticate', async (req: any) => { req.user = { id: USER_ID }; }); + app.decorate('authenticate', async (request: any) => { request.user = { id: USER_ID }; }); app.register(followRoutes, { prefix: '/api/follow' }); return app.ready().then(() => app); } diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index 82cdb4b7..61495520 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -7,7 +7,6 @@ import helmet from '@fastify/helmet'; import jwt from '@fastify/jwt'; import multipart from '@fastify/multipart'; import rateLimit from '@fastify/rate-limit'; -import fastifyStatic from '@fastify/static'; import Fastify, {type FastifyInstance} from 'fastify'; import { prismaPlugin } from './plugins/prisma.js'; @@ -93,7 +92,7 @@ export async function buildApp():Promise { // Ensure the verified payload is assigned to `request.user` like the original plugin. const payload = await request.jwtVerify(); if (payload) {request.user = payload;} - } catch (error) { + } catch { reply.status(401).send({ error: 'Unauthorized' }); } }); diff --git a/apps/backend/src/env.ts b/apps/backend/src/env.ts index eb4ff4be..4840d20c 100644 --- a/apps/backend/src/env.ts +++ b/apps/backend/src/env.ts @@ -1,5 +1,4 @@ import path from 'node:path'; -import process from 'node:process'; import { fileURLToPath } from 'node:url'; import dotenv from 'dotenv'; diff --git a/apps/backend/src/plugins/redis.ts b/apps/backend/src/plugins/redis.ts index 25b53552..881c289b 100644 --- a/apps/backend/src/plugins/redis.ts +++ b/apps/backend/src/plugins/redis.ts @@ -18,7 +18,7 @@ export const redisPlugin = fp(async (app: FastifyInstance) => { try { await redis.connect(); app.log.info('🔴 Redis connected'); - } catch (error) { + } catch { app.log.warn('⚠️ Redis connection failed — running without cache'); } diff --git a/apps/backend/src/routes/analytics.ts b/apps/backend/src/routes/analytics.ts index 6f16176c..3b7fced4 100644 --- a/apps/backend/src/routes/analytics.ts +++ b/apps/backend/src/routes/analytics.ts @@ -12,7 +12,7 @@ export async function analyticsRoutes( '/overview', { - preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }], + preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch { reply.status(401).send({ error: 'Unauthorized' }) } }], }, async ( request: FastifyRequest, @@ -97,7 +97,7 @@ export async function analyticsRoutes( '/views', { - preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }], + preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch { reply.status(401).send({ error: 'Unauthorized' }) } }], }, async ( request: FastifyRequest<{ diff --git a/apps/backend/src/routes/auth.ts b/apps/backend/src/routes/auth.ts index dadf5424..190b2014 100644 --- a/apps/backend/src/routes/auth.ts +++ b/apps/backend/src/routes/auth.ts @@ -258,7 +258,7 @@ export async function authRoutes(app: FastifyInstance) { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } - try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } + try { await request.jwtVerify() } catch { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async (request: FastifyRequest, reply: FastifyReply) => { const userId = (request.user as any).id; const user = await app.prisma.user.findUnique({ diff --git a/apps/backend/src/routes/cards.ts b/apps/backend/src/routes/cards.ts index 09dd6aae..4988fa39 100644 --- a/apps/backend/src/routes/cards.ts +++ b/apps/backend/src/routes/cards.ts @@ -3,7 +3,6 @@ import { handleDbError } from '../utils/error.util.js'; import { createCardSchema, updateCardSchema } from '../utils/validators.js'; import type { Card } from '@devcard/shared'; -import type { Prisma } from '@prisma/client'; import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; @@ -21,40 +20,16 @@ interface CardParams { id: string; } -interface PlatformLink { - id: string; - userId: string; - platform: string; - username: string; - url: string; - displayOrder: number; - createdAt: Date; -} -interface CardLinkWithPlatform { - id: string; - cardId: string; - platformLinkId: string; - displayOrder: number; - platformLink: PlatformLink; -} -interface CardWithLinks { - id: string; - userId: string; - title: string; - isDefault: boolean; - createdAt: Date; - updatedAt: Date; - cardLinks: CardLinkWithPlatform[]; -} + export async function cardRoutes(app: FastifyInstance): Promise { app.addHook('preHandler', async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } - try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } + try { await request.jwtVerify() } catch { reply.status(401).send({ error: 'Unauthorized' }) } }); // ─── List Cards ─── diff --git a/apps/backend/src/routes/connect.ts b/apps/backend/src/routes/connect.ts index 1e7632f6..158a3eb2 100644 --- a/apps/backend/src/routes/connect.ts +++ b/apps/backend/src/routes/connect.ts @@ -1,8 +1,6 @@ -import { randomBytes } from 'node:crypto'; - -import { encrypt } from '../utils/encryption.js'; - import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { randomBytes } from 'crypto'; +import { encrypt } from '../utils/encryption.js'; const GITHUB_AUTH_URL = 'https://github.com/login/oauth/authorize'; const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token'; @@ -24,7 +22,7 @@ interface ParsedOAuthState { nonce: string; } -export async function connectRoutes(app: FastifyInstance) { +export async function connectRoutes(app: FastifyInstance): Promise { // ─── Status ─── app.get('/status', { @@ -32,9 +30,9 @@ export async function connectRoutes(app: FastifyInstance) { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } - try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } + try { await request.jwtVerify() } catch { reply.status(401).send({ error: 'Unauthorized' }) } }], - }, async (request: FastifyRequest, reply: FastifyReply) => { + }, async (request: FastifyRequest, _reply: FastifyReply) => { const userId = (request.user as any).id; const tokens = await app.prisma.oAuthToken.findMany({ @@ -52,7 +50,7 @@ export async function connectRoutes(app: FastifyInstance) { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } - try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } + try { await request.jwtVerify() } catch { reply.status(401).send({ error: 'Unauthorized' }) } }], }, async (request: FastifyRequest, reply: FastifyReply) => { const userId = (request.user as any).id; @@ -104,7 +102,7 @@ export async function connectRoutes(app: FastifyInstance) { } // Consume the nonce -- one-time use only (if redis configured) - if (app.redis) {await app.redis.del(`oauth:nonce:${decodedState.nonce}`);} + if (app.redis) await app.redis.del(`oauth:nonce:${decodedState.nonce}`); const userId = decodedState.userId; @@ -177,7 +175,7 @@ export async function connectRoutes(app: FastifyInstance) { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } - try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } + try { await request.jwtVerify() } catch { reply.status(401).send({ error: 'Unauthorized' }) } }], }, async (request: FastifyRequest<{ Params: { platform: string } }>, reply: FastifyReply) => { const userId = (request.user as any).id; @@ -198,7 +196,7 @@ export async function connectRoutes(app: FastifyInstance) { }, }); return { success: true }; - } catch (error) { + } catch { return reply.status(404).send({ error: 'Connection not found' }); } }); diff --git a/apps/backend/src/routes/event.ts b/apps/backend/src/routes/event.ts index 3acbaea9..b4521e09 100644 --- a/apps/backend/src/routes/event.ts +++ b/apps/backend/src/routes/event.ts @@ -1,5 +1,5 @@ import {generateUniqueSlug} from '../utils/slug' -import { createEventSchema, joinEventSchema} from '../validations/event.validation'; +import { createEventSchema } from '../validations/event.validation'; import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; @@ -63,7 +63,7 @@ export async function eventRoutes(app:FastifyInstance) { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } - try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } + try { await request.jwtVerify() } catch { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async (request: FastifyRequest<{ Body: { name: string, @@ -105,7 +105,7 @@ export async function eventRoutes(app:FastifyInstance) { }) return reply.status(201).send(newEvent); - } catch (error) { + } catch { app.log.error('Failed to create event'); return reply.status(500).send({error: 'Failed to create event'}) } @@ -154,7 +154,7 @@ export async function eventRoutes(app:FastifyInstance) { return response; }) - app.post('/:slug/join', { preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request: FastifyRequest<{Params: {slug: string}}>, reply: FastifyReply) => { + app.post('/:slug/join', { preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request: FastifyRequest<{Params: {slug: string}}>, reply: FastifyReply) => { const userId = (request.user as any).id; const paramsSlug = request.params.slug; @@ -188,7 +188,7 @@ export async function eventRoutes(app:FastifyInstance) { }) - app.delete('/:slug/leave', { preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request: FastifyRequest<{Params: {slug: string}}>, reply: FastifyReply) => { + app.delete('/:slug/leave', { preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request: FastifyRequest<{Params: {slug: string}}>, reply: FastifyReply) => { const userId = (request.user as any).id; const paramsSlug = request.params.slug; diff --git a/apps/backend/src/routes/follow.ts b/apps/backend/src/routes/follow.ts index 7a1fded2..51c8ecb0 100644 --- a/apps/backend/src/routes/follow.ts +++ b/apps/backend/src/routes/follow.ts @@ -1,4 +1,4 @@ -import { getPlatform, getProfileUrl, getWebViewUrl, resolveDeepLink } from '@devcard/shared'; +import { getPlatform, resolveDeepLink } from '@devcard/shared'; import { decrypt } from '../utils/encryption.js'; import { getErrorMessage } from '../utils/error.util.js'; @@ -6,12 +6,12 @@ import { followLogSchema } from '../validations/follow.validation.js'; import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -export async function followRoutes(app: FastifyInstance) { +export async function followRoutes(app: FastifyInstance): Promise { app.addHook('preHandler', async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } - try { const payload = await request.jwtVerify(); if (payload) {(request as any).user = payload;} } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } + try { const payload = await request.jwtVerify(); if (payload) {(request as any).user = payload;} } catch { reply.status(401).send({ error: 'Unauthorized' }) } }); // ─── Follow via API (Layer 1) ─── diff --git a/apps/backend/src/routes/nfc.ts b/apps/backend/src/routes/nfc.ts index 84f80a0d..e7e483c3 100644 --- a/apps/backend/src/routes/nfc.ts +++ b/apps/backend/src/routes/nfc.ts @@ -11,7 +11,7 @@ const nfcQuerySchema = z.object({ card: z.string().uuid('Invalid card ID format').optional(), }); -export async function nfcRoutes(app: FastifyInstance) { +export async function nfcRoutes(app: FastifyInstance): Promise { app.addHook('preHandler', async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { @@ -24,7 +24,7 @@ export async function nfcRoutes(app: FastifyInstance) { } try { await request.jwtVerify(); - } catch (e) { + } catch { reply.status(401).send({ error: 'Unauthorized' }); } }); diff --git a/apps/backend/src/routes/profiles.ts b/apps/backend/src/routes/profiles.ts index f369ef6e..2890e566 100644 --- a/apps/backend/src/routes/profiles.ts +++ b/apps/backend/src/routes/profiles.ts @@ -1,7 +1,5 @@ -import { getProfileUrl } from '@devcard/shared'; import * as profileService from '../services/profileService' -import { getErrorMessage } from '../utils/error.util.js'; import { updateProfileSchema, createLinkSchema, reorderLinksSchema } from '../utils/validators.js'; import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; @@ -10,20 +8,8 @@ import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; // Declared explicitly so the API contract is visible without tracing through // Prisma's generic return types. Follows the convention in public.ts. -type ProfileUpdateResponse = { - id: string; - email: string; - username: string; - displayName: string; - bio: string | null; - pronouns: string | null; - role: string | null; - company: string | null; - avatarUrl: string | null; - accentColor: string; -}; - -export async function profileRoutes(app: FastifyInstance) { + +export async function profileRoutes(app: FastifyInstance): Promise { // All profile routes require auth app.addHook('preHandler', async (request, reply) => { const server = request.server as any; @@ -37,7 +23,7 @@ export async function profileRoutes(app: FastifyInstance) { } try { await request.jwtVerify(); - } catch (e) { + } catch { reply.status(401).send({ error: 'Unauthorized' }); } }); diff --git a/apps/backend/src/routes/public.ts b/apps/backend/src/routes/public.ts index d6ac5ca2..5ccf211c 100644 --- a/apps/backend/src/routes/public.ts +++ b/apps/backend/src/routes/public.ts @@ -1,8 +1,6 @@ import * as publicService from '../services/publicService' -import { getErrorMessage } from '../utils/error.util.js'; import { generateQRBuffer, generateQRSvg } from '../utils/qr.js'; -import type { PlatformLink } from '@devcard/shared'; import type { FastifyContextConfig, FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; @@ -18,80 +16,23 @@ const MAX_QR_SIZE = 2048; // Public profile cache TTL matches the Cache-Control max-age (5 minutes). // The QR session JWT TTL is 10 minutes so an offline scan remains valid well // beyond the HTTP cache window. -const PROFILE_CACHE_TTL = 300; // seconds (5 minutes) const CACHE_CONTROL_HEADER = 'public, max-age=300, stale-while-revalidate=60'; -type PublicProfileLink = { - id: string; - platform: string; - username: string; - url: string; - displayOrder: number; - followed?: boolean; -} -type UsernamePublicProfileResponse = { - username: string; - displayName: string; - bio: string | null; - pronouns: string | null; - role: string | null; - company: string | null; - avatarUrl: string | null; - accentColor: string; - links: PublicProfileLink[] -} -type PublicProfileCardLink = { - id: string; - platform: string; - username: string; - url: string; - followed?: boolean; -} -type CardPublicProfileResponse = { - id: string; - title: string; - owner: { - username: string; - displayName: string; - bio: string | null; - avatarUrl: string | null; - accentColor: string; - }; - links: PublicProfileCardLink[] -} -type UsernameCardPublicProfileResponse = { - title: string; - owner: { - username: string; - displayName: string; - bio: string | null; - pronouns: string | null; - role: string | null; - company: string | null; - avatarUrl: string | null; - accentColor: string; - }; - links: PublicProfileCardLink[] -} + + // Represents a CardLink record with the joined PlatformLink relation -interface CardLinkWithPlatform { - id: string; - displayOrder: number; - platformLink: PlatformLink; -} // ── Internal Redis cache shape ──────────────────────────────────────────────── // Extends the public response with the owner's DB id so that background view // tracking can still fire on cache-HIT requests without an extra DB read. -type CachedProfileEntry = UsernamePublicProfileResponse & { _userId: string }; -export async function publicRoutes(app: FastifyInstance) { +export async function publicRoutes(app: FastifyInstance): Promise { // ─── Public Profile ─────────────────────────────────────────────────────── // ─── Public Profile ─── /** @@ -107,7 +48,6 @@ export async function publicRoutes(app: FastifyInstance) { }, }, async (request: FastifyRequest<{ Params: { username: string } }>, reply: FastifyReply) => { const { username } = request.params; - const cacheKey = `profile:${username}`; // Try to extract viewer from Authorization header (soft auth). let viewerId: string | null = null @@ -211,7 +151,6 @@ export async function publicRoutes(app: FastifyInstance) { } as FastifyContextConfig }, async (request: FastifyRequest<{ Params: { username: string } }>, reply: FastifyReply) => { const { username } = request.params; - const cacheKey = `profile:${username}`; try { const result = await publicService.getPublicProfile(app, username, null, request) diff --git a/apps/backend/src/routes/team.ts b/apps/backend/src/routes/team.ts index d974a1ea..1f9d30c1 100644 --- a/apps/backend/src/routes/team.ts +++ b/apps/backend/src/routes/team.ts @@ -24,12 +24,12 @@ type TeamProfile = { members: TeamMember[]; } -export async function teamRoutes(app:FastifyInstance){ +export async function teamRoutes(app:FastifyInstance): Promise{ app.post('/', { preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } - try { const payload = await request.jwtVerify(); if (payload) {(request as any).user = payload;} } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } + try { const payload = await request.jwtVerify(); if (payload) {(request as any).user = payload;} } catch { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request:FastifyRequest<{ Body: {name: string, description? : string, avatarUrl?: string } }>, reply: FastifyReply) => { @@ -48,7 +48,7 @@ export async function teamRoutes(app:FastifyInstance){ try { const team = await app.prisma.$transaction(async (tx) => { - const team = await tx.team.create({ + const newTeam = await tx.team.create({ data: { name, slug: finalSlug, @@ -60,13 +60,13 @@ export async function teamRoutes(app:FastifyInstance){ await tx.teamMember.create({ data: { - teamId : team.id, + teamId : newTeam.id, userId, role: TeamRole.OWNER, joinedAt: new Date(), } }) - return team + return newTeam }) return reply.status(201).send(team) @@ -161,7 +161,7 @@ export async function teamRoutes(app:FastifyInstance){ const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } - try { const payload = await request.jwtVerify(); if (payload) {(request as any).user = payload;} } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } + try { const payload = await request.jwtVerify(); if (payload) {(request as any).user = payload;} } catch { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request: FastifyRequest<{Params: {slug:string}, Body:{username:string}}>, reply: FastifyReply) => { const paramsSlug = request.params.slug; const userId = (request.user as any).id; @@ -224,7 +224,7 @@ export async function teamRoutes(app:FastifyInstance){ } }) - app.delete('/:slug/members/:userId', { preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request: FastifyRequest<{Params: {slug: string, userId: string}}>, reply: FastifyReply) => { + app.delete('/:slug/members/:userId', { preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request: FastifyRequest<{Params: {slug: string, userId: string}}>, reply: FastifyReply) => { const paramsSlug = request.params.slug const paramsUserId = request.params.userId const userID = (request.user as any).id; @@ -286,7 +286,7 @@ export async function teamRoutes(app:FastifyInstance){ } }) - app.patch('/:slug',{ preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request: FastifyRequest<{Params: {slug: string},Body: {description?:string, name?:string, avatarUrl?:string}}>, reply: FastifyReply) => { + app.patch('/:slug',{ preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request: FastifyRequest<{Params: {slug: string},Body: {description?:string, name?:string, avatarUrl?:string}}>, reply: FastifyReply) => { const userId = (request.user as any).id; const paramsSlug = request.params.slug; const parsed = updateTeam.safeParse(request.body); @@ -328,7 +328,7 @@ export async function teamRoutes(app:FastifyInstance){ }) - app.delete('/:slug',{ preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request:FastifyRequest<{Params:{slug: string}}>, reply:FastifyReply) => { + app.delete('/:slug',{ preHandler: [async (request, reply) => { const server = request.server as any; if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return } if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return } try { await request.jwtVerify() } catch { reply.status(401).send({ error: 'Unauthorized' }) } }] }, async(request:FastifyRequest<{Params:{slug: string}}>, reply:FastifyReply) => { const userId = (request.user as any).id; const paramsSlug = request.params.slug; diff --git a/apps/backend/src/services/profileService.ts b/apps/backend/src/services/profileService.ts index e3e20618..385709b1 100644 --- a/apps/backend/src/services/profileService.ts +++ b/apps/backend/src/services/profileService.ts @@ -2,7 +2,6 @@ import { getProfileUrl } from '@devcard/shared' import { getErrorMessage } from '../utils/error.util.js' -import type { PlatformLink } from '@devcard/shared' import type { FastifyInstance } from 'fastify' export async function getOwnProfile(app: FastifyInstance, userId: string) { @@ -16,7 +15,7 @@ export async function getOwnProfile(app: FastifyInstance, userId: string) { if (!user) {return null} - const { provider, providerId, ...profileData } = user as any + const { provider: _provider, providerId: _providerId, ...profileData } = user as any return { ...profileData, defaultCardId: user.cards[0]?.id || null } } diff --git a/apps/backend/src/services/publicService.ts b/apps/backend/src/services/publicService.ts index 8b8a0ecf..0e58cdc9 100644 --- a/apps/backend/src/services/publicService.ts +++ b/apps/backend/src/services/publicService.ts @@ -3,7 +3,6 @@ import { getErrorMessage } from '../utils/error.util.js' import type { FastifyInstance } from 'fastify' const PROFILE_CACHE_TTL = 300 -const CACHE_CONTROL_HEADER = 'public, max-age=300, stale-while-revalidate=60' export async function getPublicProfile(app: FastifyInstance, username: string, viewerId: string | null, request: any) { const cacheKey = `profile:${username}` diff --git a/apps/mobile/src/screens/EventsScreen.tsx b/apps/mobile/src/screens/EventsScreen.tsx index c4dbf7bf..c45da697 100644 --- a/apps/mobile/src/screens/EventsScreen.tsx +++ b/apps/mobile/src/screens/EventsScreen.tsx @@ -1,4 +1,4 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState } from 'react'; import { View, Text, StyleSheet, TextInput, TouchableOpacity, StatusBar, Alert, diff --git a/apps/mobile/src/screens/ScanScreen.tsx b/apps/mobile/src/screens/ScanScreen.tsx index 7ab207f2..0bbfaee1 100644 --- a/apps/mobile/src/screens/ScanScreen.tsx +++ b/apps/mobile/src/screens/ScanScreen.tsx @@ -104,7 +104,7 @@ export default function ScanScreen({ navigation }: Props) { title: 'My DevCard QR', url: uri, }); - } catch (err) { + } catch { Alert.alert('Error', 'Failed to save QR code'); } } diff --git a/apps/mobile/src/screens/SettingsScreen.tsx b/apps/mobile/src/screens/SettingsScreen.tsx index d235b9d0..532c5589 100644 --- a/apps/mobile/src/screens/SettingsScreen.tsx +++ b/apps/mobile/src/screens/SettingsScreen.tsx @@ -15,8 +15,22 @@ import Icon from 'react-native-vector-icons/MaterialIcons'; import { SafeAreaView } from 'react-native-safe-area-context'; import { COLORS, SPACING, FONT_SIZE, BORDER_RADIUS } from '../theme/tokens'; import { useAuth } from '../context/AuthContext'; -import { API_BASE_URL } from '../config'; -import { put } from '../services/api'; +import { useTheme } from '../context/ThemeContext'; + +const SETTINGS_KEY = 'devcard.settings'; +const ACCENT_COLORS = ['#2C2C2C', '#EF4444', '#65A30D', '#3B82F6']; + +type LocalSettings = { + discoverableViaBle: boolean; + inAppConnect: boolean; + accentColor: string; +}; + +const DEFAULT_SETTINGS: LocalSettings = { + discoverableViaBle: true, + inAppConnect: true, + accentColor: '#65A30D', +}; export default function SettingsScreen() { const navigation = useNavigation(); @@ -70,44 +84,59 @@ export default function SettingsScreen() { navigation.navigate('ConnectPlatforms')} /> - - - {saving ? 'Saving...' : 'Save Changes'} - - +
+ Alert.alert('OAuth Tokens', 'Token management will be available here.')} + /> + updateSettings({ discoverableViaBle: value })} />} + /> + updateSettings({ inAppConnect: value })} />} + /> +
- {/* Integration Settings */} - - Integrations - (navigation as any).navigate('ConnectPlatforms')} - accessibilityLabel="Connected Platforms" - accessibilityRole="button" - accessibilityHint="Opens the screen to manage your connected developer platforms" - > - - 🔌 - Connected Platforms - - +
+ } + /> + + {ACCENT_COLORS.map(color => ( + updateSettings({ accentColor: color })} + style={[ + styles.swatch, + { backgroundColor: color }, + settings.accentColor === color && styles.swatchActive, + ]} + /> + ))} + + } + /> +
+ +
+ + Delete All My Data
- - Log Out + + Sign Out @@ -142,21 +171,20 @@ function SettingRow({ right?: React.ReactNode; onPress?: () => void; }) { - return ( - - {label} - - + const content = ( + <> + + {label}{detail ? ` ${detail}` : ''} + {!!subtitle && {subtitle}} + + {right || (onPress && )} + + ); + + return onPress ? ( + {content} + ) : ( + {content} ); } diff --git a/apps/mobile/src/screens/TeamDetailScreen.tsx b/apps/mobile/src/screens/TeamDetailScreen.tsx index 9503bb72..ceb88925 100644 --- a/apps/mobile/src/screens/TeamDetailScreen.tsx +++ b/apps/mobile/src/screens/TeamDetailScreen.tsx @@ -4,7 +4,6 @@ import { StatusBar, Alert, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import Avatar from '../components/Avatar'; import { LoadingPlaceholder } from '../components/LoadingPlaceholder'; import { EmptyState } from '../components/EmptyState'; diff --git a/apps/mobile/src/screens/TeamsScreen.tsx b/apps/mobile/src/screens/TeamsScreen.tsx index c64e047e..71bb527f 100644 --- a/apps/mobile/src/screens/TeamsScreen.tsx +++ b/apps/mobile/src/screens/TeamsScreen.tsx @@ -1,6 +1,6 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState } from 'react'; import { - View, Text, StyleSheet, FlatList, TouchableOpacity, + View, Text, StyleSheet, TouchableOpacity, TextInput, StatusBar, Alert, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; diff --git a/apps/web/src/lib/theme.tsx b/apps/web/src/lib/theme.tsx index 7beda8bd..276d0c78 100644 --- a/apps/web/src/lib/theme.tsx +++ b/apps/web/src/lib/theme.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-refresh/only-export-components */ import { createContext, useContext, useEffect, useState, type ReactNode } from 'react'; type Theme = 'light' | 'dark'; diff --git a/apps/web/src/pages/CardPage.tsx b/apps/web/src/pages/CardPage.tsx index 690ce574..2267fec1 100644 --- a/apps/web/src/pages/CardPage.tsx +++ b/apps/web/src/pages/CardPage.tsx @@ -24,6 +24,7 @@ export default function CardPage() { useEffect(() => { if (!id) return; + // eslint-disable-next-line react-hooks/set-state-in-effect setLoading(true); apiFetch(`/api/u/card/${id}`) .then((data) => { diff --git a/apps/web/src/pages/ProfilePage.tsx b/apps/web/src/pages/ProfilePage.tsx index 94a84f54..871119fd 100644 --- a/apps/web/src/pages/ProfilePage.tsx +++ b/apps/web/src/pages/ProfilePage.tsx @@ -23,11 +23,13 @@ export default function ProfilePage() { const [copyStatus, setCopyStatus] = useState<'success' | 'error'>('success'); useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect setMounted(true); }, []); useEffect(() => { if (!username) return; + // eslint-disable-next-line react-hooks/set-state-in-effect setLoading(true); apiFetch(`/api/u/${username}?source=web`) .then((data) => { From 161d25f549bbbc4e89b1a50310ce0e5ce7389407 Mon Sep 17 00:00:00 2001 From: itzzavdhesh Date: Sat, 6 Jun 2026 13:38:45 +0530 Subject: [PATCH 5/7] fix(backend): resolve eslint imports and curly brace styling in connect.ts --- apps/backend/src/routes/connect.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/backend/src/routes/connect.ts b/apps/backend/src/routes/connect.ts index 158a3eb2..141d7d69 100644 --- a/apps/backend/src/routes/connect.ts +++ b/apps/backend/src/routes/connect.ts @@ -1,7 +1,9 @@ -import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { randomBytes } from 'crypto'; +import { randomBytes } from 'node:crypto'; + import { encrypt } from '../utils/encryption.js'; +import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; + const GITHUB_AUTH_URL = 'https://github.com/login/oauth/authorize'; const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token'; @@ -102,7 +104,9 @@ export async function connectRoutes(app: FastifyInstance): Promise { } // Consume the nonce -- one-time use only (if redis configured) - if (app.redis) await app.redis.del(`oauth:nonce:${decodedState.nonce}`); + if (app.redis) { + await app.redis.del(`oauth:nonce:${decodedState.nonce}`); + } const userId = decodedState.userId; From b5651755e962cb53a3fe0528648c859f3a6fc7a8 Mon Sep 17 00:00:00 2001 From: itzzavdhesh Date: Sat, 6 Jun 2026 17:47:38 +0530 Subject: [PATCH 6/7] fix(ci): ensure prisma client generation and fallback test env vars are set --- .github/workflows/ci.yml | 3 +++ apps/backend/src/app.ts | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8cedac7..6d5d1294 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,9 @@ jobs: - name: Install backend dependencies run: npm --prefix apps/backend install + - name: Generate Prisma Client + run: cd apps/backend && npx prisma generate + - name: Backend lint id: backend_lint continue-on-error: true diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index 61495520..0407161a 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -29,6 +29,10 @@ export async function buildApp():Promise { // Validate all required secrets before registering any plugin. // If validation fails the process exits here — no partially-initialised // auth state can exist because Fastify is not yet instantiated. + if (process.env.NODE_ENV === 'test') { + process.env.JWT_SECRET = process.env.JWT_SECRET || 'test-jwt-secret-that-is-sufficiently-long-and-secure'; + process.env.ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || 'test-encryption-key-for-testing-purposes-32-chars'; + } validateEnv(); const app = Fastify({ From 88a4d6f4301b010ea5da97c04222fb672df8b4b5 Mon Sep 17 00:00:00 2001 From: itzzavdhesh Date: Sat, 6 Jun 2026 17:54:57 +0530 Subject: [PATCH 7/7] fix(backend): add prisma generate to postinstall script --- apps/backend/package-lock.json | 1 + apps/backend/package.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/backend/package-lock.json b/apps/backend/package-lock.json index 64f44440..832b4eee 100644 --- a/apps/backend/package-lock.json +++ b/apps/backend/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "@devcard/backend", "version": "1.0.0", + "hasInstallScript": true, "dependencies": { "@devcard/shared": "file:../../packages/shared", "@fastify/cookie": "^11.0.0", diff --git a/apps/backend/package.json b/apps/backend/package.json index 995ce916..cf8bb532 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -15,7 +15,8 @@ "db:deploy": "prisma migrate deploy", "db:seed": "tsx prisma/seed.ts", "db:studio": "prisma studio", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "postinstall": "prisma generate" }, "dependencies": { "@devcard/shared": "file:../../packages/shared",