From 5411e3ec661c9693c3498244ee5e9fc3b6ee42f6 Mon Sep 17 00:00:00 2001 From: premsgr77 Date: Tue, 17 Feb 2026 16:28:55 +0545 Subject: [PATCH] feat: enable/disable account users and block disabled users on account routes --- .../src/lib/ensureUserEnabledForAccount.ts | 47 +++++++++++++++ .../model/accountInvitations/controller.ts | 16 +++-- .../src/model/accountUsers/controller.ts | 24 +++++++- .../handlers/disableAccountUser.ts | 60 +++++++++++++++++++ .../handlers/enableAccountUser.ts | 60 +++++++++++++++++++ .../src/model/accountUsers/handlers/index.ts | 4 ++ .../src/model/accountUsers/sqlFactory.ts | 2 + .../fastify/src/model/accounts/controller.ts | 14 +++-- .../src/model/accounts/handlers/myAccounts.ts | 6 +- .../fastify/src/model/accounts/sqlFactory.ts | 2 +- packages/fastify/src/types/accountUser.ts | 1 + packages/fastify/src/types/config.ts | 2 + 12 files changed, 226 insertions(+), 12 deletions(-) create mode 100644 packages/fastify/src/lib/ensureUserEnabledForAccount.ts create mode 100644 packages/fastify/src/model/accountUsers/handlers/disableAccountUser.ts create mode 100644 packages/fastify/src/model/accountUsers/handlers/enableAccountUser.ts diff --git a/packages/fastify/src/lib/ensureUserEnabledForAccount.ts b/packages/fastify/src/lib/ensureUserEnabledForAccount.ts new file mode 100644 index 00000000..b5dbb380 --- /dev/null +++ b/packages/fastify/src/lib/ensureUserEnabledForAccount.ts @@ -0,0 +1,47 @@ +import AccountUserService from "../model/accountUsers/service"; + +import type { FastifyReply, FastifyRequest } from "fastify"; +import type { SessionRequest } from "supertokens-node/framework/fastify"; + +/** + * PreHandler that blocks access when the session user is disabled for the + * current account. Use after verifySession() on account-scoped routes. + * Uses request.account?.database ?? undefined so the check runs in public + * or the account schema as appropriate. + */ +const ensureUserEnabledForAccount = async ( + request: FastifyRequest, + reply: FastifyReply, +): Promise => { + const sessionRequest = request as SessionRequest; + + if (!sessionRequest.account || !sessionRequest.user) { + return; + } + + const { config, slonik } = sessionRequest; + const accountId = sessionRequest.account.id; + const userId = sessionRequest.user.id; + const dbSchema = sessionRequest.account.database ?? undefined; + + const service = new AccountUserService(config, slonik, accountId, dbSchema); + + const row = await service.findOne({ + AND: [ + { key: "account_id", operator: "eq", value: accountId }, + { key: "user_id", operator: "eq", value: userId }, + ], + }); + + const disabled = row && (row as { disabled?: boolean }).disabled; + + if (!row || disabled) { + return reply.status(403).send({ + error: "Forbidden", + message: "User is disabled for this account", + statusCode: 403, + }); + } +}; + +export default ensureUserEnabledForAccount; diff --git a/packages/fastify/src/model/accountInvitations/controller.ts b/packages/fastify/src/model/accountInvitations/controller.ts index 5e38396e..619bb2c8 100644 --- a/packages/fastify/src/model/accountInvitations/controller.ts +++ b/packages/fastify/src/model/accountInvitations/controller.ts @@ -1,14 +1,20 @@ import handlers from "./handlers"; +import ensureUserEnabledForAccount from "../../lib/ensureUserEnabledForAccount"; import type { FastifyInstance } from "fastify"; const plugin = async (fastify: FastifyInstance) => { const handlersConfig = fastify.config.saas?.handlers?.accountInvitation; + const accountScopedPreHandler = [ + fastify.verifySession(), + ensureUserEnabledForAccount, + ]; + fastify.get( "/accounts/:accountId/invitations", { - preHandler: fastify.verifySession(), + preHandler: accountScopedPreHandler, }, handlersConfig?.getByAccountId || handlers.getByAccountId, ); @@ -16,7 +22,7 @@ const plugin = async (fastify: FastifyInstance) => { fastify.post( "/accounts/:accountId/invitations", { - preHandler: fastify.verifySession(), + preHandler: accountScopedPreHandler, }, handlersConfig?.create || handlers.create, ); @@ -24,7 +30,7 @@ const plugin = async (fastify: FastifyInstance) => { fastify.post( String.raw`/accounts/:accountId/invitations/:id(^\d+)/resend`, { - preHandler: fastify.verifySession(), + preHandler: accountScopedPreHandler, }, handlersConfig?.resend || handlers.resend, ); @@ -32,7 +38,7 @@ const plugin = async (fastify: FastifyInstance) => { fastify.post( String.raw`/accounts/:accountId/invitations/:id(^\d+)/revoke`, { - preHandler: fastify.verifySession(), + preHandler: accountScopedPreHandler, }, handlersConfig?.revoke || handlers.revoke, ); @@ -52,7 +58,7 @@ const plugin = async (fastify: FastifyInstance) => { fastify.delete( String.raw`/accounts/:accountId/invitations/:id(^\d+)`, { - preHandler: fastify.verifySession(), + preHandler: accountScopedPreHandler, }, handlersConfig?.remove || handlers.remove, ); diff --git a/packages/fastify/src/model/accountUsers/controller.ts b/packages/fastify/src/model/accountUsers/controller.ts index 329d3120..1b8ae6cd 100644 --- a/packages/fastify/src/model/accountUsers/controller.ts +++ b/packages/fastify/src/model/accountUsers/controller.ts @@ -1,17 +1,39 @@ import handlers from "./handlers"; +import ensureUserEnabledForAccount from "../../lib/ensureUserEnabledForAccount"; import type { FastifyInstance } from "fastify"; const plugin = async (fastify: FastifyInstance) => { const handlersConfig = fastify.config.saas?.handlers?.accountUser; + const accountScopedPreHandler = [ + fastify.verifySession(), + ensureUserEnabledForAccount, + ]; + fastify.get( String.raw`/accounts/:accountId(^[0-9a-fa-f-]{36}$)/users`, { - preHandler: fastify.verifySession(), + preHandler: accountScopedPreHandler, }, handlersConfig?.getByAccountId || handlers.getByAccountId, ); + + fastify.post( + String.raw`/accounts/:accountId(^[0-9a-fa-f-]{36}$)/users/:userId/disable`, + { + preHandler: accountScopedPreHandler, + }, + handlersConfig?.disableAccountUser || handlers.disableAccountUser, + ); + + fastify.post( + String.raw`/accounts/:accountId(^[0-9a-fa-f-]{36}$)/users/:userId/enable`, + { + preHandler: accountScopedPreHandler, + }, + handlersConfig?.enableAccountUser || handlers.enableAccountUser, + ); }; export default plugin; diff --git a/packages/fastify/src/model/accountUsers/handlers/disableAccountUser.ts b/packages/fastify/src/model/accountUsers/handlers/disableAccountUser.ts new file mode 100644 index 00000000..28af94e0 --- /dev/null +++ b/packages/fastify/src/model/accountUsers/handlers/disableAccountUser.ts @@ -0,0 +1,60 @@ +import Service from "../service"; + +import type { FastifyReply } from "fastify"; +import type { SessionRequest } from "supertokens-node/framework/fastify"; + +const disableAccountUser = async ( + request: SessionRequest, + reply: FastifyReply, +) => { + if (!request.account) { + return reply.status(404).send({ + error: "Not Found", + message: "Account not found", + statusCode: 404, + }); + } + + const requestParameters = request.params as { + accountId: string; + userId: string; + }; + + if (request.account.id !== requestParameters.accountId) { + return reply.status(400).send({ + error: "Bad Request", + message: "Bad Request", + statusCode: 400, + }); + } + + const { config, slonik } = request; + const accountId = request.account.id; + const userId = requestParameters.userId; + const dbSchema = request.account.database ?? undefined; + + const service = new Service(config, slonik, accountId, dbSchema); + + const accountUser = await service.findOne({ + AND: [ + { key: "account_id", operator: "eq", value: accountId }, + { key: "user_id", operator: "eq", value: userId }, + ], + }); + + if (!accountUser) { + return reply.status(404).send({ + error: "Not Found", + message: "Account user not found", + statusCode: 404, + }); + } + + const data = await service.update((accountUser as { id: number }).id, { + disabled: true, + }); + + reply.send(data); +}; + +export default disableAccountUser; diff --git a/packages/fastify/src/model/accountUsers/handlers/enableAccountUser.ts b/packages/fastify/src/model/accountUsers/handlers/enableAccountUser.ts new file mode 100644 index 00000000..def56d4f --- /dev/null +++ b/packages/fastify/src/model/accountUsers/handlers/enableAccountUser.ts @@ -0,0 +1,60 @@ +import Service from "../service"; + +import type { FastifyReply } from "fastify"; +import type { SessionRequest } from "supertokens-node/framework/fastify"; + +const enableAccountUser = async ( + request: SessionRequest, + reply: FastifyReply, +) => { + if (!request.account) { + return reply.status(404).send({ + error: "Not Found", + message: "Account not found", + statusCode: 404, + }); + } + + const requestParameters = request.params as { + accountId: string; + userId: string; + }; + + if (request.account.id !== requestParameters.accountId) { + return reply.status(400).send({ + error: "Bad Request", + message: "Bad Request", + statusCode: 400, + }); + } + + const { config, slonik } = request; + const accountId = request.account.id; + const userId = requestParameters.userId; + const dbSchema = request.account.database ?? undefined; + + const service = new Service(config, slonik, accountId, dbSchema); + + const accountUser = await service.findOne({ + AND: [ + { key: "account_id", operator: "eq", value: accountId }, + { key: "user_id", operator: "eq", value: userId }, + ], + }); + + if (!accountUser) { + return reply.status(404).send({ + error: "Not Found", + message: "Account user not found", + statusCode: 404, + }); + } + + const data = await service.update((accountUser as { id: number }).id, { + disabled: false, + }); + + reply.send(data); +}; + +export default enableAccountUser; diff --git a/packages/fastify/src/model/accountUsers/handlers/index.ts b/packages/fastify/src/model/accountUsers/handlers/index.ts index 51891c5d..682b193c 100644 --- a/packages/fastify/src/model/accountUsers/handlers/index.ts +++ b/packages/fastify/src/model/accountUsers/handlers/index.ts @@ -1,7 +1,11 @@ +import disableAccountUser from "./disableAccountUser"; +import enableAccountUser from "./enableAccountUser"; import getByAccountId from "./getByAccountId"; import list from "./list"; export default { getByAccountId, list, + disableAccountUser, + enableAccountUser, }; diff --git a/packages/fastify/src/model/accountUsers/sqlFactory.ts b/packages/fastify/src/model/accountUsers/sqlFactory.ts index e6c4718d..76a18a95 100644 --- a/packages/fastify/src/model/accountUsers/sqlFactory.ts +++ b/packages/fastify/src/model/accountUsers/sqlFactory.ts @@ -47,6 +47,7 @@ class AccountUserSqlFactory extends AccountAwareSqlFactory { SELECT ${this.getUserTableIdentifier()}.*, ${this.tableIdentifier}.role_id as role, + ${this.tableIdentifier}.disabled, ${this.tableIdentifier}.date_start, ${this.tableIdentifier}.date_end, ${this.tableIdentifier}.created_at, @@ -69,6 +70,7 @@ class AccountUserSqlFactory extends AccountAwareSqlFactory { SELECT ${this.getUserTableIdentifier()}.*, ${this.tableIdentifier}.role_id as role, + ${this.tableIdentifier}.disabled, ${this.tableIdentifier}.date_start, ${this.tableIdentifier}.date_end, ${this.tableIdentifier}.created_at, diff --git a/packages/fastify/src/model/accounts/controller.ts b/packages/fastify/src/model/accounts/controller.ts index 361e4371..e2eddee9 100644 --- a/packages/fastify/src/model/accounts/controller.ts +++ b/packages/fastify/src/model/accounts/controller.ts @@ -1,10 +1,16 @@ import handlers from "./handlers"; +import ensureUserEnabledForAccount from "../../lib/ensureUserEnabledForAccount"; import type { FastifyInstance } from "fastify"; const plugin = async (fastify: FastifyInstance) => { const handlersConfig = fastify.config.saas?.handlers?.account; + const accountScopedPreHandler = [ + fastify.verifySession(), + ensureUserEnabledForAccount, + ]; + fastify.get( "/accounts", { @@ -16,7 +22,7 @@ const plugin = async (fastify: FastifyInstance) => { fastify.get( String.raw`/accounts/:id(^[0-9a-fa-f-]{36}$)`, { - preHandler: fastify.verifySession(), + preHandler: accountScopedPreHandler, }, handlersConfig?.getById || handlers.getById, ); @@ -24,7 +30,7 @@ const plugin = async (fastify: FastifyInstance) => { fastify.delete( String.raw`/accounts/:id(^[0-9a-fa-f-]{36}$)`, { - preHandler: fastify.verifySession(), + preHandler: accountScopedPreHandler, }, handlersConfig?.delete || handlers.delete, ); @@ -40,7 +46,7 @@ const plugin = async (fastify: FastifyInstance) => { fastify.put( String.raw`/accounts/:id(^[0-9a-fa-f-]{36}$)`, { - preHandler: fastify.verifySession(), + preHandler: accountScopedPreHandler, }, handlersConfig?.update || handlers.update, ); @@ -48,7 +54,7 @@ const plugin = async (fastify: FastifyInstance) => { fastify.put( String.raw`/accounts/:id(^[0-9a-fa-f-]{36}$)/users`, { - preHandler: fastify.verifySession(), + preHandler: accountScopedPreHandler, }, handlersConfig?.update || handlers.update, ); diff --git a/packages/fastify/src/model/accounts/handlers/myAccounts.ts b/packages/fastify/src/model/accounts/handlers/myAccounts.ts index 9e69a504..1da4c664 100644 --- a/packages/fastify/src/model/accounts/handlers/myAccounts.ts +++ b/packages/fastify/src/model/accounts/handlers/myAccounts.ts @@ -30,10 +30,14 @@ const myAccounts = async (request: SessionRequest, reply: FastifyReply) => { value: user.id, }); + const enabledAccountUsers = accountUsers.filter( + (accountUser) => !(accountUser as AccountUser).disabled, + ); + const accounts = await accountService.find({ key: "id", operator: "in", - value: accountUsers + value: enabledAccountUsers .map((accountUser) => (accountUser as unknown as AccountUser).accountId) .join(","), }); diff --git a/packages/fastify/src/model/accounts/sqlFactory.ts b/packages/fastify/src/model/accounts/sqlFactory.ts index 8754507a..ce0d5634 100644 --- a/packages/fastify/src/model/accounts/sqlFactory.ts +++ b/packages/fastify/src/model/accounts/sqlFactory.ts @@ -54,7 +54,7 @@ class AccountSqlFactory extends DefaultSqlFactory { JOIN ${accountUsersTable} AS ${this.getAccountUserTableIdentifier} on ${this.tableIdentifier}.id = ${this.getAccountUserTableIdentifier}.account_id ${this.getWhereFragment({ - filterFragment: sql.fragment`${this.getAccountUserTableIdentifier}.user_id = ${userId}`, + filterFragment: sql.fragment`${this.getAccountUserTableIdentifier}.user_id = ${userId} AND ${this.getAccountUserTableIdentifier}.disabled = false`, })}; `; } diff --git a/packages/fastify/src/types/accountUser.ts b/packages/fastify/src/types/accountUser.ts index e9e6b31c..55997295 100644 --- a/packages/fastify/src/types/accountUser.ts +++ b/packages/fastify/src/types/accountUser.ts @@ -3,6 +3,7 @@ interface AccountUser { accountId: string; userId: string; roleId: string; + disabled: boolean; } type AccountUserCreateInput = Partial> & { diff --git a/packages/fastify/src/types/config.ts b/packages/fastify/src/types/config.ts index 33bc7a3a..b3ae1339 100644 --- a/packages/fastify/src/types/config.ts +++ b/packages/fastify/src/types/config.ts @@ -56,6 +56,8 @@ interface SaasOptions { accountUser?: { getByAccountId?: typeof accountUserHandlers.getByAccountId; list?: typeof accountUserHandlers.list; + disableAccountUser?: typeof accountUserHandlers.disableAccountUser; + enableAccountUser?: typeof accountUserHandlers.enableAccountUser; }; }; invalid?: {