2.0.0
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m49s

This commit is contained in:
2026-01-18 14:27:47 -07:00
parent ce47de9e56
commit 5e70dd6bb8
8 changed files with 217 additions and 163 deletions

View File

@@ -37,6 +37,10 @@ export const POST: APIRoute = async ({ request, cookies, redirect }) => {
return redirect("/signup?error=missing_fields");
}
if (password.length < 8) {
return redirect("/signup?error=password_too_short");
}
const existingUser = await db
.select()
.from(users)

View File

@@ -1,18 +1,19 @@
import type { APIRoute } from 'astro';
import { db } from '../../../db';
import { timeEntries, members, timeEntryTags, categories, clients } from '../../../db/schema';
import { eq, and } from 'drizzle-orm';
import { nanoid } from 'nanoid';
import type { APIRoute } from "astro";
import { db } from "../../../db";
import { timeEntries, members, timeEntryTags } from "../../../db/schema";
import { eq } from "drizzle-orm";
import { nanoid } from "nanoid";
import {
validateTimeEntryResources,
validateTimeRange,
} from "../../../lib/validation";
export const POST: APIRoute = async ({ request, locals }) => {
if (!locals.user) {
return new Response(
JSON.stringify({ error: 'Unauthorized' }),
{
status: 401,
headers: { 'Content-Type': 'application/json' }
}
);
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
headers: { "Content-Type": "application/json" },
});
}
const body = await request.json();
@@ -20,67 +21,47 @@ export const POST: APIRoute = async ({ request, locals }) => {
// Validation
if (!clientId) {
return new Response(
JSON.stringify({ error: 'Client is required' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' }
}
);
return new Response(JSON.stringify({ error: "Client is required" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
if (!categoryId) {
return new Response(
JSON.stringify({ error: 'Category is required' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' }
}
);
return new Response(JSON.stringify({ error: "Category is required" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
if (!startTime) {
return new Response(
JSON.stringify({ error: 'Start time is required' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' }
}
);
return new Response(JSON.stringify({ error: "Start time is required" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
if (!endTime) {
return new Response(
JSON.stringify({ error: 'End time is required' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' }
}
);
return new Response(JSON.stringify({ error: "End time is required" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
const startDate = new Date(startTime);
const endDate = new Date(endTime);
const timeValidation = validateTimeRange(startTime, endTime);
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
return new Response(
JSON.stringify({ error: 'Invalid date format' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' }
}
);
if (
!timeValidation.valid ||
!timeValidation.startDate ||
!timeValidation.endDate
) {
return new Response(JSON.stringify({ error: timeValidation.error }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
if (endDate <= startDate) {
return new Response(
JSON.stringify({ error: 'End time must be after start time' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' }
}
);
}
const { startDate, endDate } = timeValidation;
// Get user's organization
const member = await db
@@ -91,57 +72,24 @@ export const POST: APIRoute = async ({ request, locals }) => {
.get();
if (!member) {
return new Response(
JSON.stringify({ error: 'No organization found' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' }
}
);
return new Response(JSON.stringify({ error: "No organization found" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
// Verify category belongs to organization
const category = await db
.select()
.from(categories)
.where(
and(
eq(categories.id, categoryId),
eq(categories.organizationId, member.organizationId)
)
)
.get();
const resourceValidation = await validateTimeEntryResources({
organizationId: member.organizationId,
clientId,
categoryId,
tagIds: Array.isArray(tags) ? tags : undefined,
});
if (!category) {
return new Response(
JSON.stringify({ error: 'Invalid category' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' }
}
);
}
// Verify client belongs to organization
const client = await db
.select()
.from(clients)
.where(
and(
eq(clients.id, clientId),
eq(clients.organizationId, member.organizationId)
)
)
.get();
if (!client) {
return new Response(
JSON.stringify({ error: 'Invalid client' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' }
}
);
if (!resourceValidation.valid) {
return new Response(JSON.stringify({ error: resourceValidation.error }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
const id = nanoid();
@@ -166,7 +114,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
tags.map((tagId: string) => ({
timeEntryId: id,
tagId,
}))
})),
);
}
@@ -179,17 +127,17 @@ export const POST: APIRoute = async ({ request, locals }) => {
}),
{
status: 201,
headers: { 'Content-Type': 'application/json' }
}
headers: { "Content-Type": "application/json" },
},
);
} catch (error) {
console.error('Error creating manual time entry:', error);
console.error("Error creating manual time entry:", error);
return new Response(
JSON.stringify({ error: 'Failed to create time entry' }),
JSON.stringify({ error: "Failed to create time entry" }),
{
status: 500,
headers: { 'Content-Type': 'application/json' }
}
headers: { "Content-Type": "application/json" },
},
);
}
};

View File

@@ -1,13 +1,9 @@
import type { APIRoute } from "astro";
import { db } from "../../../db";
import {
timeEntries,
members,
timeEntryTags,
categories,
} from "../../../db/schema";
import { timeEntries, members, timeEntryTags } from "../../../db/schema";
import { eq, and, isNull } from "drizzle-orm";
import { nanoid } from "nanoid";
import { validateTimeEntryResources } from "../../../lib/validation";
export const POST: APIRoute = async ({ request, locals }) => {
if (!locals.user) return new Response("Unauthorized", { status: 401 });
@@ -48,19 +44,15 @@ export const POST: APIRoute = async ({ request, locals }) => {
return new Response("No organization found", { status: 400 });
}
const category = await db
.select()
.from(categories)
.where(
and(
eq(categories.id, categoryId),
eq(categories.organizationId, member.organizationId),
),
)
.get();
const validation = await validateTimeEntryResources({
organizationId: member.organizationId,
clientId,
categoryId,
tagIds: tags,
});
if (!category) {
return new Response("Invalid category", { status: 400 });
if (!validation.valid) {
return new Response(validation.error, { status: 400 });
}
const startTime = new Date();