Files
pantree/src/routes/auth.ts
Azriel e633d693da feat: implement backend API for Pantree Phase 1 MVP
- Project setup: package.json, tsconfig.json, jest.config.js, .env.example
- Config: env.ts, constants.ts (ALLOWED_UNITS, bcrypt rounds, JWT, deletion windows)
- DB: Knex connection, knexfile, migrations 001-006 (users, password_reset_tokens,
  pantry_items, recipes+recipe_ingredients, shopping_lists+items, deleted_records)
- Seeds: 10 seeded recipes with full ingredient lists
- Middleware: JWT authMiddleware (validates token + user existence), errorHandler
- Utils: Pino logger, JWT sign helper, Zod validators for all request shapes
- Services: authService (signup/signin/google/pwd-reset/soft-delete/restore),
  pantryService (CRUD + case-insensitive duplicate guard),
  recipeService (browse+filter+scale), shoppingListService (CRUD+merge logic),
  syncService (delta sync + tombstone cleanup), emailService (SendGrid)
- Routes: /v1/auth, /v1/pantry, /v1/recipes, /v1/shopping-lists, /v1/sync
- App: Express factory (createApp), server entry point
- Jobs: node-cron daily hard-delete + tombstone cleanup
- Tests: validators, utils, auth, pantry, recipes, shopping lists, sync
2026-05-10 15:00:15 +00:00

135 lines
3.8 KiB
TypeScript

import { Router, Response, NextFunction } from 'express';
import { authService } from '../services/authService';
import { AuthenticatedRequest, authMiddleware } from '../middleware/auth';
import {
signupSchema,
signinSchema,
googleAuthSchema,
passwordResetRequestSchema,
passwordResetConfirmSchema,
} from '../utils/validators';
const router = Router();
// POST /auth/signup
router.post('/signup', async (req, res: Response, next: NextFunction) => {
try {
const { email, password, name } = signupSchema.parse(req.body);
const result = await authService.signup(email, password, name);
res.status(201).json(result);
} catch (err) {
next(err);
}
});
// POST /auth/signin
router.post('/signin', async (req, res: Response, next: NextFunction) => {
try {
const { email, password } = signinSchema.parse(req.body);
const result = await authService.signin(email, password);
res.status(200).json(result);
} catch (err: unknown) {
if (
err instanceof Error &&
(err as { code?: string }).code === 'ACCOUNT_PENDING_DELETION'
) {
const appErr = err as { code?: string; statusCode?: number; deletion_scheduled_at?: string };
res.status(403).json({
error: err.message,
code: appErr.code,
deletion_scheduled_at: appErr.deletion_scheduled_at,
timestamp: new Date().toISOString(),
});
return;
}
next(err);
}
});
// POST /auth/google
router.post('/google', async (req, res: Response, next: NextFunction) => {
try {
const { id_token } = googleAuthSchema.parse(req.body);
const result = await authService.googleAuth(id_token);
const statusCode = result.is_new_user ? 201 : 200;
res.status(statusCode).json(result);
} catch (err: unknown) {
if (
err instanceof Error &&
(err as { code?: string }).code === 'ACCOUNT_PENDING_DELETION'
) {
const appErr = err as { code?: string; statusCode?: number; deletion_scheduled_at?: string };
res.status(403).json({
error: err.message,
code: appErr.code,
deletion_scheduled_at: appErr.deletion_scheduled_at,
timestamp: new Date().toISOString(),
});
return;
}
next(err);
}
});
// POST /auth/password-reset
router.post('/password-reset', async (req, res: Response, next: NextFunction) => {
try {
const { email } = passwordResetRequestSchema.parse(req.body);
await authService.requestPasswordReset(email);
res.status(200).json({
message: 'If an account exists with this email, a reset link has been sent.',
timestamp: new Date().toISOString(),
});
} catch (err) {
next(err);
}
});
// PUT /auth/password-reset
router.put('/password-reset', async (req, res: Response, next: NextFunction) => {
try {
const { token, new_password } = passwordResetConfirmSchema.parse(req.body);
await authService.confirmPasswordReset(token, new_password);
res.status(200).json({
message: 'Password updated successfully.',
timestamp: new Date().toISOString(),
});
} catch (err) {
next(err);
}
});
// DELETE /auth/account (protected)
router.delete(
'/account',
authMiddleware,
async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
await authService.deleteAccount(req.userId!);
res.status(204).send();
} catch (err) {
next(err);
}
}
);
// POST /auth/restore-account (protected)
router.post(
'/restore-account',
authMiddleware,
async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
try {
const user = await authService.restoreAccount(req.userId!);
res.status(200).json({
user,
message: 'Account restored successfully.',
timestamp: new Date().toISOString(),
});
} catch (err) {
next(err);
}
}
);
export default router;