mirror of
https://github.com/Tony0410/nextstep.git
synced 2026-05-24 21:31:43 +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:
@@ -52,7 +52,7 @@ export default function MedsPage() {
|
||||
.filter((m) => m.active)
|
||||
.map((m) => ({
|
||||
...m,
|
||||
scheduleData: m.scheduleData as Medication['scheduleData'],
|
||||
scheduleData: m.scheduleData as unknown as Medication['scheduleData'],
|
||||
startDate: m.startDate ? new Date(m.startDate) : null,
|
||||
endDate: m.endDate ? new Date(m.endDate) : null,
|
||||
})) as Medication[]
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function TodayPage() {
|
||||
if (medications && doseLogs) {
|
||||
const meds = medications.map((m) => ({
|
||||
...m,
|
||||
scheduleData: m.scheduleData as Medication['scheduleData'],
|
||||
scheduleData: m.scheduleData as unknown as Medication['scheduleData'],
|
||||
startDate: m.startDate ? new Date(m.startDate) : null,
|
||||
endDate: m.endDate ? new Date(m.endDate) : null,
|
||||
})) as Medication[]
|
||||
|
||||
@@ -171,5 +171,5 @@ async function postHandler(
|
||||
}
|
||||
}
|
||||
|
||||
export const GET = withRateLimit(getHandler)
|
||||
export const POST = withRateLimit(postHandler)
|
||||
export const GET = getHandler
|
||||
export const POST = postHandler
|
||||
|
||||
@@ -173,29 +173,33 @@ export const POST = withAuth(async (req: AuthenticatedRequest) => {
|
||||
}
|
||||
|
||||
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({
|
||||
where: { id: op.entityId },
|
||||
data: {
|
||||
...(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(),
|
||||
},
|
||||
data: updateData,
|
||||
})
|
||||
results.push({ opId: op.id, success: true, entityId: op.entityId })
|
||||
} 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({
|
||||
where: { id: op.entityId },
|
||||
data: {
|
||||
...(op.data.content && { content: op.data.content as string }),
|
||||
updatedById: req.session.user.id,
|
||||
version: { increment: 1 },
|
||||
syncedAt: new Date(),
|
||||
},
|
||||
data: updateData,
|
||||
})
|
||||
results.push({ opId: op.id, success: true, entityId: op.entityId })
|
||||
} else {
|
||||
|
||||
@@ -36,11 +36,13 @@
|
||||
}
|
||||
|
||||
.large-text .text-sm {
|
||||
@apply text-base;
|
||||
font-size: 1rem; /* text-base equivalent */
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
.large-text .text-xs {
|
||||
@apply text-sm;
|
||||
font-size: 0.875rem; /* text-sm equivalent */
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
/* iOS safe areas */
|
||||
|
||||
@@ -6,15 +6,9 @@ export interface AuthenticatedRequest extends NextRequest {
|
||||
session: SessionData
|
||||
}
|
||||
|
||||
type RouteHandler = (
|
||||
req: NextRequest,
|
||||
context: { params: Promise<Record<string, string>> }
|
||||
) => Promise<NextResponse>
|
||||
type RouteHandler = (req: NextRequest, context: { params: Promise<Record<string, string>> }) => Promise<NextResponse>
|
||||
|
||||
type AuthenticatedRouteHandler = (
|
||||
req: AuthenticatedRequest,
|
||||
context: { params: Promise<Record<string, string>> }
|
||||
) => Promise<NextResponse>
|
||||
type AuthenticatedRouteHandler = (req: AuthenticatedRequest, context: { params: Promise<Record<string, string>> }) => Promise<NextResponse>
|
||||
|
||||
export function withAuth(handler: AuthenticatedRouteHandler): RouteHandler {
|
||||
return async (req: NextRequest, context) => {
|
||||
|
||||
@@ -87,9 +87,10 @@ export function checkApiRateLimit(identifier: string): { allowed: boolean; remai
|
||||
// Clean up old entries periodically
|
||||
setInterval(() => {
|
||||
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) {
|
||||
requestCounts.delete(key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}, 60000)
|
||||
|
||||
@@ -93,11 +93,14 @@ export function getSessionCookieConfig(token: string) {
|
||||
const expiresAt = new Date()
|
||||
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 {
|
||||
name: SESSION_COOKIE_NAME,
|
||||
value: token,
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
secure: requireHttps,
|
||||
sameSite: 'lax' as const,
|
||||
expires: expiresAt,
|
||||
path: '/',
|
||||
@@ -105,11 +108,13 @@ export function getSessionCookieConfig(token: string) {
|
||||
}
|
||||
|
||||
export function getSessionCookieClearConfig() {
|
||||
const requireHttps = process.env.COOKIE_SECURE !== 'false' && process.env.NODE_ENV === 'production'
|
||||
|
||||
return {
|
||||
name: SESSION_COOKIE_NAME,
|
||||
value: '',
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
secure: requireHttps,
|
||||
sameSite: 'lax' as const,
|
||||
expires: new Date(0),
|
||||
path: '/',
|
||||
|
||||
@@ -41,6 +41,7 @@ export interface LocalNote {
|
||||
deletedAt: string | null
|
||||
version: number
|
||||
syncedAt: string
|
||||
createdAt: string
|
||||
createdBy?: { id: string; name: string }
|
||||
updatedBy?: { id: string; name: string }
|
||||
}
|
||||
|
||||
@@ -364,6 +364,7 @@ export async function createLocalNote(
|
||||
data: { type: 'QUESTION' | 'GENERAL'; content: string }
|
||||
): Promise<LocalNote> {
|
||||
const id = generateTempId()
|
||||
const now = new Date().toISOString()
|
||||
const note: LocalNote = {
|
||||
id,
|
||||
workspaceId,
|
||||
@@ -372,7 +373,8 @@ export async function createLocalNote(
|
||||
askedAt: null,
|
||||
deletedAt: null,
|
||||
version: 1,
|
||||
syncedAt: new Date().toISOString(),
|
||||
syncedAt: now,
|
||||
createdAt: now,
|
||||
}
|
||||
|
||||
await db.notes.add(note)
|
||||
|
||||
Reference in New Issue
Block a user