- Try making your browser window small to test the mobile view. You will see the new mobile Sheet sidebar and the expanding search bar! -
-- Try making your browser window small to test the mobile view. You will see the new mobile Sheet sidebar and the expanding search bar! -
-Recent Searches
-+ {statusMessage.text} +
+ ) : null; + + if (!session?.user) { + return null; + } + + return ( + <> +Create a post
+@{session.user.username || session.user.name || "user"}
+{post.body}
: null} +{actionError}
: null} +Saved in PostgreSQL
++ {session.user.username || session.user.name || "User"} +
+{session.user.email}
+No posts yet
+Be the first to start the conversation.
+Recent searches
+Please click the link below to verify your email and activate your account.
+ + `, + }); + } catch (err) { + console.error("Failed to send verification email. (Did you configure SMTP in .env?)", err); + } +}; diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts new file mode 100644 index 0000000..b8d2d25 --- /dev/null +++ b/src/lib/prisma.ts @@ -0,0 +1,13 @@ +import { Pool } from "pg"; +import { PrismaPg } from "@prisma/adapter-pg"; +import { PrismaClient } from "../generated/prisma/client"; + +const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }; + +const pool = new Pool({ connectionString: process.env.DATABASE_URL }); +const adapter = new PrismaPg(pool); + +export const prisma = + globalForPrisma.prisma || new PrismaClient({ adapter }); + +if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; diff --git a/src/lib/rate-limit.ts b/src/lib/rate-limit.ts new file mode 100644 index 0000000..5211349 --- /dev/null +++ b/src/lib/rate-limit.ts @@ -0,0 +1,169 @@ +import { RateLimiterRedis } from 'rate-limiter-flexible'; +import Redis, { RedisOptions } from 'ioredis'; + +function buildRedisOptions(): RedisOptions { + const commonOptions: RedisOptions = { + enableOfflineQueue: false, + maxRetriesPerRequest: 1, + }; + + const redisUrl = process.env.REDIS_URL; + if (!redisUrl) { + return { + ...commonOptions, + host: '127.0.0.1', + port: 6379, + }; + } + + try { + const parsed = new URL(redisUrl); + const dbFromPath = parsed.pathname && parsed.pathname !== '/' + ? Number(parsed.pathname.slice(1)) + : undefined; + + return { + ...commonOptions, + host: parsed.hostname || '127.0.0.1', + port: parsed.port ? Number(parsed.port) : 6379, + username: parsed.username || undefined, + password: parsed.password || undefined, + db: Number.isNaN(dbFromPath) ? undefined : dbFromPath, + ...(parsed.protocol === 'rediss:' ? { tls: {} } : {}), + }; + } catch { + return { + ...commonOptions, + host: '127.0.0.1', + port: 6379, + }; + } +} + +const redisClient = new Redis(buildRedisOptions()); +let redisReady = false; +let loggedRedisUnavailable = false; + +function getErrorMessage(reason: unknown) { + if (reason instanceof Error) return reason.message; + return String(reason); +} + +function logRedisUnavailableOnce(reason?: unknown) { + if (!loggedRedisUnavailable) { + const suffix = reason ? ` Reason: ${getErrorMessage(reason)}` : ''; + console.warn(`WARNING: Rate limiting skipped because local Redis is offline or unreachable.${suffix}`); + loggedRedisUnavailable = true; + } +} + +redisClient.on('error', () => { + redisReady = false; +}); + +redisClient.on('ready', () => { + redisReady = true; + loggedRedisUnavailable = false; +}); + +redisClient.on('close', () => { + redisReady = false; +}); + +async function ensureRedisReady() { + if (redisReady) return true; + + try { + if (redisClient.status === 'wait') { + await redisClient.connect(); + } + + if (!redisReady) { + await new Promise