Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 38 additions & 42 deletions apps/mail/app/api/auth/early-access/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { type NextRequest, NextResponse } from 'next/server';
import { Ratelimit } from '@upstash/ratelimit';
import { earlyAccess } from '@zero/db/schema';
import { redis } from '@/lib/redis';
import { db } from '@zero/db';
import { type NextRequest, NextResponse } from "next/server";
import { Ratelimit } from "@upstash/ratelimit";
import { earlyAccess } from "@zero/db/schema";
import { redis } from "@/lib/redis";
import { db } from "@zero/db";

type PostgresError = {
code: string;
Expand All @@ -11,9 +11,9 @@ type PostgresError = {

const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(2, '30m'),
limiter: Ratelimit.slidingWindow(2, "30m"),
analytics: true,
prefix: 'ratelimit:early-access',
prefix: "ratelimit:early-access",
});

function isEmail(email: string): boolean {
Expand All @@ -22,98 +22,94 @@ function isEmail(email: string): boolean {
}

const emailRegex = new RegExp(
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
);

return emailRegex.test(email);
}

export async function POST(req: NextRequest) {
try {
const ip = req.headers.get('CF-Connecting-IP');
const ip = req.headers.get("CF-Connecting-IP");
if (!ip) {
console.log('No IP detected');
return NextResponse.json({ error: 'No IP detected' }, { status: 400 });
console.log("No IP detected");
return NextResponse.json({ error: "No IP detected" }, { status: 400 });
}
console.log(
'Request from IP:',
ip,
req.headers.get('x-forwarded-for'),
req.headers.get('CF-Connecting-IP'),
);
console.log("Request from IP:", ip, req.headers.get("x-forwarded-for"), req.headers.get('CF-Connecting-IP'));
const { success, limit, reset, remaining } = await ratelimit.limit(ip);

const headers = {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString(),
"X-RateLimit-Limit": limit.toString(),
"X-RateLimit-Remaining": remaining.toString(),
"X-RateLimit-Reset": reset.toString(),
};

if (!success) {
console.log(`Rate limit exceeded for IP ${ip}. Remaining: ${remaining}`);
return NextResponse.json(
{ error: 'Too many requests. Please try again later.' },
{ error: "Too many requests. Please try again later." },
{ status: 429, headers },
);
}

const body = await req.json();
console.log('Request body:', body);
console.log("Request body:", body);

const { email } = body;

if (!email) {
console.log('Email missing from request');
return NextResponse.json({ error: 'Email is required' }, { status: 400 });
console.log("Email missing from request");
return NextResponse.json({ error: "Email is required" }, { status: 400 });
}

if (!isEmail(email)) {
console.log('Invalid email format');
return NextResponse.json({ error: 'Invalid email format' }, { status: 400 });
console.log("Invalid email format");
return NextResponse.json({ error: "Invalid email format" }, { status: 400 });
}

const nowDate = new Date();

try {
console.log('Attempting to insert email:', email);
console.log("Attempting to insert email:", email);

const result = await db.insert(earlyAccess).values({
id: crypto.randomUUID(),
email,
createdAt: nowDate,
updatedAt: nowDate,
});

console.log('Insert successful:', result);
console.log("Insert successful:", result);

return NextResponse.json(
{ message: 'Successfully joined early access' },
{ message: "Successfully joined early access" },
{
status: 201,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString(),
"X-RateLimit-Limit": limit.toString(),
"X-RateLimit-Remaining": remaining.toString(),
"X-RateLimit-Reset": reset.toString(),
},
},
);
} catch (err) {
const pgError = err as PostgresError;
console.error('Database error:', {
console.error("Database error:", {
code: pgError.code,
message: pgError.message,
fullError: err,
});

if (pgError.code === '23505') {
if (pgError.code === "23505") {
// Return 200 for existing emails
return NextResponse.json(
{ message: 'Email already registered for early access' },
{ message: "Email already registered for early access" },
{
status: 200,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString(),
"X-RateLimit-Limit": limit.toString(),
"X-RateLimit-Remaining": remaining.toString(),
"X-RateLimit-Reset": reset.toString(),
},
},
);
Expand All @@ -124,21 +120,21 @@ export async function POST(req: NextRequest) {

// This line is now unreachable due to the returns in the try/catch above
} catch (error) {
console.error('Early access registration error:', {
console.error("Early access registration error:", {
error,
stack: error instanceof Error ? error.stack : undefined,
});

if (process.env.NODE_ENV === 'development') {
if (process.env.NODE_ENV === "development") {
return NextResponse.json(
{
error: 'Internal server error',
error: "Internal server error",
details: error instanceof Error ? error.message : String(error),
},
{ status: 500 },
);
}

return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
113 changes: 56 additions & 57 deletions apps/mail/app/api/notes/db.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { eq, and, desc, asc, sql } from 'drizzle-orm';
import type { Note, NotesManager } from './types';
import { note } from '@zero/db/schema';
import { db } from '@zero/db';
import { eq, and, desc, asc, sql } from "drizzle-orm";
import { db } from "@zero/db";
import { note } from "@zero/db/schema";
import type { Note, NotesManager } from "./types";

export const notesManager: NotesManager = {
async getNotes(userId: string): Promise<Note[]> {
Expand All @@ -24,8 +24,8 @@ export const notesManager: NotesManager = {
userId: string,
threadId: string,
content: string,
color: string = 'default',
isPinned: boolean = false,
color: string = "default",
isPinned: boolean = false
): Promise<Note> {
const userNotes = await db
.select()
Expand All @@ -34,10 +34,11 @@ export const notesManager: NotesManager = {
.orderBy(desc(note.order));

const highestOrder = userNotes[0]?.order ?? -1;

const result = await db
.insert(note)
.values({
id: sql`gen_random_uuid()`,
userId,
threadId,
content,
Expand All @@ -48,15 +49,15 @@ export const notesManager: NotesManager = {
.returning();

if (!result[0]) {
throw new Error('Failed to create note');
throw new Error("Failed to create note");
}
return result[0];
},

async updateNote(
userId: string,
noteId: string,
data: Partial<Omit<Note, 'id' | 'userId' | 'threadId' | 'createdAt' | 'updatedAt'>>,
data: Partial<Omit<Note, "id" | "userId" | "threadId" | "createdAt" | "updatedAt">>
): Promise<Note> {
const existingNote = await db
.select()
Expand All @@ -65,7 +66,7 @@ export const notesManager: NotesManager = {
.limit(1);

if (existingNote.length === 0) {
throw new Error('Note not found or unauthorized');
throw new Error("Note not found or unauthorized");
}

const result = await db
Expand All @@ -78,7 +79,7 @@ export const notesManager: NotesManager = {
.returning();

if (!result[0]) {
throw new Error('Failed to update note');
throw new Error("Failed to update note");
}
return result[0];
},
Expand All @@ -91,68 +92,66 @@ export const notesManager: NotesManager = {
.limit(1);

if (existingNote.length === 0) {
throw new Error('Note not found or unauthorized');
throw new Error("Note not found or unauthorized");
}

await db.delete(note).where(eq(note.id, noteId));
await db
.delete(note)
.where(eq(note.id, noteId));

return true;
},

async reorderNotes(
userId: string,
notes: { id: string; order: number; isPinned?: boolean }[],
notes: { id: string; order: number; isPinned?: boolean }[]
): Promise<boolean> {
if (!notes || notes.length === 0) {
return true;
}

const noteIds = notes.map((n) => n.id);

const noteIds = notes.map(n => n.id);
const userNotes = await db
.select({ id: note.id })
.from(note)
.where(
and(
eq(note.userId, userId),
sql`${note.id} IN (${sql.join(
noteIds.map((id) => sql`${id}`),
sql`, `,
)})`,
),
);

const foundNoteIds = new Set(userNotes.map((n) => n.id));

.where(and(
eq(note.userId, userId),
sql`${note.id} IN (${sql.join(noteIds.map(id => sql`${id}`), sql`, `)})`
));

const foundNoteIds = new Set(userNotes.map(n => n.id));

if (foundNoteIds.size !== noteIds.length) {
const missingNotes = noteIds.filter((id) => !foundNoteIds.has(id));
const missingNotes = noteIds.filter(id => !foundNoteIds.has(id));
console.error(`Notes not found or unauthorized: ${missingNotes.join(', ')}`);
throw new Error('One or more notes not found or unauthorized');
throw new Error("One or more notes not found or unauthorized");
}

return await db
.transaction(async (tx) => {
for (const n of notes) {
const updateData: Record<string, any> = {
order: n.order,
updatedAt: new Date(),
};

if (n.isPinned !== undefined) {
updateData.isPinned = n.isPinned;
}

await tx
.update(note)
.set(updateData)
.where(and(eq(note.id, n.id), eq(note.userId, userId)));

return await db.transaction(async (tx) => {
for (const n of notes) {
const updateData: Record<string, any> = {
order: n.order,
updatedAt: new Date(),
};

if (n.isPinned !== undefined) {
updateData.isPinned = n.isPinned;
}

return true;
})
.catch((error) => {
console.error('Error in reorderNotes transaction:', error);
throw new Error('Failed to reorder notes: ' + error.message);
});
},
};

await tx
.update(note)
.set(updateData)
.where(and(
eq(note.id, n.id),
eq(note.userId, userId)
));
}

return true;
}).catch(error => {
console.error("Error in reorderNotes transaction:", error);
throw new Error("Failed to reorder notes: " + error.message);
});
}
};
Loading