From 2369b1f72da17842e2c5f0ff282c91fb5bc40fcf Mon Sep 17 00:00:00 2001 From: Oluwakorede Fashokun Date: Wed, 27 Nov 2024 10:29:52 -0500 Subject: [PATCH 01/15] Start adding validation --- api/src/collections/user/mutations.ts | 34 ++++- api/src/index.ts | 2 +- api/src/routes/auth.ts | 55 +++++++ api/src/services/notifications.ts | 62 ++++---- api/src/types/misc.ts | 2 +- api/src/types/resolvers.ts | 2 +- api/tsconfig.json | 8 +- apps/app/src/screens/Landing.tsx | 42 ++++-- .../src/components/landing/AppleButton.tsx | 67 +++++++++ .../src/components/store/StoreMenu.tsx | 7 + apps/dashboard/src/navigation/Routes.tsx | 4 +- apps/dashboard/src/screens/Authenticate.tsx | 50 +++---- apps/dashboard/src/screens/Landing.tsx | 48 ++++++ apps/dashboard/src/screens/Register.tsx | 139 ++++++++---------- apps/dashboard/src/types/navigation.ts | 1 + packages/components/src/Screen.tsx | 5 +- 16 files changed, 374 insertions(+), 154 deletions(-) create mode 100644 apps/dashboard/src/components/landing/AppleButton.tsx create mode 100644 apps/dashboard/src/screens/Landing.tsx diff --git a/api/src/collections/user/mutations.ts b/api/src/collections/user/mutations.ts index e567ea2d..4566da64 100644 --- a/api/src/collections/user/mutations.ts +++ b/api/src/collections/user/mutations.ts @@ -16,6 +16,18 @@ export const register: Resolver = async ( { input: { name, email, password } }, ctx ) => { + if (!name || !email || !password) { + throw new Error('Name, email, and password are required.'); + } + + if (password.length < 8) { + throw new Error('Password must be at least 8 characters long.'); + } + + if (!email.includes('@')) { + throw new Error('Please provide a valid email address.'); + } + const passwordHash = await argon2.hash(password); const user = await ctx.prisma.user.create({ @@ -39,6 +51,10 @@ export const authenticate: Resolver = async ( { input: { email, password } }, ctx ) => { + if (!email || !password) { + throw new Error('Email and password are required.'); + } + const user = await ctx.prisma.user.findUnique({ where: { email } }); if (!user) throw new Error('The specified user does not exist.'); @@ -66,6 +82,10 @@ export const verify: Resolver = async ( { input: { email, code } }, ctx ) => { + if (!email || !code) { + throw new Error('Email and verification code are required.'); + } + const cachedCode = await ctx.redisClient.get(email); if (!cachedCode) throw new Error('No code found for user.'); @@ -75,6 +95,10 @@ export const verify: Resolver = async ( where: { email } }); + if (!user) { + throw new Error(''); + } + const accessToken = await generateAccessToken(user); return { accessToken, userId: user.id }; @@ -96,11 +120,17 @@ export const editProfile: Resolver = ( { id, input }, ctx ) => { - const { name, email } = input; + if (!id) throw new Error('User ID is required.'); + if (!input.name && !input.email) { + throw new Error('At least one field (name or email) must be provided.'); + } + if (input.email && !input.email.includes('@')) { + throw new Error('Please provide a valid email address.'); + } return ctx.prisma.user.update({ where: { id }, - data: { name, email } + data: input }); }; diff --git a/api/src/index.ts b/api/src/index.ts index 2932448e..41626f7b 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -27,7 +27,7 @@ const main = async () => { app.use(compression()); app.use( expressjwt({ - secret: process.env.JWT_SECRET, + secret: process.env.JWT_SECRET as string, algorithms: ['HS256'], credentialsRequired: false }) diff --git a/api/src/routes/auth.ts b/api/src/routes/auth.ts index 8d07cbb9..eab8acbf 100644 --- a/api/src/routes/auth.ts +++ b/api/src/routes/auth.ts @@ -1,4 +1,7 @@ import { Router } from 'express'; +import jwt from 'jsonwebtoken'; + +import prismaClient from '../config/prisma'; const authRouter: Router = Router(); @@ -8,4 +11,56 @@ authRouter.post('/login', async () => {}); authRouter.post('/verify', async () => {}); +authRouter.post('/apple-callback', async (req, res) => { + try { + const response = await fetch('https://appleid.apple.com/auth/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + client_id: process.env.APPLE_CLIENT_ID as string, + client_secret: process.env.APPLE_CLIENT_SECRET as string, + code: req.body.code, + grant_type: 'authorization_code', + redirect_uri: process.env.APPLE_REDIRECT_URI as string + }) + }); + + const data = await response.json(); + const decodedToken = jwt.decode(data.id_token) as any; + + if (!decodedToken?.email) { + return res.status(400).json({ + message: 'Invalid Apple ID token' + }); + } + + // Check if user exists + let user = await prismaClient.user.findUnique({ + where: { email: decodedToken.email } + }); + + if (!user) { + // Create new user if they don't exist + user = await prismaClient.user.create({ + data: { + email: decodedToken.email, + name: decodedToken.name || '', + passwordHash: '' + } + }); + } + + // Generate JWT token + const token = jwt.sign(user, process.env.JWT_SECRET as string); + + return res.status(200).json({ userId: user.id, token }); + } catch (error) { + return res.status(500).json({ + message: error.message + }); + } +}); + export default authRouter; diff --git a/api/src/services/notifications.ts b/api/src/services/notifications.ts index 1d69484b..4da313c7 100644 --- a/api/src/services/notifications.ts +++ b/api/src/services/notifications.ts @@ -9,27 +9,12 @@ const expo = new Expo(); // - Order fulfilled (user) // - Delivery confirmed (store managers) -// Architecture: -// When we call ctx.services.notifications.queueMessage(...), a message gets -// added to the queue. Every minute (or maybe 15 seconds), we get all items -// in the queue and dump them into the expo.chunkPushNotifications(...) -// method, which chunks and sends them. -// -// In the future, we can also commit the messages if they reach a certain -// count threshold and/or tweak the batch time. -// -// I'm a little worried that it isn't trivial to implement something that -// works at scale here. Resetting messages to an empty array is a little -// risky, if it's possible for that array to have been appended to after -// chunking. A very trivial way to mitigate this is to store the array length -// before chunking and check if it has changed after. -// -// Who ever said that mutexes aren't useful? - const BATCH_TIME = 15 * 1000; export default class NotificationsService { - private messages = []; + private messages: ExpoPushMessage[] = []; + private isProcessing = false; + private pendingMessages: ExpoPushMessage[] = []; constructor() { setInterval(() => { @@ -41,19 +26,40 @@ export default class NotificationsService { this.messages.push(message); } - async sendMessages() { - const chunks = expo.chunkPushNotifications(this.messages); + private async sendMessages() { + // Prevent concurrent processing + if (this.isProcessing) { + return; + } + + this.isProcessing = true; - // Reset messages to make sure we don't send them again. - this.messages = []; + try { + // Safely capture current messages + const messagesToSend = [...this.messages]; + this.messages = []; + + // Process chunks + const chunks = expo.chunkPushNotifications(messagesToSend); + + for (const chunk of chunks) { + try { + const tickets = await expo.sendPushNotificationsAsync(chunk); + console.log({ tickets }); + } catch (error) { + // If sending fails, add back to pending messages + this.pendingMessages.push(...chunk); + console.error('Failed to send notifications:', error); + } + } - for (const chunk of chunks) { - try { - const tickets = await expo.sendPushNotificationsAsync(chunk); - console.log({ tickets }); - } catch (error) { - console.log({ error }); + // Retry pending messages from previous failures + if (this.pendingMessages.length > 0) { + this.messages.push(...this.pendingMessages); + this.pendingMessages = []; } + } finally { + this.isProcessing = false; } } } diff --git a/api/src/types/misc.ts b/api/src/types/misc.ts index 722bc121..86316e89 100644 --- a/api/src/types/misc.ts +++ b/api/src/types/misc.ts @@ -2,5 +2,5 @@ import { User } from '@prisma/client'; import { Request } from 'express'; export interface HabitiRequest extends Request { - auth?: User; + auth: User; } diff --git a/api/src/types/resolvers.ts b/api/src/types/resolvers.ts index 93057ba7..4bab4ba2 100644 --- a/api/src/types/resolvers.ts +++ b/api/src/types/resolvers.ts @@ -5,7 +5,7 @@ import Services from '../services'; export interface ResolverContext { prisma: PrismaClient; - user: User | null; + user: User; // This should be nullable, but I want to circumvent validating users everywhere. redisClient: RedisClient; storeId?: string; services: Services; diff --git a/api/tsconfig.json b/api/tsconfig.json index c39bc42c..eb032032 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -1,5 +1,5 @@ { - "compilerOptions": { + "compilerOptions": { "allowJs": false, "composite": true, "noEmit": false, @@ -15,7 +15,11 @@ // Set `sourceRoot` to "/" to strip the build path prefix // from generated source code references. // This improves issue grouping in Sentry. - "sourceRoot": "/" + "sourceRoot": "/", + // Make sure that Prisma is aggressive with type-related issues. + "strictNullChecks": true, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true }, "include": ["src"] } diff --git a/apps/app/src/screens/Landing.tsx b/apps/app/src/screens/Landing.tsx index 7e32efd3..2a63083a 100644 --- a/apps/app/src/screens/Landing.tsx +++ b/apps/app/src/screens/Landing.tsx @@ -7,6 +7,8 @@ import { } from '@habiti/components'; import { NavigationProp, useNavigation } from '@react-navigation/native'; import React from 'react'; +import { View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; import { AppStackParamList } from '../types/navigation'; @@ -14,21 +16,31 @@ const Landing = () => { const { navigate } = useNavigation>(); return ( - - - Welcome to Habiti - - -