feat: split research workflows and results into dedicated workspaces
This commit is contained in:
@@ -53,9 +53,15 @@ export function setSessionCookie(reply: FastifyReply, token: string, expiresAt:
|
||||
}
|
||||
|
||||
export function clearSessionCookie(reply: FastifyReply) {
|
||||
reply.clearCookie(SESSION_COOKIE_NAME, {
|
||||
const env = getEnv();
|
||||
|
||||
reply.setCookie(SESSION_COOKIE_NAME, '', {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: env.NODE_ENV === 'production',
|
||||
expires: new Date(0),
|
||||
maxAge: 0,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -89,6 +95,10 @@ export async function deleteSessionByToken(db: DbClient, token: string) {
|
||||
await db.query('delete from public.sessions where token_hash = $1', [hashSessionToken(token)]);
|
||||
}
|
||||
|
||||
export async function deleteSessionById(db: DbClient, sessionId: string) {
|
||||
await db.query('delete from public.sessions where id = $1', [sessionId]);
|
||||
}
|
||||
|
||||
export async function getSessionUserByToken(db: DbClient, token: string) {
|
||||
const tokenHash = hashSessionToken(token);
|
||||
const result = await db.query<SessionRow>(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { FastifyPluginAsync, FastifyRequest } from 'fastify';
|
||||
import { ZodError, z } from 'zod';
|
||||
import { hashPassword, verifyPassword } from '../auth/passwords.js';
|
||||
import { clearSessionCookie, createSession, deleteSessionByToken, getSessionTokenFromRequest, getSessionUserByToken, setSessionCookie, } from '../auth/sessions.js';
|
||||
import { clearSessionCookie, createSession, deleteSessionById, deleteSessionByToken, getSessionTokenFromRequest, getSessionUserByToken, setSessionCookie, } from '../auth/sessions.js';
|
||||
import { createUser, getUserByEmail, toAppUser } from '../auth/users.js';
|
||||
import { getDbPool } from '../db/pool.js';
|
||||
|
||||
@@ -16,6 +16,10 @@ const loginSchema = z.object({
|
||||
password: z.string().min(1),
|
||||
});
|
||||
|
||||
const logoutSchema = z.object({
|
||||
sessionId: z.string().uuid().optional(),
|
||||
});
|
||||
|
||||
function getRequestMetadata(request: FastifyRequest) {
|
||||
return {
|
||||
userAgent: request.headers['user-agent'],
|
||||
@@ -101,9 +105,16 @@ export const authRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
app.post('/auth/logout', async (request, reply) => {
|
||||
const token = getSessionTokenFromRequest(request);
|
||||
const payload = logoutSchema.safeParse(request.body);
|
||||
const sessionId = payload.success ? payload.data.sessionId : undefined;
|
||||
const db = getDbPool();
|
||||
|
||||
if (token) {
|
||||
await deleteSessionByToken(getDbPool(), token);
|
||||
await deleteSessionByToken(db, token);
|
||||
}
|
||||
|
||||
if (sessionId) {
|
||||
await deleteSessionById(db, sessionId);
|
||||
}
|
||||
|
||||
clearSessionCookie(reply);
|
||||
|
||||
@@ -11,6 +11,8 @@ const runSearchSchema = z.object({
|
||||
radiusKm: z.coerce.number().positive().max(50),
|
||||
businessType: z.string().trim().min(1),
|
||||
keywords: z.string().trim().optional(),
|
||||
lat: z.number().finite().min(-90).max(90).optional(),
|
||||
lng: z.number().finite().min(-180).max(180).optional(),
|
||||
});
|
||||
|
||||
const jobParamsSchema = z.object({
|
||||
|
||||
@@ -66,7 +66,19 @@ async function executeSearchJobAtCoordinates(
|
||||
}
|
||||
|
||||
export async function runSearchForUser(db: Pool, userId: string, payload: RunSearchInput): Promise<RunSearchResult> {
|
||||
const job = await createSearchJob(db, userId, payload);
|
||||
const hasProvidedCoordinates = typeof payload.lat === 'number' && typeof payload.lng === 'number';
|
||||
const job = hasProvidedCoordinates
|
||||
? await createSearchJobForCoordinates(db, userId, {
|
||||
name: payload.name || `${payload.businessType} in ${payload.location}`,
|
||||
city: payload.location,
|
||||
address: payload.location,
|
||||
radiusKm: payload.radiusKm,
|
||||
businessType: payload.businessType,
|
||||
keywords: payload.keywords,
|
||||
lat: payload.lat!,
|
||||
lng: payload.lng!,
|
||||
})
|
||||
: await createSearchJob(db, userId, payload);
|
||||
const jobId = job.id;
|
||||
|
||||
try {
|
||||
@@ -75,14 +87,19 @@ export async function runSearchForUser(db: Pool, userId: string, payload: RunSea
|
||||
throw new Error('GOOGLE_MAPS_SERVER_KEY is required for running research.');
|
||||
}
|
||||
|
||||
const geocoded = await geocodeLocation(payload.location, env.GOOGLE_MAPS_SERVER_KEY);
|
||||
await updateSearchJobCenter(db, jobId, geocoded.lat, geocoded.lng);
|
||||
const resolvedCenter = hasProvidedCoordinates
|
||||
? { lat: payload.lat!, lng: payload.lng! }
|
||||
: await geocodeLocation(payload.location, env.GOOGLE_MAPS_SERVER_KEY);
|
||||
|
||||
if (!hasProvidedCoordinates) {
|
||||
await updateSearchJobCenter(db, jobId, resolvedCenter.lat, resolvedCenter.lng);
|
||||
}
|
||||
|
||||
const completedJob = await executeSearchJobAtCoordinates(db, {
|
||||
jobId,
|
||||
userId,
|
||||
lat: geocoded.lat,
|
||||
lng: geocoded.lng,
|
||||
lat: resolvedCenter.lat,
|
||||
lng: resolvedCenter.lng,
|
||||
radiusKm: payload.radiusKm,
|
||||
businessType: payload.businessType,
|
||||
keywords: payload.keywords,
|
||||
|
||||
@@ -56,6 +56,8 @@ export type RunSearchInput = {
|
||||
radiusKm: number;
|
||||
businessType: string;
|
||||
keywords?: string;
|
||||
lat?: number;
|
||||
lng?: number;
|
||||
};
|
||||
|
||||
export type RunSearchResult = {
|
||||
|
||||
Reference in New Issue
Block a user