diff --git a/apps/backend/src/__tests__/profiles.test.ts b/apps/backend/src/__tests__/profiles.test.ts index ef1aad6..da6d10a 100644 --- a/apps/backend/src/__tests__/profiles.test.ts +++ b/apps/backend/src/__tests__/profiles.test.ts @@ -29,7 +29,7 @@ const mockPrisma = { async function buildApp() { const app = Fastify(); - app.decorate('prisma', mockPrisma); + app.decorate('prisma', mockPrisma as any); app.decorate('authenticate', async (request: any) => { request.user = { id: 'user-123' }; }); diff --git a/apps/backend/src/routes/analytics.ts b/apps/backend/src/routes/analytics.ts index e9a75bb..e65305b 100644 --- a/apps/backend/src/routes/analytics.ts +++ b/apps/backend/src/routes/analytics.ts @@ -57,9 +57,9 @@ export async function analyticsRoutes(app: FastifyInstance) { }; }); - app.get('/views', { + app.get<{ Querystring: { page?: string; cardId?: string } }>('/views', { preHandler: [app.authenticate], - }, async (request: FastifyRequest<{ Querystring: { page?: string, cardId?: string } }>, reply: FastifyReply) => { + }, async (request, reply) => { const userId = (request.user as any).id; const page = parseInt(request.query.page || '1', 10); const limit = 20; diff --git a/apps/backend/src/routes/auth.ts b/apps/backend/src/routes/auth.ts index febc41d..df8631e 100644 --- a/apps/backend/src/routes/auth.ts +++ b/apps/backend/src/routes/auth.ts @@ -134,7 +134,7 @@ export async function authRoutes(app: FastifyInstance) { return reply.redirect(`${process.env.PUBLIC_APP_URL}/dashboard`); } catch (err) { - app.log.error('GitHub auth error:', err); + app.log.error({ err }, 'GitHub auth error'); return reply.status(500).send({ error: 'Authentication failed' }); } }); @@ -235,7 +235,7 @@ export async function authRoutes(app: FastifyInstance) { return reply.redirect(`${process.env.PUBLIC_APP_URL}/dashboard`); } catch (err) { - app.log.error('Google auth error:', err); + app.log.error({ err }, 'Google auth error'); return reply.status(500).send({ error: 'Authentication failed' }); } }); diff --git a/apps/backend/src/routes/connect.ts b/apps/backend/src/routes/connect.ts index 68f8671..e2a38c0 100644 --- a/apps/backend/src/routes/connect.ts +++ b/apps/backend/src/routes/connect.ts @@ -1,4 +1,6 @@ import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import { encrypt } from '../utils/encryption.js'; +import { randomBytes } from 'crypto'; const GITHUB_AUTH_URL = 'https://github.com/login/oauth/authorize'; const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token'; @@ -52,7 +54,7 @@ export async function connectRoutes(app: FastifyInstance) { return reply.redirect(`${GITHUB_AUTH_URL}?${params}`); }); - app.get('/github/callback', async (request: FastifyRequest<{ Querystring: OAuthCallbackQuery }>, reply: FastifyReply) => { + app.get<{ Querystring: OAuthCallbackQuery }>('/github/callback', async (request, reply) => { const { code, state } = request.query; if (!code || !state) { @@ -95,7 +97,7 @@ export async function connectRoutes(app: FastifyInstance) { } // Encrypt and store the token - const encryptedToken = app.encryption.encrypt(tokenData.access_token); + const encryptedToken = encrypt(tokenData.access_token); await app.prisma.oAuthToken.upsert({ where: { @@ -125,7 +127,7 @@ export async function connectRoutes(app: FastifyInstance) { return reply.redirect(`${process.env.PUBLIC_APP_URL}/settings?connected=github`); } catch (err) { - app.log.error('GitHub connect error:', err); + app.log.error({ err }, 'GitHub connect error'); return reply.redirect(`${process.env.PUBLIC_APP_URL}/settings?error=server_error`); } }); @@ -133,9 +135,9 @@ export async function connectRoutes(app: FastifyInstance) { // ─── Disconnect ─── - app.delete('/:platform', { + app.delete<{ Params: { platform: string } }>('/:platform', { preHandler: [app.authenticate], - }, async (request: FastifyRequest<{ Params: { platform: string } }>, reply: FastifyReply) => { + }, async (request, reply) => { const userId = (request.user as any).id; const { platform } = request.params; diff --git a/apps/mobile/src/components/Skeleton.tsx b/apps/mobile/src/components/Skeleton.tsx index 23f52d2..a20ec7f 100644 --- a/apps/mobile/src/components/Skeleton.tsx +++ b/apps/mobile/src/components/Skeleton.tsx @@ -1,10 +1,10 @@ import React, { useEffect, useRef } from 'react'; -import { View, Animated, StyleSheet, ViewStyle } from 'react-native'; +import { View, Animated, StyleSheet, ViewStyle, DimensionValue } from 'react-native'; import { COLORS } from '../theme/tokens'; interface SkeletonProps { - width?: number | string; - height?: number | string; + width?: DimensionValue; + height?: DimensionValue; borderRadius?: number; style?: ViewStyle; } diff --git a/apps/mobile/src/context/AuthContext.tsx b/apps/mobile/src/context/AuthContext.tsx index 77559e4..63e1e25 100644 --- a/apps/mobile/src/context/AuthContext.tsx +++ b/apps/mobile/src/context/AuthContext.tsx @@ -1,5 +1,6 @@ import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; import { API_BASE_URL } from '../config'; +import AsyncStorage from '@react-native-async-storage/async-storage'; interface User { id: string; @@ -33,14 +34,34 @@ export function AuthProvider({ children }: { children: ReactNode }) { const [isLoading, setIsLoading] = useState(true); useEffect(() => { - // TODO: Load token from secure storage on app start - setIsLoading(false); + const loadToken = async () => { + try { + const storedToken = await AsyncStorage.getItem('devcard_auth_token'); + if (storedToken) { + setToken(storedToken); + const res = await fetch(`${API_BASE_URL}/api/profiles/me`, { + headers: { Authorization: `Bearer ${storedToken}` }, + }); + if (res.ok) { + const userData = await res.json(); + setUser(userData); + } else { + await AsyncStorage.removeItem('devcard_auth_token'); + } + } + } catch (err) { + console.error('Failed to load token:', err); + } finally { + setIsLoading(false); + } + }; + loadToken(); }, []); const login = async (newToken: string) => { setToken(newToken); - // TODO: Save token to secure storage try { + await AsyncStorage.setItem('devcard_auth_token', newToken); const res = await fetch(`${API_BASE_URL}/api/profiles/me`, { headers: { Authorization: `Bearer ${newToken}` }, }); @@ -56,7 +77,9 @@ export function AuthProvider({ children }: { children: ReactNode }) { const logout = () => { setToken(null); setUser(null); - // TODO: Clear token from secure storage + AsyncStorage.removeItem('devcard_auth_token').catch(err => { + console.error('Failed to clear token:', err); + }); }; const refreshUser = async () => { diff --git a/apps/mobile/src/navigation/MainTabs.tsx b/apps/mobile/src/navigation/MainTabs.tsx index 11e4e9a..10ba23f 100644 --- a/apps/mobile/src/navigation/MainTabs.tsx +++ b/apps/mobile/src/navigation/MainTabs.tsx @@ -12,8 +12,8 @@ import ScanScreen from '../screens/ScanScreen'; import DevCardViewScreen from '../screens/DevCardViewScreen'; import WebViewScreen from '../screens/WebViewScreen'; -import ConnectPlatformsScreen from '../screens/ConnectPlatformsScreen'; -import ViewsScreen from '../screens/ViewsScreen'; +import { ConnectPlatformsScreen } from '../screens/ConnectPlatformsScreen'; +import { ViewsScreen } from '../screens/ViewsScreen'; // ─── Types ─── diff --git a/apps/mobile/src/screens/SettingsScreen.tsx b/apps/mobile/src/screens/SettingsScreen.tsx index 7d282a6..64528fe 100644 --- a/apps/mobile/src/screens/SettingsScreen.tsx +++ b/apps/mobile/src/screens/SettingsScreen.tsx @@ -14,9 +14,13 @@ 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 { useNavigation } from '@react-navigation/native'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import type { RootStackParamList } from '../navigation/MainTabs'; export default function SettingsScreen() { const { user, token, refreshUser, logout } = useAuth(); + const navigation = useNavigation>(); const [displayName, setDisplayName] = useState(user?.displayName || ''); const [bio, setBio] = useState(user?.bio || ''); const [pronouns, setPronouns] = useState(user?.pronouns || '');