First pass
This commit is contained in:
40
src/pages/api/admin/settings.ts
Normal file
40
src/pages/api/admin/settings.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { db } from '../../../db';
|
||||
import { siteSettings } from '../../../db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export const POST: APIRoute = async ({ request, locals, redirect }) => {
|
||||
const user = locals.user;
|
||||
if (!user || !user.isSiteAdmin) {
|
||||
return new Response('Unauthorized', { status: 403 });
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const registrationEnabled = formData.get('registration_enabled') === 'on';
|
||||
|
||||
// Check if setting exists
|
||||
const existingSetting = await db.select()
|
||||
.from(siteSettings)
|
||||
.where(eq(siteSettings.key, 'registration_enabled'))
|
||||
.get();
|
||||
|
||||
if (existingSetting) {
|
||||
// Update
|
||||
await db.update(siteSettings)
|
||||
.set({
|
||||
value: registrationEnabled ? 'true' : 'false',
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(siteSettings.key, 'registration_enabled'));
|
||||
} else {
|
||||
// Insert
|
||||
await db.insert(siteSettings).values({
|
||||
id: nanoid(),
|
||||
key: 'registration_enabled',
|
||||
value: registrationEnabled ? 'true' : 'false',
|
||||
});
|
||||
}
|
||||
|
||||
return redirect('/admin');
|
||||
};
|
||||
33
src/pages/api/auth/login.ts
Normal file
33
src/pages/api/auth/login.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { db } from '../../../db';
|
||||
import { users } from '../../../db/schema';
|
||||
import { verifyPassword, createSession } from '../../../lib/auth';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
export const POST: APIRoute = async ({ request, cookies, redirect }) => {
|
||||
const formData = await request.formData();
|
||||
const email = formData.get('email')?.toString();
|
||||
const password = formData.get('password')?.toString();
|
||||
|
||||
if (!email || !password) {
|
||||
return new Response('Missing fields', { status: 400 });
|
||||
}
|
||||
|
||||
const user = await db.select().from(users).where(eq(users.email, email)).get();
|
||||
|
||||
if (!user || !(await verifyPassword(password, user.passwordHash))) {
|
||||
return new Response('Invalid email or password', { status: 400 });
|
||||
}
|
||||
|
||||
const { sessionId, expiresAt } = await createSession(user.id);
|
||||
|
||||
cookies.set('session_id', sessionId, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: import.meta.env.PROD,
|
||||
sameSite: 'lax',
|
||||
expires: expiresAt,
|
||||
});
|
||||
|
||||
return redirect('/dashboard');
|
||||
};
|
||||
11
src/pages/api/auth/logout.ts
Normal file
11
src/pages/api/auth/logout.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { invalidateSession } from '../../../lib/auth';
|
||||
|
||||
export const POST: APIRoute = async ({ cookies, redirect }) => {
|
||||
const sessionId = cookies.get('session_id')?.value;
|
||||
if (sessionId) {
|
||||
await invalidateSession(sessionId);
|
||||
cookies.delete('session_id', { path: '/' });
|
||||
}
|
||||
return redirect('/login');
|
||||
};
|
||||
80
src/pages/api/auth/signup.ts
Normal file
80
src/pages/api/auth/signup.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { db } from '../../../db';
|
||||
import { users, organizations, members, siteSettings } from '../../../db/schema';
|
||||
import { hashPassword, createSession } from '../../../lib/auth';
|
||||
import { eq, count, sql } from 'drizzle-orm';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export const POST: APIRoute = async ({ request, cookies, redirect }) => {
|
||||
// Check if this is the first user
|
||||
const userCountResult = await db.select({ count: count() }).from(users).get();
|
||||
const isFirstUser = userCountResult ? userCountResult.count === 0 : true;
|
||||
|
||||
// If not first user, check if registration is enabled
|
||||
if (!isFirstUser) {
|
||||
const registrationSetting = await db.select()
|
||||
.from(siteSettings)
|
||||
.where(eq(siteSettings.key, 'registration_enabled'))
|
||||
.get();
|
||||
|
||||
const registrationEnabled = registrationSetting?.value === 'true';
|
||||
|
||||
if (!registrationEnabled) {
|
||||
return new Response('Registration is currently disabled', { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const name = formData.get('name')?.toString();
|
||||
const email = formData.get('email')?.toString();
|
||||
const password = formData.get('password')?.toString();
|
||||
|
||||
if (!name || !email || !password) {
|
||||
return new Response('Missing fields', { status: 400 });
|
||||
}
|
||||
|
||||
// Check if user exists
|
||||
const existingUser = await db.select().from(users).where(eq(users.email, email)).get();
|
||||
if (existingUser) {
|
||||
return new Response('User already exists', { status: 400 });
|
||||
}
|
||||
|
||||
const passwordHash = await hashPassword(password);
|
||||
const userId = nanoid();
|
||||
|
||||
// Create user
|
||||
await db.insert(users).values({
|
||||
id: userId,
|
||||
name,
|
||||
email,
|
||||
passwordHash,
|
||||
isSiteAdmin: isFirstUser,
|
||||
});
|
||||
|
||||
// Create default organization
|
||||
const orgId = nanoid();
|
||||
await db.insert(organizations).values({
|
||||
id: orgId,
|
||||
name: `${name}'s Organization`,
|
||||
});
|
||||
|
||||
// Add user to organization
|
||||
await db.insert(members).values({
|
||||
userId,
|
||||
organizationId: orgId,
|
||||
role: 'owner',
|
||||
});
|
||||
|
||||
// Create session
|
||||
const { sessionId, expiresAt } = await createSession(userId);
|
||||
|
||||
cookies.set('session_id', sessionId, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: import.meta.env.PROD,
|
||||
sameSite: 'lax',
|
||||
expires: expiresAt,
|
||||
});
|
||||
|
||||
return redirect('/dashboard');
|
||||
};
|
||||
47
src/pages/api/categories/[id]/delete.ts
Normal file
47
src/pages/api/categories/[id]/delete.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { db } from '../../../../db';
|
||||
import { categories, members, timeEntries } from '../../../../db/schema';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
|
||||
export const POST: APIRoute = async ({ locals, redirect, params }) => {
|
||||
const user = locals.user;
|
||||
if (!user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = params;
|
||||
|
||||
// Get user's organization
|
||||
const userOrg = await db.select()
|
||||
.from(members)
|
||||
.where(eq(members.userId, user.id))
|
||||
.get();
|
||||
|
||||
if (!userOrg) {
|
||||
return new Response('No organization found', { status: 400 });
|
||||
}
|
||||
|
||||
const isAdmin = userOrg.role === 'owner' || userOrg.role === 'admin';
|
||||
if (!isAdmin) {
|
||||
return new Response('Forbidden', { status: 403 });
|
||||
}
|
||||
|
||||
// Check if category has time entries
|
||||
const hasEntries = await db.select()
|
||||
.from(timeEntries)
|
||||
.where(eq(timeEntries.categoryId, id!))
|
||||
.get();
|
||||
|
||||
if (hasEntries) {
|
||||
return new Response('Cannot delete category with time entries', { status: 400 });
|
||||
}
|
||||
|
||||
// Delete category
|
||||
await db.delete(categories)
|
||||
.where(and(
|
||||
eq(categories.id, id!),
|
||||
eq(categories.organizationId, userOrg.organizationId)
|
||||
));
|
||||
|
||||
return redirect('/dashboard/team/settings');
|
||||
};
|
||||
48
src/pages/api/categories/[id]/update.ts
Normal file
48
src/pages/api/categories/[id]/update.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { db } from '../../../../db';
|
||||
import { categories, members } from '../../../../db/schema';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
|
||||
export const POST: APIRoute = async ({ request, locals, redirect, params }) => {
|
||||
const user = locals.user;
|
||||
if (!user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = params;
|
||||
const formData = await request.formData();
|
||||
const name = formData.get('name')?.toString();
|
||||
const color = formData.get('color')?.toString();
|
||||
|
||||
if (!name) {
|
||||
return new Response('Name is required', { status: 400 });
|
||||
}
|
||||
|
||||
// Get user's organization
|
||||
const userOrg = await db.select()
|
||||
.from(members)
|
||||
.where(eq(members.userId, user.id))
|
||||
.get();
|
||||
|
||||
if (!userOrg) {
|
||||
return new Response('No organization found', { status: 400 });
|
||||
}
|
||||
|
||||
const isAdmin = userOrg.role === 'owner' || userOrg.role === 'admin';
|
||||
if (!isAdmin) {
|
||||
return new Response('Forbidden', { status: 403 });
|
||||
}
|
||||
|
||||
// Update category
|
||||
await db.update(categories)
|
||||
.set({
|
||||
name,
|
||||
color: color || null,
|
||||
})
|
||||
.where(and(
|
||||
eq(categories.id, id!),
|
||||
eq(categories.organizationId, userOrg.organizationId)
|
||||
));
|
||||
|
||||
return redirect('/dashboard/team/settings');
|
||||
};
|
||||
40
src/pages/api/categories/create.ts
Normal file
40
src/pages/api/categories/create.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { db } from '../../../db';
|
||||
import { categories, members } from '../../../db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export const POST: APIRoute = async ({ request, locals, redirect }) => {
|
||||
const user = locals.user;
|
||||
if (!user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const name = formData.get('name')?.toString();
|
||||
const color = formData.get('color')?.toString();
|
||||
|
||||
if (!name) {
|
||||
return new Response('Name is required', { status: 400 });
|
||||
}
|
||||
|
||||
// Get user's first organization
|
||||
const userOrg = await db.select()
|
||||
.from(members)
|
||||
.where(eq(members.userId, user.id))
|
||||
.get();
|
||||
|
||||
if (!userOrg) {
|
||||
return new Response('No organization found', { status: 400 });
|
||||
}
|
||||
|
||||
// Create category
|
||||
await db.insert(categories).values({
|
||||
id: nanoid(),
|
||||
organizationId: userOrg.organizationId,
|
||||
name,
|
||||
color: color || null,
|
||||
});
|
||||
|
||||
return redirect('/dashboard/team/settings');
|
||||
};
|
||||
40
src/pages/api/clients/create.ts
Normal file
40
src/pages/api/clients/create.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { db } from '../../../db';
|
||||
import { clients, members } from '../../../db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export const POST: APIRoute = async ({ request, locals, redirect }) => {
|
||||
const user = locals.user;
|
||||
if (!user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const name = formData.get('name')?.toString();
|
||||
const email = formData.get('email')?.toString();
|
||||
|
||||
if (!name) {
|
||||
return new Response('Name is required', { status: 400 });
|
||||
}
|
||||
|
||||
// Get user's first organization
|
||||
const userOrg = await db.select()
|
||||
.from(members)
|
||||
.where(eq(members.userId, user.id))
|
||||
.get();
|
||||
|
||||
if (!userOrg) {
|
||||
return new Response('No organization found', { status: 400 });
|
||||
}
|
||||
|
||||
// Create client
|
||||
await db.insert(clients).values({
|
||||
id: nanoid(),
|
||||
organizationId: userOrg.organizationId,
|
||||
name,
|
||||
email: email || null,
|
||||
});
|
||||
|
||||
return redirect('/dashboard/clients');
|
||||
};
|
||||
34
src/pages/api/organizations/create.ts
Normal file
34
src/pages/api/organizations/create.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { db } from '../../../db';
|
||||
import { organizations, members } from '../../../db/schema';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export const POST: APIRoute = async ({ request, locals, redirect }) => {
|
||||
const user = locals.user;
|
||||
if (!user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const name = formData.get('name')?.toString();
|
||||
|
||||
if (!name) {
|
||||
return new Response('Name is required', { status: 400 });
|
||||
}
|
||||
|
||||
// Create organization
|
||||
const orgId = nanoid();
|
||||
await db.insert(organizations).values({
|
||||
id: orgId,
|
||||
name,
|
||||
});
|
||||
|
||||
// Add user as owner
|
||||
await db.insert(members).values({
|
||||
userId: user.id,
|
||||
organizationId: orgId,
|
||||
role: 'owner',
|
||||
});
|
||||
|
||||
return redirect('/dashboard');
|
||||
};
|
||||
60
src/pages/api/team/change-role.ts
Normal file
60
src/pages/api/team/change-role.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { db } from '../../../db';
|
||||
import { members } from '../../../db/schema';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
|
||||
export const POST: APIRoute = async ({ request, locals, redirect }) => {
|
||||
const user = locals.user;
|
||||
if (!user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
// Check if user is admin
|
||||
const userMembership = await db.select()
|
||||
.from(members)
|
||||
.where(eq(members.userId, user.id))
|
||||
.get();
|
||||
|
||||
if (!userMembership || (userMembership.role !== 'owner' && userMembership.role !== 'admin')) {
|
||||
return new Response('Unauthorized', { status: 403 });
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const targetUserId = formData.get('userId')?.toString();
|
||||
const newRole = formData.get('role')?.toString();
|
||||
|
||||
if (!targetUserId || !newRole) {
|
||||
return new Response('Missing parameters', { status: 400 });
|
||||
}
|
||||
|
||||
if (!['member', 'admin'].includes(newRole)) {
|
||||
return new Response('Invalid role', { status: 400 });
|
||||
}
|
||||
|
||||
// Can't change owner's role
|
||||
const targetMember = await db.select()
|
||||
.from(members)
|
||||
.where(and(
|
||||
eq(members.userId, targetUserId),
|
||||
eq(members.organizationId, userMembership.organizationId)
|
||||
))
|
||||
.get();
|
||||
|
||||
if (!targetMember) {
|
||||
return new Response('Member not found', { status: 404 });
|
||||
}
|
||||
|
||||
if (targetMember.role === 'owner') {
|
||||
return new Response('Cannot change owner role', { status: 403 });
|
||||
}
|
||||
|
||||
// Update role
|
||||
await db.update(members)
|
||||
.set({ role: newRole })
|
||||
.where(and(
|
||||
eq(members.userId, targetUserId),
|
||||
eq(members.organizationId, userMembership.organizationId)
|
||||
));
|
||||
|
||||
return redirect('/dashboard/team');
|
||||
};
|
||||
65
src/pages/api/team/invite.ts
Normal file
65
src/pages/api/team/invite.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { db } from '../../../db';
|
||||
import { users, members } from '../../../db/schema';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
|
||||
export const POST: APIRoute = async ({ request, locals, redirect }) => {
|
||||
const user = locals.user;
|
||||
if (!user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
// Check if user is admin
|
||||
const userMembership = await db.select()
|
||||
.from(members)
|
||||
.where(eq(members.userId, user.id))
|
||||
.get();
|
||||
|
||||
if (!userMembership || (userMembership.role !== 'owner' && userMembership.role !== 'admin')) {
|
||||
return new Response('Unauthorized', { status: 403 });
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const email = formData.get('email')?.toString();
|
||||
const role = formData.get('role')?.toString() || 'member';
|
||||
|
||||
if (!email) {
|
||||
return new Response('Email is required', { status: 400 });
|
||||
}
|
||||
|
||||
if (!['member', 'admin'].includes(role)) {
|
||||
return new Response('Invalid role', { status: 400 });
|
||||
}
|
||||
|
||||
// Find user by email
|
||||
const invitedUser = await db.select()
|
||||
.from(users)
|
||||
.where(eq(users.email, email))
|
||||
.get();
|
||||
|
||||
if (!invitedUser) {
|
||||
return new Response('User not found. They must create an account first.', { status: 404 });
|
||||
}
|
||||
|
||||
// Check if already a member
|
||||
const existingMember = await db.select()
|
||||
.from(members)
|
||||
.where(and(
|
||||
eq(members.userId, invitedUser.id),
|
||||
eq(members.organizationId, userMembership.organizationId)
|
||||
))
|
||||
.get();
|
||||
|
||||
if (existingMember) {
|
||||
return new Response('User is already a member', { status: 400 });
|
||||
}
|
||||
|
||||
// Add to organization
|
||||
await db.insert(members).values({
|
||||
userId: invitedUser.id,
|
||||
organizationId: userMembership.organizationId,
|
||||
role,
|
||||
});
|
||||
|
||||
return redirect('/dashboard/team');
|
||||
};
|
||||
59
src/pages/api/team/remove.ts
Normal file
59
src/pages/api/team/remove.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { db } from '../../../db';
|
||||
import { members } from '../../../db/schema';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
|
||||
export const POST: APIRoute = async ({ request, locals, redirect }) => {
|
||||
const user = locals.user;
|
||||
if (!user) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
// Check if user is admin
|
||||
const userMembership = await db.select()
|
||||
.from(members)
|
||||
.where(eq(members.userId, user.id))
|
||||
.get();
|
||||
|
||||
if (!userMembership || (userMembership.role !== 'owner' && userMembership.role !== 'admin')) {
|
||||
return new Response('Unauthorized', { status: 403 });
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const targetUserId = formData.get('userId')?.toString();
|
||||
|
||||
if (!targetUserId) {
|
||||
return new Response('Missing user ID', { status: 400 });
|
||||
}
|
||||
|
||||
// Can't remove self
|
||||
if (targetUserId === user.id) {
|
||||
return new Response('Cannot remove yourself', { status: 403 });
|
||||
}
|
||||
|
||||
// Can't remove owner
|
||||
const targetMember = await db.select()
|
||||
.from(members)
|
||||
.where(and(
|
||||
eq(members.userId, targetUserId),
|
||||
eq(members.organizationId, userMembership.organizationId)
|
||||
))
|
||||
.get();
|
||||
|
||||
if (!targetMember) {
|
||||
return new Response('Member not found', { status: 404 });
|
||||
}
|
||||
|
||||
if (targetMember.role === 'owner') {
|
||||
return new Response('Cannot remove owner', { status: 403 });
|
||||
}
|
||||
|
||||
// Remove member
|
||||
await db.delete(members)
|
||||
.where(and(
|
||||
eq(members.userId, targetUserId),
|
||||
eq(members.organizationId, userMembership.organizationId)
|
||||
));
|
||||
|
||||
return redirect('/dashboard/team');
|
||||
};
|
||||
36
src/pages/api/time-entries/[id]/delete.ts
Normal file
36
src/pages/api/time-entries/[id]/delete.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { db } from '../../../../db';
|
||||
import { timeEntries } from '../../../../db/schema';
|
||||
import { eq, and } from 'drizzle-orm';
|
||||
|
||||
export const POST: APIRoute = async ({ params, locals, redirect }) => {
|
||||
const user = locals.user;
|
||||
if (!user) {
|
||||
return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 });
|
||||
}
|
||||
|
||||
const entryId = params.id;
|
||||
if (!entryId) {
|
||||
return new Response(JSON.stringify({ error: 'Entry ID required' }), { status: 400 });
|
||||
}
|
||||
|
||||
// Verify the entry belongs to the user
|
||||
const entry = await db.select()
|
||||
.from(timeEntries)
|
||||
.where(and(
|
||||
eq(timeEntries.id, entryId),
|
||||
eq(timeEntries.userId, user.id)
|
||||
))
|
||||
.get();
|
||||
|
||||
if (!entry) {
|
||||
return new Response(JSON.stringify({ error: 'Entry not found' }), { status: 404 });
|
||||
}
|
||||
|
||||
// Delete the entry
|
||||
await db.delete(timeEntries)
|
||||
.where(eq(timeEntries.id, entryId))
|
||||
.run();
|
||||
|
||||
return redirect('/dashboard/tracker');
|
||||
};
|
||||
78
src/pages/api/time-entries/start.ts
Normal file
78
src/pages/api/time-entries/start.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { db } from '../../../db';
|
||||
import { timeEntries, members, timeEntryTags, categories } from '../../../db/schema';
|
||||
import { eq, and, isNull } from 'drizzle-orm';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export const POST: APIRoute = async ({ request, locals }) => {
|
||||
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
||||
|
||||
const body = await request.json();
|
||||
const description = body.description || '';
|
||||
const clientId = body.clientId;
|
||||
const categoryId = body.categoryId;
|
||||
const tags = body.tags || [];
|
||||
|
||||
if (!clientId) {
|
||||
return new Response('Client is required', { status: 400 });
|
||||
}
|
||||
|
||||
if (!categoryId) {
|
||||
return new Response('Category is required', { status: 400 });
|
||||
}
|
||||
|
||||
// Check for running entry
|
||||
const runningEntry = await db.select().from(timeEntries).where(
|
||||
and(
|
||||
eq(timeEntries.userId, locals.user.id),
|
||||
isNull(timeEntries.endTime)
|
||||
)
|
||||
).get();
|
||||
|
||||
if (runningEntry) {
|
||||
return new Response('Timer already running', { status: 400 });
|
||||
}
|
||||
|
||||
// Get default org (first one)
|
||||
const member = await db.select().from(members).where(eq(members.userId, locals.user.id)).limit(1).get();
|
||||
if (!member) {
|
||||
return new Response('No organization found', { status: 400 });
|
||||
}
|
||||
|
||||
// Verify category belongs to user's organization
|
||||
const category = await db.select().from(categories).where(
|
||||
and(
|
||||
eq(categories.id, categoryId),
|
||||
eq(categories.organizationId, member.organizationId)
|
||||
)
|
||||
).get();
|
||||
|
||||
if (!category) {
|
||||
return new Response('Invalid category', { status: 400 });
|
||||
}
|
||||
|
||||
const startTime = new Date();
|
||||
const id = nanoid();
|
||||
|
||||
await db.insert(timeEntries).values({
|
||||
id,
|
||||
userId: locals.user.id,
|
||||
organizationId: member.organizationId,
|
||||
clientId,
|
||||
categoryId,
|
||||
startTime,
|
||||
description,
|
||||
});
|
||||
|
||||
// Add tags if provided
|
||||
if (tags.length > 0) {
|
||||
await db.insert(timeEntryTags).values(
|
||||
tags.map((tagId: string) => ({
|
||||
timeEntryId: id,
|
||||
tagId,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ id, startTime }), { status: 200 });
|
||||
};
|
||||
25
src/pages/api/time-entries/stop.ts
Normal file
25
src/pages/api/time-entries/stop.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { db } from '../../../db';
|
||||
import { timeEntries } from '../../../db/schema';
|
||||
import { eq, and, isNull } from 'drizzle-orm';
|
||||
|
||||
export const POST: APIRoute = async ({ locals }) => {
|
||||
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
||||
|
||||
const runningEntry = await db.select().from(timeEntries).where(
|
||||
and(
|
||||
eq(timeEntries.userId, locals.user.id),
|
||||
isNull(timeEntries.endTime)
|
||||
)
|
||||
).get();
|
||||
|
||||
if (!runningEntry) {
|
||||
return new Response('No timer running', { status: 400 });
|
||||
}
|
||||
|
||||
await db.update(timeEntries)
|
||||
.set({ endTime: new Date() })
|
||||
.where(eq(timeEntries.id, runningEntry.id));
|
||||
|
||||
return new Response(JSON.stringify({ success: true }), { status: 200 });
|
||||
};
|
||||
Reference in New Issue
Block a user