Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### Bug Fixes

* **fastify/migrations:** increase accounts table database column from VARCHAR(10) to VARCHAR(24)

# [0.28.0](https://github.com/prefabs-tech/saas/compare/v0.27.2...v0.28.0) (2025-12-23)


Expand Down
47 changes: 47 additions & 0 deletions packages/fastify/src/lib/ensureUserEnabledForAccount.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
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;
2 changes: 1 addition & 1 deletion packages/fastify/src/migrations/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const createAccountsTableQuery = (
individual BOOLEAN NOT NULL DEFAULT FALSE,
type_id INTEGER,
slug VARCHAR(24),
database VARCHAR(10),
database VARCHAR(24),
domain VARCHAR(255),
UNIQUE (slug),
UNIQUE (database),
Expand Down
16 changes: 11 additions & 5 deletions packages/fastify/src/model/accountInvitations/controller.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,44 @@
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,
);

fastify.post(
"/accounts/:accountId/invitations",
{
preHandler: fastify.verifySession(),
preHandler: accountScopedPreHandler,
},
handlersConfig?.create || handlers.create,
);

fastify.post(
String.raw`/accounts/:accountId/invitations/:id(^\d+)/resend`,
{
preHandler: fastify.verifySession(),
preHandler: accountScopedPreHandler,
},
handlersConfig?.resend || handlers.resend,
);

fastify.post(
String.raw`/accounts/:accountId/invitations/:id(^\d+)/revoke`,
{
preHandler: fastify.verifySession(),
preHandler: accountScopedPreHandler,
},
handlersConfig?.revoke || handlers.revoke,
);
Expand All @@ -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,
);
Expand Down
24 changes: 23 additions & 1 deletion packages/fastify/src/model/accountUsers/controller.ts
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 4 additions & 0 deletions packages/fastify/src/model/accountUsers/handlers/index.ts
Original file line number Diff line number Diff line change
@@ -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,
};
2 changes: 2 additions & 0 deletions packages/fastify/src/model/accountUsers/sqlFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
14 changes: 10 additions & 4 deletions packages/fastify/src/model/accounts/controller.ts
Original file line number Diff line number Diff line change
@@ -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",
{
Expand All @@ -16,15 +22,15 @@ 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,
);

fastify.delete(
String.raw`/accounts/:id(^[0-9a-fa-f-]{36}$)`,
{
preHandler: fastify.verifySession(),
preHandler: accountScopedPreHandler,
},
handlersConfig?.delete || handlers.delete,
);
Expand All @@ -40,15 +46,15 @@ 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,
);

fastify.put(
String.raw`/accounts/:id(^[0-9a-fa-f-]{36}$)/users`,
{
preHandler: fastify.verifySession(),
preHandler: accountScopedPreHandler,
},
handlersConfig?.update || handlers.update,
);
Expand Down
6 changes: 5 additions & 1 deletion packages/fastify/src/model/accounts/handlers/myAccounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(","),
});
Expand Down
2 changes: 1 addition & 1 deletion packages/fastify/src/model/accounts/sqlFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
})};
`;
}
Expand Down
1 change: 1 addition & 0 deletions packages/fastify/src/types/accountUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ interface AccountUser {
accountId: string;
userId: string;
roleId: string;
disabled: boolean;
}

type AccountUserCreateInput = Partial<Omit<AccountUser, "id">> & {
Expand Down
Loading
Loading