basic working app currently wworking on the sync to work
Build APK / build (push) Canceled after 0s

This commit is contained in:
2026-08-06 11:16:47 +02:00
commit 793e2d9044
192 changed files with 42523 additions and 0 deletions
+9
View File
@@ -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);
};
}
+85
View File
@@ -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();
}
+158
View File
@@ -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(),
});