This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
type AsyncHandler = (req: Request, res: Response, next: NextFunction) => Promise<unknown>;
|
||||
|
||||
export function asyncHandler(handler: AsyncHandler) {
|
||||
return (req: Request, res: Response, next: NextFunction): void => {
|
||||
handler(req, res, next).catch(next);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { db } from '../db';
|
||||
import { users } from '../db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { AuthPayload } from '../types';
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-super-secret-jwt-key-change-in-production';
|
||||
const JWT_EXPIRES_IN = '7d';
|
||||
|
||||
export function generateToken(payload: AuthPayload): string {
|
||||
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
|
||||
}
|
||||
|
||||
export function verifyToken(token: string): AuthPayload | null {
|
||||
try {
|
||||
return jwt.verify(token, JWT_SECRET) as AuthPayload;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function authMiddleware(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
res.status(401).json({
|
||||
error: {
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'Missing or invalid authorization header',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const token = authHeader.slice(7);
|
||||
const payload = verifyToken(token);
|
||||
|
||||
if (!payload) {
|
||||
res.status(401).json({
|
||||
error: {
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'Invalid or expired token',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await db.select({ id: users.id }).from(users).where(eq(users.id, payload.userId)).limit(1);
|
||||
if (user.length === 0) {
|
||||
res.status(401).json({
|
||||
error: {
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'User no longer exists, please log in again',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
req.user = payload;
|
||||
next();
|
||||
}
|
||||
|
||||
export function optionalAuthMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||
const token = authHeader.slice(7);
|
||||
const payload = verifyToken(token);
|
||||
if (payload) {
|
||||
req.user = payload;
|
||||
}
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
export function generateId(prefix: string = ''): string {
|
||||
const timestamp = Date.now().toString(36);
|
||||
const random = Math.random().toString(36).slice(2, 10);
|
||||
return `${prefix}${prefix ? '_' : ''}${timestamp}${random}`;
|
||||
}
|
||||
|
||||
export function getCurrentTimestamp(): number {
|
||||
return Date.now();
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export function canCompleteTask(dueDate: number): boolean {
|
||||
if (dueDate === 0) return true;
|
||||
const now = new Date();
|
||||
const endOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999).getTime();
|
||||
return dueDate <= endOfToday;
|
||||
}
|
||||
|
||||
export const categoryCreateSchema = z.object({
|
||||
name: z.string().min(1).max(50),
|
||||
color: z.string().regex(/^#[0-9A-Fa-f]{6}$/),
|
||||
order: z.number().int().min(0).optional(),
|
||||
});
|
||||
|
||||
export const categoryUpdateSchema = z.object({
|
||||
name: z.string().min(1).max(50).optional(),
|
||||
color: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(),
|
||||
order: z.number().int().min(0).optional(),
|
||||
});
|
||||
|
||||
export const repeatSchema = z.enum(['none', 'daily', 'weekly', 'monthly', 'custom']);
|
||||
|
||||
export const taskCreateSchema = z.object({
|
||||
title: z.string().min(1).max(100),
|
||||
description: z.string().max(1000).optional(),
|
||||
categoryId: z.string().min(1),
|
||||
priority: z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(),
|
||||
dueDate: z.number().int().min(0).optional(),
|
||||
dueTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(z.literal('')).transform(v => v ?? ''),
|
||||
endTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(z.literal('')),
|
||||
allDay: z.boolean().optional(),
|
||||
repeat: repeatSchema.optional(),
|
||||
repeatInterval: z.number().int().min(1).max(30).optional(),
|
||||
repeatDays: z.string().max(20).optional(),
|
||||
seriesId: z.string().max(50).optional(),
|
||||
reminder: z.enum(['none', 'at_time', '15', '30', '60', '120', '1440']).optional(),
|
||||
reminders: z.string().max(100).optional(),
|
||||
assigneeId: z.string().nullable().optional(),
|
||||
subtasks: z.array(z.object({ title: z.string().min(1).max(100) })).optional(),
|
||||
});
|
||||
|
||||
export const taskUpdateSchema = z.object({
|
||||
title: z.string().min(1).max(100).optional(),
|
||||
description: z.string().max(1000).optional(),
|
||||
categoryId: z.string().min(1).optional(),
|
||||
priority: z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(),
|
||||
completed: z.boolean().optional(),
|
||||
dueDate: z.number().int().min(0).optional(),
|
||||
dueTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(z.literal('')).transform(v => v === '' ? undefined : v),
|
||||
endTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional(),
|
||||
reminders: z.string().max(100).optional(),
|
||||
assigneeId: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export const subtaskCreateSchema = z.object({
|
||||
title: z.string().min(1).max(100),
|
||||
description: z.string().max(1000).optional(),
|
||||
priority: z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(),
|
||||
dueDate: z.number().int().min(0).optional(),
|
||||
dueTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(z.literal('')).transform(v => v ?? ''),
|
||||
endTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(z.literal('')),
|
||||
allDay: z.boolean().optional(),
|
||||
repeat: repeatSchema.optional(),
|
||||
repeatInterval: z.number().int().min(1).max(30).optional(),
|
||||
repeatDays: z.string().max(20).optional(),
|
||||
seriesId: z.string().max(50).optional(),
|
||||
reminder: z.enum(['none', 'at_time', '15', '30', '60', '120', '1440']).optional(),
|
||||
reminders: z.string().max(100).optional(),
|
||||
assigneeId: z.string().nullable().optional(),
|
||||
order: z.number().int().min(0).optional(),
|
||||
});
|
||||
|
||||
export const subtaskUpdateSchema = z.object({
|
||||
title: z.string().min(1).max(100).optional(),
|
||||
description: z.string().max(1000).optional(),
|
||||
priority: z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(),
|
||||
completed: z.boolean().optional(),
|
||||
dueDate: z.number().int().min(0).optional(),
|
||||
dueTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(z.literal('')).transform(v => v === '' ? undefined : v),
|
||||
endTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional(),
|
||||
allDay: z.boolean().optional(),
|
||||
repeat: repeatSchema.optional(),
|
||||
repeatInterval: z.number().int().min(1).max(30).optional(),
|
||||
repeatDays: z.string().max(20).optional(),
|
||||
seriesId: z.string().max(50).optional(),
|
||||
reminder: z.enum(['none', 'at_time', '15', '30', '60', '120', '1440']).optional(),
|
||||
reminders: z.string().max(100).optional(),
|
||||
assigneeId: z.string().nullable().optional(),
|
||||
order: z.number().int().min(0).optional(),
|
||||
});
|
||||
|
||||
export const userSettingsSchema = z.object({
|
||||
darkMode: z.boolean().optional(),
|
||||
notifications: z.boolean().optional(),
|
||||
reminderTime: z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional(),
|
||||
defaultCategory: z.string().optional().nullable(),
|
||||
sortBy: z.enum(['dueDate', 'priority', 'title', 'createdAt']).optional(),
|
||||
sortOrder: z.enum(['asc', 'desc']).optional(),
|
||||
});
|
||||
|
||||
export const repeatProfileSchema = z.object({
|
||||
name: z.string().min(1).max(50),
|
||||
repeat: repeatSchema,
|
||||
repeatInterval: z.number().int().min(1).max(30),
|
||||
repeatDays: z.string().max(20),
|
||||
});
|
||||
|
||||
export const repeatProfileUpdateSchema = z.object({
|
||||
name: z.string().min(1).max(50).optional(),
|
||||
repeat: repeatSchema.optional(),
|
||||
repeatInterval: z.number().int().min(1).max(30).optional(),
|
||||
repeatDays: z.string().max(20).optional(),
|
||||
});
|
||||
|
||||
export const searchQuerySchema = z.object({
|
||||
q: z.string().min(1).max(50),
|
||||
});
|
||||
|
||||
export const friendRequestSchema = z.object({
|
||||
username: z.string().min(1).max(50),
|
||||
});
|
||||
|
||||
export const friendshipSchema = z.object({
|
||||
id: z.string(),
|
||||
userId: z.string(),
|
||||
friendId: z.string(),
|
||||
status: z.enum(['pending', 'accepted']),
|
||||
createdAt: z.number(),
|
||||
updatedAt: z.number(),
|
||||
});
|
||||
|
||||
export const pushChangesSchema = z.object({
|
||||
changes: z.object({
|
||||
categories: z.array(categoryCreateSchema.extend({ id: z.string(), createdAt: z.number(), updatedAt: z.number() })).optional(),
|
||||
tasks: z.array(taskCreateSchema.extend({ id: z.string(), completed: z.boolean(), createdAt: z.number(), updatedAt: z.number() })).optional(),
|
||||
subtasks: z.array(subtaskCreateSchema.extend({ id: z.string(), taskId: z.string(), completed: z.boolean(), createdAt: z.number(), updatedAt: z.number() })).optional(),
|
||||
repeatProfiles: z.array(repeatProfileSchema.extend({ id: z.string(), createdAt: z.number(), updatedAt: z.number() })).optional(),
|
||||
friendships: z.array(friendshipSchema).optional(),
|
||||
}),
|
||||
lastPulledAt: z.number().int().min(0),
|
||||
});
|
||||
|
||||
export const syncQuerySchema = z.object({
|
||||
since: z.string().transform(Number).pipe(z.number().int().min(0)),
|
||||
});
|
||||
|
||||
export const taskQuerySchema = z.object({
|
||||
categoryId: z.string().optional(),
|
||||
completed: z.string().transform(v => v === 'true').optional(),
|
||||
dueBefore: z.string().transform(Number).pipe(z.number().int().min(0)).optional(),
|
||||
dueAfter: z.string().transform(Number).pipe(z.number().int().min(0)).optional(),
|
||||
priority: z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(),
|
||||
sortBy: z.enum(['dueDate', 'priority', 'createdAt', 'title']).optional(),
|
||||
sortOrder: z.enum(['asc', 'desc']).optional(),
|
||||
limit: z.string().transform(Number).pipe(z.number().int().min(1).max(100)).optional(),
|
||||
offset: z.string().transform(Number).pipe(z.number().int().min(0)).optional(),
|
||||
});
|
||||
Reference in New Issue
Block a user