memory: log task worker session - due dates, subtasks, task page

This commit is contained in:
2026-01-29 07:07:45 +00:00
parent ad145c9ec3
commit 4663a45b45
39 changed files with 1044 additions and 3 deletions

View File

@@ -0,0 +1,20 @@
# Database
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/APP_TEMPLATE
# Server
PORT=3001
NODE_ENV=development
# URLs
APP_URL=http://localhost:5173
ALLOWED_ORIGINS=http://localhost:5173
# Auth
BETTER_AUTH_SECRET=dev-secret-change-in-production
# Hammer Service Account
HAMMER_API_KEY=hammer-dev-key-12345
# Email (optional)
# RESEND_API_KEY=
# FROM_EMAIL=

View File

@@ -0,0 +1,5 @@
node_modules/
dist/
.env
*.log
.DS_Store

View File

@@ -0,0 +1,7 @@
FROM oven/bun:1-alpine
WORKDIR /app
COPY package.json bun.lock* ./
RUN bun install --frozen-lockfile 2>/dev/null || bun install
COPY . .
EXPOSE 3001
CMD ["bun", "run", "src/index.ts"]

View File

@@ -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!,
},
});

View File

@@ -0,0 +1,23 @@
{
"name": "APP_TEMPLATE-api",
"version": "0.0.1",
"scripts": {
"dev": "bun run --watch src/index.ts",
"start": "bun run src/index.ts",
"db:generate": "drizzle-kit generate",
"db:push": "drizzle-kit push",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio",
"test": "bun test"
},
"dependencies": {
"better-auth": "^1.0.0",
"drizzle-orm": "^0.38.0",
"elysia": "^1.2.0",
"postgres": "^3.4.0"
},
"devDependencies": {
"@types/bun": "latest",
"drizzle-kit": "^0.30.0"
}
}

View File

@@ -0,0 +1,6 @@
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
const client = postgres(process.env.DATABASE_URL!);
export const db = drizzle(client, { schema });

View File

@@ -0,0 +1,54 @@
import { pgTable, text, timestamp, boolean, uuid } from 'drizzle-orm/pg-core';
// BetterAuth tables (managed by BetterAuth, do not modify)
export const users = pgTable('users', {
id: text('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
emailVerified: boolean('email_verified').default(false),
image: text('image'),
role: text('role').default('user'),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
export const sessions = pgTable('sessions', {
id: text('id').primaryKey(),
userId: text('user_id').notNull().references(() => users.id),
token: text('token').notNull().unique(),
expiresAt: timestamp('expires_at').notNull(),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
export const accounts = pgTable('accounts', {
id: text('id').primaryKey(),
userId: text('user_id').notNull().references(() => users.id),
accountId: text('account_id').notNull(),
providerId: text('provider_id').notNull(),
password: text('password'),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
export const verifications = pgTable('verifications', {
id: text('id').primaryKey(),
identifier: text('identifier').notNull(),
value: text('value').notNull(),
expiresAt: timestamp('expires_at').notNull(),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
// ============================================
// APP-SPECIFIC TABLES — Customize below
// ============================================
// Example:
// export const items = pgTable('items', {
// id: uuid('id').primaryKey().defaultRandom(),
// title: text('title').notNull(),
// userId: text('user_id').references(() => users.id),
// createdAt: timestamp('created_at').defaultNow(),
// updatedAt: timestamp('updated_at').defaultNow(),
// });

View File

@@ -0,0 +1,14 @@
import { Elysia } from 'elysia';
import { cors } from '@elysiajs/cors';
import { hammerRoutes } from './routes/hammer';
const app = new Elysia()
.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:5173'],
credentials: true,
}))
.get('/api/health', () => ({ status: 'ok', timestamp: new Date().toISOString() }))
.use(hammerRoutes)
.listen(Number(process.env.PORT) || 3001);
console.log(`🚀 Server running on port ${app.server?.port}`);

View File

@@ -0,0 +1,17 @@
import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { db } from '../db';
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: 'pg' }),
emailAndPassword: {
enabled: true,
disableSignUp: true, // Invite-only
},
trustedOrigins: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:5173'],
advanced: {
crossSubDomainCookies: process.env.NODE_ENV === 'production'
? { domain: process.env.COOKIE_DOMAIN || '' }
: undefined,
},
});

View File

@@ -0,0 +1,38 @@
import { Elysia, t } from 'elysia';
import { db } from '../db';
import { users } from '../db/schema';
import { eq } from 'drizzle-orm';
const validateHammerAuth = (authHeader: string | undefined): boolean => {
if (!authHeader) return false;
const token = authHeader.replace('Bearer ', '');
return token === process.env.HAMMER_API_KEY;
};
export const hammerRoutes = new Elysia({ prefix: '/api/hammer' })
.derive(({ request, set }) => {
const authHeader = request.headers.get('authorization');
if (!validateHammerAuth(authHeader)) {
set.status = 401;
throw new Error('Invalid API key');
}
return {};
})
.get('/me', async ({ set }) => {
const hammerUser = await db.query.users.findFirst({
where: eq(users.role, 'service'),
});
if (!hammerUser) {
set.status = 404;
throw new Error('Hammer service account not found');
}
return {
id: hammerUser.id,
name: hammerUser.name,
email: hammerUser.email,
role: hammerUser.role,
};
});
// Add app-specific Hammer routes below

View File

@@ -0,0 +1,27 @@
services:
api:
build:
context: ./api
dockerfile: Dockerfile
restart: unless-stopped
ports:
- 3001
environment:
- DATABASE_URL=${DATABASE_URL}
- PORT=3001
- NODE_ENV=production
- APP_URL=${APP_URL}
- ALLOWED_ORIGINS=${ALLOWED_ORIGINS}
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
- HAMMER_API_KEY=${HAMMER_API_KEY}
command: sh -c 'bun run db:push && bun run src/index.ts'
web:
build:
context: ./web
dockerfile: Dockerfile
restart: unless-stopped
ports:
- 80
depends_on:
- api

View File

@@ -0,0 +1,40 @@
services:
db:
image: postgres:16-alpine
restart: unless-stopped
ports:
- 5432:5432
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: APP_TEMPLATE
volumes:
- pgdata:/var/lib/postgresql/data
api:
build:
context: ./api
dockerfile: Dockerfile
restart: unless-stopped
ports:
- 3001:3001
env_file: .env
depends_on:
- db
volumes:
- ./api/src:/app/src
web:
build:
context: ./web
dockerfile: Dockerfile
restart: unless-stopped
ports:
- 5173:5173
depends_on:
- api
volumes:
- ./web/src:/app/src
volumes:
pgdata:

View File

@@ -0,0 +1,12 @@
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -0,0 +1,17 @@
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://api:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@@ -0,0 +1,28 @@
{
"name": "APP_TEMPLATE-web",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"test": "vitest"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.0",
"autoprefixer": "^10.4.0",
"postcss": "^8.4.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.6.0",
"vite": "^6.0.0",
"vitest": "^2.0.0"
}
}