mirror of
https://github.com/Tony0410/nextstep.git
synced 2026-05-24 13:21:39 +08:00
Fix Docker deployment and add Tailscale Funnel support
- Fix argon2 native module build in Docker (add build-essential, python3) - Switch Docker base image from Alpine to Debian-slim for OpenSSL compatibility - Fix session cookies for HTTP access (COOKIE_SECURE env var) - Fix TypeScript type errors in sync routes and middleware - Fix CSS circular dependency in globals.css - Fix Map iteration in rate-limit cleanup - Add createdAt field to LocalNote interface - Configure Tailscale Funnel on port 10000 - Update NEXT_PUBLIC_APP_URL for public funnel access - Add initial Prisma migration Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": ["next/core-web-vitals"],
|
"extends": ["next/core-web-vitals"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
|
|
||||||
"react/no-unescaped-entities": "off"
|
"react/no-unescaped-entities": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
Dockerfile
22
Dockerfile
@@ -1,6 +1,6 @@
|
|||||||
# Stage 1: Dependencies
|
# Stage 1: Dependencies
|
||||||
FROM node:20-alpine AS deps
|
FROM node:20-slim AS deps
|
||||||
RUN apk add --no-cache libc6-compat python3 make g++
|
RUN apt-get update && apt-get install -y openssl build-essential python3 && rm -rf /var/lib/apt/lists/*
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json package-lock.json* ./
|
COPY package.json package-lock.json* ./
|
||||||
@@ -9,7 +9,8 @@ COPY prisma ./prisma/
|
|||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
# Stage 2: Builder
|
# Stage 2: Builder
|
||||||
FROM node:20-alpine AS builder
|
FROM node:20-slim AS builder
|
||||||
|
RUN apt-get update && apt-get install -y openssl && rm -rf /var/lib/apt/lists/*
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
@@ -24,8 +25,8 @@ ENV NODE_ENV=production
|
|||||||
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Stage 3: Runner
|
# Stage 3: Runner (using slim Debian for better OpenSSL compatibility)
|
||||||
FROM node:20-alpine AS runner
|
FROM node:20-slim AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
@@ -35,11 +36,14 @@ ENV TZ=Australia/Perth
|
|||||||
RUN addgroup --system --gid 1001 nodejs
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
RUN adduser --system --uid 1001 nextjs
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
# Install tzdata for timezone support
|
# Install OpenSSL and CA certificates for Prisma
|
||||||
RUN apk add --no-cache tzdata
|
RUN apt-get update && apt-get install -y openssl ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY --from=builder /app/public ./public
|
COPY --from=builder /app/public ./public
|
||||||
COPY --from=builder /app/prisma ./prisma
|
COPY --from=builder /app/prisma ./prisma
|
||||||
|
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
|
||||||
|
COPY --from=builder /app/node_modules/argon2 ./node_modules/argon2
|
||||||
|
COPY --from=builder /app/node_modules/node-gyp-build ./node_modules/node-gyp-build
|
||||||
|
|
||||||
# Set the correct permission for prerender cache
|
# Set the correct permission for prerender cache
|
||||||
RUN mkdir .next
|
RUN mkdir .next
|
||||||
@@ -56,5 +60,5 @@ EXPOSE 3000
|
|||||||
ENV PORT=3000
|
ENV PORT=3000
|
||||||
ENV HOSTNAME="0.0.0.0"
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
# Run database migrations before starting
|
# Start the app (run migrations separately with: docker exec nextstep-app npx prisma migrate deploy)
|
||||||
CMD ["sh", "-c", "npx prisma migrate deploy && node server.js"]
|
CMD ["node", "server.js"]
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
build:
|
build:
|
||||||
@@ -8,11 +6,11 @@ services:
|
|||||||
container_name: nextstep-app
|
container_name: nextstep-app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:3000:3000" # Bind to localhost only for Tailscale Funnel
|
- "4678:3000" # Bind to all interfaces for Tailscale access
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=postgresql://nextstep:${DB_PASSWORD:-nextstep}@db:5432/nextstep?schema=public
|
- DATABASE_URL=postgresql://nextstep:${DB_PASSWORD:-nextstep}@db:5432/nextstep?schema=public
|
||||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
|
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
|
||||||
- NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000}
|
- NEXT_PUBLIC_APP_URL=https://debianvm.kangaroo-eel.ts.net:10000
|
||||||
- TZ=Australia/Perth
|
- TZ=Australia/Perth
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -48,7 +46,7 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
start_period: 10s
|
start_period: 10s
|
||||||
# Do not expose PostgreSQL to the host - only accessible within the network
|
# Do not expose PostgreSQL to the host - only accessible within the network
|
||||||
# If you need direct access, uncomment below:
|
# If you need direct access for migrations, uncomment below:
|
||||||
# ports:
|
# ports:
|
||||||
# - "127.0.0.1:5432:5432"
|
# - "127.0.0.1:5432:5432"
|
||||||
|
|
||||||
|
|||||||
8200
package-lock.json
generated
Normal file
8200
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
330
prisma/migrations/20260119034459_init/migration.sql
Normal file
330
prisma/migrations/20260119034459_init/migration.sql
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "WorkspaceRole" AS ENUM ('OWNER', 'EDITOR', 'VIEWER');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "ScheduleType" AS ENUM ('FIXED_TIMES', 'INTERVAL', 'WEEKDAYS', 'PRN');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "NoteType" AS ENUM ('QUESTION', 'GENERAL');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "User" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"passwordHash" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Session" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"token" TEXT NOT NULL,
|
||||||
|
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"userAgent" TEXT,
|
||||||
|
"ipAddress" TEXT,
|
||||||
|
|
||||||
|
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "LoginAttempt" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"ipAddress" TEXT,
|
||||||
|
"success" BOOLEAN NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "LoginAttempt_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Workspace" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"clinicPhone" TEXT,
|
||||||
|
"emergencyPhone" TEXT,
|
||||||
|
"quietHoursStart" TEXT,
|
||||||
|
"quietHoursEnd" TEXT,
|
||||||
|
"largeTextMode" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Workspace_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "WorkspaceMember" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"workspaceId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"role" "WorkspaceRole" NOT NULL DEFAULT 'VIEWER',
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "WorkspaceMember_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "InviteToken" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"workspaceId" TEXT NOT NULL,
|
||||||
|
"token" TEXT NOT NULL,
|
||||||
|
"role" "WorkspaceRole" NOT NULL DEFAULT 'VIEWER',
|
||||||
|
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"usedAt" TIMESTAMP(3),
|
||||||
|
"usedById" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "InviteToken_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Appointment" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"workspaceId" TEXT NOT NULL,
|
||||||
|
"title" TEXT NOT NULL,
|
||||||
|
"datetime" TIMESTAMP(3) NOT NULL,
|
||||||
|
"location" TEXT,
|
||||||
|
"mapUrl" TEXT,
|
||||||
|
"notes" TEXT,
|
||||||
|
"deletedAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"createdById" TEXT NOT NULL,
|
||||||
|
"updatedById" TEXT NOT NULL,
|
||||||
|
"version" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
"syncedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "Appointment_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Medication" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"workspaceId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"instructions" TEXT,
|
||||||
|
"scheduleType" "ScheduleType" NOT NULL,
|
||||||
|
"scheduleData" JSONB NOT NULL,
|
||||||
|
"startDate" TIMESTAMP(3),
|
||||||
|
"endDate" TIMESTAMP(3),
|
||||||
|
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"deletedAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"createdById" TEXT NOT NULL,
|
||||||
|
"updatedById" TEXT NOT NULL,
|
||||||
|
"version" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
"syncedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "Medication_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "DoseLog" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"medicationId" TEXT NOT NULL,
|
||||||
|
"workspaceId" TEXT NOT NULL,
|
||||||
|
"takenAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"loggedById" TEXT NOT NULL,
|
||||||
|
"undoneAt" TIMESTAMP(3),
|
||||||
|
"undoneById" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"syncedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "DoseLog_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Note" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"workspaceId" TEXT NOT NULL,
|
||||||
|
"type" "NoteType" NOT NULL,
|
||||||
|
"content" TEXT NOT NULL,
|
||||||
|
"askedAt" TIMESTAMP(3),
|
||||||
|
"deletedAt" TIMESTAMP(3),
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"createdById" TEXT NOT NULL,
|
||||||
|
"updatedById" TEXT NOT NULL,
|
||||||
|
"version" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
"syncedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "Note_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "AuditLog" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"workspaceId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"action" TEXT NOT NULL,
|
||||||
|
"entityType" TEXT NOT NULL,
|
||||||
|
"entityId" TEXT NOT NULL,
|
||||||
|
"details" JSONB,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "SyncCursor" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"workspaceId" TEXT NOT NULL,
|
||||||
|
"cursor" BIGINT NOT NULL DEFAULT 0,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "SyncCursor_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "User_email_idx" ON "User"("email");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Session_token_key" ON "Session"("token");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Session_token_idx" ON "Session"("token");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Session_userId_idx" ON "Session"("userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Session_expiresAt_idx" ON "Session"("expiresAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "LoginAttempt_email_createdAt_idx" ON "LoginAttempt"("email", "createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "LoginAttempt_ipAddress_createdAt_idx" ON "LoginAttempt"("ipAddress", "createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Workspace_name_idx" ON "Workspace"("name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "WorkspaceMember_userId_idx" ON "WorkspaceMember"("userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "WorkspaceMember_workspaceId_userId_key" ON "WorkspaceMember"("workspaceId", "userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "InviteToken_token_key" ON "InviteToken"("token");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "InviteToken_token_idx" ON "InviteToken"("token");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "InviteToken_workspaceId_idx" ON "InviteToken"("workspaceId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Appointment_workspaceId_datetime_idx" ON "Appointment"("workspaceId", "datetime");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Appointment_workspaceId_deletedAt_idx" ON "Appointment"("workspaceId", "deletedAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Appointment_syncedAt_idx" ON "Appointment"("syncedAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Medication_workspaceId_active_idx" ON "Medication"("workspaceId", "active");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Medication_workspaceId_deletedAt_idx" ON "Medication"("workspaceId", "deletedAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Medication_syncedAt_idx" ON "Medication"("syncedAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "DoseLog_medicationId_takenAt_idx" ON "DoseLog"("medicationId", "takenAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "DoseLog_workspaceId_takenAt_idx" ON "DoseLog"("workspaceId", "takenAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "DoseLog_syncedAt_idx" ON "DoseLog"("syncedAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Note_workspaceId_type_idx" ON "Note"("workspaceId", "type");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Note_workspaceId_deletedAt_idx" ON "Note"("workspaceId", "deletedAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "Note_syncedAt_idx" ON "Note"("syncedAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "AuditLog_workspaceId_createdAt_idx" ON "AuditLog"("workspaceId", "createdAt");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "AuditLog_entityType_entityId_idx" ON "AuditLog"("entityType", "entityId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "SyncCursor_workspaceId_key" ON "SyncCursor"("workspaceId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "WorkspaceMember" ADD CONSTRAINT "WorkspaceMember_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "WorkspaceMember" ADD CONSTRAINT "WorkspaceMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "InviteToken" ADD CONSTRAINT "InviteToken_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Appointment" ADD CONSTRAINT "Appointment_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Appointment" ADD CONSTRAINT "Appointment_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Appointment" ADD CONSTRAINT "Appointment_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Medication" ADD CONSTRAINT "Medication_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Medication" ADD CONSTRAINT "Medication_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Medication" ADD CONSTRAINT "Medication_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "DoseLog" ADD CONSTRAINT "DoseLog_medicationId_fkey" FOREIGN KEY ("medicationId") REFERENCES "Medication"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "DoseLog" ADD CONSTRAINT "DoseLog_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "DoseLog" ADD CONSTRAINT "DoseLog_loggedById_fkey" FOREIGN KEY ("loggedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "DoseLog" ADD CONSTRAINT "DoseLog_undoneById_fkey" FOREIGN KEY ("undoneById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Note" ADD CONSTRAINT "Note_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Note" ADD CONSTRAINT "Note_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Note" ADD CONSTRAINT "Note_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "SyncCursor" ADD CONSTRAINT "SyncCursor_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (i.e. Git)
|
||||||
|
provider = "postgresql"
|
||||||
@@ -52,7 +52,7 @@ export default function MedsPage() {
|
|||||||
.filter((m) => m.active)
|
.filter((m) => m.active)
|
||||||
.map((m) => ({
|
.map((m) => ({
|
||||||
...m,
|
...m,
|
||||||
scheduleData: m.scheduleData as Medication['scheduleData'],
|
scheduleData: m.scheduleData as unknown as Medication['scheduleData'],
|
||||||
startDate: m.startDate ? new Date(m.startDate) : null,
|
startDate: m.startDate ? new Date(m.startDate) : null,
|
||||||
endDate: m.endDate ? new Date(m.endDate) : null,
|
endDate: m.endDate ? new Date(m.endDate) : null,
|
||||||
})) as Medication[]
|
})) as Medication[]
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default function TodayPage() {
|
|||||||
if (medications && doseLogs) {
|
if (medications && doseLogs) {
|
||||||
const meds = medications.map((m) => ({
|
const meds = medications.map((m) => ({
|
||||||
...m,
|
...m,
|
||||||
scheduleData: m.scheduleData as Medication['scheduleData'],
|
scheduleData: m.scheduleData as unknown as Medication['scheduleData'],
|
||||||
startDate: m.startDate ? new Date(m.startDate) : null,
|
startDate: m.startDate ? new Date(m.startDate) : null,
|
||||||
endDate: m.endDate ? new Date(m.endDate) : null,
|
endDate: m.endDate ? new Date(m.endDate) : null,
|
||||||
})) as Medication[]
|
})) as Medication[]
|
||||||
|
|||||||
@@ -171,5 +171,5 @@ async function postHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GET = withRateLimit(getHandler)
|
export const GET = getHandler
|
||||||
export const POST = withRateLimit(postHandler)
|
export const POST = postHandler
|
||||||
|
|||||||
@@ -173,29 +173,33 @@ export const POST = withAuth(async (req: AuthenticatedRequest) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (op.entityType === 'APPOINTMENT' && op.data) {
|
if (op.entityType === 'APPOINTMENT' && op.data) {
|
||||||
|
const updateData: Record<string, unknown> = {
|
||||||
|
updatedById: req.session.user.id,
|
||||||
|
version: { increment: 1 },
|
||||||
|
syncedAt: new Date(),
|
||||||
|
}
|
||||||
|
if (op.data.title) updateData.title = op.data.title as string
|
||||||
|
if (op.data.datetime) updateData.datetime = new Date(op.data.datetime as string)
|
||||||
|
if (op.data.location !== undefined) updateData.location = op.data.location as string | null
|
||||||
|
if (op.data.mapUrl !== undefined) updateData.mapUrl = op.data.mapUrl as string | null
|
||||||
|
if (op.data.notes !== undefined) updateData.notes = op.data.notes as string | null
|
||||||
|
|
||||||
await prisma.appointment.update({
|
await prisma.appointment.update({
|
||||||
where: { id: op.entityId },
|
where: { id: op.entityId },
|
||||||
data: {
|
data: updateData,
|
||||||
...(op.data.title && { title: op.data.title as string }),
|
|
||||||
...(op.data.datetime && { datetime: new Date(op.data.datetime as string) }),
|
|
||||||
...(op.data.location !== undefined && { location: op.data.location as string | null }),
|
|
||||||
...(op.data.mapUrl !== undefined && { mapUrl: op.data.mapUrl as string | null }),
|
|
||||||
...(op.data.notes !== undefined && { notes: op.data.notes as string | null }),
|
|
||||||
updatedById: req.session.user.id,
|
|
||||||
version: { increment: 1 },
|
|
||||||
syncedAt: new Date(),
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
results.push({ opId: op.id, success: true, entityId: op.entityId })
|
results.push({ opId: op.id, success: true, entityId: op.entityId })
|
||||||
} else if (op.entityType === 'NOTE' && op.data) {
|
} else if (op.entityType === 'NOTE' && op.data) {
|
||||||
|
const updateData: Record<string, unknown> = {
|
||||||
|
updatedById: req.session.user.id,
|
||||||
|
version: { increment: 1 },
|
||||||
|
syncedAt: new Date(),
|
||||||
|
}
|
||||||
|
if (op.data.content) updateData.content = op.data.content as string
|
||||||
|
|
||||||
await prisma.note.update({
|
await prisma.note.update({
|
||||||
where: { id: op.entityId },
|
where: { id: op.entityId },
|
||||||
data: {
|
data: updateData,
|
||||||
...(op.data.content && { content: op.data.content as string }),
|
|
||||||
updatedById: req.session.user.id,
|
|
||||||
version: { increment: 1 },
|
|
||||||
syncedAt: new Date(),
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
results.push({ opId: op.id, success: true, entityId: op.entityId })
|
results.push({ opId: op.id, success: true, entityId: op.entityId })
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -36,11 +36,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.large-text .text-sm {
|
.large-text .text-sm {
|
||||||
@apply text-base;
|
font-size: 1rem; /* text-base equivalent */
|
||||||
|
line-height: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.large-text .text-xs {
|
.large-text .text-xs {
|
||||||
@apply text-sm;
|
font-size: 0.875rem; /* text-sm equivalent */
|
||||||
|
line-height: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* iOS safe areas */
|
/* iOS safe areas */
|
||||||
|
|||||||
@@ -6,15 +6,9 @@ export interface AuthenticatedRequest extends NextRequest {
|
|||||||
session: SessionData
|
session: SessionData
|
||||||
}
|
}
|
||||||
|
|
||||||
type RouteHandler = (
|
type RouteHandler = (req: NextRequest, context: { params: Promise<Record<string, string>> }) => Promise<NextResponse>
|
||||||
req: NextRequest,
|
|
||||||
context: { params: Promise<Record<string, string>> }
|
|
||||||
) => Promise<NextResponse>
|
|
||||||
|
|
||||||
type AuthenticatedRouteHandler = (
|
type AuthenticatedRouteHandler = (req: AuthenticatedRequest, context: { params: Promise<Record<string, string>> }) => Promise<NextResponse>
|
||||||
req: AuthenticatedRequest,
|
|
||||||
context: { params: Promise<Record<string, string>> }
|
|
||||||
) => Promise<NextResponse>
|
|
||||||
|
|
||||||
export function withAuth(handler: AuthenticatedRouteHandler): RouteHandler {
|
export function withAuth(handler: AuthenticatedRouteHandler): RouteHandler {
|
||||||
return async (req: NextRequest, context) => {
|
return async (req: NextRequest, context) => {
|
||||||
|
|||||||
@@ -87,9 +87,10 @@ export function checkApiRateLimit(identifier: string): { allowed: boolean; remai
|
|||||||
// Clean up old entries periodically
|
// Clean up old entries periodically
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
for (const [key, value] of requestCounts.entries()) {
|
const entries = Array.from(requestCounts.entries())
|
||||||
|
entries.forEach(([key, value]) => {
|
||||||
if (value.resetAt < now) {
|
if (value.resetAt < now) {
|
||||||
requestCounts.delete(key)
|
requestCounts.delete(key)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}, 60000)
|
}, 60000)
|
||||||
|
|||||||
@@ -93,11 +93,14 @@ export function getSessionCookieConfig(token: string) {
|
|||||||
const expiresAt = new Date()
|
const expiresAt = new Date()
|
||||||
expiresAt.setDate(expiresAt.getDate() + SESSION_MAX_AGE_DAYS)
|
expiresAt.setDate(expiresAt.getDate() + SESSION_MAX_AGE_DAYS)
|
||||||
|
|
||||||
|
// Allow disabling secure cookies for internal/Tailscale networks
|
||||||
|
const requireHttps = process.env.COOKIE_SECURE !== 'false' && process.env.NODE_ENV === 'production'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: SESSION_COOKIE_NAME,
|
name: SESSION_COOKIE_NAME,
|
||||||
value: token,
|
value: token,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: requireHttps,
|
||||||
sameSite: 'lax' as const,
|
sameSite: 'lax' as const,
|
||||||
expires: expiresAt,
|
expires: expiresAt,
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -105,11 +108,13 @@ export function getSessionCookieConfig(token: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getSessionCookieClearConfig() {
|
export function getSessionCookieClearConfig() {
|
||||||
|
const requireHttps = process.env.COOKIE_SECURE !== 'false' && process.env.NODE_ENV === 'production'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: SESSION_COOKIE_NAME,
|
name: SESSION_COOKIE_NAME,
|
||||||
value: '',
|
value: '',
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
secure: requireHttps,
|
||||||
sameSite: 'lax' as const,
|
sameSite: 'lax' as const,
|
||||||
expires: new Date(0),
|
expires: new Date(0),
|
||||||
path: '/',
|
path: '/',
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export interface LocalNote {
|
|||||||
deletedAt: string | null
|
deletedAt: string | null
|
||||||
version: number
|
version: number
|
||||||
syncedAt: string
|
syncedAt: string
|
||||||
|
createdAt: string
|
||||||
createdBy?: { id: string; name: string }
|
createdBy?: { id: string; name: string }
|
||||||
updatedBy?: { id: string; name: string }
|
updatedBy?: { id: string; name: string }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -364,6 +364,7 @@ export async function createLocalNote(
|
|||||||
data: { type: 'QUESTION' | 'GENERAL'; content: string }
|
data: { type: 'QUESTION' | 'GENERAL'; content: string }
|
||||||
): Promise<LocalNote> {
|
): Promise<LocalNote> {
|
||||||
const id = generateTempId()
|
const id = generateTempId()
|
||||||
|
const now = new Date().toISOString()
|
||||||
const note: LocalNote = {
|
const note: LocalNote = {
|
||||||
id,
|
id,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@@ -372,7 +373,8 @@ export async function createLocalNote(
|
|||||||
askedAt: null,
|
askedAt: null,
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
version: 1,
|
version: 1,
|
||||||
syncedAt: new Date().toISOString(),
|
syncedAt: now,
|
||||||
|
createdAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.notes.add(note)
|
await db.notes.add(note)
|
||||||
|
|||||||
Reference in New Issue
Block a user