This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
.git
|
||||
*.log
|
||||
@@ -0,0 +1,12 @@
|
||||
# Database
|
||||
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/carry_your_live
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-in-production-min-32-chars
|
||||
|
||||
# Server
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
|
||||
# Frontend URL (for CORS)
|
||||
FRONTEND_URL=http://localhost:8081
|
||||
@@ -0,0 +1,21 @@
|
||||
FROM node:20-alpine AS base
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
# Copy source
|
||||
COPY . .
|
||||
|
||||
# Build
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
FROM base AS dev
|
||||
CMD ["npm", "run", "dev"]
|
||||
|
||||
FROM base AS prod
|
||||
CMD ["npm", "start"]
|
||||
@@ -0,0 +1,143 @@
|
||||
# Carry Your Live - Backend API
|
||||
|
||||
REST API for the Carry Your Live task management app with offline-first sync support.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Runtime**: Node.js 20+ with TypeScript
|
||||
- **Framework**: Express.js
|
||||
- **Database**: PostgreSQL with Drizzle ORM
|
||||
- **Authentication**: JWT (JSON Web Tokens)
|
||||
- **Validation**: Zod
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Option A — Docker (Postgres + API in containers)
|
||||
|
||||
Requires Docker. Runs the database **and** the Node API together:
|
||||
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
This starts:
|
||||
- `carry-your-live-db` — PostgreSQL 16 on port 5432
|
||||
- `carry-your-live-api` — the API in dev mode (tsx watch, hot reload) on port 3000, schema auto-applied on start
|
||||
|
||||
Server runs at `http://localhost:3000` (health check: `GET /health`).
|
||||
|
||||
For production-style serving of the built app:
|
||||
|
||||
```bash
|
||||
# Build and run the slim prod image instead
|
||||
docker build --target prod -t carry-your-live-api:prod .
|
||||
docker run --rm -p 3000:3000 --env-file .env carry-your-live-api:prod
|
||||
```
|
||||
|
||||
### Option B — Local Node + Docker Postgres
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 20+
|
||||
- PostgreSQL 16+ (or use Docker)
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Start PostgreSQL** (using Docker):
|
||||
```bash
|
||||
docker-compose up -d postgres
|
||||
```
|
||||
|
||||
2. **Install dependencies**:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Configure environment**:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your settings
|
||||
```
|
||||
|
||||
4. **Run database migrations**:
|
||||
```bash
|
||||
npm run db:push
|
||||
```
|
||||
|
||||
5. **Start development server**:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Server runs at `http://localhost:3000`
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Authentication
|
||||
- `POST /api/auth/register` - Register new user
|
||||
- `POST /api/auth/login` - Login
|
||||
- `GET /api/auth/me` - Get current user
|
||||
|
||||
### Categories
|
||||
- `GET /api/categories` - List categories
|
||||
- `POST /api/categories` - Create category
|
||||
- `PATCH /api/categories/:id` - Update category
|
||||
- `DELETE /api/categories/:id` - Delete category
|
||||
|
||||
### Tasks
|
||||
- `GET /api/tasks` - List tasks (with filters)
|
||||
- `GET /api/tasks/:id` - Get task with subtasks
|
||||
- `POST /api/tasks` - Create task
|
||||
- `PATCH /api/tasks/:id` - Update task
|
||||
- `DELETE /api/tasks/:id` - Delete task
|
||||
- `POST /api/tasks/batch` - Batch operations
|
||||
|
||||
### Subtasks
|
||||
- `GET /api/subtasks/task/:taskId` - List subtasks for task
|
||||
- `POST /api/subtasks/task/:taskId` - Create subtask
|
||||
- `PATCH /api/subtasks/:id` - Update subtask
|
||||
- `DELETE /api/subtasks/:id` - Delete subtask
|
||||
|
||||
### Users
|
||||
- `GET /api/users/me` - Get user with settings
|
||||
- `PATCH /api/users/me/settings` - Update settings
|
||||
|
||||
### Sync (Offline-first)
|
||||
- `GET /api/sync?since=<timestamp>` - Pull changes since timestamp
|
||||
- `POST /api/sync/push` - Push local changes
|
||||
|
||||
## Database Schema
|
||||
|
||||
See `src/db/schema.ts` for Drizzle schema definitions.
|
||||
|
||||
## Sync Protocol
|
||||
|
||||
The sync endpoint uses a cursor-based approach:
|
||||
|
||||
1. **Pull**: Client sends `since` timestamp, server returns all changes since then
|
||||
2. **Push**: Client sends batched changes with `lastPulledAt`, server applies with conflict resolution (last-write-wins)
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `DATABASE_URL` | PostgreSQL connection string | Required |
|
||||
| `JWT_SECRET` | Secret for JWT signing | Required (32+ chars) |
|
||||
| `PORT` | Server port | 3000 |
|
||||
| `NODE_ENV` | Environment | development |
|
||||
| `FRONTEND_URL` | CORS origin | http://localhost:8081 |
|
||||
|
||||
## Production Deployment
|
||||
|
||||
1. Set `NODE_ENV=production`
|
||||
2. Use strong `JWT_SECRET` (32+ random chars)
|
||||
3. Configure proper `DATABASE_URL`
|
||||
4. Run `npm run build` then `npm start`
|
||||
5. Use process manager (PM2, systemd) or container orchestration
|
||||
|
||||
The included `Dockerfile` has a `prod` stage that builds TypeScript and serves `dist/`
|
||||
with production-only dependencies on a minimal `node:20-alpine` image.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import * as schema from './schema';
|
||||
export declare const db: import("drizzle-orm/node-postgres").NodePgDatabase<typeof schema>;
|
||||
export type DB = typeof db;
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,MAAM,MAAM,UAAU,CAAC;AAOnC,eAAO,MAAM,EAAE,mEAA4B,CAAC;AAE5C,MAAM,MAAM,EAAE,GAAG,OAAO,EAAE,CAAC"}
|
||||
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.db = void 0;
|
||||
const node_postgres_1 = require("drizzle-orm/node-postgres");
|
||||
const pg_1 = require("pg");
|
||||
const schema = __importStar(require("./schema"));
|
||||
const pool = new pg_1.Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
max: 20,
|
||||
});
|
||||
exports.db = (0, node_postgres_1.drizzle)(pool, { schema });
|
||||
//# sourceMappingURL=index.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,6DAAoD;AACpD,2BAA0B;AAC1B,iDAAmC;AAEnC,MAAM,IAAI,GAAG,IAAI,SAAI,CAAC;IACpB,gBAAgB,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY;IAC1C,GAAG,EAAE,EAAE;CACR,CAAC,CAAC;AAEU,QAAA,EAAE,GAAG,IAAA,uBAAO,EAAC,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC"}
|
||||
Vendored
+994
@@ -0,0 +1,994 @@
|
||||
export declare const users: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
||||
name: "users";
|
||||
schema: undefined;
|
||||
columns: {
|
||||
id: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "id";
|
||||
tableName: "users";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: true;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
username: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "username";
|
||||
tableName: "users";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
passwordHash: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "password_hash";
|
||||
tableName: "users";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "created_at";
|
||||
tableName: "users";
|
||||
dataType: "number";
|
||||
columnType: "PgBigInt53";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
};
|
||||
dialect: "pg";
|
||||
}>;
|
||||
export declare const friendships: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
||||
name: "friendships";
|
||||
schema: undefined;
|
||||
columns: {
|
||||
id: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "id";
|
||||
tableName: "friendships";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: true;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
userId: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "user_id";
|
||||
tableName: "friendships";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
friendId: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "friend_id";
|
||||
tableName: "friendships";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
status: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "status";
|
||||
tableName: "friendships";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: "pending" | "accepted";
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: ["pending", "accepted"];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "created_at";
|
||||
tableName: "friendships";
|
||||
dataType: "number";
|
||||
columnType: "PgBigInt53";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
updatedAt: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "updated_at";
|
||||
tableName: "friendships";
|
||||
dataType: "number";
|
||||
columnType: "PgBigInt53";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
};
|
||||
dialect: "pg";
|
||||
}>;
|
||||
export declare const categories: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
||||
name: "categories";
|
||||
schema: undefined;
|
||||
columns: {
|
||||
id: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "id";
|
||||
tableName: "categories";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: true;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
userId: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "user_id";
|
||||
tableName: "categories";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
name: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "name";
|
||||
tableName: "categories";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
color: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "color";
|
||||
tableName: "categories";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
order: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "order";
|
||||
tableName: "categories";
|
||||
dataType: "number";
|
||||
columnType: "PgInteger";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "created_at";
|
||||
tableName: "categories";
|
||||
dataType: "number";
|
||||
columnType: "PgBigInt53";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
updatedAt: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "updated_at";
|
||||
tableName: "categories";
|
||||
dataType: "number";
|
||||
columnType: "PgBigInt53";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
};
|
||||
dialect: "pg";
|
||||
}>;
|
||||
export declare const tasks: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
||||
name: "tasks";
|
||||
schema: undefined;
|
||||
columns: {
|
||||
id: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "id";
|
||||
tableName: "tasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: true;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
userId: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "user_id";
|
||||
tableName: "tasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
categoryId: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "category_id";
|
||||
tableName: "tasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
title: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "title";
|
||||
tableName: "tasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
description: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "description";
|
||||
tableName: "tasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
priority: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "priority";
|
||||
tableName: "tasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: "none" | "low" | "medium" | "high" | "critical";
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: ["none", "low", "medium", "high", "critical"];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
completed: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "completed";
|
||||
tableName: "tasks";
|
||||
dataType: "boolean";
|
||||
columnType: "PgBoolean";
|
||||
data: boolean;
|
||||
driverParam: boolean;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
dueDate: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "due_date";
|
||||
tableName: "tasks";
|
||||
dataType: "number";
|
||||
columnType: "PgBigInt53";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
dueTime: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "due_time";
|
||||
tableName: "tasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
endTime: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "end_time";
|
||||
tableName: "tasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
repeat: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "repeat";
|
||||
tableName: "tasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: "custom" | "none" | "daily" | "weekly" | "monthly";
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: ["none", "daily", "weekly", "monthly", "custom"];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
repeatInterval: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "repeat_interval";
|
||||
tableName: "tasks";
|
||||
dataType: "number";
|
||||
columnType: "PgInteger";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
repeatDays: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "repeat_days";
|
||||
tableName: "tasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
seriesId: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "series_id";
|
||||
tableName: "tasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
assigneeId: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "assignee_id";
|
||||
tableName: "tasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: false;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
reminder: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "reminder";
|
||||
tableName: "tasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440";
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: ["none", "at_time", "15", "30", "60", "120", "1440"];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "created_at";
|
||||
tableName: "tasks";
|
||||
dataType: "number";
|
||||
columnType: "PgBigInt53";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
updatedAt: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "updated_at";
|
||||
tableName: "tasks";
|
||||
dataType: "number";
|
||||
columnType: "PgBigInt53";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
};
|
||||
dialect: "pg";
|
||||
}>;
|
||||
export declare const repeatProfiles: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
||||
name: "repeat_profiles";
|
||||
schema: undefined;
|
||||
columns: {
|
||||
id: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "id";
|
||||
tableName: "repeat_profiles";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: true;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
userId: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "user_id";
|
||||
tableName: "repeat_profiles";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
name: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "name";
|
||||
tableName: "repeat_profiles";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
repeat: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "repeat";
|
||||
tableName: "repeat_profiles";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: "custom" | "none" | "daily" | "weekly" | "monthly";
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: ["none", "daily", "weekly", "monthly", "custom"];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
repeatInterval: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "repeat_interval";
|
||||
tableName: "repeat_profiles";
|
||||
dataType: "number";
|
||||
columnType: "PgInteger";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
repeatDays: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "repeat_days";
|
||||
tableName: "repeat_profiles";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "created_at";
|
||||
tableName: "repeat_profiles";
|
||||
dataType: "number";
|
||||
columnType: "PgBigInt53";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
updatedAt: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "updated_at";
|
||||
tableName: "repeat_profiles";
|
||||
dataType: "number";
|
||||
columnType: "PgBigInt53";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
};
|
||||
dialect: "pg";
|
||||
}>;
|
||||
export declare const subtasks: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
||||
name: "subtasks";
|
||||
schema: undefined;
|
||||
columns: {
|
||||
id: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "id";
|
||||
tableName: "subtasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: true;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
userId: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "user_id";
|
||||
tableName: "subtasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
taskId: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "task_id";
|
||||
tableName: "subtasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
title: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "title";
|
||||
tableName: "subtasks";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
completed: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "completed";
|
||||
tableName: "subtasks";
|
||||
dataType: "boolean";
|
||||
columnType: "PgBoolean";
|
||||
data: boolean;
|
||||
driverParam: boolean;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
order: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "order";
|
||||
tableName: "subtasks";
|
||||
dataType: "number";
|
||||
columnType: "PgInteger";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "created_at";
|
||||
tableName: "subtasks";
|
||||
dataType: "number";
|
||||
columnType: "PgBigInt53";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
updatedAt: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "updated_at";
|
||||
tableName: "subtasks";
|
||||
dataType: "number";
|
||||
columnType: "PgBigInt53";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
};
|
||||
dialect: "pg";
|
||||
}>;
|
||||
export declare const userSettings: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
||||
name: "user_settings";
|
||||
schema: undefined;
|
||||
columns: {
|
||||
userId: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "user_id";
|
||||
tableName: "user_settings";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: true;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
darkMode: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "dark_mode";
|
||||
tableName: "user_settings";
|
||||
dataType: "boolean";
|
||||
columnType: "PgBoolean";
|
||||
data: boolean;
|
||||
driverParam: boolean;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
notifications: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "notifications";
|
||||
tableName: "user_settings";
|
||||
dataType: "boolean";
|
||||
columnType: "PgBoolean";
|
||||
data: boolean;
|
||||
driverParam: boolean;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
reminderTime: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "reminder_time";
|
||||
tableName: "user_settings";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
defaultCategory: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "default_category";
|
||||
tableName: "user_settings";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: string;
|
||||
driverParam: string;
|
||||
notNull: false;
|
||||
hasDefault: false;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: [string, ...string[]];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
sortBy: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "sort_by";
|
||||
tableName: "user_settings";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: "createdAt" | "title" | "priority" | "dueDate";
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: ["dueDate", "priority", "title", "createdAt"];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
sortOrder: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "sort_order";
|
||||
tableName: "user_settings";
|
||||
dataType: "string";
|
||||
columnType: "PgText";
|
||||
data: "asc" | "desc";
|
||||
driverParam: string;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: ["asc", "desc"];
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
updatedAt: import("drizzle-orm/pg-core").PgColumn<{
|
||||
name: "updated_at";
|
||||
tableName: "user_settings";
|
||||
dataType: "number";
|
||||
columnType: "PgBigInt53";
|
||||
data: number;
|
||||
driverParam: string | number;
|
||||
notNull: true;
|
||||
hasDefault: true;
|
||||
isPrimaryKey: false;
|
||||
isAutoincrement: false;
|
||||
hasRuntimeDefault: false;
|
||||
enumValues: undefined;
|
||||
baseColumn: never;
|
||||
generated: undefined;
|
||||
}, {}, {}>;
|
||||
};
|
||||
dialect: "pg";
|
||||
}>;
|
||||
//# sourceMappingURL=schema.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/db/schema.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAKhB,CAAC;AAEH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAevB,CAAC;AAEF,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAQrB,CAAC;AAEH,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmBhB,CAAC;AAEH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EASzB,CAAC;AAEH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EASnB,CAAC;AAEH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EASvB,CAAC"}
|
||||
Vendored
+83
@@ -0,0 +1,83 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.userSettings = exports.subtasks = exports.repeatProfiles = exports.tasks = exports.categories = exports.friendships = exports.users = void 0;
|
||||
const pg_core_1 = require("drizzle-orm/pg-core");
|
||||
const drizzle_orm_1 = require("drizzle-orm");
|
||||
exports.users = (0, pg_core_1.pgTable)('users', {
|
||||
id: (0, pg_core_1.text)('id').primaryKey(),
|
||||
username: (0, pg_core_1.text)('username').notNull().unique(),
|
||||
passwordHash: (0, pg_core_1.text)('password_hash').notNull(),
|
||||
createdAt: (0, pg_core_1.bigint)('created_at', { mode: 'number' }).notNull().default((0, drizzle_orm_1.sql) `EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
});
|
||||
exports.friendships = (0, pg_core_1.pgTable)('friendships', {
|
||||
id: (0, pg_core_1.text)('id').primaryKey(),
|
||||
userId: (0, pg_core_1.text)('user_id').notNull().references(() => exports.users.id, { onDelete: 'cascade' }),
|
||||
friendId: (0, pg_core_1.text)('friend_id').notNull().references(() => exports.users.id, { onDelete: 'cascade' }),
|
||||
status: (0, pg_core_1.text)('status', { enum: ['pending', 'accepted'] }).notNull().default('pending'),
|
||||
createdAt: (0, pg_core_1.bigint)('created_at', { mode: 'number' }).notNull().default((0, drizzle_orm_1.sql) `EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
updatedAt: (0, pg_core_1.bigint)('updated_at', { mode: 'number' }).notNull().default((0, drizzle_orm_1.sql) `EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
}, (t) => ({
|
||||
pairUnique: (0, pg_core_1.unique)('friendships_pair').on(t.userId, t.friendId),
|
||||
userIdx: (0, pg_core_1.index)('friendships_user_idx').on(t.userId),
|
||||
friendIdx: (0, pg_core_1.index)('friendships_friend_idx').on(t.friendId),
|
||||
}));
|
||||
exports.categories = (0, pg_core_1.pgTable)('categories', {
|
||||
id: (0, pg_core_1.text)('id').primaryKey(),
|
||||
userId: (0, pg_core_1.text)('user_id').notNull().references(() => exports.users.id, { onDelete: 'cascade' }),
|
||||
name: (0, pg_core_1.text)('name').notNull(),
|
||||
color: (0, pg_core_1.text)('color').notNull(),
|
||||
order: (0, pg_core_1.integer)('order').notNull().default(0),
|
||||
createdAt: (0, pg_core_1.bigint)('created_at', { mode: 'number' }).notNull().default((0, drizzle_orm_1.sql) `EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
updatedAt: (0, pg_core_1.bigint)('updated_at', { mode: 'number' }).notNull().default((0, drizzle_orm_1.sql) `EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
});
|
||||
exports.tasks = (0, pg_core_1.pgTable)('tasks', {
|
||||
id: (0, pg_core_1.text)('id').primaryKey(),
|
||||
userId: (0, pg_core_1.text)('user_id').notNull().references(() => exports.users.id, { onDelete: 'cascade' }),
|
||||
categoryId: (0, pg_core_1.text)('category_id').notNull().references(() => exports.categories.id, { onDelete: 'cascade' }),
|
||||
title: (0, pg_core_1.text)('title').notNull(),
|
||||
description: (0, pg_core_1.text)('description').notNull().default(''),
|
||||
priority: (0, pg_core_1.text)('priority', { enum: ['none', 'low', 'medium', 'high', 'critical'] }).notNull().default('none'),
|
||||
completed: (0, pg_core_1.boolean)('completed').notNull().default(false),
|
||||
dueDate: (0, pg_core_1.bigint)('due_date', { mode: 'number' }).notNull().default(0),
|
||||
dueTime: (0, pg_core_1.text)('due_time').notNull().default(''),
|
||||
endTime: (0, pg_core_1.text)('end_time').notNull().default(''),
|
||||
repeat: (0, pg_core_1.text)('repeat', { enum: ['none', 'daily', 'weekly', 'monthly', 'custom'] }).notNull().default('none'),
|
||||
repeatInterval: (0, pg_core_1.integer)('repeat_interval').notNull().default(1),
|
||||
repeatDays: (0, pg_core_1.text)('repeat_days').notNull().default(''),
|
||||
seriesId: (0, pg_core_1.text)('series_id').notNull().default(''),
|
||||
assigneeId: (0, pg_core_1.text)('assignee_id').references(() => exports.users.id, { onDelete: 'set null' }),
|
||||
reminder: (0, pg_core_1.text)('reminder', { enum: ['none', 'at_time', '15', '30', '60', '120', '1440'] }).notNull().default('none'),
|
||||
createdAt: (0, pg_core_1.bigint)('created_at', { mode: 'number' }).notNull().default((0, drizzle_orm_1.sql) `EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
updatedAt: (0, pg_core_1.bigint)('updated_at', { mode: 'number' }).notNull().default((0, drizzle_orm_1.sql) `EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
});
|
||||
exports.repeatProfiles = (0, pg_core_1.pgTable)('repeat_profiles', {
|
||||
id: (0, pg_core_1.text)('id').primaryKey(),
|
||||
userId: (0, pg_core_1.text)('user_id').notNull().references(() => exports.users.id, { onDelete: 'cascade' }),
|
||||
name: (0, pg_core_1.text)('name').notNull(),
|
||||
repeat: (0, pg_core_1.text)('repeat', { enum: ['none', 'daily', 'weekly', 'monthly', 'custom'] }).notNull().default('none'),
|
||||
repeatInterval: (0, pg_core_1.integer)('repeat_interval').notNull().default(1),
|
||||
repeatDays: (0, pg_core_1.text)('repeat_days').notNull().default(''),
|
||||
createdAt: (0, pg_core_1.bigint)('created_at', { mode: 'number' }).notNull().default((0, drizzle_orm_1.sql) `EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
updatedAt: (0, pg_core_1.bigint)('updated_at', { mode: 'number' }).notNull().default((0, drizzle_orm_1.sql) `EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
});
|
||||
exports.subtasks = (0, pg_core_1.pgTable)('subtasks', {
|
||||
id: (0, pg_core_1.text)('id').primaryKey(),
|
||||
userId: (0, pg_core_1.text)('user_id').notNull().references(() => exports.users.id, { onDelete: 'cascade' }),
|
||||
taskId: (0, pg_core_1.text)('task_id').notNull().references(() => exports.tasks.id, { onDelete: 'cascade' }),
|
||||
title: (0, pg_core_1.text)('title').notNull(),
|
||||
completed: (0, pg_core_1.boolean)('completed').notNull().default(false),
|
||||
order: (0, pg_core_1.integer)('order').notNull().default(0),
|
||||
createdAt: (0, pg_core_1.bigint)('created_at', { mode: 'number' }).notNull().default((0, drizzle_orm_1.sql) `EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
updatedAt: (0, pg_core_1.bigint)('updated_at', { mode: 'number' }).notNull().default((0, drizzle_orm_1.sql) `EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
});
|
||||
exports.userSettings = (0, pg_core_1.pgTable)('user_settings', {
|
||||
userId: (0, pg_core_1.text)('user_id').primaryKey().references(() => exports.users.id, { onDelete: 'cascade' }),
|
||||
darkMode: (0, pg_core_1.boolean)('dark_mode').notNull().default(false),
|
||||
notifications: (0, pg_core_1.boolean)('notifications').notNull().default(true),
|
||||
reminderTime: (0, pg_core_1.text)('reminder_time').notNull().default('09:00'),
|
||||
defaultCategory: (0, pg_core_1.text)('default_category'),
|
||||
sortBy: (0, pg_core_1.text)('sort_by', { enum: ['dueDate', 'priority', 'title', 'createdAt'] }).notNull().default('dueDate'),
|
||||
sortOrder: (0, pg_core_1.text)('sort_order', { enum: ['asc', 'desc'] }).notNull().default('asc'),
|
||||
updatedAt: (0, pg_core_1.bigint)('updated_at', { mode: 'number' }).notNull().default((0, drizzle_orm_1.sql) `EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
});
|
||||
//# sourceMappingURL=schema.js.map
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import 'dotenv/config';
|
||||
declare const app: import("express-serve-static-core").Express;
|
||||
export default app;
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,CAAC;AAYvB,QAAA,MAAM,GAAG,6CAAY,CAAC;AAkCtB,eAAe,GAAG,CAAC"}
|
||||
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
require("dotenv/config");
|
||||
const express_1 = __importDefault(require("express"));
|
||||
const cors_1 = __importDefault(require("cors"));
|
||||
const auth_1 = __importDefault(require("./routes/auth"));
|
||||
const categories_1 = __importDefault(require("./routes/categories"));
|
||||
const tasks_1 = __importDefault(require("./routes/tasks"));
|
||||
const subtasks_1 = __importDefault(require("./routes/subtasks"));
|
||||
const users_1 = __importDefault(require("./routes/users"));
|
||||
const friends_1 = __importDefault(require("./routes/friends"));
|
||||
const sync_1 = __importDefault(require("./routes/sync"));
|
||||
const errorHandler_1 = require("./middleware/errorHandler");
|
||||
const app = (0, express_1.default)();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.use((0, cors_1.default)({
|
||||
origin: true,
|
||||
credentials: true,
|
||||
}));
|
||||
app.use(express_1.default.json());
|
||||
// Health check
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: Date.now() });
|
||||
});
|
||||
// API routes
|
||||
app.use('/api/auth', auth_1.default);
|
||||
app.use('/api/categories', categories_1.default);
|
||||
app.use('/api/tasks', tasks_1.default);
|
||||
app.use('/api/subtasks', subtasks_1.default);
|
||||
app.use('/api/users', users_1.default);
|
||||
app.use('/api/sync', sync_1.default);
|
||||
app.use('/api/friends', friends_1.default);
|
||||
// 404 handler
|
||||
app.use(errorHandler_1.notFoundHandler);
|
||||
// Error handler
|
||||
app.use(errorHandler_1.errorHandler);
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 Server running on http://localhost:${PORT}`);
|
||||
console.log(`📚 API available at http://localhost:${PORT}/api`);
|
||||
});
|
||||
exports.default = app;
|
||||
//# sourceMappingURL=index.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;AAAA,yBAAuB;AACvB,sDAA8B;AAC9B,gDAAwB;AACxB,yDAAuC;AACvC,qEAAiD;AACjD,2DAAwC;AACxC,iEAA8C;AAC9C,2DAAwC;AACxC,+DAA4C;AAC5C,yDAAuC;AACvC,4DAA0E;AAE1E,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;AACtB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC;AAEtC,GAAG,CAAC,GAAG,CAAC,IAAA,cAAI,EAAC;IACX,MAAM,EAAE,IAAI;IACZ,WAAW,EAAE,IAAI;CAClB,CAAC,CAAC,CAAC;AACJ,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,IAAI,EAAE,CAAC,CAAC;AAExB,eAAe;AACf,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IAC9B,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;AACpD,CAAC,CAAC,CAAC;AAEH,aAAa;AACb,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,cAAU,CAAC,CAAC;AACjC,GAAG,CAAC,GAAG,CAAC,iBAAiB,EAAE,oBAAc,CAAC,CAAC;AAC3C,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,eAAU,CAAC,CAAC;AAClC,GAAG,CAAC,GAAG,CAAC,eAAe,EAAE,kBAAa,CAAC,CAAC;AACxC,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,eAAU,CAAC,CAAC;AAClC,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,cAAU,CAAC,CAAC;AACjC,GAAG,CAAC,GAAG,CAAC,cAAc,EAAE,iBAAY,CAAC,CAAC;AAEtC,cAAc;AACd,GAAG,CAAC,GAAG,CAAC,8BAAe,CAAC,CAAC;AAEzB,gBAAgB;AAChB,GAAG,CAAC,GAAG,CAAC,2BAAY,CAAC,CAAC;AAEtB,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;IACpB,OAAO,CAAC,GAAG,CAAC,yCAAyC,IAAI,EAAE,CAAC,CAAC;IAC7D,OAAO,CAAC,GAAG,CAAC,wCAAwC,IAAI,MAAM,CAAC,CAAC;AAClE,CAAC,CAAC,CAAC;AAEH,kBAAe,GAAG,CAAC"}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
export declare class AppError extends Error {
|
||||
code: string;
|
||||
message: string;
|
||||
statusCode: number;
|
||||
details?: Record<string, any> | undefined;
|
||||
constructor(code: string, message: string, statusCode?: number, details?: Record<string, any> | undefined);
|
||||
}
|
||||
export declare function errorHandler(err: Error, req: Request, res: Response, next: NextFunction): void;
|
||||
export declare function notFoundHandler(req: Request, res: Response): void;
|
||||
//# sourceMappingURL=errorHandler.d.ts.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"errorHandler.d.ts","sourceRoot":"","sources":["../../src/middleware/errorHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG1D,qBAAa,QAAS,SAAQ,KAAK;IAExB,IAAI,EAAE,MAAM;IACZ,OAAO,EAAE,MAAM;IACf,UAAU,EAAE,MAAM;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;gBAH7B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,UAAU,GAAE,MAAY,EACxB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,YAAA;CAKvC;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CA+B9F;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,GAAG,IAAI,CAOjE"}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AppError = void 0;
|
||||
exports.errorHandler = errorHandler;
|
||||
exports.notFoundHandler = notFoundHandler;
|
||||
const zod_1 = require("zod");
|
||||
class AppError extends Error {
|
||||
code;
|
||||
message;
|
||||
statusCode;
|
||||
details;
|
||||
constructor(code, message, statusCode = 500, details) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.statusCode = statusCode;
|
||||
this.details = details;
|
||||
this.name = 'AppError';
|
||||
}
|
||||
}
|
||||
exports.AppError = AppError;
|
||||
function errorHandler(err, req, res, next) {
|
||||
console.error('Error:', err);
|
||||
if (err instanceof zod_1.ZodError) {
|
||||
res.status(400).json({
|
||||
error: {
|
||||
code: 'INVALID_PAYLOAD',
|
||||
message: 'Request validation failed',
|
||||
details: err.flatten().fieldErrors,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (err instanceof AppError) {
|
||||
res.status(err.statusCode).json({
|
||||
error: {
|
||||
code: err.code,
|
||||
message: err.message,
|
||||
details: err.details,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.status(500).json({
|
||||
error: {
|
||||
code: 'SERVER_ERROR',
|
||||
message: 'Internal server error',
|
||||
},
|
||||
});
|
||||
}
|
||||
function notFoundHandler(req, res) {
|
||||
res.status(404).json({
|
||||
error: {
|
||||
code: 'NOT_FOUND',
|
||||
message: `Route ${req.method} ${req.path} not found`,
|
||||
},
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=errorHandler.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"errorHandler.js","sourceRoot":"","sources":["../../src/middleware/errorHandler.ts"],"names":[],"mappings":";;;AAeA,oCA+BC;AAED,0CAOC;AAtDD,6BAA+B;AAE/B,MAAa,QAAS,SAAQ,KAAK;IAExB;IACA;IACA;IACA;IAJT,YACS,IAAY,EACZ,OAAe,EACf,aAAqB,GAAG,EACxB,OAA6B;QAEpC,KAAK,CAAC,OAAO,CAAC,CAAC;QALR,SAAI,GAAJ,IAAI,CAAQ;QACZ,YAAO,GAAP,OAAO,CAAQ;QACf,eAAU,GAAV,UAAU,CAAc;QACxB,YAAO,GAAP,OAAO,CAAsB;QAGpC,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;IACzB,CAAC;CACF;AAVD,4BAUC;AAED,SAAgB,YAAY,CAAC,GAAU,EAAE,GAAY,EAAE,GAAa,EAAE,IAAkB;IACtF,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAE7B,IAAI,GAAG,YAAY,cAAQ,EAAE,CAAC;QAC5B,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACnB,KAAK,EAAE;gBACL,IAAI,EAAE,iBAAiB;gBACvB,OAAO,EAAE,2BAA2B;gBACpC,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,WAAW;aACnC;SACF,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,IAAI,GAAG,YAAY,QAAQ,EAAE,CAAC;QAC5B,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC;YAC9B,KAAK,EAAE;gBACL,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,OAAO,EAAE,GAAG,CAAC,OAAO;gBACpB,OAAO,EAAE,GAAG,CAAC,OAAO;aACrB;SACF,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;QACnB,KAAK,EAAE;YACL,IAAI,EAAE,cAAc;YACpB,OAAO,EAAE,uBAAuB;SACjC;KACF,CAAC,CAAC;AACL,CAAC;AAED,SAAgB,eAAe,CAAC,GAAY,EAAE,GAAa;IACzD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;QACnB,KAAK,EAAE;YACL,IAAI,EAAE,WAAW;YACjB,OAAO,EAAE,SAAS,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,IAAI,YAAY;SACrD;KACF,CAAC,CAAC;AACL,CAAC"}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
declare const router: import("express-serve-static-core").Router;
|
||||
export default router;
|
||||
//# sourceMappingURL=auth.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/routes/auth.ts"],"names":[],"mappings":"AASA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAmFxB,eAAe,MAAM,CAAC"}
|
||||
Vendored
+76
@@ -0,0 +1,76 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const express_1 = require("express");
|
||||
const asyncHandler_1 = require("../utils/asyncHandler");
|
||||
const db_1 = require("../db");
|
||||
const schema_1 = require("../db/schema");
|
||||
const drizzle_orm_1 = require("drizzle-orm");
|
||||
const auth_1 = require("../utils/auth");
|
||||
const errorHandler_1 = require("../middleware/errorHandler");
|
||||
const zod_1 = require("zod");
|
||||
const router = (0, express_1.Router)();
|
||||
const usernameSchema = zod_1.z
|
||||
.string()
|
||||
.min(3)
|
||||
.max(30)
|
||||
.regex(/^[a-zA-Z0-9_.-]+$/, 'Username can only contain letters, numbers, dots, dashes and underscores');
|
||||
const registerSchema = zod_1.z.object({
|
||||
username: usernameSchema,
|
||||
password: zod_1.z.string().min(8),
|
||||
});
|
||||
const loginSchema = zod_1.z.object({
|
||||
username: zod_1.z.string(),
|
||||
password: zod_1.z.string(),
|
||||
});
|
||||
// In production, use bcrypt or argon2 for password hashing
|
||||
function hashPassword(password) {
|
||||
// Simple hash for demo - replace with bcrypt in production
|
||||
return Buffer.from(password).toString('base64');
|
||||
}
|
||||
function verifyPassword(password, hash) {
|
||||
return hashPassword(password) === hash;
|
||||
}
|
||||
router.post('/register', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const data = registerSchema.parse(req.body);
|
||||
const existing = await db_1.db.select().from(schema_1.users).where((0, drizzle_orm_1.eq)(schema_1.users.username, data.username)).limit(1);
|
||||
if (existing.length > 0) {
|
||||
throw new errorHandler_1.AppError('USERNAME_EXISTS', 'Username already registered', 409);
|
||||
}
|
||||
const userId = (0, auth_1.generateId)('user');
|
||||
const now = (0, auth_1.getCurrentTimestamp)();
|
||||
await db_1.db.insert(schema_1.users).values({
|
||||
id: userId,
|
||||
username: data.username,
|
||||
passwordHash: hashPassword(data.password),
|
||||
createdAt: now,
|
||||
});
|
||||
const token = (0, auth_1.generateToken)({ userId, username: data.username });
|
||||
res.status(201).json({
|
||||
user: { id: userId, username: data.username },
|
||||
token,
|
||||
});
|
||||
}));
|
||||
router.post('/login', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const data = loginSchema.parse(req.body);
|
||||
const user = await db_1.db.select().from(schema_1.users).where((0, drizzle_orm_1.eq)(schema_1.users.username, data.username)).limit(1);
|
||||
if (user.length === 0 || !verifyPassword(data.password, user[0].passwordHash)) {
|
||||
throw new errorHandler_1.AppError('INVALID_CREDENTIALS', 'Invalid username or password', 401);
|
||||
}
|
||||
const token = (0, auth_1.generateToken)({ userId: user[0].id, username: user[0].username });
|
||||
res.json({
|
||||
user: { id: user[0].id, username: user[0].username },
|
||||
token,
|
||||
});
|
||||
}));
|
||||
router.get('/me', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
if (!req.user) {
|
||||
throw new errorHandler_1.AppError('UNAUTHORIZED', 'Authentication required', 401);
|
||||
}
|
||||
const user = await db_1.db.select().from(schema_1.users).where((0, drizzle_orm_1.eq)(schema_1.users.id, req.user.userId)).limit(1);
|
||||
if (user.length === 0) {
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'User not found', 404);
|
||||
}
|
||||
res.json({ id: user[0].id, username: user[0].username });
|
||||
}));
|
||||
exports.default = router;
|
||||
//# sourceMappingURL=auth.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/routes/auth.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
declare const router: import("express-serve-static-core").Router;
|
||||
export default router;
|
||||
//# sourceMappingURL=categories.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"categories.d.ts","sourceRoot":"","sources":["../../src/routes/categories.ts"],"names":[],"mappings":"AASA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAqFxB,eAAe,MAAM,CAAC"}
|
||||
Vendored
+78
@@ -0,0 +1,78 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const express_1 = require("express");
|
||||
const asyncHandler_1 = require("../utils/asyncHandler");
|
||||
const db_1 = require("../db");
|
||||
const schema_1 = require("../db/schema");
|
||||
const drizzle_orm_1 = require("drizzle-orm");
|
||||
const auth_1 = require("../utils/auth");
|
||||
const errorHandler_1 = require("../middleware/errorHandler");
|
||||
const validation_1 = require("../utils/validation");
|
||||
const router = (0, express_1.Router)();
|
||||
router.use(auth_1.authMiddleware);
|
||||
router.get('/', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const userCategories = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.categories)
|
||||
.where((0, drizzle_orm_1.eq)(schema_1.categories.userId, req.user.userId))
|
||||
.orderBy((0, drizzle_orm_1.asc)(schema_1.categories.order), (0, drizzle_orm_1.asc)(schema_1.categories.createdAt));
|
||||
res.json({ categories: userCategories });
|
||||
}));
|
||||
router.post('/', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const data = validation_1.categoryCreateSchema.parse(req.body);
|
||||
const userId = req.user.userId;
|
||||
const now = Date.now();
|
||||
const maxOrder = await db_1.db
|
||||
.select({ order: schema_1.categories.order })
|
||||
.from(schema_1.categories)
|
||||
.where((0, drizzle_orm_1.eq)(schema_1.categories.userId, userId))
|
||||
.orderBy((0, drizzle_orm_1.desc)(schema_1.categories.order))
|
||||
.limit(1);
|
||||
const newCategory = {
|
||||
id: `cat_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
|
||||
userId,
|
||||
name: data.name,
|
||||
color: data.color,
|
||||
order: data.order ?? (maxOrder[0]?.order ?? -1) + 1,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
await db_1.db.insert(schema_1.categories).values(newCategory);
|
||||
res.status(201).json(newCategory);
|
||||
}));
|
||||
router.patch('/:id', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const data = validation_1.categoryUpdateSchema.parse(req.body);
|
||||
const userId = req.user.userId;
|
||||
const existing = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.categories)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.categories.id, req.params.id), (0, drizzle_orm_1.eq)(schema_1.categories.userId, userId)))
|
||||
.limit(1);
|
||||
if (existing.length === 0) {
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'Category not found', 404);
|
||||
}
|
||||
const now = Date.now();
|
||||
const updated = await db_1.db
|
||||
.update(schema_1.categories)
|
||||
.set({ ...data, updatedAt: now })
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.categories.id, req.params.id), (0, drizzle_orm_1.eq)(schema_1.categories.userId, userId)))
|
||||
.returning();
|
||||
res.json(updated[0]);
|
||||
}));
|
||||
router.delete('/:id', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const userId = req.user.userId;
|
||||
const existing = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.categories)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.categories.id, req.params.id), (0, drizzle_orm_1.eq)(schema_1.categories.userId, userId)))
|
||||
.limit(1);
|
||||
if (existing.length === 0) {
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'Category not found', 404);
|
||||
}
|
||||
await db_1.db
|
||||
.delete(schema_1.categories)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.categories.id, req.params.id), (0, drizzle_orm_1.eq)(schema_1.categories.userId, userId)));
|
||||
res.status(204).send();
|
||||
}));
|
||||
exports.default = router;
|
||||
//# sourceMappingURL=categories.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"categories.js","sourceRoot":"","sources":["../../src/routes/categories.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
declare const router: import("express-serve-static-core").Router;
|
||||
export default router;
|
||||
//# sourceMappingURL=friends.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"friends.d.ts","sourceRoot":"","sources":["../../src/routes/friends.ts"],"names":[],"mappings":"AAUA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAsKxB,eAAe,MAAM,CAAC"}
|
||||
Vendored
+130
@@ -0,0 +1,130 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const express_1 = require("express");
|
||||
const asyncHandler_1 = require("../utils/asyncHandler");
|
||||
const db_1 = require("../db");
|
||||
const schema_1 = require("../db/schema");
|
||||
const drizzle_orm_1 = require("drizzle-orm");
|
||||
const auth_1 = require("../utils/auth");
|
||||
const auth_2 = require("../utils/auth");
|
||||
const errorHandler_1 = require("../middleware/errorHandler");
|
||||
const validation_1 = require("../utils/validation");
|
||||
const router = (0, express_1.Router)();
|
||||
router.use(auth_2.authMiddleware);
|
||||
async function findUsernames(ids) {
|
||||
if (ids.length === 0)
|
||||
return new Map();
|
||||
const rows = await db_1.db.select().from(schema_1.users).where((0, drizzle_orm_1.or)(...ids.map((id) => (0, drizzle_orm_1.eq)(schema_1.users.id, id))));
|
||||
return new Map(rows.map((u) => [u.id, u.username]));
|
||||
}
|
||||
router.get('/', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const userId = req.user.userId;
|
||||
const outgoingRows = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.friendships)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.friendships.userId, userId), (0, drizzle_orm_1.eq)(schema_1.friendships.status, 'pending')));
|
||||
const incomingRows = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.friendships)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.friendships.friendId, userId), (0, drizzle_orm_1.eq)(schema_1.friendships.status, 'pending')));
|
||||
const acceptedRows = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.friendships)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.or)((0, drizzle_orm_1.eq)(schema_1.friendships.userId, userId), (0, drizzle_orm_1.eq)(schema_1.friendships.friendId, userId)), (0, drizzle_orm_1.eq)(schema_1.friendships.status, 'accepted')));
|
||||
const friendIds = acceptedRows.map((r) => (r.userId === userId ? r.friendId : r.userId));
|
||||
const outgoingIds = outgoingRows.map((r) => r.friendId);
|
||||
const incomingIds = incomingRows.map((r) => r.userId);
|
||||
const usernames = await findUsernames([...friendIds, ...outgoingIds, ...incomingIds]);
|
||||
const friends = acceptedRows.map((r) => {
|
||||
const friendId = r.userId === userId ? r.friendId : r.userId;
|
||||
return { id: friendId, username: usernames.get(friendId) ?? '' };
|
||||
});
|
||||
const outgoing = outgoingRows.map((r) => ({
|
||||
id: r.id,
|
||||
username: usernames.get(r.friendId) ?? '',
|
||||
requestId: r.id,
|
||||
status: 'pending',
|
||||
}));
|
||||
const incoming = incomingRows.map((r) => ({
|
||||
id: r.userId,
|
||||
username: usernames.get(r.userId) ?? '',
|
||||
requestId: r.id,
|
||||
status: 'pending',
|
||||
}));
|
||||
res.json({ friends, incoming, outgoing });
|
||||
}));
|
||||
router.post('/requests', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const { username } = validation_1.friendRequestSchema.parse(req.body);
|
||||
const userId = req.user.userId;
|
||||
if (username.toLowerCase() === (await meUsername(userId))) {
|
||||
throw new errorHandler_1.AppError('SELF_REQUEST', 'You cannot add yourself', 400);
|
||||
}
|
||||
const target = await db_1.db.select().from(schema_1.users).where((0, drizzle_orm_1.eq)(schema_1.users.username, username)).limit(1);
|
||||
if (target.length === 0) {
|
||||
throw new errorHandler_1.AppError('USER_NOT_FOUND', 'No user with that username found', 404);
|
||||
}
|
||||
const existing = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.friendships)
|
||||
.where((0, drizzle_orm_1.or)((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.friendships.userId, userId), (0, drizzle_orm_1.eq)(schema_1.friendships.friendId, target[0].id)), (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.friendships.userId, target[0].id), (0, drizzle_orm_1.eq)(schema_1.friendships.friendId, userId))))
|
||||
.limit(1);
|
||||
if (existing.length > 0) {
|
||||
throw new errorHandler_1.AppError('ALREADY_FRIENDS', existing[0].status === 'accepted' ? 'You are already friends' : 'Friend request already pending', 409);
|
||||
}
|
||||
const now = Date.now();
|
||||
await db_1.db.insert(schema_1.friendships).values({
|
||||
id: (0, auth_1.generateId)('friend'),
|
||||
userId,
|
||||
friendId: target[0].id,
|
||||
status: 'pending',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
res.status(201).json({ success: true });
|
||||
}));
|
||||
router.post('/requests/:id/accept', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const userId = req.user.userId;
|
||||
const requestId = req.params.id;
|
||||
const row = await db_1.db.select().from(schema_1.friendships).where((0, drizzle_orm_1.eq)(schema_1.friendships.id, requestId)).limit(1);
|
||||
if (row.length === 0) {
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'Request not found', 404);
|
||||
}
|
||||
if (row[0].friendId !== userId) {
|
||||
throw new errorHandler_1.AppError('FORBIDDEN', 'This request was not sent to you', 403);
|
||||
}
|
||||
if (row[0].status !== 'pending') {
|
||||
throw new errorHandler_1.AppError('INVALID_STATE', 'Request is no longer pending', 409);
|
||||
}
|
||||
await db_1.db
|
||||
.update(schema_1.friendships)
|
||||
.set({ status: 'accepted', updatedAt: Date.now() })
|
||||
.where((0, drizzle_orm_1.eq)(schema_1.friendships.id, requestId));
|
||||
res.json({ success: true });
|
||||
}));
|
||||
router.delete('/requests/:id', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const userId = req.user.userId;
|
||||
const requestId = req.params.id;
|
||||
const row = await db_1.db.select().from(schema_1.friendships).where((0, drizzle_orm_1.eq)(schema_1.friendships.id, requestId)).limit(1);
|
||||
if (row.length === 0) {
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'Request not found', 404);
|
||||
}
|
||||
if (row[0].userId !== userId && row[0].friendId !== userId) {
|
||||
throw new errorHandler_1.AppError('FORBIDDEN', 'Not allowed', 403);
|
||||
}
|
||||
await db_1.db.delete(schema_1.friendships).where((0, drizzle_orm_1.eq)(schema_1.friendships.id, requestId));
|
||||
res.json({ success: true });
|
||||
}));
|
||||
router.delete('/:friendId', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const userId = req.user.userId;
|
||||
const friendId = req.params.friendId;
|
||||
await db_1.db
|
||||
.delete(schema_1.friendships)
|
||||
.where((0, drizzle_orm_1.or)((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.friendships.userId, userId), (0, drizzle_orm_1.eq)(schema_1.friendships.friendId, friendId)), (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.friendships.userId, friendId), (0, drizzle_orm_1.eq)(schema_1.friendships.friendId, userId))));
|
||||
res.json({ success: true });
|
||||
}));
|
||||
async function meUsername(userId) {
|
||||
const me = await db_1.db.select().from(schema_1.users).where((0, drizzle_orm_1.eq)(schema_1.users.id, userId)).limit(1);
|
||||
return me.length > 0 ? me[0].username.toLowerCase() : '';
|
||||
}
|
||||
exports.default = router;
|
||||
//# sourceMappingURL=friends.js.map
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
declare const router: import("express-serve-static-core").Router;
|
||||
export default router;
|
||||
//# sourceMappingURL=subtasks.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"subtasks.d.ts","sourceRoot":"","sources":["../../src/routes/subtasks.ts"],"names":[],"mappings":"AASA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAmIxB,eAAe,MAAM,CAAC"}
|
||||
Vendored
+114
@@ -0,0 +1,114 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const express_1 = require("express");
|
||||
const asyncHandler_1 = require("../utils/asyncHandler");
|
||||
const db_1 = require("../db");
|
||||
const schema_1 = require("../db/schema");
|
||||
const drizzle_orm_1 = require("drizzle-orm");
|
||||
const auth_1 = require("../utils/auth");
|
||||
const errorHandler_1 = require("../middleware/errorHandler");
|
||||
const validation_1 = require("../utils/validation");
|
||||
const router = (0, express_1.Router)();
|
||||
router.use(auth_1.authMiddleware);
|
||||
router.get('/task/:taskId', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const userId = req.user.userId;
|
||||
// Verify task exists and belongs to user
|
||||
const task = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.tasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, req.params.taskId), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)))
|
||||
.limit(1);
|
||||
if (task.length === 0) {
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'Task not found', 404);
|
||||
}
|
||||
const taskSubtasks = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.subtasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.taskId, req.params.taskId), (0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId)))
|
||||
.orderBy((0, drizzle_orm_1.asc)(schema_1.subtasks.order));
|
||||
res.json({ subtasks: taskSubtasks });
|
||||
}));
|
||||
router.post('/task/:taskId', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const data = validation_1.subtaskCreateSchema.parse(req.body);
|
||||
const userId = req.user.userId;
|
||||
const task = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.tasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, req.params.taskId), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)))
|
||||
.limit(1);
|
||||
if (task.length === 0) {
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'Task not found', 404);
|
||||
}
|
||||
const now = Date.now();
|
||||
const maxOrder = await db_1.db
|
||||
.select({ order: schema_1.subtasks.order })
|
||||
.from(schema_1.subtasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.taskId, req.params.taskId), (0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId)))
|
||||
.orderBy((0, drizzle_orm_1.desc)(schema_1.subtasks.order))
|
||||
.limit(1);
|
||||
const subtaskId = `sub_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
||||
const newSubtask = {
|
||||
id: subtaskId,
|
||||
userId,
|
||||
taskId: req.params.taskId,
|
||||
title: data.title,
|
||||
completed: false,
|
||||
order: data.order ?? (maxOrder[0]?.order ?? -1) + 1,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
await db_1.db.insert(schema_1.subtasks).values(newSubtask);
|
||||
// Update task updatedAt
|
||||
await db_1.db
|
||||
.update(schema_1.tasks)
|
||||
.set({ updatedAt: now })
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, req.params.taskId), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)));
|
||||
res.status(201).json(newSubtask);
|
||||
}));
|
||||
router.patch('/:id', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const data = validation_1.subtaskUpdateSchema.parse(req.body);
|
||||
const userId = req.user.userId;
|
||||
const existing = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.subtasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.id, req.params.id), (0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId)))
|
||||
.limit(1);
|
||||
if (existing.length === 0) {
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'Subtask not found', 404);
|
||||
}
|
||||
const now = Date.now();
|
||||
const updated = await db_1.db
|
||||
.update(schema_1.subtasks)
|
||||
.set({ ...data, updatedAt: now })
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.id, req.params.id), (0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId)))
|
||||
.returning();
|
||||
// Update task updatedAt
|
||||
await db_1.db
|
||||
.update(schema_1.tasks)
|
||||
.set({ updatedAt: now })
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, existing[0].taskId), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)));
|
||||
res.json(updated[0]);
|
||||
}));
|
||||
router.delete('/:id', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const userId = req.user.userId;
|
||||
const existing = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.subtasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.id, req.params.id), (0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId)))
|
||||
.limit(1);
|
||||
if (existing.length === 0) {
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'Subtask not found', 404);
|
||||
}
|
||||
const taskId = existing[0].taskId;
|
||||
await db_1.db
|
||||
.delete(schema_1.subtasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.id, req.params.id), (0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId)));
|
||||
// Update task updatedAt
|
||||
await db_1.db
|
||||
.update(schema_1.tasks)
|
||||
.set({ updatedAt: Date.now() })
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, taskId), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)));
|
||||
res.status(204).send();
|
||||
}));
|
||||
exports.default = router;
|
||||
//# sourceMappingURL=subtasks.js.map
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
declare const router: import("express-serve-static-core").Router;
|
||||
export default router;
|
||||
//# sourceMappingURL=sync.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/routes/sync.ts"],"names":[],"mappings":"AASA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA8SxB,eAAe,MAAM,CAAC"}
|
||||
Vendored
+289
@@ -0,0 +1,289 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const express_1 = require("express");
|
||||
const asyncHandler_1 = require("../utils/asyncHandler");
|
||||
const db_1 = require("../db");
|
||||
const schema_1 = require("../db/schema");
|
||||
const drizzle_orm_1 = require("drizzle-orm");
|
||||
const auth_1 = require("../utils/auth");
|
||||
const errorHandler_1 = require("../middleware/errorHandler");
|
||||
const validation_1 = require("../utils/validation");
|
||||
const router = (0, express_1.Router)();
|
||||
router.use(auth_1.authMiddleware);
|
||||
router.get('/', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const { since } = validation_1.syncQuerySchema.parse(req.query);
|
||||
const userId = req.user.userId;
|
||||
const sinceDate = since;
|
||||
// Fetch categories changed since timestamp
|
||||
const changedCategories = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.categories)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.categories.userId, userId), (0, drizzle_orm_1.gte)(schema_1.categories.updatedAt, sinceDate)))
|
||||
.orderBy((0, drizzle_orm_1.asc)(schema_1.categories.updatedAt));
|
||||
// Fetch tasks changed since timestamp
|
||||
const changedTasks = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.tasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId), (0, drizzle_orm_1.gte)(schema_1.tasks.updatedAt, sinceDate)))
|
||||
.orderBy((0, drizzle_orm_1.asc)(schema_1.tasks.updatedAt));
|
||||
// Fetch subtasks changed since timestamp
|
||||
const taskIds = changedTasks.map(t => t.id);
|
||||
let changedSubtasks = [];
|
||||
if (taskIds.length > 0) {
|
||||
changedSubtasks = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.subtasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId), (0, drizzle_orm_1.gte)(schema_1.subtasks.updatedAt, sinceDate)))
|
||||
.orderBy((0, drizzle_orm_1.asc)(schema_1.subtasks.updatedAt));
|
||||
}
|
||||
else {
|
||||
// Also fetch subtasks for tasks that might have been deleted (we track by updatedAt)
|
||||
changedSubtasks = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.subtasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId), (0, drizzle_orm_1.gte)(schema_1.subtasks.updatedAt, sinceDate)))
|
||||
.orderBy((0, drizzle_orm_1.asc)(schema_1.subtasks.updatedAt));
|
||||
}
|
||||
// Fetch repeat profiles changed since timestamp
|
||||
const changedRepeatProfiles = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.repeatProfiles)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.repeatProfiles.userId, userId), (0, drizzle_orm_1.gte)(schema_1.repeatProfiles.updatedAt, sinceDate)))
|
||||
.orderBy((0, drizzle_orm_1.asc)(schema_1.repeatProfiles.updatedAt));
|
||||
// Fetch friendships changed since timestamp
|
||||
const changedFriendships = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.friendships)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.or)((0, drizzle_orm_1.eq)(schema_1.friendships.userId, userId), (0, drizzle_orm_1.eq)(schema_1.friendships.friendId, userId)), (0, drizzle_orm_1.gte)(schema_1.friendships.updatedAt, sinceDate)))
|
||||
.orderBy((0, drizzle_orm_1.asc)(schema_1.friendships.updatedAt));
|
||||
const timestamp = Date.now();
|
||||
res.json({
|
||||
categories: changedCategories,
|
||||
tasks: changedTasks,
|
||||
subtasks: changedSubtasks,
|
||||
repeatProfiles: changedRepeatProfiles,
|
||||
friendships: changedFriendships,
|
||||
timestamp,
|
||||
});
|
||||
}));
|
||||
router.post('/push', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const data = validation_1.pushChangesSchema.parse(req.body);
|
||||
const userId = req.user.userId;
|
||||
const conflicts = [];
|
||||
const timestamp = Date.now();
|
||||
try {
|
||||
await db_1.db.transaction(async (tx) => {
|
||||
// Process categories
|
||||
if (data.changes.categories && data.changes.categories.length > 0) {
|
||||
for (const cat of data.changes.categories) {
|
||||
const existing = await tx
|
||||
.select()
|
||||
.from(schema_1.categories)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.categories.id, cat.id), (0, drizzle_orm_1.eq)(schema_1.categories.userId, userId)))
|
||||
.limit(1);
|
||||
if (existing.length > 0) {
|
||||
// Check for conflict
|
||||
if (existing[0].updatedAt > cat.updatedAt) {
|
||||
conflicts.push({
|
||||
entity: 'categories',
|
||||
id: cat.id,
|
||||
serverVersion: existing[0],
|
||||
clientVersion: cat,
|
||||
resolution: 'server_wins',
|
||||
});
|
||||
continue; // Server wins
|
||||
}
|
||||
await tx
|
||||
.update(schema_1.categories)
|
||||
.set({
|
||||
name: cat.name,
|
||||
color: cat.color,
|
||||
order: cat.order,
|
||||
updatedAt: cat.updatedAt,
|
||||
})
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.categories.id, cat.id), (0, drizzle_orm_1.eq)(schema_1.categories.userId, userId)));
|
||||
}
|
||||
else {
|
||||
await tx.insert(schema_1.categories).values({
|
||||
...cat,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Process tasks
|
||||
if (data.changes.tasks && data.changes.tasks.length > 0) {
|
||||
for (const task of data.changes.tasks) {
|
||||
const existing = await tx
|
||||
.select()
|
||||
.from(schema_1.tasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, task.id), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)))
|
||||
.limit(1);
|
||||
if (existing.length > 0) {
|
||||
// Check for conflict
|
||||
if (existing[0].updatedAt > task.updatedAt) {
|
||||
conflicts.push({
|
||||
entity: 'tasks',
|
||||
id: task.id,
|
||||
serverVersion: existing[0],
|
||||
clientVersion: task,
|
||||
resolution: 'server_wins',
|
||||
});
|
||||
continue; // Server wins
|
||||
}
|
||||
await tx
|
||||
.update(schema_1.tasks)
|
||||
.set({
|
||||
title: task.title,
|
||||
description: task.description,
|
||||
categoryId: task.categoryId,
|
||||
priority: task.priority,
|
||||
completed: task.completed,
|
||||
dueDate: task.dueDate,
|
||||
dueTime: task.dueTime,
|
||||
endTime: task.endTime ?? '',
|
||||
repeat: task.repeat ?? 'none',
|
||||
repeatInterval: task.repeatInterval ?? 1,
|
||||
repeatDays: task.repeatDays ?? '',
|
||||
seriesId: task.seriesId ?? '',
|
||||
reminder: task.reminder ?? 'none',
|
||||
assigneeId: task.assigneeId ?? null,
|
||||
updatedAt: task.updatedAt,
|
||||
})
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, task.id), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)));
|
||||
}
|
||||
else {
|
||||
await tx.insert(schema_1.tasks).values({
|
||||
...task,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Process subtasks
|
||||
if (data.changes.subtasks && data.changes.subtasks.length > 0) {
|
||||
for (const sub of data.changes.subtasks) {
|
||||
const existing = await tx
|
||||
.select()
|
||||
.from(schema_1.subtasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.id, sub.id), (0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId)))
|
||||
.limit(1);
|
||||
if (existing.length > 0) {
|
||||
// Check for conflict
|
||||
if (existing[0].updatedAt > sub.updatedAt) {
|
||||
conflicts.push({
|
||||
entity: 'subtasks',
|
||||
id: sub.id,
|
||||
serverVersion: existing[0],
|
||||
clientVersion: sub,
|
||||
resolution: 'server_wins',
|
||||
});
|
||||
continue; // Server wins
|
||||
}
|
||||
await tx
|
||||
.update(schema_1.subtasks)
|
||||
.set({
|
||||
taskId: sub.taskId,
|
||||
title: sub.title,
|
||||
completed: sub.completed,
|
||||
order: sub.order,
|
||||
updatedAt: sub.updatedAt,
|
||||
})
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.id, sub.id), (0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId)));
|
||||
}
|
||||
else {
|
||||
await tx.insert(schema_1.subtasks).values({
|
||||
...sub,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Process repeat profiles
|
||||
if (data.changes.repeatProfiles && data.changes.repeatProfiles.length > 0) {
|
||||
for (const profile of data.changes.repeatProfiles) {
|
||||
const existing = await tx
|
||||
.select()
|
||||
.from(schema_1.repeatProfiles)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.repeatProfiles.id, profile.id), (0, drizzle_orm_1.eq)(schema_1.repeatProfiles.userId, userId)))
|
||||
.limit(1);
|
||||
if (existing.length > 0) {
|
||||
// Check for conflict
|
||||
if (existing[0].updatedAt > profile.updatedAt) {
|
||||
conflicts.push({
|
||||
entity: 'repeatProfiles',
|
||||
id: profile.id,
|
||||
serverVersion: existing[0],
|
||||
clientVersion: profile,
|
||||
resolution: 'server_wins',
|
||||
});
|
||||
continue; // Server wins
|
||||
}
|
||||
await tx
|
||||
.update(schema_1.repeatProfiles)
|
||||
.set({
|
||||
name: profile.name,
|
||||
repeat: profile.repeat,
|
||||
repeatInterval: profile.repeatInterval,
|
||||
repeatDays: profile.repeatDays,
|
||||
updatedAt: profile.updatedAt,
|
||||
})
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.repeatProfiles.id, profile.id), (0, drizzle_orm_1.eq)(schema_1.repeatProfiles.userId, userId)));
|
||||
}
|
||||
else {
|
||||
await tx.insert(schema_1.repeatProfiles).values({
|
||||
...profile,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Process friendships
|
||||
if (data.changes.friendships && data.changes.friendships.length > 0) {
|
||||
for (const friendship of data.changes.friendships) {
|
||||
const existing = await tx
|
||||
.select()
|
||||
.from(schema_1.friendships)
|
||||
.where((0, drizzle_orm_1.eq)(schema_1.friendships.id, friendship.id))
|
||||
.limit(1);
|
||||
if (existing.length > 0) {
|
||||
// Check for conflict
|
||||
if (existing[0].updatedAt > friendship.updatedAt) {
|
||||
conflicts.push({
|
||||
entity: 'friendships',
|
||||
id: friendship.id,
|
||||
serverVersion: existing[0],
|
||||
clientVersion: friendship,
|
||||
resolution: 'server_wins',
|
||||
});
|
||||
continue; // Server wins
|
||||
}
|
||||
await tx
|
||||
.update(schema_1.friendships)
|
||||
.set({
|
||||
userId: friendship.userId,
|
||||
friendId: friendship.friendId,
|
||||
status: friendship.status,
|
||||
updatedAt: friendship.updatedAt,
|
||||
})
|
||||
.where((0, drizzle_orm_1.eq)(schema_1.friendships.id, friendship.id));
|
||||
}
|
||||
else {
|
||||
await tx.insert(schema_1.friendships).values(friendship);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Sync push error:', error);
|
||||
throw new errorHandler_1.AppError('SERVER_ERROR', 'Failed to process sync push', 500);
|
||||
}
|
||||
res.json({
|
||||
success: true,
|
||||
timestamp,
|
||||
conflicts,
|
||||
});
|
||||
}));
|
||||
exports.default = router;
|
||||
//# sourceMappingURL=sync.js.map
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
declare const router: import("express-serve-static-core").Router;
|
||||
export default router;
|
||||
//# sourceMappingURL=tasks.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/routes/tasks.ts"],"names":[],"mappings":"AAUA,QAAA,MAAM,MAAM,4CAAW,CAAC;AA8RxB,eAAe,MAAM,CAAC"}
|
||||
Vendored
+254
@@ -0,0 +1,254 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const express_1 = require("express");
|
||||
const asyncHandler_1 = require("../utils/asyncHandler");
|
||||
const db_1 = require("../db");
|
||||
const schema_1 = require("../db/schema");
|
||||
const drizzle_orm_1 = require("drizzle-orm");
|
||||
const auth_1 = require("../utils/auth");
|
||||
const errorHandler_1 = require("../middleware/errorHandler");
|
||||
const validation_1 = require("../utils/validation");
|
||||
const zod_1 = require("zod");
|
||||
const router = (0, express_1.Router)();
|
||||
router.use(auth_1.authMiddleware);
|
||||
function applyTaskFilters(query, userId, filters) {
|
||||
const conditions = [(0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)];
|
||||
if (filters.categoryId) {
|
||||
conditions.push((0, drizzle_orm_1.eq)(schema_1.tasks.categoryId, filters.categoryId));
|
||||
}
|
||||
if (filters.completed !== undefined) {
|
||||
conditions.push((0, drizzle_orm_1.eq)(schema_1.tasks.completed, filters.completed));
|
||||
}
|
||||
if (filters.dueBefore) {
|
||||
conditions.push((0, drizzle_orm_1.lte)(schema_1.tasks.dueDate, filters.dueBefore));
|
||||
}
|
||||
if (filters.dueAfter) {
|
||||
conditions.push((0, drizzle_orm_1.gte)(schema_1.tasks.dueDate, filters.dueAfter));
|
||||
}
|
||||
if (filters.priority) {
|
||||
conditions.push((0, drizzle_orm_1.eq)(schema_1.tasks.priority, filters.priority));
|
||||
}
|
||||
return query.where((0, drizzle_orm_1.and)(...conditions));
|
||||
}
|
||||
function applyTaskSorting(query, sortBy = 'dueDate', sortOrder = 'asc') {
|
||||
const orderFn = sortOrder === 'desc' ? drizzle_orm_1.desc : drizzle_orm_1.asc;
|
||||
const columnMap = {
|
||||
dueDate: schema_1.tasks.dueDate,
|
||||
priority: schema_1.tasks.priority,
|
||||
createdAt: schema_1.tasks.createdAt,
|
||||
title: schema_1.tasks.title,
|
||||
};
|
||||
return query.orderBy(orderFn(columnMap[sortBy] || schema_1.tasks.dueDate));
|
||||
}
|
||||
router.get('/', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const filters = validation_1.taskQuerySchema.parse(req.query);
|
||||
const userId = req.user.userId;
|
||||
let query = db_1.db.select().from(schema_1.tasks);
|
||||
query = applyTaskFilters(query, userId, filters);
|
||||
query = applyTaskSorting(query, filters.sortBy, filters.sortOrder);
|
||||
const limit = filters.limit ?? 50;
|
||||
const offset = filters.offset ?? 0;
|
||||
query = query.limit(limit).offset(offset);
|
||||
const results = await query;
|
||||
// Fetch subtasks for each task
|
||||
const taskIds = results.map((t) => t.id);
|
||||
let taskSubtasks = [];
|
||||
if (taskIds.length > 0) {
|
||||
taskSubtasks = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.subtasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId), (0, drizzle_orm_1.inArray)(schema_1.subtasks.taskId, taskIds)))
|
||||
.orderBy((0, drizzle_orm_1.asc)(schema_1.subtasks.order));
|
||||
}
|
||||
const subtasksByTask = taskSubtasks.reduce((acc, st) => {
|
||||
if (!acc[st.taskId])
|
||||
acc[st.taskId] = [];
|
||||
acc[st.taskId].push(st);
|
||||
return acc;
|
||||
}, {});
|
||||
const tasksWithSubtasks = results.map((task) => ({
|
||||
...task,
|
||||
subtasks: subtasksByTask[task.id] || [],
|
||||
}));
|
||||
// Get total count
|
||||
const countQuery = db_1.db.select({ count: (0, drizzle_orm_1.sql) `count(*)` }).from(schema_1.tasks);
|
||||
const countResult = await applyTaskFilters(countQuery, userId, filters);
|
||||
const total = Number(countResult[0]?.count ?? 0);
|
||||
res.json({ tasks: tasksWithSubtasks, total, limit, offset });
|
||||
}));
|
||||
router.get('/:id', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const userId = req.user.userId;
|
||||
const task = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.tasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, req.params.id), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)))
|
||||
.limit(1);
|
||||
if (task.length === 0) {
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'Task not found', 404);
|
||||
}
|
||||
const taskSubtasks = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.subtasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.taskId, req.params.id), (0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId)))
|
||||
.orderBy((0, drizzle_orm_1.asc)(schema_1.subtasks.order));
|
||||
res.json({ ...task[0], subtasks: taskSubtasks });
|
||||
}));
|
||||
router.post('/', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const data = validation_1.taskCreateSchema.parse(req.body);
|
||||
const userId = req.user.userId;
|
||||
const now = Date.now();
|
||||
// Verify category exists and belongs to user
|
||||
const cat = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.categories)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.categories.id, data.categoryId), (0, drizzle_orm_1.eq)(schema_1.categories.userId, userId)))
|
||||
.limit(1);
|
||||
if (cat.length === 0) {
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'Category not found', 404);
|
||||
}
|
||||
const taskId = `task_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
||||
const newTask = {
|
||||
id: taskId,
|
||||
userId,
|
||||
categoryId: data.categoryId,
|
||||
title: data.title,
|
||||
description: data.description ?? '',
|
||||
priority: data.priority ?? 'none',
|
||||
completed: false,
|
||||
dueDate: data.dueDate ?? 0,
|
||||
dueTime: data.dueTime ?? '',
|
||||
endTime: data.endTime ?? '',
|
||||
assigneeId: data.assigneeId ?? null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
await db_1.db.insert(schema_1.tasks).values(newTask);
|
||||
// Create subtasks if provided
|
||||
if (data.subtasks && data.subtasks.length > 0) {
|
||||
const subtaskValues = data.subtasks.map((st, index) => ({
|
||||
id: `sub_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}${index}`,
|
||||
userId,
|
||||
taskId,
|
||||
title: st.title,
|
||||
completed: false,
|
||||
order: index,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}));
|
||||
await db_1.db.insert(schema_1.subtasks).values(subtaskValues);
|
||||
}
|
||||
const createdSubtasks = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.subtasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.taskId, taskId), (0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId)))
|
||||
.orderBy((0, drizzle_orm_1.asc)(schema_1.subtasks.order));
|
||||
res.status(201).json({ ...newTask, subtasks: createdSubtasks });
|
||||
}));
|
||||
router.patch('/:id', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const data = validation_1.taskUpdateSchema.parse(req.body);
|
||||
const userId = req.user.userId;
|
||||
const existing = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.tasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, req.params.id), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)))
|
||||
.limit(1);
|
||||
if (existing.length === 0) {
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'Task not found', 404);
|
||||
}
|
||||
// Verify category if provided
|
||||
if (data.categoryId) {
|
||||
const cat = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.categories)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.categories.id, data.categoryId), (0, drizzle_orm_1.eq)(schema_1.categories.userId, userId)))
|
||||
.limit(1);
|
||||
if (cat.length === 0) {
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'Category not found', 404);
|
||||
}
|
||||
}
|
||||
const now = Date.now();
|
||||
const updated = await db_1.db
|
||||
.update(schema_1.tasks)
|
||||
.set({ ...data, updatedAt: now })
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, req.params.id), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)))
|
||||
.returning();
|
||||
const taskSubtasks = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.subtasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.subtasks.taskId, req.params.id), (0, drizzle_orm_1.eq)(schema_1.subtasks.userId, userId)))
|
||||
.orderBy((0, drizzle_orm_1.asc)(schema_1.subtasks.order));
|
||||
res.json({ ...updated[0], subtasks: taskSubtasks });
|
||||
}));
|
||||
router.delete('/:id', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const userId = req.user.userId;
|
||||
const existing = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.tasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, req.params.id), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)))
|
||||
.limit(1);
|
||||
if (existing.length === 0) {
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'Task not found', 404);
|
||||
}
|
||||
await db_1.db
|
||||
.delete(schema_1.tasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, req.params.id), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)));
|
||||
res.status(204).send();
|
||||
}));
|
||||
router.post('/batch', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const { operations } = zod_1.z.object({
|
||||
operations: zod_1.z.array(zod_1.z.union([
|
||||
zod_1.z.object({ type: zod_1.z.literal('create'), data: validation_1.taskCreateSchema }),
|
||||
zod_1.z.object({ type: zod_1.z.literal('update'), id: zod_1.z.string(), data: validation_1.taskUpdateSchema }),
|
||||
zod_1.z.object({ type: zod_1.z.literal('delete'), id: zod_1.z.string() }),
|
||||
])),
|
||||
}).parse(req.body);
|
||||
const userId = req.user.userId;
|
||||
const results = [];
|
||||
for (const op of operations) {
|
||||
try {
|
||||
if (op.type === 'create') {
|
||||
const cat = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.categories)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.categories.id, op.data.categoryId), (0, drizzle_orm_1.eq)(schema_1.categories.userId, userId)))
|
||||
.limit(1);
|
||||
if (cat.length === 0)
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'Category not found', 404);
|
||||
const taskId = `task_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
||||
const now = Date.now();
|
||||
await db_1.db.insert(schema_1.tasks).values({
|
||||
id: taskId,
|
||||
userId,
|
||||
...op.data,
|
||||
completed: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
results.push({ id: taskId, success: true });
|
||||
}
|
||||
else if (op.type === 'update') {
|
||||
await db_1.db
|
||||
.update(schema_1.tasks)
|
||||
.set({ ...op.data, updatedAt: Date.now() })
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, op.id), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)));
|
||||
results.push({ id: op.id, success: true });
|
||||
}
|
||||
else if (op.type === 'delete') {
|
||||
await db_1.db
|
||||
.delete(schema_1.tasks)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.tasks.id, op.id), (0, drizzle_orm_1.eq)(schema_1.tasks.userId, userId)));
|
||||
results.push({ id: op.id, success: true });
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
results.push({
|
||||
id: 'id' in op ? op.id : 'unknown',
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
}
|
||||
res.json({ results });
|
||||
}));
|
||||
exports.default = router;
|
||||
//# sourceMappingURL=tasks.js.map
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
declare const router: import("express-serve-static-core").Router;
|
||||
export default router;
|
||||
//# sourceMappingURL=users.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../src/routes/users.ts"],"names":[],"mappings":"AASA,QAAA,MAAM,MAAM,4CAAW,CAAC;AAqGxB,eAAe,MAAM,CAAC"}
|
||||
Vendored
+98
@@ -0,0 +1,98 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const express_1 = require("express");
|
||||
const asyncHandler_1 = require("../utils/asyncHandler");
|
||||
const db_1 = require("../db");
|
||||
const schema_1 = require("../db/schema");
|
||||
const drizzle_orm_1 = require("drizzle-orm");
|
||||
const auth_1 = require("../utils/auth");
|
||||
const errorHandler_1 = require("../middleware/errorHandler");
|
||||
const validation_1 = require("../utils/validation");
|
||||
const router = (0, express_1.Router)();
|
||||
router.use(auth_1.authMiddleware);
|
||||
router.get('/me', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const userId = req.user.userId;
|
||||
const user = await db_1.db.select().from(schema_1.users).where((0, drizzle_orm_1.eq)(schema_1.users.id, userId)).limit(1);
|
||||
if (user.length === 0) {
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'User not found', 404);
|
||||
}
|
||||
const settings = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.userSettings)
|
||||
.where((0, drizzle_orm_1.eq)(schema_1.userSettings.userId, userId))
|
||||
.limit(1);
|
||||
let defaultCategory = settings[0]?.defaultCategory;
|
||||
if (defaultCategory) {
|
||||
const cat = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.categories)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.categories.id, defaultCategory), (0, drizzle_orm_1.eq)(schema_1.categories.userId, userId)))
|
||||
.limit(1);
|
||||
if (cat.length === 0) {
|
||||
defaultCategory = null;
|
||||
}
|
||||
}
|
||||
res.json({
|
||||
id: user[0].id,
|
||||
username: user[0].username,
|
||||
settings: {
|
||||
darkMode: settings[0]?.darkMode ?? false,
|
||||
notifications: settings[0]?.notifications ?? true,
|
||||
reminderTime: settings[0]?.reminderTime ?? '09:00',
|
||||
defaultCategory,
|
||||
sortBy: settings[0]?.sortBy ?? 'dueDate',
|
||||
sortOrder: settings[0]?.sortOrder ?? 'asc',
|
||||
},
|
||||
createdAt: user[0].createdAt,
|
||||
});
|
||||
}));
|
||||
router.patch('/me/settings', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const data = validation_1.userSettingsSchema.parse(req.body);
|
||||
const userId = req.user.userId;
|
||||
// Validate defaultCategory if provided
|
||||
if (data.defaultCategory) {
|
||||
const cat = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.categories)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.categories.id, data.defaultCategory), (0, drizzle_orm_1.eq)(schema_1.categories.userId, userId)))
|
||||
.limit(1);
|
||||
if (cat.length === 0) {
|
||||
throw new errorHandler_1.AppError('NOT_FOUND', 'Default category not found', 404);
|
||||
}
|
||||
}
|
||||
const now = Date.now();
|
||||
const existing = await db_1.db
|
||||
.select()
|
||||
.from(schema_1.userSettings)
|
||||
.where((0, drizzle_orm_1.eq)(schema_1.userSettings.userId, userId))
|
||||
.limit(1);
|
||||
if (existing.length === 0) {
|
||||
await db_1.db.insert(schema_1.userSettings).values({
|
||||
userId,
|
||||
...data,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
else {
|
||||
await db_1.db
|
||||
.update(schema_1.userSettings)
|
||||
.set({ ...data, updatedAt: now })
|
||||
.where((0, drizzle_orm_1.eq)(schema_1.userSettings.userId, userId));
|
||||
}
|
||||
res.status(200).json({ success: true });
|
||||
}));
|
||||
router.get('/search', (0, asyncHandler_1.asyncHandler)(async (req, res) => {
|
||||
const { q } = req.query;
|
||||
const userId = req.user.userId;
|
||||
if (!q || typeof q !== 'string' || q.length < 2) {
|
||||
return res.json([]);
|
||||
}
|
||||
const results = await db_1.db
|
||||
.select({ id: schema_1.users.id, username: schema_1.users.username })
|
||||
.from(schema_1.users)
|
||||
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.ilike)(schema_1.users.username, `%${q}%`), (0, drizzle_orm_1.eq)(schema_1.users.id, userId)))
|
||||
.limit(10);
|
||||
res.json(results);
|
||||
}));
|
||||
exports.default = router;
|
||||
//# sourceMappingURL=users.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"users.js","sourceRoot":"","sources":["../../src/routes/users.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+83
@@ -0,0 +1,83 @@
|
||||
export interface Category {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
order: number;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
export interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
categoryId: string;
|
||||
priority: 'none' | 'low' | 'medium' | 'high' | 'critical';
|
||||
completed: boolean;
|
||||
dueDate: number;
|
||||
dueTime: string;
|
||||
endTime: string;
|
||||
assigneeId: string | null;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
subtasks?: Subtask[];
|
||||
}
|
||||
export interface Subtask {
|
||||
id: string;
|
||||
taskId: string;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
order: number;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
export interface UserSettings {
|
||||
darkMode: boolean;
|
||||
notifications: boolean;
|
||||
reminderTime: string;
|
||||
defaultCategory: string | null;
|
||||
sortBy: 'dueDate' | 'priority' | 'title' | 'createdAt';
|
||||
sortOrder: 'asc' | 'desc';
|
||||
}
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
settings: UserSettings;
|
||||
createdAt: number;
|
||||
}
|
||||
export interface SyncResponse {
|
||||
categories: Category[];
|
||||
tasks: Task[];
|
||||
subtasks: Subtask[];
|
||||
timestamp: number;
|
||||
}
|
||||
export interface PushChangesRequest {
|
||||
changes: {
|
||||
categories: Category[];
|
||||
tasks: Task[];
|
||||
subtasks: Subtask[];
|
||||
};
|
||||
lastPulledAt: number;
|
||||
}
|
||||
export interface PushChangesResponse {
|
||||
success: boolean;
|
||||
timestamp: number;
|
||||
conflicts: Array<{
|
||||
entity: string;
|
||||
id: string;
|
||||
serverVersion: any;
|
||||
clientVersion: any;
|
||||
resolution: 'server_wins' | 'client_wins' | 'merge';
|
||||
}>;
|
||||
}
|
||||
export interface AuthPayload {
|
||||
userId: string;
|
||||
username: string;
|
||||
}
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user?: AuthPayload;
|
||||
}
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=index.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;IAC1D,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,OAAO,CAAC;IAClB,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,OAAO,GAAG,WAAW,CAAC;IACvD,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,YAAY,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,QAAQ,EAAE,CAAC;IACvB,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE;QACP,UAAU,EAAE,QAAQ,EAAE,CAAC;QACvB,KAAK,EAAE,IAAI,EAAE,CAAC;QACd,QAAQ,EAAE,OAAO,EAAE,CAAC;KACrB,CAAC;IACF,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,KAAK,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;QACf,EAAE,EAAE,MAAM,CAAC;QACX,aAAa,EAAE,GAAG,CAAC;QACnB,aAAa,EAAE,GAAG,CAAC;QACnB,UAAU,EAAE,aAAa,GAAG,aAAa,GAAG,OAAO,CAAC;KACrD,CAAC,CAAC;CACJ;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,IAAI,CAAC,EAAE,WAAW,CAAC;SACpB;KACF;CACF"}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
//# sourceMappingURL=index.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":""}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
type AsyncHandler = (req: Request, res: Response, next: NextFunction) => Promise<unknown>;
|
||||
export declare function asyncHandler(handler: AsyncHandler): (req: Request, res: Response, next: NextFunction) => void;
|
||||
export {};
|
||||
//# sourceMappingURL=asyncHandler.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"asyncHandler.d.ts","sourceRoot":"","sources":["../../src/utils/asyncHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE1D,KAAK,YAAY,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAE1F,wBAAgB,YAAY,CAAC,OAAO,EAAE,YAAY,IACxC,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,KAAG,IAAI,CAG/D"}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.asyncHandler = asyncHandler;
|
||||
function asyncHandler(handler) {
|
||||
return (req, res, next) => {
|
||||
handler(req, res, next).catch(next);
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=asyncHandler.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"asyncHandler.js","sourceRoot":"","sources":["../../src/utils/asyncHandler.ts"],"names":[],"mappings":";;AAIA,oCAIC;AAJD,SAAgB,YAAY,CAAC,OAAqB;IAChD,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAQ,EAAE;QAC/D,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC,CAAC;AACJ,CAAC"}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { AuthPayload } from '../types';
|
||||
export declare function generateToken(payload: AuthPayload): string;
|
||||
export declare function verifyToken(token: string): AuthPayload | null;
|
||||
export declare function authMiddleware(req: Request, res: Response, next: NextFunction): void;
|
||||
export declare function optionalAuthMiddleware(req: Request, res: Response, next: NextFunction): void;
|
||||
export declare function generateId(prefix?: string): string;
|
||||
export declare function getCurrentTimestamp(): number;
|
||||
//# sourceMappingURL=auth.d.ts.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/utils/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE1D,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAKvC,wBAAgB,aAAa,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAE1D;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAM7D;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CA4BpF;AAED,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAW5F;AAED,wBAAgB,UAAU,CAAC,MAAM,GAAE,MAAW,GAAG,MAAM,CAItD;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C"}
|
||||
Vendored
+70
@@ -0,0 +1,70 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.generateToken = generateToken;
|
||||
exports.verifyToken = verifyToken;
|
||||
exports.authMiddleware = authMiddleware;
|
||||
exports.optionalAuthMiddleware = optionalAuthMiddleware;
|
||||
exports.generateId = generateId;
|
||||
exports.getCurrentTimestamp = getCurrentTimestamp;
|
||||
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-super-secret-jwt-key-change-in-production';
|
||||
const JWT_EXPIRES_IN = '7d';
|
||||
function generateToken(payload) {
|
||||
return jsonwebtoken_1.default.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
|
||||
}
|
||||
function verifyToken(token) {
|
||||
try {
|
||||
return jsonwebtoken_1.default.verify(token, JWT_SECRET);
|
||||
}
|
||||
catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function authMiddleware(req, res, next) {
|
||||
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;
|
||||
}
|
||||
req.user = payload;
|
||||
next();
|
||||
}
|
||||
function optionalAuthMiddleware(req, res, next) {
|
||||
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();
|
||||
}
|
||||
function generateId(prefix = '') {
|
||||
const timestamp = Date.now().toString(36);
|
||||
const random = Math.random().toString(36).slice(2, 10);
|
||||
return `${prefix}${prefix ? '_' : ''}${timestamp}${random}`;
|
||||
}
|
||||
function getCurrentTimestamp() {
|
||||
return Date.now();
|
||||
}
|
||||
//# sourceMappingURL=auth.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/utils/auth.ts"],"names":[],"mappings":";;;;;AAOA,sCAEC;AAED,kCAMC;AAED,wCA4BC;AAED,wDAWC;AAED,gCAIC;AAED,kDAEC;AArED,gEAA+B;AAG/B,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,gDAAgD,CAAC;AAC9F,MAAM,cAAc,GAAG,IAAI,CAAC;AAE5B,SAAgB,aAAa,CAAC,OAAoB;IAChD,OAAO,sBAAG,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC,CAAC;AACtE,CAAC;AAED,SAAgB,WAAW,CAAC,KAAa;IACvC,IAAI,CAAC;QACH,OAAO,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,UAAU,CAAgB,CAAC;IACtD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAgB,cAAc,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IAC5E,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;IAE7C,IAAI,CAAC,UAAU,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACrD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACnB,KAAK,EAAE;gBACL,IAAI,EAAE,cAAc;gBACpB,OAAO,EAAE,yCAAyC;aACnD;SACF,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAClC,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IAEnC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACnB,KAAK,EAAE;gBACL,IAAI,EAAE,cAAc;gBACpB,OAAO,EAAE,0BAA0B;aACpC;SACF,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,GAAG,CAAC,IAAI,GAAG,OAAO,CAAC;IACnB,IAAI,EAAE,CAAC;AACT,CAAC;AAED,SAAgB,sBAAsB,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IACpF,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;IAE7C,IAAI,UAAU,IAAI,UAAU,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACnD,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAClC,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;QACnC,IAAI,OAAO,EAAE,CAAC;YACZ,GAAG,CAAC,IAAI,GAAG,OAAO,CAAC;QACrB,CAAC;IACH,CAAC;IACD,IAAI,EAAE,CAAC;AACT,CAAC;AAED,SAAgB,UAAU,CAAC,SAAiB,EAAE;IAC5C,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACvD,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,SAAS,GAAG,MAAM,EAAE,CAAC;AAC9D,CAAC;AAED,SAAgB,mBAAmB;IACjC,OAAO,IAAI,CAAC,GAAG,EAAE,CAAC;AACpB,CAAC"}
|
||||
Vendored
+649
@@ -0,0 +1,649 @@
|
||||
import { z } from 'zod';
|
||||
export declare const categoryCreateSchema: z.ZodObject<{
|
||||
name: z.ZodString;
|
||||
color: z.ZodString;
|
||||
order: z.ZodOptional<z.ZodNumber>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
name: string;
|
||||
color: string;
|
||||
order?: number | undefined;
|
||||
}, {
|
||||
name: string;
|
||||
color: string;
|
||||
order?: number | undefined;
|
||||
}>;
|
||||
export declare const categoryUpdateSchema: z.ZodObject<{
|
||||
name: z.ZodOptional<z.ZodString>;
|
||||
color: z.ZodOptional<z.ZodString>;
|
||||
order: z.ZodOptional<z.ZodNumber>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
name?: string | undefined;
|
||||
color?: string | undefined;
|
||||
order?: number | undefined;
|
||||
}, {
|
||||
name?: string | undefined;
|
||||
color?: string | undefined;
|
||||
order?: number | undefined;
|
||||
}>;
|
||||
export declare const repeatSchema: z.ZodEnum<["none", "daily", "weekly", "monthly", "custom"]>;
|
||||
export declare const taskCreateSchema: z.ZodObject<{
|
||||
title: z.ZodString;
|
||||
description: z.ZodOptional<z.ZodString>;
|
||||
categoryId: z.ZodString;
|
||||
priority: z.ZodOptional<z.ZodEnum<["none", "low", "medium", "high", "critical"]>>;
|
||||
dueDate: z.ZodOptional<z.ZodNumber>;
|
||||
dueTime: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
||||
endTime: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
||||
repeat: z.ZodOptional<z.ZodEnum<["none", "daily", "weekly", "monthly", "custom"]>>;
|
||||
repeatInterval: z.ZodOptional<z.ZodNumber>;
|
||||
repeatDays: z.ZodOptional<z.ZodString>;
|
||||
seriesId: z.ZodOptional<z.ZodString>;
|
||||
reminder: z.ZodOptional<z.ZodEnum<["none", "at_time", "15", "30", "60", "120", "1440"]>>;
|
||||
assigneeId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
subtasks: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
title: z.ZodString;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
title: string;
|
||||
}, {
|
||||
title: string;
|
||||
}>, "many">>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
categoryId: string;
|
||||
title: string;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | null | undefined;
|
||||
endTime?: string | undefined;
|
||||
repeat?: "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
subtasks?: {
|
||||
title: string;
|
||||
}[] | undefined;
|
||||
}, {
|
||||
categoryId: string;
|
||||
title: string;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | null | undefined;
|
||||
endTime?: string | undefined;
|
||||
repeat?: "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
subtasks?: {
|
||||
title: string;
|
||||
}[] | undefined;
|
||||
}>;
|
||||
export declare const taskUpdateSchema: z.ZodObject<{
|
||||
title: z.ZodOptional<z.ZodString>;
|
||||
description: z.ZodOptional<z.ZodString>;
|
||||
categoryId: z.ZodOptional<z.ZodString>;
|
||||
priority: z.ZodOptional<z.ZodEnum<["none", "low", "medium", "high", "critical"]>>;
|
||||
completed: z.ZodOptional<z.ZodBoolean>;
|
||||
dueDate: z.ZodOptional<z.ZodNumber>;
|
||||
dueTime: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
||||
endTime: z.ZodOptional<z.ZodString>;
|
||||
assigneeId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
categoryId?: string | undefined;
|
||||
title?: string | undefined;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
completed?: boolean | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | null | undefined;
|
||||
endTime?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
}, {
|
||||
categoryId?: string | undefined;
|
||||
title?: string | undefined;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
completed?: boolean | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | null | undefined;
|
||||
endTime?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
}>;
|
||||
export declare const subtaskCreateSchema: z.ZodObject<{
|
||||
title: z.ZodString;
|
||||
order: z.ZodOptional<z.ZodNumber>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}, {
|
||||
title: string;
|
||||
order?: number | undefined;
|
||||
}>;
|
||||
export declare const subtaskUpdateSchema: z.ZodObject<{
|
||||
title: z.ZodOptional<z.ZodString>;
|
||||
completed: z.ZodOptional<z.ZodBoolean>;
|
||||
order: z.ZodOptional<z.ZodNumber>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
order?: number | undefined;
|
||||
title?: string | undefined;
|
||||
completed?: boolean | undefined;
|
||||
}, {
|
||||
order?: number | undefined;
|
||||
title?: string | undefined;
|
||||
completed?: boolean | undefined;
|
||||
}>;
|
||||
export declare const userSettingsSchema: z.ZodObject<{
|
||||
darkMode: z.ZodOptional<z.ZodBoolean>;
|
||||
notifications: z.ZodOptional<z.ZodBoolean>;
|
||||
reminderTime: z.ZodOptional<z.ZodString>;
|
||||
defaultCategory: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
||||
sortBy: z.ZodOptional<z.ZodEnum<["dueDate", "priority", "title", "createdAt"]>>;
|
||||
sortOrder: z.ZodOptional<z.ZodEnum<["asc", "desc"]>>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
darkMode?: boolean | undefined;
|
||||
notifications?: boolean | undefined;
|
||||
reminderTime?: string | undefined;
|
||||
defaultCategory?: string | null | undefined;
|
||||
sortBy?: "createdAt" | "title" | "priority" | "dueDate" | undefined;
|
||||
sortOrder?: "asc" | "desc" | undefined;
|
||||
}, {
|
||||
darkMode?: boolean | undefined;
|
||||
notifications?: boolean | undefined;
|
||||
reminderTime?: string | undefined;
|
||||
defaultCategory?: string | null | undefined;
|
||||
sortBy?: "createdAt" | "title" | "priority" | "dueDate" | undefined;
|
||||
sortOrder?: "asc" | "desc" | undefined;
|
||||
}>;
|
||||
export declare const repeatProfileSchema: z.ZodObject<{
|
||||
name: z.ZodString;
|
||||
repeat: z.ZodEnum<["none", "daily", "weekly", "monthly", "custom"]>;
|
||||
repeatInterval: z.ZodNumber;
|
||||
repeatDays: z.ZodString;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
name: string;
|
||||
repeat: "custom" | "none" | "daily" | "weekly" | "monthly";
|
||||
repeatInterval: number;
|
||||
repeatDays: string;
|
||||
}, {
|
||||
name: string;
|
||||
repeat: "custom" | "none" | "daily" | "weekly" | "monthly";
|
||||
repeatInterval: number;
|
||||
repeatDays: string;
|
||||
}>;
|
||||
export declare const friendRequestSchema: z.ZodObject<{
|
||||
username: z.ZodString;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
username: string;
|
||||
}, {
|
||||
username: string;
|
||||
}>;
|
||||
export declare const friendshipSchema: z.ZodObject<{
|
||||
id: z.ZodString;
|
||||
userId: z.ZodString;
|
||||
friendId: z.ZodString;
|
||||
status: z.ZodEnum<["pending", "accepted"]>;
|
||||
createdAt: z.ZodNumber;
|
||||
updatedAt: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
userId: string;
|
||||
friendId: string;
|
||||
status: "pending" | "accepted";
|
||||
updatedAt: number;
|
||||
}, {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
userId: string;
|
||||
friendId: string;
|
||||
status: "pending" | "accepted";
|
||||
updatedAt: number;
|
||||
}>;
|
||||
export declare const pushChangesSchema: z.ZodObject<{
|
||||
changes: z.ZodObject<{
|
||||
categories: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
name: z.ZodString;
|
||||
color: z.ZodString;
|
||||
order: z.ZodOptional<z.ZodNumber>;
|
||||
} & {
|
||||
id: z.ZodString;
|
||||
createdAt: z.ZodNumber;
|
||||
updatedAt: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
color: string;
|
||||
order?: number | undefined;
|
||||
}, {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
color: string;
|
||||
order?: number | undefined;
|
||||
}>, "many">>;
|
||||
tasks: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
title: z.ZodString;
|
||||
description: z.ZodOptional<z.ZodString>;
|
||||
categoryId: z.ZodString;
|
||||
priority: z.ZodOptional<z.ZodEnum<["none", "low", "medium", "high", "critical"]>>;
|
||||
dueDate: z.ZodOptional<z.ZodNumber>;
|
||||
dueTime: z.ZodNullable<z.ZodOptional<z.ZodString>>;
|
||||
endTime: z.ZodUnion<[z.ZodOptional<z.ZodString>, z.ZodLiteral<"">]>;
|
||||
repeat: z.ZodOptional<z.ZodEnum<["none", "daily", "weekly", "monthly", "custom"]>>;
|
||||
repeatInterval: z.ZodOptional<z.ZodNumber>;
|
||||
repeatDays: z.ZodOptional<z.ZodString>;
|
||||
seriesId: z.ZodOptional<z.ZodString>;
|
||||
reminder: z.ZodOptional<z.ZodEnum<["none", "at_time", "15", "30", "60", "120", "1440"]>>;
|
||||
assigneeId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
||||
subtasks: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
title: z.ZodString;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
title: string;
|
||||
}, {
|
||||
title: string;
|
||||
}>, "many">>;
|
||||
} & {
|
||||
id: z.ZodString;
|
||||
completed: z.ZodBoolean;
|
||||
createdAt: z.ZodNumber;
|
||||
updatedAt: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
categoryId: string;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | null | undefined;
|
||||
endTime?: string | undefined;
|
||||
repeat?: "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
subtasks?: {
|
||||
title: string;
|
||||
}[] | undefined;
|
||||
}, {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
categoryId: string;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | null | undefined;
|
||||
endTime?: string | undefined;
|
||||
repeat?: "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
subtasks?: {
|
||||
title: string;
|
||||
}[] | undefined;
|
||||
}>, "many">>;
|
||||
subtasks: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
title: z.ZodString;
|
||||
order: z.ZodOptional<z.ZodNumber>;
|
||||
} & {
|
||||
id: z.ZodString;
|
||||
taskId: z.ZodString;
|
||||
completed: z.ZodBoolean;
|
||||
createdAt: z.ZodNumber;
|
||||
updatedAt: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
taskId: string;
|
||||
order?: number | undefined;
|
||||
}, {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
taskId: string;
|
||||
order?: number | undefined;
|
||||
}>, "many">>;
|
||||
repeatProfiles: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
name: z.ZodString;
|
||||
repeat: z.ZodEnum<["none", "daily", "weekly", "monthly", "custom"]>;
|
||||
repeatInterval: z.ZodNumber;
|
||||
repeatDays: z.ZodString;
|
||||
} & {
|
||||
id: z.ZodString;
|
||||
createdAt: z.ZodNumber;
|
||||
updatedAt: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
repeat: "custom" | "none" | "daily" | "weekly" | "monthly";
|
||||
repeatInterval: number;
|
||||
repeatDays: string;
|
||||
}, {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
repeat: "custom" | "none" | "daily" | "weekly" | "monthly";
|
||||
repeatInterval: number;
|
||||
repeatDays: string;
|
||||
}>, "many">>;
|
||||
friendships: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
||||
id: z.ZodString;
|
||||
userId: z.ZodString;
|
||||
friendId: z.ZodString;
|
||||
status: z.ZodEnum<["pending", "accepted"]>;
|
||||
createdAt: z.ZodNumber;
|
||||
updatedAt: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
userId: string;
|
||||
friendId: string;
|
||||
status: "pending" | "accepted";
|
||||
updatedAt: number;
|
||||
}, {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
userId: string;
|
||||
friendId: string;
|
||||
status: "pending" | "accepted";
|
||||
updatedAt: number;
|
||||
}>, "many">>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
friendships?: {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
userId: string;
|
||||
friendId: string;
|
||||
status: "pending" | "accepted";
|
||||
updatedAt: number;
|
||||
}[] | undefined;
|
||||
categories?: {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
color: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
tasks?: {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
categoryId: string;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | null | undefined;
|
||||
endTime?: string | undefined;
|
||||
repeat?: "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
subtasks?: {
|
||||
title: string;
|
||||
}[] | undefined;
|
||||
}[] | undefined;
|
||||
subtasks?: {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
taskId: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
repeatProfiles?: {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
repeat: "custom" | "none" | "daily" | "weekly" | "monthly";
|
||||
repeatInterval: number;
|
||||
repeatDays: string;
|
||||
}[] | undefined;
|
||||
}, {
|
||||
friendships?: {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
userId: string;
|
||||
friendId: string;
|
||||
status: "pending" | "accepted";
|
||||
updatedAt: number;
|
||||
}[] | undefined;
|
||||
categories?: {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
color: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
tasks?: {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
categoryId: string;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | null | undefined;
|
||||
endTime?: string | undefined;
|
||||
repeat?: "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
subtasks?: {
|
||||
title: string;
|
||||
}[] | undefined;
|
||||
}[] | undefined;
|
||||
subtasks?: {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
taskId: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
repeatProfiles?: {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
repeat: "custom" | "none" | "daily" | "weekly" | "monthly";
|
||||
repeatInterval: number;
|
||||
repeatDays: string;
|
||||
}[] | undefined;
|
||||
}>;
|
||||
lastPulledAt: z.ZodNumber;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
changes: {
|
||||
friendships?: {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
userId: string;
|
||||
friendId: string;
|
||||
status: "pending" | "accepted";
|
||||
updatedAt: number;
|
||||
}[] | undefined;
|
||||
categories?: {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
color: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
tasks?: {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
categoryId: string;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | null | undefined;
|
||||
endTime?: string | undefined;
|
||||
repeat?: "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
subtasks?: {
|
||||
title: string;
|
||||
}[] | undefined;
|
||||
}[] | undefined;
|
||||
subtasks?: {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
taskId: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
repeatProfiles?: {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
repeat: "custom" | "none" | "daily" | "weekly" | "monthly";
|
||||
repeatInterval: number;
|
||||
repeatDays: string;
|
||||
}[] | undefined;
|
||||
};
|
||||
lastPulledAt: number;
|
||||
}, {
|
||||
changes: {
|
||||
friendships?: {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
userId: string;
|
||||
friendId: string;
|
||||
status: "pending" | "accepted";
|
||||
updatedAt: number;
|
||||
}[] | undefined;
|
||||
categories?: {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
color: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
tasks?: {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
categoryId: string;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
description?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
dueDate?: number | undefined;
|
||||
dueTime?: string | null | undefined;
|
||||
endTime?: string | undefined;
|
||||
repeat?: "custom" | "none" | "daily" | "weekly" | "monthly" | undefined;
|
||||
repeatInterval?: number | undefined;
|
||||
repeatDays?: string | undefined;
|
||||
seriesId?: string | undefined;
|
||||
assigneeId?: string | null | undefined;
|
||||
reminder?: "none" | "at_time" | "15" | "30" | "60" | "120" | "1440" | undefined;
|
||||
subtasks?: {
|
||||
title: string;
|
||||
}[] | undefined;
|
||||
}[] | undefined;
|
||||
subtasks?: {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
taskId: string;
|
||||
order?: number | undefined;
|
||||
}[] | undefined;
|
||||
repeatProfiles?: {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
repeat: "custom" | "none" | "daily" | "weekly" | "monthly";
|
||||
repeatInterval: number;
|
||||
repeatDays: string;
|
||||
}[] | undefined;
|
||||
};
|
||||
lastPulledAt: number;
|
||||
}>;
|
||||
export declare const syncQuerySchema: z.ZodObject<{
|
||||
since: z.ZodPipeline<z.ZodEffects<z.ZodString, number, string>, z.ZodNumber>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
since: number;
|
||||
}, {
|
||||
since: string;
|
||||
}>;
|
||||
export declare const taskQuerySchema: z.ZodObject<{
|
||||
categoryId: z.ZodOptional<z.ZodString>;
|
||||
completed: z.ZodOptional<z.ZodEffects<z.ZodString, boolean, string>>;
|
||||
dueBefore: z.ZodOptional<z.ZodPipeline<z.ZodEffects<z.ZodString, number, string>, z.ZodNumber>>;
|
||||
dueAfter: z.ZodOptional<z.ZodPipeline<z.ZodEffects<z.ZodString, number, string>, z.ZodNumber>>;
|
||||
priority: z.ZodOptional<z.ZodEnum<["none", "low", "medium", "high", "critical"]>>;
|
||||
sortBy: z.ZodOptional<z.ZodEnum<["dueDate", "priority", "createdAt", "title"]>>;
|
||||
sortOrder: z.ZodOptional<z.ZodEnum<["asc", "desc"]>>;
|
||||
limit: z.ZodOptional<z.ZodPipeline<z.ZodEffects<z.ZodString, number, string>, z.ZodNumber>>;
|
||||
offset: z.ZodOptional<z.ZodPipeline<z.ZodEffects<z.ZodString, number, string>, z.ZodNumber>>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
categoryId?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
completed?: boolean | undefined;
|
||||
sortBy?: "createdAt" | "title" | "priority" | "dueDate" | undefined;
|
||||
sortOrder?: "asc" | "desc" | undefined;
|
||||
limit?: number | undefined;
|
||||
offset?: number | undefined;
|
||||
dueBefore?: number | undefined;
|
||||
dueAfter?: number | undefined;
|
||||
}, {
|
||||
categoryId?: string | undefined;
|
||||
priority?: "none" | "low" | "medium" | "high" | "critical" | undefined;
|
||||
completed?: string | undefined;
|
||||
sortBy?: "createdAt" | "title" | "priority" | "dueDate" | undefined;
|
||||
sortOrder?: "asc" | "desc" | undefined;
|
||||
limit?: string | undefined;
|
||||
offset?: string | undefined;
|
||||
dueBefore?: string | undefined;
|
||||
dueAfter?: string | undefined;
|
||||
}>;
|
||||
//# sourceMappingURL=validation.d.ts.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,oBAAoB;;;;;;;;;;;;EAI/B,CAAC;AAEH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;EAI/B,CAAC;AAEH,eAAO,MAAM,YAAY,6DAA2D,CAAC;AAErF,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAe3B,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAU3B,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;EAG9B,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;EAI9B,CAAC;AAEH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;EAO7B,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;EAK9B,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;EAE9B,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;EAO3B,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAS5B,CAAC;AAEH,eAAO,MAAM,eAAe;;;;;;EAE1B,CAAC;AAEH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAU1B,CAAC"}
|
||||
Vendored
+101
@@ -0,0 +1,101 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.taskQuerySchema = exports.syncQuerySchema = exports.pushChangesSchema = exports.friendshipSchema = exports.friendRequestSchema = exports.repeatProfileSchema = exports.userSettingsSchema = exports.subtaskUpdateSchema = exports.subtaskCreateSchema = exports.taskUpdateSchema = exports.taskCreateSchema = exports.repeatSchema = exports.categoryUpdateSchema = exports.categoryCreateSchema = void 0;
|
||||
const zod_1 = require("zod");
|
||||
exports.categoryCreateSchema = zod_1.z.object({
|
||||
name: zod_1.z.string().min(1).max(50),
|
||||
color: zod_1.z.string().regex(/^#[0-9A-Fa-f]{6}$/),
|
||||
order: zod_1.z.number().int().min(0).optional(),
|
||||
});
|
||||
exports.categoryUpdateSchema = zod_1.z.object({
|
||||
name: zod_1.z.string().min(1).max(50).optional(),
|
||||
color: zod_1.z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(),
|
||||
order: zod_1.z.number().int().min(0).optional(),
|
||||
});
|
||||
exports.repeatSchema = zod_1.z.enum(['none', 'daily', 'weekly', 'monthly', 'custom']);
|
||||
exports.taskCreateSchema = zod_1.z.object({
|
||||
title: zod_1.z.string().min(1).max(100),
|
||||
description: zod_1.z.string().max(1000).optional(),
|
||||
categoryId: zod_1.z.string().min(1),
|
||||
priority: zod_1.z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(),
|
||||
dueDate: zod_1.z.number().int().min(0).optional(),
|
||||
dueTime: zod_1.z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().nullable(),
|
||||
endTime: zod_1.z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().or(zod_1.z.literal('')),
|
||||
repeat: exports.repeatSchema.optional(),
|
||||
repeatInterval: zod_1.z.number().int().min(1).max(30).optional(),
|
||||
repeatDays: zod_1.z.string().max(20).optional(),
|
||||
seriesId: zod_1.z.string().max(50).optional(),
|
||||
reminder: zod_1.z.enum(['none', 'at_time', '15', '30', '60', '120', '1440']).optional(),
|
||||
assigneeId: zod_1.z.string().nullable().optional(),
|
||||
subtasks: zod_1.z.array(zod_1.z.object({ title: zod_1.z.string().min(1).max(100) })).optional(),
|
||||
});
|
||||
exports.taskUpdateSchema = zod_1.z.object({
|
||||
title: zod_1.z.string().min(1).max(100).optional(),
|
||||
description: zod_1.z.string().max(1000).optional(),
|
||||
categoryId: zod_1.z.string().min(1).optional(),
|
||||
priority: zod_1.z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(),
|
||||
completed: zod_1.z.boolean().optional(),
|
||||
dueDate: zod_1.z.number().int().min(0).optional(),
|
||||
dueTime: zod_1.z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional().nullable(),
|
||||
endTime: zod_1.z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional(),
|
||||
assigneeId: zod_1.z.string().nullable().optional(),
|
||||
});
|
||||
exports.subtaskCreateSchema = zod_1.z.object({
|
||||
title: zod_1.z.string().min(1).max(100),
|
||||
order: zod_1.z.number().int().min(0).optional(),
|
||||
});
|
||||
exports.subtaskUpdateSchema = zod_1.z.object({
|
||||
title: zod_1.z.string().min(1).max(100).optional(),
|
||||
completed: zod_1.z.boolean().optional(),
|
||||
order: zod_1.z.number().int().min(0).optional(),
|
||||
});
|
||||
exports.userSettingsSchema = zod_1.z.object({
|
||||
darkMode: zod_1.z.boolean().optional(),
|
||||
notifications: zod_1.z.boolean().optional(),
|
||||
reminderTime: zod_1.z.string().regex(/^([01]\d|2[0-3]):([0-5]\d)$/).optional(),
|
||||
defaultCategory: zod_1.z.string().optional().nullable(),
|
||||
sortBy: zod_1.z.enum(['dueDate', 'priority', 'title', 'createdAt']).optional(),
|
||||
sortOrder: zod_1.z.enum(['asc', 'desc']).optional(),
|
||||
});
|
||||
exports.repeatProfileSchema = zod_1.z.object({
|
||||
name: zod_1.z.string().min(1).max(50),
|
||||
repeat: exports.repeatSchema,
|
||||
repeatInterval: zod_1.z.number().int().min(1).max(30),
|
||||
repeatDays: zod_1.z.string().max(20),
|
||||
});
|
||||
exports.friendRequestSchema = zod_1.z.object({
|
||||
username: zod_1.z.string().min(1).max(50),
|
||||
});
|
||||
exports.friendshipSchema = zod_1.z.object({
|
||||
id: zod_1.z.string(),
|
||||
userId: zod_1.z.string(),
|
||||
friendId: zod_1.z.string(),
|
||||
status: zod_1.z.enum(['pending', 'accepted']),
|
||||
createdAt: zod_1.z.number(),
|
||||
updatedAt: zod_1.z.number(),
|
||||
});
|
||||
exports.pushChangesSchema = zod_1.z.object({
|
||||
changes: zod_1.z.object({
|
||||
categories: zod_1.z.array(exports.categoryCreateSchema.extend({ id: zod_1.z.string(), createdAt: zod_1.z.number(), updatedAt: zod_1.z.number() })).optional(),
|
||||
tasks: zod_1.z.array(exports.taskCreateSchema.extend({ id: zod_1.z.string(), completed: zod_1.z.boolean(), createdAt: zod_1.z.number(), updatedAt: zod_1.z.number() })).optional(),
|
||||
subtasks: zod_1.z.array(exports.subtaskCreateSchema.extend({ id: zod_1.z.string(), taskId: zod_1.z.string(), completed: zod_1.z.boolean(), createdAt: zod_1.z.number(), updatedAt: zod_1.z.number() })).optional(),
|
||||
repeatProfiles: zod_1.z.array(exports.repeatProfileSchema.extend({ id: zod_1.z.string(), createdAt: zod_1.z.number(), updatedAt: zod_1.z.number() })).optional(),
|
||||
friendships: zod_1.z.array(exports.friendshipSchema).optional(),
|
||||
}),
|
||||
lastPulledAt: zod_1.z.number().int().min(0),
|
||||
});
|
||||
exports.syncQuerySchema = zod_1.z.object({
|
||||
since: zod_1.z.string().transform(Number).pipe(zod_1.z.number().int().min(0)),
|
||||
});
|
||||
exports.taskQuerySchema = zod_1.z.object({
|
||||
categoryId: zod_1.z.string().optional(),
|
||||
completed: zod_1.z.string().transform(v => v === 'true').optional(),
|
||||
dueBefore: zod_1.z.string().transform(Number).pipe(zod_1.z.number().int().min(0)).optional(),
|
||||
dueAfter: zod_1.z.string().transform(Number).pipe(zod_1.z.number().int().min(0)).optional(),
|
||||
priority: zod_1.z.enum(['none', 'low', 'medium', 'high', 'critical']).optional(),
|
||||
sortBy: zod_1.z.enum(['dueDate', 'priority', 'createdAt', 'title']).optional(),
|
||||
sortOrder: zod_1.z.enum(['asc', 'desc']).optional(),
|
||||
limit: zod_1.z.string().transform(Number).pipe(zod_1.z.number().int().min(1).max(100)).optional(),
|
||||
offset: zod_1.z.string().transform(Number).pipe(zod_1.z.number().int().min(0)).optional(),
|
||||
});
|
||||
//# sourceMappingURL=validation.js.map
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,46 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: carry-your-live-db
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: carry_your_live
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
target: dev
|
||||
container_name: carry-your-live-api
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/carry_your_live
|
||||
JWT_SECRET: your-super-secret-jwt-key-change-in-production-min-32-chars
|
||||
PORT: 3000
|
||||
NODE_ENV: development
|
||||
FRONTEND_URL: http://localhost:8081
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ./:/app
|
||||
- backend_node_modules:/app/node_modules
|
||||
command: >
|
||||
sh -c "npm run db:push || true
|
||||
&& npm run dev"
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
backend_node_modules:
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
export default defineConfig({
|
||||
schema: './src/db/schema.ts',
|
||||
out: './drizzle',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL!,
|
||||
},
|
||||
});
|
||||
Generated
+3029
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "carry-your-live-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Backend API for Carry Your Live",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:studio": "drizzle-kit studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"drizzle-orm": "^0.33.0",
|
||||
"express": "^4.21.0",
|
||||
"express-rate-limit": "^8.6.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"pg": "^8.13.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jsonwebtoken": "^9.0.7",
|
||||
"@types/node": "^22.5.5",
|
||||
"@types/pg": "^8.11.10",
|
||||
"drizzle-kit": "^0.24.2",
|
||||
"tsx": "^4.19.1",
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { Pool } from 'pg';
|
||||
import * as schema from './schema';
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
max: 20,
|
||||
});
|
||||
|
||||
export const db = drizzle(pool, { schema });
|
||||
|
||||
export type DB = typeof db;
|
||||
@@ -0,0 +1,107 @@
|
||||
import { pgTable, text, integer, bigint, boolean, timestamp, unique, index } from 'drizzle-orm/pg-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const users = pgTable('users', {
|
||||
id: text('id').primaryKey(),
|
||||
username: text('username').notNull().unique(),
|
||||
passwordHash: text('password_hash').notNull(),
|
||||
resetToken: text('reset_token'),
|
||||
resetTokenExpiry: bigint('reset_token_expiry', { mode: 'number' }),
|
||||
createdAt: bigint('created_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
});
|
||||
|
||||
export const friendships = pgTable(
|
||||
'friendships',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
friendId: text('friend_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
status: text('status', { enum: ['pending', 'accepted'] }).notNull().default('pending'),
|
||||
createdAt: bigint('created_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
updatedAt: bigint('updated_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
},
|
||||
(t) => ({
|
||||
pairUnique: unique('friendships_pair').on(t.userId, t.friendId),
|
||||
userIdx: index('friendships_user_idx').on(t.userId),
|
||||
friendIdx: index('friendships_friend_idx').on(t.friendId),
|
||||
})
|
||||
);
|
||||
|
||||
export const categories = pgTable('categories', {
|
||||
id: text('id').primaryKey(),
|
||||
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
name: text('name').notNull(),
|
||||
color: text('color').notNull(),
|
||||
order: integer('order').notNull().default(0),
|
||||
createdAt: bigint('created_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
updatedAt: bigint('updated_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
});
|
||||
|
||||
export const tasks = pgTable('tasks', {
|
||||
id: text('id').primaryKey(),
|
||||
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
categoryId: text('category_id').notNull().references(() => categories.id, { onDelete: 'cascade' }),
|
||||
title: text('title').notNull(),
|
||||
description: text('description').notNull().default(''),
|
||||
priority: text('priority', { enum: ['none', 'low', 'medium', 'high', 'critical'] }).notNull().default('none'),
|
||||
completed: boolean('completed').notNull().default(false),
|
||||
dueDate: bigint('due_date', { mode: 'number' }).notNull().default(0),
|
||||
dueTime: text('due_time').notNull().default(''),
|
||||
endTime: text('end_time').notNull().default(''),
|
||||
allDay: boolean('all_day').notNull().default(false),
|
||||
repeat: text('repeat', { enum: ['none', 'daily', 'weekly', 'monthly', 'custom'] }).notNull().default('none'),
|
||||
repeatInterval: integer('repeat_interval').notNull().default(1),
|
||||
repeatDays: text('repeat_days').notNull().default(''),
|
||||
seriesId: text('series_id').notNull().default(''),
|
||||
assigneeId: text('assignee_id').references(() => users.id, { onDelete: 'set null' }),
|
||||
reminder: text('reminder', { enum: ['none', 'at_time', '15', '30', '60', '120', '1440'] }).notNull().default('none'),
|
||||
reminders: text('reminders').notNull().default(''),
|
||||
createdAt: bigint('created_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
updatedAt: bigint('updated_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
});
|
||||
|
||||
export const repeatProfiles = pgTable('repeat_profiles', {
|
||||
id: text('id').primaryKey(),
|
||||
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
name: text('name').notNull(),
|
||||
repeat: text('repeat', { enum: ['none', 'daily', 'weekly', 'monthly', 'custom'] }).notNull().default('none'),
|
||||
repeatInterval: integer('repeat_interval').notNull().default(1),
|
||||
repeatDays: text('repeat_days').notNull().default(''),
|
||||
createdAt: bigint('created_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
updatedAt: bigint('updated_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
});
|
||||
|
||||
export const subtasks = pgTable('subtasks', {
|
||||
id: text('id').primaryKey(),
|
||||
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
taskId: text('task_id').notNull().references(() => tasks.id, { onDelete: 'cascade' }),
|
||||
title: text('title').notNull(),
|
||||
description: text('description').notNull().default(''),
|
||||
priority: text('priority', { enum: ['none', 'low', 'medium', 'high', 'critical'] }).notNull().default('none'),
|
||||
completed: boolean('completed').notNull().default(false),
|
||||
dueDate: bigint('due_date', { mode: 'number' }).notNull().default(0),
|
||||
dueTime: text('due_time').notNull().default(''),
|
||||
endTime: text('end_time').notNull().default(''),
|
||||
allDay: boolean('all_day').notNull().default(false),
|
||||
repeat: text('repeat', { enum: ['none', 'daily', 'weekly', 'monthly', 'custom'] }).notNull().default('none'),
|
||||
repeatInterval: integer('repeat_interval').notNull().default(1),
|
||||
repeatDays: text('repeat_days').notNull().default(''),
|
||||
seriesId: text('series_id').notNull().default(''),
|
||||
reminder: text('reminder', { enum: ['none', 'at_time', '15', '30', '60', '120', '1440'] }).notNull().default('none'),
|
||||
reminders: text('reminders').notNull().default(''),
|
||||
assigneeId: text('assignee_id').references(() => users.id, { onDelete: 'set null' }),
|
||||
order: integer('order').notNull().default(0),
|
||||
createdAt: bigint('created_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
updatedAt: bigint('updated_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
});
|
||||
|
||||
export const userSettings = pgTable('user_settings', {
|
||||
userId: text('user_id').primaryKey().references(() => users.id, { onDelete: 'cascade' }),
|
||||
darkMode: boolean('dark_mode').notNull().default(false),
|
||||
notifications: boolean('notifications').notNull().default(true),
|
||||
reminderTime: text('reminder_time').notNull().default('09:00'),
|
||||
defaultCategory: text('default_category'),
|
||||
sortBy: text('sort_by', { enum: ['dueDate', 'priority', 'title', 'createdAt'] }).notNull().default('dueDate'),
|
||||
sortOrder: text('sort_order', { enum: ['asc', 'desc'] }).notNull().default('asc'),
|
||||
updatedAt: bigint('updated_at', { mode: 'number' }).notNull().default(sql`EXTRACT(EPOCH FROM NOW()) * 1000`),
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'dotenv/config';
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import authRoutes from './routes/auth';
|
||||
import categoryRoutes from './routes/categories';
|
||||
import taskRoutes from './routes/tasks';
|
||||
import subtaskRoutes from './routes/subtasks';
|
||||
import userRoutes from './routes/users';
|
||||
import friendRoutes from './routes/friends';
|
||||
import repeatProfileRoutes from './routes/repeatProfiles';
|
||||
import syncRoutes from './routes/sync';
|
||||
import { errorHandler, notFoundHandler } from './middleware/errorHandler';
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
app.use(cors({
|
||||
origin: true,
|
||||
credentials: true,
|
||||
}));
|
||||
app.use(express.json());
|
||||
|
||||
// Health check
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: Date.now() });
|
||||
});
|
||||
|
||||
// API routes
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/categories', categoryRoutes);
|
||||
app.use('/api/tasks', taskRoutes);
|
||||
app.use('/api/subtasks', subtaskRoutes);
|
||||
app.use('/api/users', userRoutes);
|
||||
app.use('/api/repeat-profiles', repeatProfileRoutes);
|
||||
app.use('/api/sync', syncRoutes);
|
||||
app.use('/api/friends', friendRoutes);
|
||||
|
||||
// 404 handler
|
||||
app.use(notFoundHandler);
|
||||
|
||||
// Error handler
|
||||
app.use(errorHandler);
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 Server running on http://localhost:${PORT}`);
|
||||
console.log(`📚 API available at http://localhost:${PORT}/api`);
|
||||
});
|
||||
|
||||
export default app;
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { ZodError } from 'zod';
|
||||
|
||||
export class AppError extends Error {
|
||||
constructor(
|
||||
public code: string,
|
||||
public message: string,
|
||||
public statusCode: number = 500,
|
||||
public details?: Record<string, any>
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'AppError';
|
||||
}
|
||||
}
|
||||
|
||||
export function errorHandler(err: Error, req: Request, res: Response, next: NextFunction): void {
|
||||
console.error('Error:', err);
|
||||
|
||||
if (err instanceof ZodError) {
|
||||
res.status(400).json({
|
||||
error: {
|
||||
code: 'INVALID_PAYLOAD',
|
||||
message: 'Request validation failed',
|
||||
details: err.flatten().fieldErrors,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (err instanceof AppError) {
|
||||
res.status(err.statusCode).json({
|
||||
error: {
|
||||
code: err.code,
|
||||
message: err.message,
|
||||
details: err.details,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
error: {
|
||||
code: 'SERVER_ERROR',
|
||||
message: 'Internal server error',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function notFoundHandler(req: Request, res: Response): void {
|
||||
res.status(404).json({
|
||||
error: {
|
||||
code: 'NOT_FOUND',
|
||||
message: `Route ${req.method} ${req.path} not found`,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { asyncHandler } from '../utils/asyncHandler';
|
||||
import { db } from '../db';
|
||||
import { users } from '../db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { generateToken, generateId, getCurrentTimestamp } from '../utils/auth';
|
||||
import { AppError } from '../middleware/errorHandler';
|
||||
import { z } from 'zod';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import crypto from 'crypto';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// Rate limiting for auth endpoints
|
||||
const authLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 10, // limit each IP to 10 requests per windowMs
|
||||
message: {
|
||||
error: {
|
||||
code: 'RATE_LIMITED',
|
||||
message: 'Too many attempts. Please try again later.',
|
||||
},
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
const loginLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 5, // stricter limit for login
|
||||
message: {
|
||||
error: {
|
||||
code: 'RATE_LIMITED',
|
||||
message: 'Too many login attempts. Please try again later.',
|
||||
},
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
const passwordResetLimiter = rateLimit({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 3, // very strict for password reset
|
||||
message: {
|
||||
error: {
|
||||
code: 'RATE_LIMITED',
|
||||
message: 'Too many password reset requests. Please try again later.',
|
||||
},
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
const usernameSchema = z
|
||||
.string()
|
||||
.min(3)
|
||||
.max(30)
|
||||
.regex(/^[a-zA-Z0-9_.-]+$/, 'Username can only contain letters, numbers, dots, dashes and underscores');
|
||||
|
||||
const registerSchema = z.object({
|
||||
username: usernameSchema,
|
||||
password: z.string().min(8),
|
||||
});
|
||||
|
||||
const loginSchema = z.object({
|
||||
username: z.string(),
|
||||
password: z.string(),
|
||||
});
|
||||
|
||||
const forgotPasswordSchema = z.object({
|
||||
username: z.string().min(1),
|
||||
});
|
||||
|
||||
const resetPasswordSchema = z.object({
|
||||
token: z.string().min(1),
|
||||
password: z.string().min(8),
|
||||
});
|
||||
|
||||
const BCRYPT_ROUNDS = 12;
|
||||
|
||||
async function hashPassword(password: string): Promise<string> {
|
||||
return bcrypt.hash(password, BCRYPT_ROUNDS);
|
||||
}
|
||||
|
||||
async function verifyPassword(password: string, hash: string): Promise<boolean> {
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
||||
|
||||
function generateResetToken(): string {
|
||||
return crypto.randomBytes(32).toString('hex');
|
||||
}
|
||||
|
||||
router.post('/register', authLimiter, asyncHandler(async (req: Request, res: Response) => {
|
||||
const data = registerSchema.parse(req.body);
|
||||
|
||||
const existing = await db.select().from(users).where(eq(users.username, data.username)).limit(1);
|
||||
if (existing.length > 0) {
|
||||
throw new AppError('USERNAME_EXISTS', 'Username already registered', 409);
|
||||
}
|
||||
|
||||
const userId = generateId('user');
|
||||
const now = getCurrentTimestamp();
|
||||
|
||||
await db.insert(users).values({
|
||||
id: userId,
|
||||
username: data.username,
|
||||
passwordHash: await hashPassword(data.password),
|
||||
createdAt: now,
|
||||
});
|
||||
|
||||
const token = generateToken({ userId, username: data.username });
|
||||
|
||||
res.status(201).json({
|
||||
user: { id: userId, username: data.username },
|
||||
token,
|
||||
});
|
||||
}));
|
||||
|
||||
router.post('/login', loginLimiter, asyncHandler(async (req: Request, res: Response) => {
|
||||
const data = loginSchema.parse(req.body);
|
||||
|
||||
const user = await db.select().from(users).where(eq(users.username, data.username)).limit(1);
|
||||
if (user.length === 0 || !verifyPassword(data.password, user[0].passwordHash)) {
|
||||
throw new AppError('INVALID_CREDENTIALS', 'Invalid username or password', 401);
|
||||
}
|
||||
|
||||
const token = generateToken({ userId: user[0].id, username: user[0].username });
|
||||
|
||||
res.json({
|
||||
user: { id: user[0].id, username: user[0].username },
|
||||
token,
|
||||
});
|
||||
}));
|
||||
|
||||
router.get('/me', asyncHandler(async (req: Request, res: Response) => {
|
||||
if (!req.user) {
|
||||
throw new AppError('UNAUTHORIZED', 'Authentication required', 401);
|
||||
}
|
||||
|
||||
const user = await db.select().from(users).where(eq(users.id, req.user.userId)).limit(1);
|
||||
if (user.length === 0) {
|
||||
throw new AppError('NOT_FOUND', 'User not found', 404);
|
||||
}
|
||||
|
||||
res.json({ id: user[0].id, username: user[0].username });
|
||||
}));
|
||||
|
||||
router.post('/forgot-password', passwordResetLimiter, asyncHandler(async (req: Request, res: Response) => {
|
||||
const data = forgotPasswordSchema.parse(req.body);
|
||||
|
||||
const user = await db.select().from(users).where(eq(users.username, data.username)).limit(1);
|
||||
// Always return success to prevent username enumeration
|
||||
if (user.length === 0) {
|
||||
res.json({ success: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const resetToken = generateResetToken();
|
||||
const resetTokenExpiry = Date.now() + 3600000; // 1 hour
|
||||
|
||||
await db
|
||||
.update(users)
|
||||
.set({ resetToken, resetTokenExpiry })
|
||||
.where(eq(users.id, user[0].id));
|
||||
|
||||
// In production, send email with reset link
|
||||
// For now, return token in response (dev only)
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
res.json({ success: true, resetToken });
|
||||
} else {
|
||||
res.json({ success: true });
|
||||
}
|
||||
}));
|
||||
|
||||
router.post('/reset-password', passwordResetLimiter, asyncHandler(async (req: Request, res: Response) => {
|
||||
const data = resetPasswordSchema.parse(req.body);
|
||||
|
||||
const user = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.resetToken, data.token))
|
||||
.limit(1);
|
||||
|
||||
if (user.length === 0 || !user[0].resetTokenExpiry || user[0].resetTokenExpiry < Date.now()) {
|
||||
throw new AppError('INVALID_TOKEN', 'Invalid or expired reset token', 400);
|
||||
}
|
||||
|
||||
const newPasswordHash = await hashPassword(data.password);
|
||||
|
||||
await db
|
||||
.update(users)
|
||||
.set({ passwordHash: newPasswordHash, resetToken: null, resetTokenExpiry: null })
|
||||
.where(eq(users.id, user[0].id));
|
||||
|
||||
res.json({ success: true });
|
||||
}));
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { asyncHandler } from '../utils/asyncHandler';
|
||||
import { db } from '../db';
|
||||
import { categories } from '../db/schema';
|
||||
import { eq, and, desc, asc } from 'drizzle-orm';
|
||||
import { authMiddleware } from '../utils/auth';
|
||||
import { AppError } from '../middleware/errorHandler';
|
||||
import { categoryCreateSchema, categoryUpdateSchema } from '../utils/validation';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
router.get('/', asyncHandler(async (req: Request, res: Response) => {
|
||||
const userCategories = await db
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(eq(categories.userId, req.user!.userId))
|
||||
.orderBy(asc(categories.order), asc(categories.createdAt));
|
||||
|
||||
res.json({ categories: userCategories });
|
||||
}));
|
||||
|
||||
router.post('/', asyncHandler(async (req: Request, res: Response) => {
|
||||
const data = categoryCreateSchema.parse(req.body);
|
||||
const userId = req.user!.userId;
|
||||
const now = Date.now();
|
||||
|
||||
const maxOrder = await db
|
||||
.select({ order: categories.order })
|
||||
.from(categories)
|
||||
.where(eq(categories.userId, userId))
|
||||
.orderBy(desc(categories.order))
|
||||
.limit(1);
|
||||
|
||||
const newCategory = {
|
||||
id: `cat_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`,
|
||||
userId,
|
||||
name: data.name,
|
||||
color: data.color,
|
||||
order: data.order ?? (maxOrder[0]?.order ?? -1) + 1,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
await db.insert(categories).values(newCategory);
|
||||
|
||||
res.status(201).json(newCategory);
|
||||
}));
|
||||
|
||||
router.patch('/:id', asyncHandler(async (req: Request, res: Response) => {
|
||||
const data = categoryUpdateSchema.parse(req.body);
|
||||
const userId = req.user!.userId;
|
||||
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(and(eq(categories.id, req.params.id), eq(categories.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length === 0) {
|
||||
throw new AppError('NOT_FOUND', 'Category not found', 404);
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const updated = await db
|
||||
.update(categories)
|
||||
.set({ ...data, updatedAt: now })
|
||||
.where(and(eq(categories.id, req.params.id), eq(categories.userId, userId)))
|
||||
.returning();
|
||||
|
||||
res.json(updated[0]);
|
||||
}));
|
||||
|
||||
router.delete('/:id', asyncHandler(async (req: Request, res: Response) => {
|
||||
const userId = req.user!.userId;
|
||||
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(and(eq(categories.id, req.params.id), eq(categories.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length === 0) {
|
||||
throw new AppError('NOT_FOUND', 'Category not found', 404);
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(categories)
|
||||
.where(and(eq(categories.id, req.params.id), eq(categories.userId, userId)));
|
||||
|
||||
res.status(204).send();
|
||||
}));
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,194 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { asyncHandler } from '../utils/asyncHandler';
|
||||
import { db } from '../db';
|
||||
import { users, friendships } from '../db/schema';
|
||||
import { eq, and, or, ilike, ne, asc } from 'drizzle-orm';
|
||||
import { generateId } from '../utils/auth';
|
||||
import { authMiddleware } from '../utils/auth';
|
||||
import { AppError } from '../middleware/errorHandler';
|
||||
import { friendRequestSchema, searchQuerySchema } from '../utils/validation';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
interface FriendRow {
|
||||
id: string;
|
||||
username: string;
|
||||
requestId?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
async function findUsernames(ids: string[]): Promise<Map<string, string>> {
|
||||
if (ids.length === 0) return new Map();
|
||||
const rows = await db.select().from(users).where(or(...ids.map((id) => eq(users.id, id))));
|
||||
return new Map(rows.map((u) => [u.id, u.username]));
|
||||
}
|
||||
|
||||
router.get('/', asyncHandler(async (req: Request, res: Response) => {
|
||||
const userId = req.user!.userId;
|
||||
|
||||
const outgoingRows = await db
|
||||
.select()
|
||||
.from(friendships)
|
||||
.where(and(eq(friendships.userId, userId), eq(friendships.status, 'pending')));
|
||||
|
||||
const incomingRows = await db
|
||||
.select()
|
||||
.from(friendships)
|
||||
.where(and(eq(friendships.friendId, userId), eq(friendships.status, 'pending')));
|
||||
|
||||
const acceptedRows = await db
|
||||
.select()
|
||||
.from(friendships)
|
||||
.where(and(
|
||||
or(eq(friendships.userId, userId), eq(friendships.friendId, userId)),
|
||||
eq(friendships.status, 'accepted')
|
||||
));
|
||||
|
||||
const friendIds = acceptedRows.map((r) => (r.userId === userId ? r.friendId : r.userId));
|
||||
const outgoingIds = outgoingRows.map((r) => r.friendId);
|
||||
const incomingIds = incomingRows.map((r) => r.userId);
|
||||
const usernames = await findUsernames([...friendIds, ...outgoingIds, ...incomingIds]);
|
||||
|
||||
const friends: FriendRow[] = acceptedRows.map((r) => {
|
||||
const friendId = r.userId === userId ? r.friendId : r.userId;
|
||||
return { id: friendId, username: usernames.get(friendId) ?? '' };
|
||||
});
|
||||
|
||||
const outgoing: FriendRow[] = outgoingRows.map((r) => ({
|
||||
id: r.id,
|
||||
username: usernames.get(r.friendId) ?? '',
|
||||
requestId: r.id,
|
||||
status: 'pending',
|
||||
}));
|
||||
|
||||
const incoming: FriendRow[] = incomingRows.map((r) => ({
|
||||
id: r.userId,
|
||||
username: usernames.get(r.userId) ?? '',
|
||||
requestId: r.id,
|
||||
status: 'pending',
|
||||
}));
|
||||
|
||||
res.json({ friends, incoming, outgoing });
|
||||
}));
|
||||
|
||||
router.get('/search', asyncHandler(async (req: Request, res: Response) => {
|
||||
const { q } = searchQuerySchema.parse(req.query);
|
||||
const userId = req.user!.userId;
|
||||
|
||||
const results = await db
|
||||
.select({ id: users.id, username: users.username })
|
||||
.from(users)
|
||||
.where(and(
|
||||
ne(users.id, userId),
|
||||
ilike(users.username, `%${q}%`)
|
||||
))
|
||||
.orderBy(asc(users.username))
|
||||
.limit(20);
|
||||
|
||||
res.json(results);
|
||||
}));
|
||||
|
||||
router.post('/requests', asyncHandler(async (req: Request, res: Response) => {
|
||||
const { username } = friendRequestSchema.parse(req.body);
|
||||
const userId = req.user!.userId;
|
||||
|
||||
if (username.toLowerCase() === (await meUsername(userId))) {
|
||||
throw new AppError('SELF_REQUEST', 'You cannot add yourself', 400);
|
||||
}
|
||||
|
||||
const target = await db.select().from(users).where(eq(users.username, username)).limit(1);
|
||||
if (target.length === 0) {
|
||||
throw new AppError('USER_NOT_FOUND', 'No user with that username found', 404);
|
||||
}
|
||||
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(friendships)
|
||||
.where(or(
|
||||
and(eq(friendships.userId, userId), eq(friendships.friendId, target[0].id)),
|
||||
and(eq(friendships.userId, target[0].id), eq(friendships.friendId, userId))
|
||||
))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length > 0) {
|
||||
throw new AppError(
|
||||
'ALREADY_FRIENDS',
|
||||
existing[0].status === 'accepted' ? 'You are already friends' : 'Friend request already pending',
|
||||
409
|
||||
);
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
await db.insert(friendships).values({
|
||||
id: generateId('friend'),
|
||||
userId,
|
||||
friendId: target[0].id,
|
||||
status: 'pending',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
res.status(201).json({ success: true });
|
||||
}));
|
||||
|
||||
router.post('/requests/:id/accept', asyncHandler(async (req: Request, res: Response) => {
|
||||
const userId = req.user!.userId;
|
||||
const requestId = req.params.id;
|
||||
|
||||
const row = await db.select().from(friendships).where(eq(friendships.id, requestId)).limit(1);
|
||||
if (row.length === 0) {
|
||||
throw new AppError('NOT_FOUND', 'Request not found', 404);
|
||||
}
|
||||
if (row[0].friendId !== userId) {
|
||||
throw new AppError('FORBIDDEN', 'This request was not sent to you', 403);
|
||||
}
|
||||
if (row[0].status !== 'pending') {
|
||||
throw new AppError('INVALID_STATE', 'Request is no longer pending', 409);
|
||||
}
|
||||
|
||||
await db
|
||||
.update(friendships)
|
||||
.set({ status: 'accepted', updatedAt: Date.now() })
|
||||
.where(eq(friendships.id, requestId));
|
||||
|
||||
res.json({ success: true });
|
||||
}));
|
||||
|
||||
router.delete('/requests/:id', asyncHandler(async (req: Request, res: Response) => {
|
||||
const userId = req.user!.userId;
|
||||
const requestId = req.params.id;
|
||||
|
||||
const row = await db.select().from(friendships).where(eq(friendships.id, requestId)).limit(1);
|
||||
if (row.length === 0) {
|
||||
throw new AppError('NOT_FOUND', 'Request not found', 404);
|
||||
}
|
||||
if (row[0].userId !== userId && row[0].friendId !== userId) {
|
||||
throw new AppError('FORBIDDEN', 'Not allowed', 403);
|
||||
}
|
||||
|
||||
await db.delete(friendships).where(eq(friendships.id, requestId));
|
||||
res.json({ success: true });
|
||||
}));
|
||||
|
||||
router.delete('/:friendId', asyncHandler(async (req: Request, res: Response) => {
|
||||
const userId = req.user!.userId;
|
||||
const friendId = req.params.friendId;
|
||||
|
||||
await db
|
||||
.delete(friendships)
|
||||
.where(or(
|
||||
and(eq(friendships.userId, userId), eq(friendships.friendId, friendId)),
|
||||
and(eq(friendships.userId, friendId), eq(friendships.friendId, userId))
|
||||
));
|
||||
|
||||
res.json({ success: true });
|
||||
}));
|
||||
|
||||
async function meUsername(userId: string): Promise<string> {
|
||||
const me = await db.select().from(users).where(eq(users.id, userId)).limit(1);
|
||||
return me.length > 0 ? me[0].username.toLowerCase() : '';
|
||||
}
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { asyncHandler } from '../utils/asyncHandler';
|
||||
import { db } from '../db';
|
||||
import { repeatProfiles } from '../db/schema';
|
||||
import { eq, and, asc } from 'drizzle-orm';
|
||||
import { authMiddleware } from '../utils/auth';
|
||||
import { generateId } from '../utils/auth';
|
||||
import { AppError } from '../middleware/errorHandler';
|
||||
import { repeatProfileSchema, repeatProfileUpdateSchema } from '../utils/validation';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
router.get('/', asyncHandler(async (req: Request, res: Response) => {
|
||||
const profiles = await db
|
||||
.select()
|
||||
.from(repeatProfiles)
|
||||
.where(eq(repeatProfiles.userId, req.user!.userId))
|
||||
.orderBy(asc(repeatProfiles.createdAt));
|
||||
|
||||
res.json({ profiles });
|
||||
}));
|
||||
|
||||
router.post('/', asyncHandler(async (req: Request, res: Response) => {
|
||||
const data = repeatProfileSchema.parse(req.body);
|
||||
const userId = req.user!.userId;
|
||||
const now = Date.now();
|
||||
|
||||
const newProfile = {
|
||||
id: generateId('rp'),
|
||||
userId,
|
||||
name: data.name,
|
||||
repeat: data.repeat,
|
||||
repeatInterval: data.repeatInterval,
|
||||
repeatDays: data.repeatDays,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
await db.insert(repeatProfiles).values(newProfile);
|
||||
|
||||
res.status(201).json(newProfile);
|
||||
}));
|
||||
|
||||
router.patch('/:id', asyncHandler(async (req: Request, res: Response) => {
|
||||
const data = repeatProfileUpdateSchema.parse(req.body);
|
||||
const userId = req.user!.userId;
|
||||
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(repeatProfiles)
|
||||
.where(and(eq(repeatProfiles.id, req.params.id), eq(repeatProfiles.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length === 0) {
|
||||
throw new AppError('NOT_FOUND', 'Repeat profile not found', 404);
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const updated = await db
|
||||
.update(repeatProfiles)
|
||||
.set({ ...data, updatedAt: now })
|
||||
.where(and(eq(repeatProfiles.id, req.params.id), eq(repeatProfiles.userId, userId)))
|
||||
.returning();
|
||||
|
||||
res.json(updated[0]);
|
||||
}));
|
||||
|
||||
router.delete('/:id', asyncHandler(async (req: Request, res: Response) => {
|
||||
const userId = req.user!.userId;
|
||||
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(repeatProfiles)
|
||||
.where(and(eq(repeatProfiles.id, req.params.id), eq(repeatProfiles.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length === 0) {
|
||||
throw new AppError('NOT_FOUND', 'Repeat profile not found', 404);
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(repeatProfiles)
|
||||
.where(and(eq(repeatProfiles.id, req.params.id), eq(repeatProfiles.userId, userId)));
|
||||
|
||||
res.status(204).send();
|
||||
}));
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,154 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { asyncHandler } from '../utils/asyncHandler';
|
||||
import { db } from '../db';
|
||||
import { subtasks, tasks } from '../db/schema';
|
||||
import { eq, and, asc, desc } from 'drizzle-orm';
|
||||
import { authMiddleware } from '../utils/auth';
|
||||
import { AppError } from '../middleware/errorHandler';
|
||||
import { subtaskCreateSchema, subtaskUpdateSchema } from '../utils/validation';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
router.get('/task/:taskId', asyncHandler(async (req: Request, res: Response) => {
|
||||
const userId = req.user!.userId;
|
||||
|
||||
// Verify task exists and belongs to user
|
||||
const task = await db
|
||||
.select()
|
||||
.from(tasks)
|
||||
.where(and(eq(tasks.id, req.params.taskId), eq(tasks.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (task.length === 0) {
|
||||
throw new AppError('NOT_FOUND', 'Task not found', 404);
|
||||
}
|
||||
|
||||
const taskSubtasks = await db
|
||||
.select()
|
||||
.from(subtasks)
|
||||
.where(and(eq(subtasks.taskId, req.params.taskId), eq(subtasks.userId, userId)))
|
||||
.orderBy(asc(subtasks.order));
|
||||
|
||||
res.json({ subtasks: taskSubtasks });
|
||||
}));
|
||||
|
||||
router.post('/task/:taskId', asyncHandler(async (req: Request, res: Response) => {
|
||||
const data = subtaskCreateSchema.parse(req.body);
|
||||
const userId = req.user!.userId;
|
||||
|
||||
const task = await db
|
||||
.select()
|
||||
.from(tasks)
|
||||
.where(and(eq(tasks.id, req.params.taskId), eq(tasks.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (task.length === 0) {
|
||||
throw new AppError('NOT_FOUND', 'Task not found', 404);
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const maxOrder = await db
|
||||
.select({ order: subtasks.order })
|
||||
.from(subtasks)
|
||||
.where(and(eq(subtasks.taskId, req.params.taskId), eq(subtasks.userId, userId)))
|
||||
.orderBy(desc(subtasks.order))
|
||||
.limit(1);
|
||||
|
||||
const subtaskId = `sub_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
||||
|
||||
const newSubtask = {
|
||||
id: subtaskId,
|
||||
userId,
|
||||
taskId: req.params.taskId,
|
||||
title: data.title,
|
||||
description: data.description ?? '',
|
||||
priority: data.priority ?? 'none',
|
||||
completed: false,
|
||||
dueDate: data.dueDate ?? 0,
|
||||
dueTime: data.dueTime ?? '',
|
||||
endTime: data.endTime ?? '',
|
||||
allDay: data.allDay ?? false,
|
||||
repeat: data.repeat ?? 'none',
|
||||
repeatInterval: data.repeatInterval ?? 1,
|
||||
repeatDays: data.repeatDays ?? '',
|
||||
seriesId: data.seriesId ?? '',
|
||||
reminder: data.reminder ?? 'none',
|
||||
reminders: data.reminders ?? '',
|
||||
assigneeId: data.assigneeId ?? null,
|
||||
order: data.order ?? (maxOrder[0]?.order ?? -1) + 1,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
await db.insert(subtasks).values(newSubtask);
|
||||
|
||||
// Update task updatedAt
|
||||
await db
|
||||
.update(tasks)
|
||||
.set({ updatedAt: now })
|
||||
.where(and(eq(tasks.id, req.params.taskId), eq(tasks.userId, userId)));
|
||||
|
||||
res.status(201).json(newSubtask);
|
||||
}));
|
||||
|
||||
router.patch('/:id', asyncHandler(async (req: Request, res: Response) => {
|
||||
const data = subtaskUpdateSchema.parse(req.body);
|
||||
const userId = req.user!.userId;
|
||||
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(subtasks)
|
||||
.where(and(eq(subtasks.id, req.params.id), eq(subtasks.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length === 0) {
|
||||
throw new AppError('NOT_FOUND', 'Subtask not found', 404);
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const updated = await db
|
||||
.update(subtasks)
|
||||
.set({ ...data, updatedAt: now })
|
||||
.where(and(eq(subtasks.id, req.params.id), eq(subtasks.userId, userId)))
|
||||
.returning();
|
||||
|
||||
// Update task updatedAt
|
||||
await db
|
||||
.update(tasks)
|
||||
.set({ updatedAt: now })
|
||||
.where(and(eq(tasks.id, existing[0].taskId), eq(tasks.userId, userId)));
|
||||
|
||||
res.json(updated[0]);
|
||||
}));
|
||||
|
||||
router.delete('/:id', asyncHandler(async (req: Request, res: Response) => {
|
||||
const userId = req.user!.userId;
|
||||
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(subtasks)
|
||||
.where(and(eq(subtasks.id, req.params.id), eq(subtasks.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length === 0) {
|
||||
throw new AppError('NOT_FOUND', 'Subtask not found', 404);
|
||||
}
|
||||
|
||||
const taskId = existing[0].taskId;
|
||||
|
||||
await db
|
||||
.delete(subtasks)
|
||||
.where(and(eq(subtasks.id, req.params.id), eq(subtasks.userId, userId)));
|
||||
|
||||
// Update task updatedAt
|
||||
await db
|
||||
.update(tasks)
|
||||
.set({ updatedAt: Date.now() })
|
||||
.where(and(eq(tasks.id, taskId), eq(tasks.userId, userId)));
|
||||
|
||||
res.status(204).send();
|
||||
}));
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,381 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { asyncHandler } from '../utils/asyncHandler';
|
||||
import { db } from '../db';
|
||||
import { categories, tasks, subtasks, repeatProfiles, users, friendships } from '../db/schema';
|
||||
import { eq, and, gte, lte, asc, or, inArray, sql } from 'drizzle-orm';
|
||||
import { authMiddleware } from '../utils/auth';
|
||||
import { AppError } from '../middleware/errorHandler';
|
||||
import { syncQuerySchema, pushChangesSchema, canCompleteTask } from '../utils/validation';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
router.get('/', asyncHandler(async (req: Request, res: Response) => {
|
||||
const { since } = syncQuerySchema.parse(req.query);
|
||||
const userId = req.user!.userId;
|
||||
|
||||
const sinceDate = since;
|
||||
|
||||
// Fetch categories changed since timestamp
|
||||
const changedCategories = await db
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(and(eq(categories.userId, userId), gte(categories.updatedAt, sinceDate)))
|
||||
.orderBy(asc(categories.updatedAt));
|
||||
|
||||
// Fetch tasks changed since timestamp
|
||||
const changedTasks = await db
|
||||
.select()
|
||||
.from(tasks)
|
||||
.where(and(eq(tasks.userId, userId), gte(tasks.updatedAt, sinceDate)))
|
||||
.orderBy(asc(tasks.updatedAt));
|
||||
|
||||
// Fetch subtasks changed since timestamp
|
||||
const taskIds = changedTasks.map(t => t.id);
|
||||
let changedSubtasks: any[] = [];
|
||||
|
||||
if (taskIds.length > 0) {
|
||||
changedSubtasks = await db
|
||||
.select()
|
||||
.from(subtasks)
|
||||
.where(and(eq(subtasks.userId, userId), gte(subtasks.updatedAt, sinceDate)))
|
||||
.orderBy(asc(subtasks.updatedAt));
|
||||
} else {
|
||||
// Also fetch subtasks for tasks that might have been deleted (we track by updatedAt)
|
||||
changedSubtasks = await db
|
||||
.select()
|
||||
.from(subtasks)
|
||||
.where(and(eq(subtasks.userId, userId), gte(subtasks.updatedAt, sinceDate)))
|
||||
.orderBy(asc(subtasks.updatedAt));
|
||||
}
|
||||
|
||||
// Fetch repeat profiles changed since timestamp
|
||||
const changedRepeatProfiles = await db
|
||||
.select()
|
||||
.from(repeatProfiles)
|
||||
.where(and(eq(repeatProfiles.userId, userId), gte(repeatProfiles.updatedAt, sinceDate)))
|
||||
.orderBy(asc(repeatProfiles.updatedAt));
|
||||
|
||||
// Fetch friendships changed since timestamp
|
||||
const changedFriendships = await db
|
||||
.select()
|
||||
.from(friendships)
|
||||
.where(and(
|
||||
or(eq(friendships.userId, userId), eq(friendships.friendId, userId)),
|
||||
gte(friendships.updatedAt, sinceDate)
|
||||
))
|
||||
.orderBy(asc(friendships.updatedAt));
|
||||
|
||||
const timestamp = Date.now();
|
||||
|
||||
res.json({
|
||||
categories: changedCategories,
|
||||
tasks: changedTasks,
|
||||
subtasks: changedSubtasks,
|
||||
repeatProfiles: changedRepeatProfiles,
|
||||
friendships: changedFriendships,
|
||||
timestamp,
|
||||
});
|
||||
}));
|
||||
|
||||
router.post('/push', asyncHandler(async (req: Request, res: Response) => {
|
||||
const data = pushChangesSchema.parse(req.body);
|
||||
const userId = req.user!.userId;
|
||||
const conflicts: any[] = [];
|
||||
const timestamp = Date.now();
|
||||
|
||||
try {
|
||||
await db.transaction(async (tx) => {
|
||||
// Ensure every referenced category exists (FK integrity) so a stale or
|
||||
// never-synced category reference cannot fail the entire push. Missing
|
||||
// categories are recreated as a fallback and the client heals on pull.
|
||||
const referencedCategoryIds = new Set<string>();
|
||||
for (const task of data.changes.tasks ?? []) {
|
||||
if (task.categoryId) referencedCategoryIds.add(task.categoryId);
|
||||
}
|
||||
for (const sub of data.changes.subtasks ?? []) {
|
||||
const task = (data.changes.tasks ?? []).find((t) => t.id === sub.taskId);
|
||||
if (task?.categoryId) referencedCategoryIds.add(task.categoryId);
|
||||
}
|
||||
if (referencedCategoryIds.size > 0) {
|
||||
const existing = await tx
|
||||
.select({ id: categories.id })
|
||||
.from(categories)
|
||||
.where(inArray(categories.id, [...referencedCategoryIds]));
|
||||
const existingIds = new Set(existing.map((c) => c.id));
|
||||
const missing = [...referencedCategoryIds].filter((id) => !existingIds.has(id));
|
||||
if (missing.length > 0) {
|
||||
const rows = await tx
|
||||
.select({ max: sql<number>`max(${categories.order})` })
|
||||
.from(categories)
|
||||
.where(eq(categories.userId, userId));
|
||||
const startOrder = (rows[0]?.max ?? -1) + 1;
|
||||
await tx.insert(categories).values(
|
||||
missing.map((id, i) => ({
|
||||
id,
|
||||
userId,
|
||||
name: 'Default',
|
||||
color: '#9E9E9E',
|
||||
order: startOrder + i,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Process categories
|
||||
if (data.changes.categories && data.changes.categories.length > 0) {
|
||||
for (const cat of data.changes.categories) {
|
||||
const existing = await tx
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(and(eq(categories.id, cat.id), eq(categories.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length > 0) {
|
||||
// Check for conflict
|
||||
if (existing[0].updatedAt > cat.updatedAt) {
|
||||
conflicts.push({
|
||||
entity: 'categories',
|
||||
id: cat.id,
|
||||
serverVersion: existing[0],
|
||||
clientVersion: cat,
|
||||
resolution: 'server_wins',
|
||||
});
|
||||
continue; // Server wins
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(categories)
|
||||
.set({
|
||||
name: cat.name,
|
||||
color: cat.color,
|
||||
order: cat.order,
|
||||
updatedAt: cat.updatedAt,
|
||||
})
|
||||
.where(and(eq(categories.id, cat.id), eq(categories.userId, userId)));
|
||||
} else {
|
||||
await tx.insert(categories).values({
|
||||
...cat,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process tasks
|
||||
if (data.changes.tasks && data.changes.tasks.length > 0) {
|
||||
for (const task of data.changes.tasks) {
|
||||
const existing = await tx
|
||||
.select()
|
||||
.from(tasks)
|
||||
.where(and(eq(tasks.id, task.id), eq(tasks.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length > 0) {
|
||||
// Check for conflict
|
||||
if (existing[0].updatedAt > task.updatedAt) {
|
||||
conflicts.push({
|
||||
entity: 'tasks',
|
||||
id: task.id,
|
||||
serverVersion: existing[0],
|
||||
clientVersion: task,
|
||||
resolution: 'server_wins',
|
||||
});
|
||||
continue; // Server wins
|
||||
}
|
||||
|
||||
// Prevent completing tasks with future due dates
|
||||
if (task.completed === true) {
|
||||
const effectiveDueDate = task.dueDate ?? existing[0].dueDate;
|
||||
if (!canCompleteTask(effectiveDueDate)) {
|
||||
conflicts.push({
|
||||
entity: 'tasks',
|
||||
id: task.id,
|
||||
serverVersion: existing[0],
|
||||
clientVersion: task,
|
||||
resolution: 'server_wins',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(tasks)
|
||||
.set({
|
||||
title: task.title,
|
||||
description: task.description,
|
||||
categoryId: task.categoryId,
|
||||
priority: task.priority,
|
||||
completed: task.completed,
|
||||
dueDate: task.dueDate,
|
||||
dueTime: task.dueTime ?? '',
|
||||
endTime: task.endTime ?? '',
|
||||
allDay: task.allDay ?? false,
|
||||
repeat: task.repeat ?? 'none',
|
||||
repeatInterval: task.repeatInterval ?? 1,
|
||||
repeatDays: task.repeatDays ?? '',
|
||||
seriesId: task.seriesId ?? '',
|
||||
reminder: task.reminder ?? 'none',
|
||||
reminders: task.reminders ?? '',
|
||||
assigneeId: task.assigneeId ?? null,
|
||||
updatedAt: task.updatedAt,
|
||||
})
|
||||
.where(and(eq(tasks.id, task.id), eq(tasks.userId, userId)));
|
||||
} else {
|
||||
await tx.insert(tasks).values({
|
||||
...task,
|
||||
allDay: task.allDay ?? false,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process subtasks
|
||||
if (data.changes.subtasks && data.changes.subtasks.length > 0) {
|
||||
for (const sub of data.changes.subtasks) {
|
||||
const existing = await tx
|
||||
.select()
|
||||
.from(subtasks)
|
||||
.where(and(eq(subtasks.id, sub.id), eq(subtasks.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length > 0) {
|
||||
// Check for conflict
|
||||
if (existing[0].updatedAt > sub.updatedAt) {
|
||||
conflicts.push({
|
||||
entity: 'subtasks',
|
||||
id: sub.id,
|
||||
serverVersion: existing[0],
|
||||
clientVersion: sub,
|
||||
resolution: 'server_wins',
|
||||
});
|
||||
continue; // Server wins
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(subtasks)
|
||||
.set({
|
||||
taskId: sub.taskId,
|
||||
title: sub.title,
|
||||
description: sub.description ?? '',
|
||||
priority: sub.priority ?? 'none',
|
||||
completed: sub.completed,
|
||||
dueDate: sub.dueDate ?? 0,
|
||||
dueTime: sub.dueTime ?? '',
|
||||
endTime: sub.endTime ?? '',
|
||||
allDay: sub.allDay ?? false,
|
||||
repeat: sub.repeat ?? 'none',
|
||||
repeatInterval: sub.repeatInterval ?? 1,
|
||||
repeatDays: sub.repeatDays ?? '',
|
||||
seriesId: sub.seriesId ?? '',
|
||||
reminder: sub.reminder ?? 'none',
|
||||
reminders: sub.reminders ?? '',
|
||||
assigneeId: sub.assigneeId ?? null,
|
||||
order: sub.order,
|
||||
updatedAt: sub.updatedAt,
|
||||
})
|
||||
.where(and(eq(subtasks.id, sub.id), eq(subtasks.userId, userId)));
|
||||
} else {
|
||||
await tx.insert(subtasks).values({
|
||||
...sub,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process repeat profiles
|
||||
if (data.changes.repeatProfiles && data.changes.repeatProfiles.length > 0) {
|
||||
for (const profile of data.changes.repeatProfiles) {
|
||||
const existing = await tx
|
||||
.select()
|
||||
.from(repeatProfiles)
|
||||
.where(and(eq(repeatProfiles.id, profile.id), eq(repeatProfiles.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length > 0) {
|
||||
// Check for conflict
|
||||
if (existing[0].updatedAt > profile.updatedAt) {
|
||||
conflicts.push({
|
||||
entity: 'repeatProfiles',
|
||||
id: profile.id,
|
||||
serverVersion: existing[0],
|
||||
clientVersion: profile,
|
||||
resolution: 'server_wins',
|
||||
});
|
||||
continue; // Server wins
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(repeatProfiles)
|
||||
.set({
|
||||
name: profile.name,
|
||||
repeat: profile.repeat,
|
||||
repeatInterval: profile.repeatInterval,
|
||||
repeatDays: profile.repeatDays,
|
||||
updatedAt: profile.updatedAt,
|
||||
})
|
||||
.where(and(eq(repeatProfiles.id, profile.id), eq(repeatProfiles.userId, userId)));
|
||||
} else {
|
||||
await tx.insert(repeatProfiles).values({
|
||||
...profile,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process friendships
|
||||
if (data.changes.friendships && data.changes.friendships.length > 0) {
|
||||
for (const friendship of data.changes.friendships) {
|
||||
const existing = await tx
|
||||
.select()
|
||||
.from(friendships)
|
||||
.where(eq(friendships.id, friendship.id))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length > 0) {
|
||||
// Check for conflict
|
||||
if (existing[0].updatedAt > friendship.updatedAt) {
|
||||
conflicts.push({
|
||||
entity: 'friendships',
|
||||
id: friendship.id,
|
||||
serverVersion: existing[0],
|
||||
clientVersion: friendship,
|
||||
resolution: 'server_wins',
|
||||
});
|
||||
continue; // Server wins
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(friendships)
|
||||
.set({
|
||||
userId: friendship.userId,
|
||||
friendId: friendship.friendId,
|
||||
status: friendship.status,
|
||||
updatedAt: friendship.updatedAt,
|
||||
})
|
||||
.where(eq(friendships.id, friendship.id));
|
||||
} else {
|
||||
await tx.insert(friendships).values(friendship);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Sync push error:', error);
|
||||
throw new AppError('SERVER_ERROR', 'Failed to process sync push', 500);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
timestamp,
|
||||
conflicts,
|
||||
});
|
||||
}));
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,326 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { asyncHandler } from '../utils/asyncHandler';
|
||||
import { db } from '../db';
|
||||
import { tasks, subtasks, categories } from '../db/schema';
|
||||
import { eq, and, desc, asc, gte, lte, inArray, sql } from 'drizzle-orm';
|
||||
import { authMiddleware } from '../utils/auth';
|
||||
import { AppError } from '../middleware/errorHandler';
|
||||
import { taskCreateSchema, taskUpdateSchema, taskQuerySchema, canCompleteTask } from '../utils/validation';
|
||||
import { z } from 'zod';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
function applyTaskFilters(query: any, userId: string, filters: any) {
|
||||
const conditions = [eq(tasks.userId, userId)];
|
||||
|
||||
if (filters.categoryId) {
|
||||
conditions.push(eq(tasks.categoryId, filters.categoryId));
|
||||
}
|
||||
if (filters.completed !== undefined) {
|
||||
conditions.push(eq(tasks.completed, filters.completed));
|
||||
}
|
||||
if (filters.dueBefore) {
|
||||
conditions.push(lte(tasks.dueDate, filters.dueBefore));
|
||||
}
|
||||
if (filters.dueAfter) {
|
||||
conditions.push(gte(tasks.dueDate, filters.dueAfter));
|
||||
}
|
||||
if (filters.priority) {
|
||||
conditions.push(eq(tasks.priority, filters.priority));
|
||||
}
|
||||
|
||||
return query.where(and(...conditions));
|
||||
}
|
||||
|
||||
function applyTaskSorting(query: any, sortBy: string = 'dueDate', sortOrder: string = 'asc') {
|
||||
const orderFn = sortOrder === 'desc' ? desc : asc;
|
||||
const columnMap: Record<string, any> = {
|
||||
dueDate: tasks.dueDate,
|
||||
priority: tasks.priority,
|
||||
createdAt: tasks.createdAt,
|
||||
title: tasks.title,
|
||||
};
|
||||
return query.orderBy(orderFn(columnMap[sortBy] || tasks.dueDate));
|
||||
}
|
||||
|
||||
router.get('/', asyncHandler(async (req: Request, res: Response) => {
|
||||
const filters = taskQuerySchema.parse(req.query);
|
||||
const userId = req.user!.userId;
|
||||
|
||||
let query: any = db.select().from(tasks);
|
||||
query = applyTaskFilters(query, userId, filters);
|
||||
query = applyTaskSorting(query, filters.sortBy, filters.sortOrder);
|
||||
|
||||
const limit = filters.limit ?? 50;
|
||||
const offset = filters.offset ?? 0;
|
||||
query = query.limit(limit).offset(offset);
|
||||
|
||||
const results = await query;
|
||||
|
||||
// Fetch subtasks for each task
|
||||
const taskIds = results.map((t: { id: string }) => t.id);
|
||||
let taskSubtasks: any[] = [];
|
||||
if (taskIds.length > 0) {
|
||||
taskSubtasks = await db
|
||||
.select()
|
||||
.from(subtasks)
|
||||
.where(and(eq(subtasks.userId, userId), inArray(subtasks.taskId, taskIds)))
|
||||
.orderBy(asc(subtasks.order));
|
||||
}
|
||||
|
||||
const subtasksByTask = taskSubtasks.reduce((acc, st) => {
|
||||
if (!acc[st.taskId]) acc[st.taskId] = [];
|
||||
acc[st.taskId].push(st);
|
||||
return acc;
|
||||
}, {} as Record<string, any[]>);
|
||||
|
||||
const tasksWithSubtasks = results.map((task: any) => ({
|
||||
...task,
|
||||
subtasks: subtasksByTask[task.id] || [],
|
||||
}));
|
||||
|
||||
// Get total count
|
||||
const countQuery: any = db.select({ count: sql`count(*)` }).from(tasks);
|
||||
const countResult = await applyTaskFilters(countQuery, userId, filters);
|
||||
const total = Number(countResult[0]?.count ?? 0);
|
||||
|
||||
res.json({ tasks: tasksWithSubtasks, total, limit, offset });
|
||||
}));
|
||||
|
||||
router.get('/:id', asyncHandler(async (req: Request, res: Response) => {
|
||||
const userId = req.user!.userId;
|
||||
|
||||
const task = await db
|
||||
.select()
|
||||
.from(tasks)
|
||||
.where(and(eq(tasks.id, req.params.id), eq(tasks.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (task.length === 0) {
|
||||
throw new AppError('NOT_FOUND', 'Task not found', 404);
|
||||
}
|
||||
|
||||
const taskSubtasks = await db
|
||||
.select()
|
||||
.from(subtasks)
|
||||
.where(and(eq(subtasks.taskId, req.params.id), eq(subtasks.userId, userId)))
|
||||
.orderBy(asc(subtasks.order));
|
||||
|
||||
res.json({ ...task[0], subtasks: taskSubtasks });
|
||||
}));
|
||||
|
||||
router.post('/', asyncHandler(async (req: Request, res: Response) => {
|
||||
const data = taskCreateSchema.parse(req.body);
|
||||
const userId = req.user!.userId;
|
||||
const now = Date.now();
|
||||
|
||||
// Verify category exists and belongs to user
|
||||
const cat = await db
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(and(eq(categories.id, data.categoryId), eq(categories.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (cat.length === 0) {
|
||||
throw new AppError('NOT_FOUND', 'Category not found', 404);
|
||||
}
|
||||
|
||||
const taskId = `task_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
||||
|
||||
const newTask = {
|
||||
id: taskId,
|
||||
userId,
|
||||
categoryId: data.categoryId,
|
||||
title: data.title,
|
||||
description: data.description ?? '',
|
||||
priority: data.priority ?? 'none',
|
||||
completed: false,
|
||||
dueDate: data.dueDate ?? 0,
|
||||
dueTime: data.dueTime ?? '',
|
||||
endTime: data.endTime ?? '',
|
||||
allDay: data.allDay ?? false,
|
||||
assigneeId: data.assigneeId ?? null,
|
||||
reminder: data.reminder ?? 'none',
|
||||
reminders: data.reminders ?? '',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
await db.insert(tasks).values(newTask);
|
||||
|
||||
// Create subtasks if provided
|
||||
if (data.subtasks && data.subtasks.length > 0) {
|
||||
const subtaskValues = data.subtasks.map((st, index) => ({
|
||||
id: `sub_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}${index}`,
|
||||
userId,
|
||||
taskId,
|
||||
title: st.title,
|
||||
completed: false,
|
||||
order: index,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}));
|
||||
await db.insert(subtasks).values(subtaskValues);
|
||||
}
|
||||
|
||||
const createdSubtasks = await db
|
||||
.select()
|
||||
.from(subtasks)
|
||||
.where(and(eq(subtasks.taskId, taskId), eq(subtasks.userId, userId)))
|
||||
.orderBy(asc(subtasks.order));
|
||||
|
||||
res.status(201).json({ ...newTask, subtasks: createdSubtasks });
|
||||
}));
|
||||
|
||||
router.patch('/:id', asyncHandler(async (req: Request, res: Response) => {
|
||||
const data = taskUpdateSchema.parse(req.body);
|
||||
const userId = req.user!.userId;
|
||||
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(tasks)
|
||||
.where(and(eq(tasks.id, req.params.id), eq(tasks.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length === 0) {
|
||||
throw new AppError('NOT_FOUND', 'Task not found', 404);
|
||||
}
|
||||
|
||||
// Verify category if provided
|
||||
if (data.categoryId) {
|
||||
const cat = await db
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(and(eq(categories.id, data.categoryId), eq(categories.userId, userId)))
|
||||
.limit(1);
|
||||
if (cat.length === 0) {
|
||||
throw new AppError('NOT_FOUND', 'Category not found', 404);
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent completing tasks with future due dates
|
||||
if (data.completed === true) {
|
||||
const effectiveDueDate = data.dueDate ?? existing[0].dueDate;
|
||||
if (!canCompleteTask(effectiveDueDate)) {
|
||||
throw new AppError('VALIDATION_ERROR', 'Cannot mark a task as completed if its due date is in the future', 400);
|
||||
}
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const updated = await db
|
||||
.update(tasks)
|
||||
.set({ ...data, updatedAt: now })
|
||||
.where(and(eq(tasks.id, req.params.id), eq(tasks.userId, userId)))
|
||||
.returning();
|
||||
|
||||
const taskSubtasks = await db
|
||||
.select()
|
||||
.from(subtasks)
|
||||
.where(and(eq(subtasks.taskId, req.params.id), eq(subtasks.userId, userId)))
|
||||
.orderBy(asc(subtasks.order));
|
||||
|
||||
res.json({ ...updated[0], subtasks: taskSubtasks });
|
||||
}));
|
||||
|
||||
router.delete('/:id', asyncHandler(async (req: Request, res: Response) => {
|
||||
const userId = req.user!.userId;
|
||||
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(tasks)
|
||||
.where(and(eq(tasks.id, req.params.id), eq(tasks.userId, userId)))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length === 0) {
|
||||
throw new AppError('NOT_FOUND', 'Task not found', 404);
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(tasks)
|
||||
.where(and(eq(tasks.id, req.params.id), eq(tasks.userId, userId)));
|
||||
|
||||
res.status(204).send();
|
||||
}));
|
||||
|
||||
router.post('/batch', asyncHandler(async (req: Request, res: Response) => {
|
||||
const { operations } = z.object({
|
||||
operations: z.array(
|
||||
z.union([
|
||||
z.object({ type: z.literal('create'), data: taskCreateSchema }),
|
||||
z.object({ type: z.literal('update'), id: z.string(), data: taskUpdateSchema }),
|
||||
z.object({ type: z.literal('delete'), id: z.string() }),
|
||||
])
|
||||
),
|
||||
}).parse(req.body);
|
||||
|
||||
const userId = req.user!.userId;
|
||||
const results: any[] = [];
|
||||
|
||||
for (const op of operations) {
|
||||
try {
|
||||
if (op.type === 'create') {
|
||||
const cat = await db
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(and(eq(categories.id, op.data.categoryId), eq(categories.userId, userId)))
|
||||
.limit(1);
|
||||
if (cat.length === 0) throw new AppError('NOT_FOUND', 'Category not found', 404);
|
||||
|
||||
const taskId = `task_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
||||
const now = Date.now();
|
||||
|
||||
await db.insert(tasks).values({
|
||||
id: taskId,
|
||||
userId,
|
||||
...op.data,
|
||||
completed: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
results.push({ id: taskId, success: true });
|
||||
} else if (op.type === 'update') {
|
||||
if (op.data.completed === true) {
|
||||
let effectiveDueDate: number;
|
||||
if (op.data.dueDate !== undefined) {
|
||||
effectiveDueDate = op.data.dueDate;
|
||||
} else {
|
||||
const existingTask = await db
|
||||
.select({ dueDate: tasks.dueDate })
|
||||
.from(tasks)
|
||||
.where(and(eq(tasks.id, op.id), eq(tasks.userId, userId)))
|
||||
.limit(1);
|
||||
if (existingTask.length === 0) throw new AppError('NOT_FOUND', 'Task not found', 404);
|
||||
effectiveDueDate = existingTask[0].dueDate;
|
||||
}
|
||||
if (!canCompleteTask(effectiveDueDate)) {
|
||||
throw new AppError('VALIDATION_ERROR', 'Cannot mark a task as completed if its due date is in the future', 400);
|
||||
}
|
||||
}
|
||||
|
||||
await db
|
||||
.update(tasks)
|
||||
.set({ ...op.data, updatedAt: Date.now() })
|
||||
.where(and(eq(tasks.id, op.id), eq(tasks.userId, userId)));
|
||||
results.push({ id: op.id, success: true });
|
||||
} else if (op.type === 'delete') {
|
||||
await db
|
||||
.delete(tasks)
|
||||
.where(and(eq(tasks.id, op.id), eq(tasks.userId, userId)));
|
||||
results.push({ id: op.id, success: true });
|
||||
}
|
||||
} catch (error) {
|
||||
results.push({
|
||||
id: 'id' in op ? op.id : 'unknown',
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ results });
|
||||
}));
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { asyncHandler } from '../utils/asyncHandler';
|
||||
import { db } from '../db';
|
||||
import { users, userSettings, categories } from '../db/schema';
|
||||
import { eq, and, ilike } from 'drizzle-orm';
|
||||
import { authMiddleware } from '../utils/auth';
|
||||
import { AppError } from '../middleware/errorHandler';
|
||||
import { userSettingsSchema } from '../utils/validation';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
router.get('/me', asyncHandler(async (req: Request, res: Response) => {
|
||||
const userId = req.user!.userId;
|
||||
|
||||
const user = await db.select().from(users).where(eq(users.id, userId)).limit(1);
|
||||
if (user.length === 0) {
|
||||
throw new AppError('NOT_FOUND', 'User not found', 404);
|
||||
}
|
||||
|
||||
const settings = await db
|
||||
.select()
|
||||
.from(userSettings)
|
||||
.where(eq(userSettings.userId, userId))
|
||||
.limit(1);
|
||||
|
||||
let defaultCategory = settings[0]?.defaultCategory;
|
||||
if (defaultCategory) {
|
||||
const cat = await db
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(and(eq(categories.id, defaultCategory), eq(categories.userId, userId)))
|
||||
.limit(1);
|
||||
if (cat.length === 0) {
|
||||
defaultCategory = null;
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
id: user[0].id,
|
||||
username: user[0].username,
|
||||
settings: {
|
||||
darkMode: settings[0]?.darkMode ?? false,
|
||||
notifications: settings[0]?.notifications ?? true,
|
||||
reminderTime: settings[0]?.reminderTime ?? '09:00',
|
||||
defaultCategory,
|
||||
sortBy: settings[0]?.sortBy ?? 'dueDate',
|
||||
sortOrder: settings[0]?.sortOrder ?? 'asc',
|
||||
},
|
||||
createdAt: user[0].createdAt,
|
||||
});
|
||||
}));
|
||||
|
||||
router.patch('/me/settings', asyncHandler(async (req: Request, res: Response) => {
|
||||
const data = userSettingsSchema.parse(req.body);
|
||||
const userId = req.user!.userId;
|
||||
|
||||
// Validate defaultCategory if provided
|
||||
if (data.defaultCategory) {
|
||||
const cat = await db
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(and(eq(categories.id, data.defaultCategory), eq(categories.userId, userId)))
|
||||
.limit(1);
|
||||
if (cat.length === 0) {
|
||||
throw new AppError('NOT_FOUND', 'Default category not found', 404);
|
||||
}
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(userSettings)
|
||||
.where(eq(userSettings.userId, userId))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length === 0) {
|
||||
await db.insert(userSettings).values({
|
||||
userId,
|
||||
...data,
|
||||
updatedAt: now,
|
||||
});
|
||||
} else {
|
||||
await db
|
||||
.update(userSettings)
|
||||
.set({ ...data, updatedAt: now })
|
||||
.where(eq(userSettings.userId, userId));
|
||||
}
|
||||
|
||||
res.status(200).json({ success: true });
|
||||
}));
|
||||
|
||||
router.get('/search', asyncHandler(async (req: Request, res: Response) => {
|
||||
const { q } = req.query;
|
||||
const userId = req.user!.userId;
|
||||
|
||||
if (!q || typeof q !== 'string' || q.length < 2) {
|
||||
return res.json([]);
|
||||
}
|
||||
|
||||
const results = await db
|
||||
.select({ id: users.id, username: users.username })
|
||||
.from(users)
|
||||
.where(and(ilike(users.username, `%${q}%`), eq(users.id, userId)))
|
||||
.limit(10);
|
||||
|
||||
res.json(results);
|
||||
}));
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,91 @@
|
||||
export interface Category {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
order: number;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
export interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
categoryId: string;
|
||||
priority: 'none' | 'low' | 'medium' | 'high' | 'critical';
|
||||
completed: boolean;
|
||||
dueDate: number;
|
||||
dueTime: string;
|
||||
endTime: string;
|
||||
assigneeId: string | null;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
subtasks?: Subtask[];
|
||||
}
|
||||
|
||||
export interface Subtask {
|
||||
id: string;
|
||||
taskId: string;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
order: number;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
export interface UserSettings {
|
||||
darkMode: boolean;
|
||||
notifications: boolean;
|
||||
reminderTime: string;
|
||||
defaultCategory: string | null;
|
||||
sortBy: 'dueDate' | 'priority' | 'title' | 'createdAt';
|
||||
sortOrder: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
settings: UserSettings;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export interface SyncResponse {
|
||||
categories: Category[];
|
||||
tasks: Task[];
|
||||
subtasks: Subtask[];
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface PushChangesRequest {
|
||||
changes: {
|
||||
categories: Category[];
|
||||
tasks: Task[];
|
||||
subtasks: Subtask[];
|
||||
};
|
||||
lastPulledAt: number;
|
||||
}
|
||||
|
||||
export interface PushChangesResponse {
|
||||
success: boolean;
|
||||
timestamp: number;
|
||||
conflicts: Array<{
|
||||
entity: string;
|
||||
id: string;
|
||||
serverVersion: any;
|
||||
clientVersion: any;
|
||||
resolution: 'server_wins' | 'client_wins' | 'merge';
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface AuthPayload {
|
||||
userId: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user?: AuthPayload;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user