Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"workbench.colorTheme": "Darcula",
"workbench.colorTheme": "Tokyo Night",
"workbench.iconTheme": "material-icon-theme",
"editor.fontFamily": "JetBrains Mono",
"editor.defaultFormatter": "esbenp.prettier-vscode",
Expand Down
12 changes: 6 additions & 6 deletions scripts/dev-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@
* - Run seed first: `pnpm seed`
* - Then add workspaces: `pnpm dev:workspace --email=alice@acmecorp.com --name="New Project"`
*/
import { db } from "@/services/db/drizzle.ts";
import { logger } from "@/helpers/index.ts";
import {
accounts,
profiles,
workspaces,
workspaceMemberships,
accounts,
workspaces,
type AccountSelectType,
type WorkspaceSelectType,
type ProfileSelectType,
type WorkspaceMembershipInsertType
type WorkspaceMembershipInsertType,
type WorkspaceSelectType
} from "@/schema.ts";
import { logger } from "@/helpers/index.ts";
import { db } from "@/services/db/drizzle.ts";
import { eq } from "drizzle-orm";

interface CreateWorkspaceOptions {
Expand Down
4 changes: 2 additions & 2 deletions scripts/token-test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import jwt from "jsonwebtoken";
import dotenv from "dotenv";
import { logger } from "@/helpers/index.ts";
import dotenv from "dotenv";
import jwt from "jsonwebtoken";

dotenv.config();

Expand Down
1 change: 1 addition & 0 deletions src/docs/openapi-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export const ErrorResponseSchema = z
})
.openapi("ErrorResponse");

// TODO I think we need a filter model schema... maybe align with ag-grid? or create an adapter for ag-Grid.
export const PaginationSchema = z
.object({
page: z.number().int().positive(),
Expand Down
39 changes: 21 additions & 18 deletions src/handlers/accounts/accounts.handlers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { HttpErrors, handleHttpError } from "@/helpers/HttpError.ts";
import { gatewayResponse, logger } from "@/helpers/index.ts";
import { HttpErrors, HttpStatusCode } from "@/helpers/Http.ts";
import { asyncHandler } from "@/helpers/request.ts";
import { apiResponse } from "@/helpers/response.ts";
import { accounts, uuidSchema, type AccountSelectType, type AccountWithRelations } from "@/schema.ts";
import { db } from "@/services/db/drizzle.ts";
import type { Request, Response } from "express";
Expand All @@ -9,51 +9,54 @@ import { createDbAccount, getAccountWithRelations } from "./accounts.methods.ts"
export const getAccounts = asyncHandler(async (_req: Request, res: Response): Promise<void> => {
const result = await db.select().from(accounts).execute();

logger.info({ msg: `Fetched accounts: ${result.length}` });

const response = gatewayResponse<AccountSelectType[]>().success(200, result);
const response = apiResponse.success<AccountSelectType[]>(
HttpStatusCode.OK,
result,
`Fetched accounts: ${result.length}`
);

res.status(response.code).send(response);
});

export const getAccount = asyncHandler(async (req: Request, res: Response): Promise<void> => {
const validationResult = uuidSchema.safeParse({ uuid: req.params.id });

if (!validationResult.success) {
handleHttpError(
HttpErrors.ValidationFailed(`Invalid account ID: ${validationResult.error.message}`),
res,
gatewayResponse
const response = apiResponse.error(
HttpErrors.ValidationFailed(`Invalid account ID: ${validationResult.error.message}`)
);
res.status(response.code).send(response);
return;
}

if (!req.params.id) {
handleHttpError(HttpErrors.MissingParameter("Account ID"), res, gatewayResponse);
const response = apiResponse.error(HttpErrors.MissingParameter("Account ID"));
res.status(response.code).send(response);
return;
}

const result = await getAccountWithRelations(req.params.id);

if (!result) {
handleHttpError(HttpErrors.AccountNotFound(), res, gatewayResponse);
const response = apiResponse.error(HttpErrors.NotFound("Account"));
res.status(response.code).send(response);
return;
}

logger.info({ msg: `Fetched account with UUID ${req.params.id}` });

const response = gatewayResponse<AccountWithRelations>().success(200, result);

const response = apiResponse.success<AccountWithRelations>(
HttpStatusCode.OK,
result,
`Fetched account with UUID ${req.params.id}`
);
res.status(response.code).send(response);
});

export const createAccount = asyncHandler(async (req: Request, res: Response): Promise<void> => {
const { fullName, phone, email } = req.body;

logger.info({ msg: `Creating account...` });

const accountId = await createDbAccount({ fullName, phone, email });

const response = gatewayResponse<string>().success(200, accountId);
const response = apiResponse.success<string>(HttpStatusCode.OK, accountId, "Account created");

res.status(response.code).send(response);
});
Expand Down
8 changes: 2 additions & 6 deletions src/handlers/accounts/accounts.methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export async function getAccountById(accountId: string): Promise<AccountSelectTy
return result;
}

export async function getAccountWithRelations(accountId: string): Promise<AccountWithRelations> {
export async function getAccountWithRelations(accountId: string): Promise<AccountWithRelations | null> {
const validationResult = uuidSchema.safeParse({ uuid: accountId });
if (!validationResult.success) {
throw new Error(`Invalid account ID: ${validationResult.error.message}`);
Expand All @@ -85,9 +85,5 @@ export async function getAccountWithRelations(accountId: string): Promise<Accoun
}
});

if (!result) {
throw new Error("Account not found");
}

return result;
return result || null;
}
88 changes: 45 additions & 43 deletions src/handlers/admin/admin.handlers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { HttpErrors, handleHttpError } from "@/helpers/HttpError.ts";
import { gatewayResponse, logger } from "@/helpers/index.ts";
import { HttpErrors, HttpStatusCode } from "@/helpers/Http.ts";
import { apiResponse } from "@/helpers/response.ts";
import { asyncHandler } from "@/helpers/request.ts";
import { accounts, workspaceMemberships, workspaces } from "@/schema.ts";
import { AUDIT_ACTIONS, ENTITY_TYPES, auditHelpers, createAuditLog } from "@/services/auditLog.ts";
Expand All @@ -17,8 +17,6 @@ export const listAllAccounts = asyncHandler(async (req: Request, res: Response):
const limit = parseInt(req.query.limit as string) || 20;
const offset = (page - 1) * limit;

logger.info({ msg: `SuperAdmin listing all accounts - page: ${page}, limit: ${limit}` });

// Get paginated accounts with total count in single query
const accountsWithCount = await db
.select({
Expand All @@ -40,8 +38,8 @@ export const listAllAccounts = asyncHandler(async (req: Request, res: Response):
// Clean data by removing totalCount from individual records
const accountsList = accountsWithCount.map(({ totalCount: _, ...account }) => account);

const response = gatewayResponse().success(
200,
const response = apiResponse.success(
HttpStatusCode.OK,
{
accounts: accountsList,
pagination: {
Expand All @@ -66,18 +64,17 @@ export const createAccountForUser = asyncHandler(async (req: Request, res: Respo
const { accountId } = req;

if (!accountId) {
handleHttpError(HttpErrors.Unauthorized(), res, gatewayResponse);
const response = apiResponse.error(HttpErrors.Unauthorized());
res.status(response.code).send(response);
return;
}

logger.info({ msg: `SuperAdmin creating account for ${email}` });

// Check if account already exists
const [existingAccount] = await db.select().from(accounts).where(eq(accounts.email, email)).limit(1);

if (existingAccount) {
logger.error({ email }, `Account with email ${email} already exists`);
handleHttpError(HttpErrors.BadRequest("Unable to create account with provided information"), res, gatewayResponse);
const response = apiResponse.error(HttpErrors.BadRequest("Unable to create account with provided information"));
res.status(response.code).send(response);
return;
}

Expand Down Expand Up @@ -121,11 +118,12 @@ export const createAccountForUser = asyncHandler(async (req: Request, res: Respo
});

if (!newAccount) {
handleHttpError(HttpErrors.DatabaseError("Failed to create account"), res, gatewayResponse);
const response = apiResponse.error(HttpErrors.DatabaseError("Failed to create account"));
res.status(response.code).send(response);
return;
}

const response = gatewayResponse().success(201, { account: newAccount }, "Account created successfully");
const response = apiResponse.success(HttpStatusCode.CREATED, { account: newAccount }, "Account created successfully");

res.status(response.code).send(response);
});
Expand All @@ -139,24 +137,25 @@ export const updateAccountRole = asyncHandler(async (req: Request, res: Response
const { accountId } = req;

if (!targetAccountId) {
handleHttpError(HttpErrors.MissingParameter("Account ID"), res, gatewayResponse);
const response = apiResponse.error(HttpErrors.MissingParameter("Account ID"));
res.status(response.code).send(response);
return;
}

if (!accountId) {
handleHttpError(HttpErrors.Unauthorized(), res, gatewayResponse);
const response = apiResponse.error(HttpErrors.Unauthorized());
res.status(response.code).send(response);
return;
}

const { isSuperAdmin } = req.body;

if (typeof isSuperAdmin !== "boolean") {
handleHttpError(HttpErrors.ValidationFailed("isSuperAdmin must be a boolean value"), res, gatewayResponse);
const response = apiResponse.error(HttpErrors.ValidationFailed("isSuperAdmin must be a boolean value"));
res.status(response.code).send(response);
return;
}

logger.info({ msg: `SuperAdmin updating account ${targetAccountId} role to isSuperAdmin: ${isSuperAdmin}` });

// Get current account to track the change (outside transaction)
const [currentAccount] = await db
.select({ isSuperAdmin: accounts.isSuperAdmin })
Expand All @@ -165,7 +164,8 @@ export const updateAccountRole = asyncHandler(async (req: Request, res: Response
.limit(1);

if (!currentAccount) {
handleHttpError(HttpErrors.AccountNotFound(), res, gatewayResponse);
const response = apiResponse.error(HttpErrors.NotFound("Account"));
res.status(response.code).send(response);
return;
}

Expand Down Expand Up @@ -195,12 +195,13 @@ export const updateAccountRole = asyncHandler(async (req: Request, res: Response
});

if (!updatedAccount) {
handleHttpError(HttpErrors.AccountNotFound(), res, gatewayResponse);
const response = apiResponse.error(HttpErrors.NotFound("Account"));
res.status(response.code).send(response);
return;
}

const response = gatewayResponse().success(
200,
const response = apiResponse.success(
HttpStatusCode.OK,
{ account: updatedAccount },
`Account role updated to SuperAdmin: ${isSuperAdmin}`
);
Expand All @@ -217,29 +218,28 @@ export const updateAccountStatus = asyncHandler(async (req: Request, res: Respon
const { accountId } = req;

if (!targetAccountId) {
handleHttpError(HttpErrors.MissingParameter("Account ID"), res, gatewayResponse);
const response = apiResponse.error(HttpErrors.MissingParameter("Account ID"));
res.status(response.code).send(response);
return;
}

if (!accountId) {
handleHttpError(HttpErrors.Unauthorized(), res, gatewayResponse);
const response = apiResponse.error(HttpErrors.Unauthorized());
res.status(response.code).send(response);
return;
}

const { status } = req.body;
const validStatuses = ["active", "inactive", "suspended"];

if (!status || !validStatuses.includes(status)) {
handleHttpError(
HttpErrors.ValidationFailed(`Status must be one of: ${validStatuses.join(", ")}`),
res,
gatewayResponse
const response = apiResponse.error(
HttpErrors.ValidationFailed(`Status must be one of: ${validStatuses.join(", ")}`)
);
res.status(response.code).send(response);
return;
}

logger.info({ msg: `SuperAdmin updating account ${targetAccountId} status to: ${status}` });

// Get current account to track the change (outside transaction)
const [currentAccount] = await db
.select({ status: accounts.status })
Expand All @@ -248,7 +248,8 @@ export const updateAccountStatus = asyncHandler(async (req: Request, res: Respon
.limit(1);

if (!currentAccount) {
handleHttpError(HttpErrors.AccountNotFound(), res, gatewayResponse);
const response = apiResponse.error(HttpErrors.NotFound("Account"));
res.status(response.code).send(response);
return;
}

Expand All @@ -267,11 +268,16 @@ export const updateAccountStatus = asyncHandler(async (req: Request, res: Respon
});

if (!updatedAccount) {
handleHttpError(HttpErrors.AccountNotFound(), res, gatewayResponse);
const response = apiResponse.error(HttpErrors.NotFound("Account"));
res.status(response.code).send(response);
return;
}

const response = gatewayResponse().success(200, { account: updatedAccount }, `Account status updated to: ${status}`);
const response = apiResponse.success(
HttpStatusCode.OK,
{ account: updatedAccount },
`Account status updated to: ${status}`
);

res.status(response.code).send(response);
});
Expand All @@ -285,8 +291,6 @@ export const listAllWorkspaces = asyncHandler(async (req: Request, res: Response
const limit = parseInt(req.query.limit as string) || 20;
const offset = (page - 1) * limit;

logger.info({ msg: `SuperAdmin listing all workspaces - page: ${page}, limit: ${limit}` });

// Get paginated workspaces with owner info, member counts, and total count in single query
const workspacesWithCount = await db
.select({
Expand All @@ -303,8 +307,8 @@ export const listAllWorkspaces = asyncHandler(async (req: Request, res: Response
email: accounts.email
},
memberCount: sql<number>`(
SELECT COUNT(*)
FROM workspace_memberships
SELECT COUNT(*)
FROM workspace_memberships
WHERE workspace_memberships.workspace_id = ${workspaces.uuid}
)`,
totalCount: sql<number>`count(*) over()`
Expand All @@ -319,8 +323,8 @@ export const listAllWorkspaces = asyncHandler(async (req: Request, res: Response
// Clean data by removing totalCount from individual records
const workspacesList = workspacesWithCount.map(({ totalCount: _, ...workspace }) => workspace);

const response = gatewayResponse().success(
200,
const response = apiResponse.success(
HttpStatusCode.OK,
{
workspaces: workspacesList,
pagination: {
Expand Down Expand Up @@ -349,8 +353,6 @@ export const listAllMemberships = asyncHandler(async (req: Request, res: Respons
const workspaceId = req.query.workspaceId as string;
const accountId = req.query.accountId as string;

logger.info({ msg: `SuperAdmin listing memberships - filters: workspace=${workspaceId}, account=${accountId}` });

// Build conditions for filtering
const conditions = [];
if (workspaceId) {
Expand Down Expand Up @@ -401,8 +403,8 @@ export const listAllMemberships = asyncHandler(async (req: Request, res: Respons
const [countResult] = await countQuery;
const count = countResult?.count || 0;

const response = gatewayResponse().success(
200,
const response = apiResponse.success(
HttpStatusCode.OK,
{
memberships: membershipsList,
pagination: {
Expand Down
Loading