diff --git a/apps/server/src/api/v2/organization/GET.ts b/apps/server/src/api/v2/organization/GET.ts new file mode 100644 index 000000000..e63d55cba --- /dev/null +++ b/apps/server/src/api/v2/organization/GET.ts @@ -0,0 +1,16 @@ +import { getOrganization } from '@buster/database'; +import type { GetOrganizationResponse } from '@buster/server-shared/organization'; +import { Hono } from 'hono'; +import { requireOrganization } from '../../../middleware/auth'; + +const app = new Hono().use('*', requireOrganization).get('/', async (c) => { + const userOrg = c.get('userOrganizationInfo'); + + const organization: GetOrganizationResponse = await getOrganization({ + organizationId: userOrg.organizationId, + }); + + return c.json(organization); +}); + +export default app; diff --git a/apps/server/src/api/v2/organization/PUT.ts b/apps/server/src/api/v2/organization/PUT.ts index cbf6fac6e..c2df5153e 100644 --- a/apps/server/src/api/v2/organization/PUT.ts +++ b/apps/server/src/api/v2/organization/PUT.ts @@ -14,7 +14,7 @@ import { requireOrganizationAdmin } from '../../../middleware/auth'; * Updates organization settings * Currently supports updating organization color palettes */ -export async function updateOrganizationHandler( +async function updateOrganizationHandler( organizationId: string, request: UpdateOrganizationRequest, user: User @@ -48,32 +48,34 @@ export async function updateOrganizationHandler( } // Create route module for the update endpoint -const app = new Hono().put( - '/', - requireOrganizationAdmin, - zValidator('json', UpdateOrganizationRequestSchema), - async (c) => { - const request = c.req.valid('json'); +const app = new Hono() + .use('*', requireOrganizationAdmin) + .put('/', zValidator('json', UpdateOrganizationRequestSchema), async (c) => { + const request = await c.req.valid('json'); const user = c.get('busterUser'); const userOrg = c.get('userOrganizationInfo'); const organizationId = userOrg.organizationId; - //const role = userOrg.role; - // if (!canEditOrganization(role)) { - // throw new HTTPException(403, { - // message: 'User does not have permission to edit organization', - // }); - // } + try { + const response: UpdateOrganizationResponse = await updateOrganizationHandler( + organizationId, + request, + user + ); + return c.json(response); + } catch (error) { + console.error('Error in updateOrganizationHandler:', { + organizationId, + userId: user.id, + requestFields: Object.keys(request), + error: error instanceof Error ? error.message : error, + }); - const response: UpdateOrganizationResponse = await updateOrganizationHandler( - organizationId, - request, - user - ); - - return c.json(response); - } -); + throw new HTTPException(500, { + message: 'Failed to update organization', + }); + } + }); export default app; diff --git a/apps/server/src/api/v2/organization/index.ts b/apps/server/src/api/v2/organization/index.ts index dce9f2baf..aab4f0aac 100644 --- a/apps/server/src/api/v2/organization/index.ts +++ b/apps/server/src/api/v2/organization/index.ts @@ -1,11 +1,13 @@ import { Hono } from 'hono'; import { requireAuth } from '../../../middleware/auth'; +import GET from './GET'; import PUT from './PUT'; const app = new Hono() // Apply authentication globally to ALL routes in this router .use('*', requireAuth) // Mount the modular routes + .route('/', GET) .route('/', PUT); export default app; diff --git a/apps/server/src/middleware/auth.ts b/apps/server/src/middleware/auth.ts index edaf1d5ba..a311722a9 100644 --- a/apps/server/src/middleware/auth.ts +++ b/apps/server/src/middleware/auth.ts @@ -97,7 +97,8 @@ export async function requireUser(c: Context, next: Next) { } } -export const requireOrganization = async (c: Context) => { +// Utility function to get user organization (can be called from other middleware) +const getUserOrganization = async (c: Context) => { const user = c.get('busterUser'); const userOrganizationInfo = c.get('userOrganizationInfo'); @@ -116,28 +117,28 @@ export const requireOrganization = async (c: Context) => { return userOrg; }; -export const requireOrganizationAdmin = async (c: Context) => { +// Middleware version that can be used in route chains +export const requireOrganization = async (c: Context, next: Next) => { + await getUserOrganization(c); + await next(); +}; + +export const requireOrganizationAdmin = async (c: Context, next: Next) => { const user = c.get('busterUser'); if (!user?.id) { console.warn('This is likely an issue where requireAuth middleware was not called first'); - return c.json({ - message: 'User not authenticated', - }); + return c.json({ message: 'User not authenticated' }, 401); } - const userOrg = await requireOrganization(c); + const userOrg = await getUserOrganization(c); const isAdmin = isOrganizationAdmin(userOrg.role); if (!isAdmin) { - return c.json( - { - message: 'User is not an organization admin', - }, - 403 - ); + return c.json({ message: 'User is not an organization admin' }, 403); } - return true; + // If all checks pass, continue to the next middleware/handler + return await next(); }; diff --git a/packages/database/src/queries/organizations/update-organization.ts b/packages/database/src/queries/organizations/update-organization.ts index 5f9af340f..8c47b24b1 100644 --- a/packages/database/src/queries/organizations/update-organization.ts +++ b/packages/database/src/queries/organizations/update-organization.ts @@ -1,13 +1,13 @@ -import { type InferSelectModel, and, eq, isNull } from 'drizzle-orm'; +import { and, eq, isNull } from 'drizzle-orm'; import { z } from 'zod'; import { db } from '../../connection'; import { organizations } from '../../schema'; -import { getUserOrganizationId } from './organizations'; +import type { OrganizationColorPalettes } from '../../schema-types'; // Organization Color Palette schema const OrganizationColorPaletteSchema = z.object({ id: z.string(), - color: z.array(z.string()), + colors: z.array(z.string()), }); // Input validation schema @@ -30,7 +30,7 @@ export const updateOrganization = async (params: UpdateOrganizationInput): Promi // Build update data const updateData: { updatedAt: string; - organizationColorPalettes?: Array<{ id: string; color: string[] }>; + organizationColorPalettes?: OrganizationColorPalettes; } = { updatedAt: new Date().toISOString(), }; @@ -44,12 +44,6 @@ export const updateOrganization = async (params: UpdateOrganizationInput): Promi .update(organizations) .set(updateData) .where(and(eq(organizations.id, organizationId), isNull(organizations.deletedAt))); - - console.info('Organization updated successfully:', { - organizationId, - - updatedFields: organizationColorPalettes !== undefined ? ['organizationColorPalettes'] : [], - }); } catch (error) { console.error('Error updating organization:', { organizationId, diff --git a/packages/database/src/schema-types/organization.ts b/packages/database/src/schema-types/organization.ts index cc6451e75..7882e2ea9 100644 --- a/packages/database/src/schema-types/organization.ts +++ b/packages/database/src/schema-types/organization.ts @@ -1,7 +1,7 @@ // Organization Color Palette Types export type OrganizationColorPalette = { id: string; - color: string[]; + colors: string[]; }; export type OrganizationColorPalettes = OrganizationColorPalette[]; diff --git a/packages/server-shared/.cursor/global.mdc b/packages/server-shared/.cursor/global.mdc index 146ecf59f..e1102e161 100644 --- a/packages/server-shared/.cursor/global.mdc +++ b/packages/server-shared/.cursor/global.mdc @@ -35,4 +35,14 @@ This structure ensures a consistent and organized approach to managing types acr ### Pagination -If we need to use pagination for a type we can take advantage of the generic types found in the type-utilities/pagination file \ No newline at end of file +If we need to use pagination for a type we can take advantage of the generic types found in the type-utilities/pagination file + + +## Database partity +There are cirumstances where we need to ensure that a schema in server-shared and a database type are the same. We should use a pattern like this: + +``` + type _OrganizationEqualityCheck = Expect>; +``` + +Where organizations is imported as a "type" from @buster/database. This ensure that we are maintaining type parity between the two packages. \ No newline at end of file diff --git a/packages/server-shared/src/organization/organization.types.ts b/packages/server-shared/src/organization/organization.types.ts index c894f667e..046f3a0ee 100644 --- a/packages/server-shared/src/organization/organization.types.ts +++ b/packages/server-shared/src/organization/organization.types.ts @@ -3,9 +3,17 @@ import { z } from 'zod'; import type { Equal, Expect } from '../type-utilities'; import { OrganizationRoleSchema } from './roles.types'; +// Hex color validation schema for 3 or 6 digit hex codes +const HexColorSchema = z + .string() + .regex( + /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/, + 'Must be a valid 3 or 6 digit hex color code (e.g., #fff or #ffffff)' + ); + export const OrganizationColorPaletteSchema = z.object({ id: z.string(), - color: z.array(z.string()), + colors: z.array(HexColorSchema).min(1).max(25), }); export const OrganizationSchema = z.object({ diff --git a/packages/server-shared/src/organization/responses.ts b/packages/server-shared/src/organization/responses.ts index d3b15305f..40b9ebe75 100644 --- a/packages/server-shared/src/organization/responses.ts +++ b/packages/server-shared/src/organization/responses.ts @@ -1,6 +1,10 @@ import type { z } from 'zod'; import { OrganizationSchema } from './organization.types'; +export const GetOrganizationResponseSchema = OrganizationSchema; + +export type GetOrganizationResponse = z.infer; + export const UpdateOrganizationResponseSchema = OrganizationSchema; export type UpdateOrganizationResponse = z.infer;