From 90708e47e749936a6a30b2c00bf9b474ec0562f9 Mon Sep 17 00:00:00 2001 From: kabin thakuri Date: Thu, 28 May 2026 16:01:20 +0545 Subject: [PATCH 01/11] feat(auth): implement supertokens abstraction --- packages/user/src/auth/adapter.ts | 174 ++++++++++ packages/user/src/auth/index.ts | 3 + packages/user/src/auth/supertokens.ts | 302 ++++++++++++++++++ packages/user/src/constants.ts | 4 + packages/user/src/index.ts | 2 + .../lib/__test__/hasUserPermission.spec.ts | 61 ++-- .../user/src/lib/__test__/seedRoles.spec.ts | 16 +- packages/user/src/lib/hasUserPermission.ts | 12 +- packages/user/src/lib/seedRoles.ts | 5 +- packages/user/src/lib/verifyEmail.ts | 22 +- .../__test__/hasPermission.spec.ts | 10 + .../user/src/middlewares/hasPermission.ts | 4 +- packages/user/src/model/roles/service.ts | 86 +++-- .../src/model/users/handlers/adminSignUp.ts | 59 ++-- packages/user/src/model/users/service.ts | 43 +-- packages/user/src/plugin.ts | 18 +- packages/user/src/types/config.ts | 2 + 17 files changed, 655 insertions(+), 168 deletions(-) create mode 100644 packages/user/src/auth/adapter.ts create mode 100644 packages/user/src/auth/index.ts create mode 100644 packages/user/src/auth/supertokens.ts diff --git a/packages/user/src/auth/adapter.ts b/packages/user/src/auth/adapter.ts new file mode 100644 index 000000000..fe9e8a9ed --- /dev/null +++ b/packages/user/src/auth/adapter.ts @@ -0,0 +1,174 @@ +import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; + +export interface AuthAdapter { + emailPassword: EmailPasswordProvider; + emailVerification?: EmailVerificationProvider; + roles: RolesProvider; + session: SessionProvider; +} + +export interface AuthProvider { + adapter: AuthAdapter; + init?: (fastify: FastifyInstance) => Promise; +} + +export type AuthResult = + | { error: string; success: false } + | { success: true; user: T }; + +export interface AuthSession { + fetchAndSetClaim?( + claim: unknown, + userContext?: AuthUserContext, + ): Promise; + getAccessTokenPayload(userContext?: AuthUserContext): unknown; + getUserId(userContext?: AuthUserContext): string; + revokeSession(userContext?: AuthUserContext): Promise; +} + +// Auth user returned from sign up/sign in +export interface AuthUser { + [key: string]: unknown; + email: string; + id: string; + timeJoined?: number; +} + +export interface AuthUserContext { + [key: string]: unknown; +} + +export interface EmailPasswordProvider { + createResetPasswordToken?(userId: string): Promise; + + emailPasswordSignIn( + email: string, + password: string, + userContext?: AuthUserContext, + ): Promise; + + emailPasswordSignUp( + email: string, + password: string, + userContext?: AuthUserContext, + ): Promise; + + getUserById(userId: string): Promise; + + getUsersByEmail?(email: string): Promise; + + resetPasswordUsingToken?( + token: string, + newPassword: string, + ): Promise; + + updateEmailOrPassword(input: { + email?: string; + password?: string; + userId: string; + }): Promise; +} + +export interface EmailVerificationProvider { + createEmailVerificationToken( + userId: string, + email?: string, + userContext?: AuthUserContext, + ): Promise; + + isEmailVerified(userId: string, email?: string): Promise; + + unverifyEmail?(userId: string, email?: string): Promise; + + verifyEmailUsingToken( + token: string, + userContext?: AuthUserContext, + ): Promise; +} + +export type ResetPasswordResult = + | { error: "INVALID_TOKEN" | "TOKEN_EXPIRED" | string; success: false } + | { success: true }; + +export interface RolesProvider { + addRoleToUser?(userId: string, role: string): Promise; + + createNewRoleOrAddPermissions( + role: string, + permissions: string[], + ): Promise; + + deleteRole(role: string): Promise; + + getAllRoles(): Promise; + + getPermissionsForRole(role: string): Promise; + + getRolesForUser(userId: string): Promise; + + getUsersThatHaveRole(role: string): Promise; + + PermissionClaim?: { + key: string; + }; + + removePermissionsFromRole(role: string, permissions: string[]): Promise; +} + +export interface SessionProvider { + createNewSession( + request: FastifyRequest, + reply: FastifyReply, + userId: string, + accessTokenPayload?: Record, + sessionData?: Record, + ): Promise; + + getSession( + request: FastifyRequest, + reply: FastifyReply, + options?: { + checkDatabase?: boolean; + sessionRequired?: boolean; + }, + ): Promise; + + revokeAllSessionsForUser( + userId: string, + userContext?: AuthUserContext, + ): Promise; +} + +export interface UpdateEmailOrPasswordResult { + error?: string; + success: boolean; +} + +let authInstance: AuthAdapter | undefined; + +export function getAuth(): AuthAdapter { + if (!authInstance) { + throw new Error("Auth adapter not initialized. Call initAuth() first."); + } + return authInstance; +} + +export async function initAuth( + fastify: FastifyInstance, + provider: AuthProvider, +): Promise { + if (!authInstance) { + authInstance = provider.adapter; + + if (provider.init) { + await provider.init(fastify); + } + } + return authInstance; +} + +export const auth = new Proxy({} as AuthAdapter, { + get(_target, property) { + return getAuth()[property as keyof AuthAdapter]; + }, +}); diff --git a/packages/user/src/auth/index.ts b/packages/user/src/auth/index.ts new file mode 100644 index 000000000..dc1beab8f --- /dev/null +++ b/packages/user/src/auth/index.ts @@ -0,0 +1,3 @@ +export { auth, getAuth, initAuth } from "./adapter"; +export type { AuthAdapter, AuthProvider } from "./adapter"; +export { supertokensProvider } from "./supertokens"; diff --git a/packages/user/src/auth/supertokens.ts b/packages/user/src/auth/supertokens.ts new file mode 100644 index 000000000..c1e93bbb5 --- /dev/null +++ b/packages/user/src/auth/supertokens.ts @@ -0,0 +1,302 @@ +import type { FastifyInstance } from "fastify"; + +import { CustomError } from "@prefabs.tech/fastify-error-handler"; +import EmailVerification from "supertokens-node/recipe/emailverification"; +import Session from "supertokens-node/recipe/session"; +import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword"; +import UserRoles from "supertokens-node/recipe/userroles"; + +import type { + AuthProvider, + AuthResult, + AuthSession, + AuthUser, + AuthUserContext, + EmailPasswordProvider, + EmailVerificationProvider, + ResetPasswordResult, + RolesProvider, + SessionProvider, + UpdateEmailOrPasswordResult, +} from "./adapter"; + +import { ERROR_CODES } from "../constants"; +import supertokensPlugin from "../supertokens"; + +// SuperTokens adapter that wraps SuperTokens API to match provider-agnostic interface +const supertokensEmailPasswordAdapter: EmailPasswordProvider = { + async createResetPasswordToken(userId: string): Promise { + const response = + await ThirdPartyEmailPassword.createResetPasswordToken(userId); + + if (response.status === "OK") { + return response.token; + } + + throw new CustomError( + `Failed to create reset password token: ${response.status}`, + ERROR_CODES.RESET_PASSWORD_TOKEN_FAILED, + ); + }, + + async emailPasswordSignIn( + email: string, + password: string, + userContext?: AuthUserContext, + ): Promise { + const response = await ThirdPartyEmailPassword.emailPasswordSignIn( + email, + password, + userContext, + ); + + if (response.status === "OK" && response.user) { + return { + success: true, + user: { + email: response.user.email, + id: response.user.id, + timeJoined: response.user.timeJoined, + }, + }; + } + + return { + error: response.status, + success: false, + }; + }, + + async emailPasswordSignUp( + email: string, + password: string, + userContext?: AuthUserContext, + ): Promise { + const response = await ThirdPartyEmailPassword.emailPasswordSignUp( + email, + password, + userContext, + ); + + if (response.status === "OK" && response.user) { + return { + success: true, + user: { + email: response.user.email, + id: response.user.id, + timeJoined: response.user.timeJoined, + }, + }; + } + + return { + error: response.status, + success: false, + }; + }, + + async getUserById(userId: string): Promise { + const user = await ThirdPartyEmailPassword.getUserById(userId); + + if (!user) return undefined; + + return { + email: user.email, + id: user.id, + timeJoined: user.timeJoined, + }; + }, + + async getUsersByEmail(email: string): Promise { + const users = await ThirdPartyEmailPassword.getUsersByEmail(email); + + return users.map((user) => ({ + email: user.email, + id: user.id, + timeJoined: user.timeJoined, + })); + }, + + async resetPasswordUsingToken( + token: string, + newPassword: string, + ): Promise { + const response = await ThirdPartyEmailPassword.resetPasswordUsingToken( + token, + newPassword, + ); + + if (response.status === "OK") { + return { success: true }; + } + + return { error: response.status, success: false }; + }, + + async updateEmailOrPassword(input: { + email?: string; + password?: string; + userId: string; + }): Promise { + const response = await ThirdPartyEmailPassword.updateEmailOrPassword(input); + + if (response.status === "OK") { + return { success: true }; + } + + return { error: response.status, success: false }; + }, +}; + +const supertokensEmailVerificationAdapter: EmailVerificationProvider = { + async createEmailVerificationToken( + userId: string, + email?: string, + userContext?: AuthUserContext, + ): Promise { + const response = await EmailVerification.createEmailVerificationToken( + userId, + email, + userContext, + ); + + if (response.status === "OK") { + return response.token; + } + + throw new CustomError( + `Failed to create email verification token: ${response.status}`, + ERROR_CODES.EMAIL_VERIFICATION_TOKEN_FAILED, + ); + }, + + async isEmailVerified(userId: string, email?: string): Promise { + return EmailVerification.isEmailVerified(userId, email); + }, + + async unverifyEmail(userId: string, email?: string): Promise { + await EmailVerification.unverifyEmail(userId, email); + }, + + async verifyEmailUsingToken( + token: string, + userContext?: AuthUserContext, + ): Promise { + const response = await EmailVerification.verifyEmailUsingToken( + token, + userContext, + ); + + return response.status === "OK"; + }, +}; + +const supertokensRolesAdapter: RolesProvider = { + async addRoleToUser(userId: string, role: string): Promise { + const response = await UserRoles.addRoleToUser(userId, role); + + if (response.status !== "OK") { + throw new CustomError( + `Failed to add role to user: ${response.status}`, + ERROR_CODES.ADD_ROLE_FAILED, + ); + } + }, + + async createNewRoleOrAddPermissions( + role: string, + permissions: string[], + ): Promise { + const response = await UserRoles.createNewRoleOrAddPermissions( + role, + permissions, + ); + + return response.createdNewRole; + }, + + async deleteRole(role: string): Promise { + const response = await UserRoles.deleteRole(role); + + return response.didRoleExist; + }, + + async getAllRoles(): Promise { + const response = await UserRoles.getAllRoles(); + + return response.roles; + }, + + async getPermissionsForRole(role: string): Promise { + const response = await UserRoles.getPermissionsForRole(role); + + if (response.status === "OK") { + return response.permissions; + } + + return []; + }, + + async getRolesForUser(userId: string): Promise { + const response = await UserRoles.getRolesForUser(userId); + return response.roles; + }, + + async getUsersThatHaveRole(role: string): Promise { + const response = await UserRoles.getUsersThatHaveRole(role); + + if (response.status === "OK") { + return response.users; + } + return []; + }, + + PermissionClaim: UserRoles.PermissionClaim, + + async removePermissionsFromRole( + role: string, + permissions: string[], + ): Promise { + await UserRoles.removePermissionsFromRole(role, permissions); + }, +}; + +const supertokensSessionAdapter: SessionProvider = { + async createNewSession( + request, + reply, + userId, + accessTokenPayload, + sessionData, + ): Promise { + return Session.createNewSession( + request, + reply, + userId, + accessTokenPayload, + sessionData, + ) as unknown as AuthSession; + }, + + async getSession(request, reply, options): Promise { + return Session.getSession(request, reply, options) as unknown as + | AuthSession + | undefined; + }, + + async revokeAllSessionsForUser(userId: string): Promise { + await Session.revokeAllSessionsForUser(userId); + }, +}; + +export const supertokensProvider: AuthProvider = { + adapter: { + emailPassword: supertokensEmailPasswordAdapter, + emailVerification: supertokensEmailVerificationAdapter, + roles: supertokensRolesAdapter, + session: supertokensSessionAdapter, + }, + init: async (fastify: FastifyInstance) => { + await fastify.register(supertokensPlugin); + }, +}; diff --git a/packages/user/src/constants.ts b/packages/user/src/constants.ts index 8ad70f6d2..d7af7ba39 100644 --- a/packages/user/src/constants.ts +++ b/packages/user/src/constants.ts @@ -52,13 +52,17 @@ const PERMISSIONS_USERS_READ = "users:read"; const DEFAULT_USER_PHOTO_MAX_SIZE_IN_MB = 5; const ERROR_CODES = { + ADD_ROLE_FAILED: "ADD_ROLE_FAILED_ERROR", + CHANGE_EMAIL: "CHANGE_EMAIL_ERROR", CHANGE_PASSWORD: "CHANGE_PASSWORD_ERROR", + EMAIL_VERIFICATION_TOKEN_FAILED: "EMAIL_VERIFICATION_TOKEN_FAILED_ERROR", INVALID_EMAIL: "INVALID_EMAIL_ERROR", INVALID_PASSWORD: "INVALID_PASSWORD_ERROR", INVITATION_ALREADY_EXISTS: "INVITATION_ALREADY_EXISTS_ERROR", INVITATION_NOT_FOUND: "INVITATION_NOT_FOUND_ERROR", PHOTO_FILE_MISSING: "PHOTO_FILE_MISSING_ERROR", PHOTO_FILE_TOO_LARGE: "PHOTO_FILE_TOO_LARGE_ERROR", + RESET_PASSWORD_TOKEN_FAILED: "RESET_PASSWORD_TOKEN_FAILED_ERROR", ROLE_ALREADY_EXISTS: "ROLE_ALREADY_EXISTS_ERROR", ROLE_IN_USE: "ROLE_IN_USE_ERROR", ROLE_NOT_FOUND: "ROLE_NOT_FOUND_ERROR", diff --git a/packages/user/src/index.ts b/packages/user/src/index.ts index 4b7a96248..d0719842c 100644 --- a/packages/user/src/index.ts +++ b/packages/user/src/index.ts @@ -25,6 +25,8 @@ declare module "@prefabs.tech/fastify-config" { } } +export * from "./auth"; + export * from "./constants"; export { default as userSchema } from "./graphql/schema"; diff --git a/packages/user/src/lib/__test__/hasUserPermission.spec.ts b/packages/user/src/lib/__test__/hasUserPermission.spec.ts index 9c78492b4..59b89c5de 100644 --- a/packages/user/src/lib/__test__/hasUserPermission.spec.ts +++ b/packages/user/src/lib/__test__/hasUserPermission.spec.ts @@ -5,19 +5,20 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { ROLE_SUPERADMIN } from "../../constants"; import hasUserPermission from "../hasUserPermission"; -// Mock supertokens UserRoles so we can control what roles/permissions come back -// without needing a running SuperTokens server. -vi.mock("supertokens-node/recipe/userroles", () => ({ - default: { - getPermissionsForRole: vi.fn(), - getRolesForUser: vi.fn(), - }, +const { mockGetPermissionsForRole, mockGetRolesForUser } = vi.hoisted(() => ({ + mockGetPermissionsForRole: vi.fn(), + mockGetRolesForUser: vi.fn(), })); -import UserRoles from "supertokens-node/recipe/userroles"; - -const mockGetRolesForUser = vi.mocked(UserRoles.getRolesForUser); -const mockGetPermissionsForRole = vi.mocked(UserRoles.getPermissionsForRole); +// Mock the auth adapter +vi.mock("../../auth/adapter", () => ({ + auth: { + roles: { + getPermissionsForRole: mockGetPermissionsForRole, + getRolesForUser: mockGetRolesForUser, + }, + }, +})); const makeFastify = (permissions?: string[]) => ({ config: { user: { permissions } } }) as unknown as FastifyInstance; @@ -64,10 +65,7 @@ describe("hasUserPermission", () => { describe("SUPERADMIN bypass", () => { it("returns true for a SUPERADMIN regardless of the requested permission", async () => { - mockGetRolesForUser.mockResolvedValue({ - roles: [ROLE_SUPERADMIN], - status: "OK", - }); + mockGetRolesForUser.mockResolvedValue([ROLE_SUPERADMIN]); const result = await hasUserPermission( makeFastify(["billing:manage"]), @@ -82,14 +80,11 @@ describe("hasUserPermission", () => { describe("permission check via role", () => { it("returns true when the user holds a role that grants the required permission", async () => { - mockGetRolesForUser.mockResolvedValue({ - roles: ["EDITOR"], - status: "OK", - }); - mockGetPermissionsForRole.mockResolvedValue({ - permissions: ["content:publish", "billing:manage"], - status: "OK", - }); + mockGetRolesForUser.mockResolvedValue(["EDITOR"]); + mockGetPermissionsForRole.mockResolvedValue([ + "content:publish", + "billing:manage", + ]); const result = await hasUserPermission( makeFastify(["billing:manage"]), @@ -101,14 +96,8 @@ describe("hasUserPermission", () => { }); it("returns false when none of the user's roles grant the required permission", async () => { - mockGetRolesForUser.mockResolvedValue({ - roles: ["VIEWER"], - status: "OK", - }); - mockGetPermissionsForRole.mockResolvedValue({ - permissions: ["content:read"], - status: "OK", - }); + mockGetRolesForUser.mockResolvedValue(["VIEWER"]); + mockGetPermissionsForRole.mockResolvedValue(["content:read"]); const result = await hasUserPermission( makeFastify(["billing:manage"]), @@ -120,15 +109,9 @@ describe("hasUserPermission", () => { }); it("de-duplicates permissions when multiple roles grant the same permission", async () => { - mockGetRolesForUser.mockResolvedValue({ - roles: ["ROLE_A", "ROLE_B"], - status: "OK", - }); + mockGetRolesForUser.mockResolvedValue(["ROLE_A", "ROLE_B"]); // Both roles grant the same permission - mockGetPermissionsForRole.mockResolvedValue({ - permissions: ["billing:manage"], - status: "OK", - }); + mockGetPermissionsForRole.mockResolvedValue(["billing:manage"]); const result = await hasUserPermission( makeFastify(["billing:manage"]), diff --git a/packages/user/src/lib/__test__/seedRoles.spec.ts b/packages/user/src/lib/__test__/seedRoles.spec.ts index 1f33426c9..34afb2f37 100644 --- a/packages/user/src/lib/__test__/seedRoles.spec.ts +++ b/packages/user/src/lib/__test__/seedRoles.spec.ts @@ -3,15 +3,19 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { ROLE_ADMIN, ROLE_SUPERADMIN, ROLE_USER } from "../../constants"; import seedRoles from "../seedRoles"; -vi.mock("supertokens-node/recipe/userroles", () => ({ - default: { - createNewRoleOrAddPermissions: vi.fn().mockResolvedValue({ status: "OK" }), - }, +const { mockCreateNewRoleOrAddPermissions } = vi.hoisted(() => ({ + mockCreateNewRoleOrAddPermissions: vi.fn().mockResolvedValue(true), })); -import UserRoles from "supertokens-node/recipe/userroles"; +vi.mock("../../auth/adapter", () => ({ + auth: { + roles: { + createNewRoleOrAddPermissions: mockCreateNewRoleOrAddPermissions, + }, + }, +})); -const mockCreate = vi.mocked(UserRoles.createNewRoleOrAddPermissions); +const mockCreate = mockCreateNewRoleOrAddPermissions; describe("seedRoles", () => { beforeEach(() => { diff --git a/packages/user/src/lib/hasUserPermission.ts b/packages/user/src/lib/hasUserPermission.ts index 5c507ae35..96fab8fb4 100644 --- a/packages/user/src/lib/hasUserPermission.ts +++ b/packages/user/src/lib/hasUserPermission.ts @@ -1,18 +1,14 @@ import type { FastifyInstance } from "fastify"; -import UserRoles from "supertokens-node/recipe/userroles"; - +import { auth } from "../auth/adapter"; import { ROLE_SUPERADMIN } from "../constants"; const getPermissions = async (roles: string[]) => { let permissions: string[] = []; for (const role of roles) { - const response = await UserRoles.getPermissionsForRole(role); - - if (response.status === "OK") { - permissions = [...new Set([...permissions, ...response.permissions])]; - } + const rolePermissions = await auth.roles.getPermissionsForRole(role); + permissions = [...new Set([...permissions, ...rolePermissions])]; } return permissions; @@ -30,7 +26,7 @@ const hasUserPermission = async ( return true; } - const { roles } = await UserRoles.getRolesForUser(userId); + const roles = await auth.roles.getRolesForUser(userId); // Allow if user has super admin role if (roles && roles.includes(ROLE_SUPERADMIN)) { diff --git a/packages/user/src/lib/seedRoles.ts b/packages/user/src/lib/seedRoles.ts index ab53de021..c82b93ed1 100644 --- a/packages/user/src/lib/seedRoles.ts +++ b/packages/user/src/lib/seedRoles.ts @@ -1,5 +1,4 @@ -import UserRoles from "supertokens-node/recipe/userroles"; - +import { auth } from "../auth/adapter"; import { ROLE_ADMIN, ROLE_SUPERADMIN, ROLE_USER } from "../constants"; import { UserConfig } from "../types"; @@ -12,7 +11,7 @@ const seedRoles = async (userConfig?: Partial) => { ]; for (const role of roles) { - await UserRoles.createNewRoleOrAddPermissions(role, []); + await auth.roles.createNewRoleOrAddPermissions(role, []); } }; diff --git a/packages/user/src/lib/verifyEmail.ts b/packages/user/src/lib/verifyEmail.ts index 375731556..9578fb60d 100644 --- a/packages/user/src/lib/verifyEmail.ts +++ b/packages/user/src/lib/verifyEmail.ts @@ -1,4 +1,6 @@ -import EmailVerification from "supertokens-node/recipe/emailverification"; +import type { AuthUserContext } from "../auth/adapter"; + +import { auth } from "../auth/adapter"; /** * Auto verify user email. @@ -6,21 +8,21 @@ import EmailVerification from "supertokens-node/recipe/emailverification"; const verifyEmail = async ( userId: string, email?: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - userContext?: any, + userContext?: AuthUserContext, ) => { - const tokenResponse = await EmailVerification.createEmailVerificationToken( + if (!auth.emailVerification) { + throw new Error( + "Email verification is not supported by the current auth provider", + ); + } + + const token = await auth.emailVerification.createEmailVerificationToken( userId, email, userContext, ); - if (tokenResponse.status === "OK") { - await EmailVerification.verifyEmailUsingToken( - tokenResponse.token, - userContext, - ); - } + await auth.emailVerification.verifyEmailUsingToken(token, userContext); }; export default verifyEmail; diff --git a/packages/user/src/middlewares/__test__/hasPermission.spec.ts b/packages/user/src/middlewares/__test__/hasPermission.spec.ts index a6494d198..515fe209d 100644 --- a/packages/user/src/middlewares/__test__/hasPermission.spec.ts +++ b/packages/user/src/middlewares/__test__/hasPermission.spec.ts @@ -14,6 +14,16 @@ vi.mock("../../lib/hasUserPermission", () => ({ default: mockHasUserPermission, })); +vi.mock("../../auth/adapter", () => ({ + auth: { + roles: { + PermissionClaim: { + key: "st-role.permissions", + }, + }, + }, +})); + const buildRequest = ( fastify: FastifyInstance, user?: { id: string }, diff --git a/packages/user/src/middlewares/hasPermission.ts b/packages/user/src/middlewares/hasPermission.ts index d7510b7f2..534289a77 100644 --- a/packages/user/src/middlewares/hasPermission.ts +++ b/packages/user/src/middlewares/hasPermission.ts @@ -1,8 +1,8 @@ import type { SessionRequest } from "supertokens-node/framework/fastify"; import { Error as STError } from "supertokens-node/recipe/session"; -import UserRoles from "supertokens-node/recipe/userroles"; +import { auth } from "../auth/adapter"; import hasUserPermission from "../lib/hasUserPermission"; const hasPermission = @@ -23,7 +23,7 @@ const hasPermission = message: "Not have enough permission", payload: [ { - id: UserRoles.PermissionClaim.key, + id: auth.roles.PermissionClaim?.key || "st-role.permissions", reason: { expectedToInclude: permission, message: "Not have enough permission", diff --git a/packages/user/src/model/roles/service.ts b/packages/user/src/model/roles/service.ts index ae1171ba3..bab19ad5d 100644 --- a/packages/user/src/model/roles/service.ts +++ b/packages/user/src/model/roles/service.ts @@ -1,14 +1,14 @@ import { CustomError } from "@prefabs.tech/fastify-error-handler"; -import UserRoles from "supertokens-node/recipe/userroles"; +import { auth } from "../../auth/adapter"; import { ERROR_CODES } from "../../constants"; class RoleService { async createRole( role: string, permissions?: string[], - ): Promise<{ status: "OK" }> { - const { roles } = await UserRoles.getAllRoles(role); + ): Promise<{ status: string }> { + const roles = await auth.roles.getAllRoles(); if (roles.includes(role)) { throw new CustomError( @@ -17,63 +17,54 @@ class RoleService { ); } - const createRoleResponse = await UserRoles.createNewRoleOrAddPermissions( + const createdNewRole = await auth.roles.createNewRoleOrAddPermissions( role, permissions || [], ); - return { status: createRoleResponse.status }; + return { status: createdNewRole ? "OK" : "ROLE_ALREADY_EXISTS" }; } - async deleteRole(role: string): Promise<{ status: "OK" }> { - const response = await UserRoles.getUsersThatHaveRole(role); + async deleteRole(role: string): Promise<{ status: string }> { + const users = await auth.roles.getUsersThatHaveRole(role); - if (response.status === "UNKNOWN_ROLE_ERROR") { - throw new CustomError("Invalid role", ERROR_CODES.UNKNOWN_ROLE_ERROR); + if (users.length === 0) { + const allRoles = await auth.roles.getAllRoles(); + if (!allRoles.includes(role)) { + throw new CustomError("Invalid role", ERROR_CODES.UNKNOWN_ROLE_ERROR); + } } - if (response.users.length > 0) { + if (users.length > 0) { throw new CustomError( "The role is currently assigned to one or more users and cannot be deleted", ERROR_CODES.ROLE_IN_USE, ); } - const deleteRoleResponse = await UserRoles.deleteRole(role); + const didRoleExist = await auth.roles.deleteRole(role); - return { status: deleteRoleResponse.status }; + return { status: didRoleExist ? "OK" : "UNKNOWN_ROLE_ERROR" }; } async getPermissionsForRole(role: string): Promise { - let permissions: string[] = []; - - const response = await UserRoles.getPermissionsForRole(role); - - if (response.status === "OK") { - permissions = response.permissions; - } - - return permissions; + return auth.roles.getPermissionsForRole(role); } async getRoles(): Promise<{ permissions: string[]; role: string }[]> { - let roles: { permissions: string[]; role: string }[] = []; - - const response = await UserRoles.getAllRoles(); - - if (response.status === "OK") { - // [DU 2024-MAR-20] This is N+1 problem - roles = await Promise.all( - response.roles.map(async (role) => { - const response = await UserRoles.getPermissionsForRole(role); - - return { - permissions: response.status === "OK" ? response.permissions : [], - role, - }; - }), - ); - } + const roleNames = await auth.roles.getAllRoles(); + + // [DU 2024-MAR-20] This is N+1 problem + const roles = await Promise.all( + roleNames.map(async (role: string) => { + const permissions = await auth.roles.getPermissionsForRole(role); + + return { + permissions, + role, + }; + }), + ); return roles; } @@ -82,24 +73,25 @@ class RoleService { role: string, permissions: string[], ): Promise<{ permissions: string[]; status: "OK" }> { - const response = await UserRoles.getPermissionsForRole(role); + const rolePermissions = await auth.roles.getPermissionsForRole(role); - if (response.status === "UNKNOWN_ROLE_ERROR") { - throw new CustomError("Invalid role", ERROR_CODES.UNKNOWN_ROLE_ERROR); + if (rolePermissions.length === 0) { + const allRoles = await auth.roles.getAllRoles(); + if (!allRoles.includes(role)) { + throw new CustomError("Invalid role", ERROR_CODES.UNKNOWN_ROLE_ERROR); + } } - const rolePermissions = response.permissions; - const newPermissions = permissions.filter( - (permission) => !rolePermissions.includes(permission), + (permission: string) => !rolePermissions.includes(permission), ); const removedPermissions = rolePermissions.filter( - (permission) => !permissions.includes(permission), + (permission: string) => !permissions.includes(permission), ); - await UserRoles.removePermissionsFromRole(role, removedPermissions); - await UserRoles.createNewRoleOrAddPermissions(role, newPermissions); + await auth.roles.removePermissionsFromRole(role, removedPermissions); + await auth.roles.createNewRoleOrAddPermissions(role, newPermissions); const permissionsResponse = await this.getPermissionsForRole(role); diff --git a/packages/user/src/model/users/handlers/adminSignUp.ts b/packages/user/src/model/users/handlers/adminSignUp.ts index 93f568507..e74684d44 100644 --- a/packages/user/src/model/users/handlers/adminSignUp.ts +++ b/packages/user/src/model/users/handlers/adminSignUp.ts @@ -1,9 +1,6 @@ import type { FastifyReply, FastifyRequest } from "fastify"; -import { createNewSession } from "supertokens-node/recipe/session"; -import { emailPasswordSignUp } from "supertokens-node/recipe/thirdpartyemailpassword"; -import UserRoles from "supertokens-node/recipe/userroles"; - +import { auth } from "../../../auth/adapter"; import { ROLE_ADMIN, ROLE_SUPERADMIN } from "../../../constants"; import validateEmail from "../../../validator/email"; import validatePassword from "../../../validator/password"; @@ -21,18 +18,16 @@ const adminSignUp = async (request: FastifyRequest, reply: FastifyReply) => { const { email, password } = body; // check if already admin user exists - const adminUsers = await UserRoles.getUsersThatHaveRole(ROLE_ADMIN); - const superAdminUsers = await UserRoles.getUsersThatHaveRole(ROLE_SUPERADMIN); - - if ( - adminUsers.status === "UNKNOWN_ROLE_ERROR" && - superAdminUsers.status === "UNKNOWN_ROLE_ERROR" - ) { - throw server.httpErrors.unprocessableEntity(adminUsers.status); - } else if ( - (adminUsers.status === "OK" && adminUsers.users.length > 0) || - (superAdminUsers.status === "OK" && superAdminUsers.users.length > 0) - ) { + const adminUsers = await auth.roles.getUsersThatHaveRole(ROLE_ADMIN); + const superAdminUsers = + await auth.roles.getUsersThatHaveRole(ROLE_SUPERADMIN); + + if (adminUsers.length === 0 && superAdminUsers.length === 0) { + const allRoles = await auth.roles.getAllRoles(); + if (!allRoles.includes(ROLE_ADMIN) && !allRoles.includes(ROLE_SUPERADMIN)) { + throw server.httpErrors.unprocessableEntity("Required roles not found"); + } + } else if (adminUsers.length > 0 || superAdminUsers.length > 0) { throw server.httpErrors.conflict("First admin user already exists"); } @@ -55,27 +50,31 @@ const adminSignUp = async (request: FastifyRequest, reply: FastifyReply) => { } // signup - const signUpResponse = await emailPasswordSignUp(email, password, { - _default: { - request: { - request, + const signUpResponse = await auth.emailPassword.emailPasswordSignUp( + email, + password, + { + _default: { + request: { + request, + }, }, + autoVerifyEmail: true, + roles: [ + ROLE_ADMIN, + ...(superAdminUsers.length === 0 ? [ROLE_SUPERADMIN] : []), + ], }, - autoVerifyEmail: true, - roles: [ - ROLE_ADMIN, - ...(superAdminUsers.status === "OK" ? [ROLE_SUPERADMIN] : []), - ], - }); + ); - if (signUpResponse.status !== "OK") { - return reply.send(signUpResponse); + if (!signUpResponse.success) { + return reply.send({ status: signUpResponse.error }); } // create new session so the user be logged in on signup - await createNewSession(request, reply, signUpResponse.user.id); + await auth.session.createNewSession(request, reply, signUpResponse.user.id); - reply.send(signUpResponse); + reply.send({ status: "OK", user: signUpResponse.user }); }; export default adminSignUp; diff --git a/packages/user/src/model/users/service.ts b/packages/user/src/model/users/service.ts index d2ce8765b..9c3bf3aec 100644 --- a/packages/user/src/model/users/service.ts +++ b/packages/user/src/model/users/service.ts @@ -1,11 +1,10 @@ import { CustomError } from "@prefabs.tech/fastify-error-handler"; import { File, FileService, Multipart } from "@prefabs.tech/fastify-s3"; import { BaseService } from "@prefabs.tech/fastify-slonik"; -import Session from "supertokens-node/recipe/session"; -import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword"; import type { User, UserCreateInput, UserUpdateInput } from "../../types"; +import { auth } from "../../auth/adapter"; import { DEFAULT_USER_PHOTO_MAX_SIZE_IN_MB, ERROR_CODES, @@ -49,13 +48,16 @@ class UserService extends BaseService { protected photoPath = "photo"; async changeEmail(id: string, email: string) { - const response = await ThirdPartyEmailPassword.updateEmailOrPassword({ + const result = await auth.emailPassword.updateEmailOrPassword({ email: email, userId: id, }); - if (response.status !== "OK") { - throw new CustomError(response.status, response.status); + if (!result.success) { + throw new CustomError( + result.error || ERROR_CODES.CHANGE_EMAIL, + result.error || ERROR_CODES.CHANGE_EMAIL, + ); } const query = this.factory.getUpdateSql(id, { email }); @@ -81,25 +83,24 @@ class UserService extends BaseService { }; } - const userInfo = await ThirdPartyEmailPassword.getUserById(userId); + const userInfo = await auth.emailPassword.getUserById(userId); if (oldPassword && newPassword) { if (userInfo) { - const isPasswordValid = - await ThirdPartyEmailPassword.emailPasswordSignIn( - userInfo.email, - oldPassword, - { dbSchema: this.schema }, - ); - - if (isPasswordValid.status === "OK") { - const result = await ThirdPartyEmailPassword.updateEmailOrPassword({ + const isPasswordValid = await auth.emailPassword.emailPasswordSignIn( + userInfo.email, + oldPassword, + { dbSchema: this.schema }, + ); + + if (isPasswordValid.success) { + const result = await auth.emailPassword.updateEmailOrPassword({ password: newPassword, userId, }); - if (result) { - await Session.revokeAllSessionsForUser(userId); + if (result.success) { + await auth.session.revokeAllSessionsForUser(userId); return { status: "OK", @@ -144,7 +145,7 @@ class UserService extends BaseService { } async deleteMe(userId: string, password: string) { - const user = await ThirdPartyEmailPassword.getUserById(userId); + const user = await auth.emailPassword.getUserById(userId); if (!user) { throw new CustomError("User not found", ERROR_CODES.USER_NOT_FOUND); @@ -154,13 +155,13 @@ class UserService extends BaseService { throw new CustomError("Invalid password", ERROR_CODES.INVALID_PASSWORD); } - const signInResponse = await ThirdPartyEmailPassword.emailPasswordSignIn( + const signInResponse = await auth.emailPassword.emailPasswordSignIn( user.email, password, { dbSchema: this.schema }, ); - if (signInResponse.status === "OK") { + if (signInResponse.success) { return await this.delete(userId); } else { throw new CustomError("Invalid password", ERROR_CODES.INVALID_PASSWORD); @@ -199,7 +200,7 @@ class UserService extends BaseService { } protected async postDelete(result: User): Promise { - await Session.revokeAllSessionsForUser(result.id); + await auth.session.revokeAllSessionsForUser(result.id); return result; } diff --git a/packages/user/src/plugin.ts b/packages/user/src/plugin.ts index b61d94b76..ea0307caa 100644 --- a/packages/user/src/plugin.ts +++ b/packages/user/src/plugin.ts @@ -3,6 +3,8 @@ import type { FastifyPluginAsync } from "fastify"; import FastifyPlugin from "fastify-plugin"; +import { initAuth } from "./auth/adapter"; +import { supertokensProvider } from "./auth/supertokens"; import seedRoles from "./lib/seedRoles"; import mercuriusAuthPlugin from "./mercurius-auth/plugin"; import hasPermission from "./middlewares/hasPermission"; @@ -11,13 +13,25 @@ import invitationsRoutes from "./model/invitations/controller"; import permissionsRoutes from "./model/permissions/controller"; import rolesRoutes from "./model/roles/controller"; import usersRoutes from "./model/users/controller"; -import supertokensPlugin from "./supertokens"; import userContext from "./userContext"; const userPlugin: FastifyPluginAsync = async (fastify) => { const { graphql, user } = fastify.config; - await fastify.register(supertokensPlugin); + let provider; + const providerName = user.authProvider || "supertokens"; + + switch (providerName) { + case "supertokens": { + provider = supertokensProvider; + break; + } + default: { + throw new Error(`Unknown auth provider: ${providerName}`); + } + } + + await initAuth(fastify, provider); fastify.addHook("onReady", async () => { await seedRoles(user); diff --git a/packages/user/src/types/config.ts b/packages/user/src/types/config.ts index 1b965494f..c0ad0e71c 100644 --- a/packages/user/src/types/config.ts +++ b/packages/user/src/types/config.ts @@ -15,7 +15,9 @@ interface EmailOptions { subject?: string; templateName?: string; } + interface UserConfig { + authProvider?: "supertokens" | string; email?: IsEmailOptions; emailOverrides?: { duplicateEmail?: EmailOptions; From 09c004f317b418506004b78378569dd918130886 Mon Sep 17 00:00:00 2001 From: kabin thakuri Date: Fri, 29 May 2026 12:08:39 +0545 Subject: [PATCH 02/11] refactor(auth): update auth provider handling and enhance claims management --- .../user/src/auth/__test__/providers.spec.ts | 31 ++++ packages/user/src/auth/adapter.ts | 73 ++++++++- packages/user/src/auth/index.ts | 23 ++- packages/user/src/auth/providers/index.ts | 27 ++++ packages/user/src/auth/supertokens.ts | 141 +++++++++++++++++- packages/user/src/auth/types.ts | 7 + packages/user/src/plugin.ts | 15 +- packages/user/src/types/config.ts | 4 + 8 files changed, 294 insertions(+), 27 deletions(-) create mode 100644 packages/user/src/auth/__test__/providers.spec.ts create mode 100644 packages/user/src/auth/providers/index.ts create mode 100644 packages/user/src/auth/types.ts diff --git a/packages/user/src/auth/__test__/providers.spec.ts b/packages/user/src/auth/__test__/providers.spec.ts new file mode 100644 index 000000000..a59650a50 --- /dev/null +++ b/packages/user/src/auth/__test__/providers.spec.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; + +import type { AuthProvider } from "../adapter"; + +import { + getAuthProvider, + registerAuthProvider, + supertokensProvider, +} from "../providers"; + +describe("auth providers registry", () => { + it("returns the built-in supertokens provider", () => { + expect(getAuthProvider("supertokens")).toBe(supertokensProvider); + }); + + it("throws for unknown provider names", () => { + expect(() => getAuthProvider("unknown-provider")).toThrow( + "Unknown auth provider: unknown-provider", + ); + }); + + it("allows registering a custom provider", () => { + const custom: AuthProvider = { + adapter: supertokensProvider.adapter, + }; + + registerAuthProvider("custom-test", custom); + + expect(getAuthProvider("custom-test")).toBe(custom); + }); +}); diff --git a/packages/user/src/auth/adapter.ts b/packages/user/src/auth/adapter.ts index fe9e8a9ed..752b6e4bf 100644 --- a/packages/user/src/auth/adapter.ts +++ b/packages/user/src/auth/adapter.ts @@ -1,12 +1,28 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; +import type { ClaimValidationError, RefreshableClaim } from "./types"; + export interface AuthAdapter { + claims: ClaimsProvider; + createUserContext( + request: FastifyRequest, + existing?: AuthUserContext, + ): AuthUserContext; emailPassword: EmailPasswordProvider; emailVerification?: EmailVerificationProvider; + errors: AuthErrorsProvider; roles: RolesProvider; session: SessionProvider; } +export interface AuthErrorsProvider { + createInvalidClaimsError(errors: ClaimValidationError[]): Error; + + createUnauthorizedError(message?: string): Error; + + isAuthError(error: unknown): boolean; +} + export interface AuthProvider { adapter: AuthAdapter; init?: (fastify: FastifyInstance) => Promise; @@ -17,6 +33,10 @@ export type AuthResult = | { success: true; user: T }; export interface AuthSession { + assertClaims?( + validators: unknown[], + userContext?: AuthUserContext, + ): Promise; fetchAndSetClaim?( claim: unknown, userContext?: AuthUserContext, @@ -26,7 +46,6 @@ export interface AuthSession { revokeSession(userContext?: AuthUserContext): Promise; } -// Auth user returned from sign up/sign in export interface AuthUser { [key: string]: unknown; email: string; @@ -38,6 +57,34 @@ export interface AuthUserContext { [key: string]: unknown; } +export interface ClaimsProvider { + assertProfileValid( + session: AuthSession, + request: FastifyRequest, + userContext?: AuthUserContext, + ): Promise; + + excludeValidatorIds( + validators: T[], + skip: RefreshableClaim[], + ): T[]; + + readonly keys: { + emailVerification: string; + profileValidation: string; + }; + + refreshSessionClaims( + session: AuthSession, + request: FastifyRequest, + claims: RefreshableClaim[], + userContext?: AuthUserContext, + ): Promise; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + verifySessionOptions(skip: RefreshableClaim[]): any; +} + export interface EmailPasswordProvider { createResetPasswordToken?(userId: string): Promise; @@ -78,6 +125,14 @@ export interface EmailVerificationProvider { isEmailVerified(userId: string, email?: string): Promise; + sendVerificationEmail?(input: { + appOrigin: string; + email: string; + token: string; + userContext?: AuthUserContext; + userId: string; + }): Promise<{ status: string; success: boolean }>; + unverifyEmail?(userId: string, email?: string): Promise; verifyEmailUsingToken( @@ -86,6 +141,12 @@ export interface EmailVerificationProvider { ): Promise; } +export interface GetSessionOptions { + checkDatabase?: boolean; + sessionRequired?: boolean; + skipClaims?: RefreshableClaim[]; +} + export type ResetPasswordResult = | { error: "INVALID_TOKEN" | "TOKEN_EXPIRED" | string; success: false } | { success: true }; @@ -113,6 +174,8 @@ export interface RolesProvider { }; removePermissionsFromRole(role: string, permissions: string[]): Promise; + + rolesExist(roles: string[]): Promise; } export interface SessionProvider { @@ -122,15 +185,13 @@ export interface SessionProvider { userId: string, accessTokenPayload?: Record, sessionData?: Record, + userContext?: AuthUserContext, ): Promise; getSession( request: FastifyRequest, reply: FastifyReply, - options?: { - checkDatabase?: boolean; - sessionRequired?: boolean; - }, + options?: GetSessionOptions, ): Promise; revokeAllSessionsForUser( @@ -144,6 +205,8 @@ export interface UpdateEmailOrPasswordResult { success: boolean; } +export type { ClaimValidationError, RefreshableClaim } from "./types"; + let authInstance: AuthAdapter | undefined; export function getAuth(): AuthAdapter { diff --git a/packages/user/src/auth/index.ts b/packages/user/src/auth/index.ts index dc1beab8f..a65decc9c 100644 --- a/packages/user/src/auth/index.ts +++ b/packages/user/src/auth/index.ts @@ -1,3 +1,22 @@ export { auth, getAuth, initAuth } from "./adapter"; -export type { AuthAdapter, AuthProvider } from "./adapter"; -export { supertokensProvider } from "./supertokens"; +export type { + AuthAdapter, + AuthErrorsProvider, + AuthProvider, + AuthSession, + AuthUserContext, + ClaimsProvider, + ClaimValidationError, + EmailPasswordProvider, + EmailVerificationProvider, + GetSessionOptions, + AuthUser as ProviderAuthUser, + RefreshableClaim, + RolesProvider, + SessionProvider, +} from "./adapter"; +export { + getAuthProvider, + registerAuthProvider, + supertokensProvider, +} from "./providers"; diff --git a/packages/user/src/auth/providers/index.ts b/packages/user/src/auth/providers/index.ts new file mode 100644 index 000000000..1e965c12e --- /dev/null +++ b/packages/user/src/auth/providers/index.ts @@ -0,0 +1,27 @@ +import type { AuthProvider } from "../adapter"; + +import { supertokensProvider } from "../supertokens"; + +const providers: Record = { + supertokens: supertokensProvider, +}; + +export function getAuthProvider(name: string): AuthProvider { + const provider = providers[name]; + + if (!provider) { + throw new Error(`Unknown auth provider: ${name}`); + } + + return provider; +} + +/** Register a custom auth provider (e.g. better-auth) before the user plugin loads. */ +export function registerAuthProvider( + name: string, + provider: AuthProvider, +): void { + providers[name] = provider; +} + +export { supertokensProvider } from "../supertokens"; diff --git a/packages/user/src/auth/supertokens.ts b/packages/user/src/auth/supertokens.ts index c1e93bbb5..4ad58a305 100644 --- a/packages/user/src/auth/supertokens.ts +++ b/packages/user/src/auth/supertokens.ts @@ -1,17 +1,22 @@ -import type { FastifyInstance } from "fastify"; +import type { FastifyInstance, FastifyRequest } from "fastify"; import { CustomError } from "@prefabs.tech/fastify-error-handler"; -import EmailVerification from "supertokens-node/recipe/emailverification"; -import Session from "supertokens-node/recipe/session"; +import { wrapResponse } from "supertokens-node/framework/fastify"; +import EmailVerification, { + EmailVerificationClaim, +} from "supertokens-node/recipe/emailverification"; +import Session, { Error as STError } from "supertokens-node/recipe/session"; import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpassword"; import UserRoles from "supertokens-node/recipe/userroles"; import type { + AuthErrorsProvider, AuthProvider, AuthResult, AuthSession, AuthUser, AuthUserContext, + ClaimsProvider, EmailPasswordProvider, EmailVerificationProvider, ResetPasswordResult, @@ -19,11 +24,98 @@ import type { SessionProvider, UpdateEmailOrPasswordResult, } from "./adapter"; +import type { ClaimValidationError, RefreshableClaim } from "./types"; import { ERROR_CODES } from "../constants"; import supertokensPlugin from "../supertokens"; +import createUserContextImpl from "../supertokens/utils/createUserContext"; +import ProfileValidationClaim from "../supertokens/utils/profileValidationClaim"; + +const claimKeyByType: Record = { + emailVerification: EmailVerificationClaim.key, + profileValidation: ProfileValidationClaim.key, +}; + +const supertokensClaimsAdapter: ClaimsProvider = { + async assertProfileValid(session, request, userContext) { + const profileValidationClaim = new ProfileValidationClaim(); + const context = createUserContextImpl(userContext, request); + + try { + await session.assertClaims?.( + [profileValidationClaim.validators.isVerified()], + context, + ); + + return; + } catch (error) { + if (error instanceof STError && error.type === "INVALID_CLAIMS") { + return (error.payload ?? []) as unknown as ClaimValidationError[]; + } + + throw error; + } + }, + + excludeValidatorIds(validators, skip) { + const skipKeys = new Set(skip.map((claim) => claimKeyByType[claim])); + + return validators.filter((validator) => !skipKeys.has(validator.id)); + }, + + keys: { + emailVerification: EmailVerificationClaim.key, + profileValidation: ProfileValidationClaim.key, + }, + + async refreshSessionClaims(session, request, claims, userContext) { + const context = createUserContextImpl(userContext, request); + + for (const claim of claims) { + if (claim === "emailVerification") { + await session.fetchAndSetClaim?.(EmailVerificationClaim, context); + } else if (claim === "profileValidation") { + await session.fetchAndSetClaim?.(new ProfileValidationClaim(), context); + } + } + }, + + verifySessionOptions(skip: RefreshableClaim[]) { + return { + overrideGlobalClaimValidators: async ( + globalValidators: T[], + ) => supertokensClaimsAdapter.excludeValidatorIds(globalValidators, skip), + }; + }, +}; + +const supertokensErrorsAdapter: AuthErrorsProvider = { + createInvalidClaimsError(errors) { + return new STError({ + message: "invalid claim", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + payload: errors as any, + type: "INVALID_CLAIMS", + }); + }, + + createUnauthorizedError(message = "unauthorised") { + return new STError({ + message, + type: "UNAUTHORISED", + }); + }, + + isAuthError(error: unknown) { + return STError.isErrorFromSuperTokens(error); + }, +}; + +const createUserContext = ( + request: FastifyRequest, + existing?: AuthUserContext, +): AuthUserContext => createUserContextImpl(existing, request); -// SuperTokens adapter that wraps SuperTokens API to match provider-agnostic interface const supertokensEmailPasswordAdapter: EmailPasswordProvider = { async createResetPasswordToken(userId: string): Promise { const response = @@ -174,6 +266,20 @@ const supertokensEmailVerificationAdapter: EmailVerificationProvider = { return EmailVerification.isEmailVerified(userId, email); }, + async sendVerificationEmail(input) { + await EmailVerification.sendEmail({ + emailVerifyLink: `${input.appOrigin}/auth/verify-email?token=${input.token}&rid=emailverification`, + type: "EMAIL_VERIFICATION", + user: { + email: input.email, + id: input.userId, + }, + userContext: input.userContext, + }); + + return { status: "OK", success: true }; + }, + async unverifyEmail(userId: string, email?: string): Promise { await EmailVerification.unverifyEmail(userId, email); }, @@ -259,6 +365,12 @@ const supertokensRolesAdapter: RolesProvider = { ): Promise { await UserRoles.removePermissionsFromRole(role, permissions); }, + + async rolesExist(roles: string[]): Promise { + const allRoles = await supertokensRolesAdapter.getAllRoles(); + + return roles.every((role) => allRoles.includes(role)); + }, }; const supertokensSessionAdapter: SessionProvider = { @@ -268,6 +380,7 @@ const supertokensSessionAdapter: SessionProvider = { userId, accessTokenPayload, sessionData, + userContext, ): Promise { return Session.createNewSession( request, @@ -275,13 +388,24 @@ const supertokensSessionAdapter: SessionProvider = { userId, accessTokenPayload, sessionData, + userContext, ) as unknown as AuthSession; }, async getSession(request, reply, options): Promise { - return Session.getSession(request, reply, options) as unknown as - | AuthSession - | undefined; + const skipClaims = options?.skipClaims; + + return Session.getSession(request, wrapResponse(reply), { + checkDatabase: options?.checkDatabase, + overrideGlobalClaimValidators: skipClaims?.length + ? async (globalValidators) => + supertokensClaimsAdapter.excludeValidatorIds( + globalValidators, + skipClaims, + ) + : undefined, + sessionRequired: options?.sessionRequired, + }) as unknown as AuthSession | undefined; }, async revokeAllSessionsForUser(userId: string): Promise { @@ -291,8 +415,11 @@ const supertokensSessionAdapter: SessionProvider = { export const supertokensProvider: AuthProvider = { adapter: { + claims: supertokensClaimsAdapter, + createUserContext, emailPassword: supertokensEmailPasswordAdapter, emailVerification: supertokensEmailVerificationAdapter, + errors: supertokensErrorsAdapter, roles: supertokensRolesAdapter, session: supertokensSessionAdapter, }, diff --git a/packages/user/src/auth/types.ts b/packages/user/src/auth/types.ts new file mode 100644 index 000000000..8a461b35e --- /dev/null +++ b/packages/user/src/auth/types.ts @@ -0,0 +1,7 @@ +export interface ClaimValidationError { + id: string; + reason: unknown; +} + +/** Session claims that can be refreshed or excluded from route verification. */ +export type RefreshableClaim = "emailVerification" | "profileValidation"; diff --git a/packages/user/src/plugin.ts b/packages/user/src/plugin.ts index ea0307caa..bdb08b55e 100644 --- a/packages/user/src/plugin.ts +++ b/packages/user/src/plugin.ts @@ -4,7 +4,7 @@ import type { FastifyPluginAsync } from "fastify"; import FastifyPlugin from "fastify-plugin"; import { initAuth } from "./auth/adapter"; -import { supertokensProvider } from "./auth/supertokens"; +import { getAuthProvider } from "./auth/providers"; import seedRoles from "./lib/seedRoles"; import mercuriusAuthPlugin from "./mercurius-auth/plugin"; import hasPermission from "./middlewares/hasPermission"; @@ -18,20 +18,9 @@ import userContext from "./userContext"; const userPlugin: FastifyPluginAsync = async (fastify) => { const { graphql, user } = fastify.config; - let provider; const providerName = user.authProvider || "supertokens"; - switch (providerName) { - case "supertokens": { - provider = supertokensProvider; - break; - } - default: { - throw new Error(`Unknown auth provider: ${providerName}`); - } - } - - await initAuth(fastify, provider); + await initAuth(fastify, getAuthProvider(providerName)); fastify.addHook("onReady", async () => { await seedRoles(user); diff --git a/packages/user/src/types/config.ts b/packages/user/src/types/config.ts index c0ad0e71c..b2699d565 100644 --- a/packages/user/src/types/config.ts +++ b/packages/user/src/types/config.ts @@ -17,6 +17,10 @@ interface EmailOptions { } interface UserConfig { + authConfig?: { + [provider: string]: unknown; + supertokens?: SupertokensConfig; + }; authProvider?: "supertokens" | string; email?: IsEmailOptions; emailOverrides?: { From d1a6ff856fe4796fc05497ae9f738bb5810216df Mon Sep 17 00:00:00 2001 From: kabin thakuri Date: Fri, 29 May 2026 14:38:33 +0545 Subject: [PATCH 03/11] chore(user/auth): decouple AuthUser type --- packages/user/src/types/user.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/user/src/types/user.ts b/packages/user/src/types/user.ts index 6c846d0e3..f96b76722 100644 --- a/packages/user/src/types/user.ts +++ b/packages/user/src/types/user.ts @@ -1,7 +1,4 @@ import type { Multipart } from "@prefabs.tech/fastify-s3"; -import type { User as SupertokensUser } from "supertokens-node/recipe/thirdpartyemailpassword"; - -interface AuthUser extends SupertokensUser, User {} interface Photo { id: number; @@ -46,4 +43,4 @@ type UserUpdateInput = Partial< photo?: Multipart; }; -export type { AuthUser, User, UserCreateInput, UserUpdateInput }; +export type { User, UserCreateInput, UserUpdateInput }; From 9e086539a41276eecbdfca3226ba09a56497559a Mon Sep 17 00:00:00 2001 From: kabin thakuri Date: Fri, 29 May 2026 18:00:37 +0545 Subject: [PATCH 04/11] refactor(user/auth): decouple handlers from supertokens node --- packages/user/src/auth/adapter.ts | 6 ++ packages/user/src/index.ts | 5 ++ .../invitations/handlers/acceptInvitation.ts | 23 ++--- .../invitations/handlers/createInvitation.ts | 5 +- .../invitations/handlers/deleteInvitation.ts | 5 +- .../invitations/handlers/listInvitation.ts | 5 +- .../invitations/handlers/resendInvitation.ts | 5 +- .../invitations/handlers/revokeInvitation.ts | 5 +- .../permissions/handlers/getPermissions.ts | 5 +- .../src/model/roles/handlers/createRole.ts | 5 +- .../src/model/roles/handlers/deleteRole.ts | 5 +- .../model/roles/handlers/getPermissions.ts | 5 +- .../user/src/model/roles/handlers/getRoles.ts | 5 +- .../model/roles/handlers/updatePermissions.ts | 5 +- .../model/users/handlers/canAdminSignUp.ts | 28 +++---- .../src/model/users/handlers/changeEmail.ts | 84 ++++++++++--------- .../model/users/handlers/changePassword.ts | 13 ++- .../user/src/model/users/handlers/deleteMe.ts | 3 +- .../user/src/model/users/handlers/disable.ts | 5 +- .../user/src/model/users/handlers/enable.ts | 5 +- packages/user/src/model/users/handlers/me.ts | 36 ++++---- .../src/model/users/handlers/removePhoto.ts | 35 ++++---- .../user/src/model/users/handlers/updateMe.ts | 33 ++++---- .../src/model/users/handlers/uploadPhoto.ts | 33 ++++---- .../user/src/model/users/handlers/user.ts | 5 +- .../user/src/model/users/handlers/users.ts | 5 +- packages/user/src/supertokens/index.ts | 5 -- packages/user/src/supertokens/init.ts | 4 +- packages/user/src/supertokens/plugin.ts | 4 +- .../sendEmailVerificationEmail.ts | 2 +- .../config/emailVerificationRecipeConfig.ts | 4 +- .../recipes/config/session/getSession.ts | 2 +- .../recipes/config/session/verifySession.ts | 2 +- .../recipes/config/sessionRecipeConfig.ts | 4 +- .../emailPasswordSignIn.ts | 4 +- .../emailPasswordSignUp.ts | 2 +- .../sendPasswordResetEmail.ts | 2 +- .../thirdPartyEmailPasswordRecipeConfig.ts | 5 +- .../recipes/config/thirdPartyProviders.ts | 2 +- .../recipes/initEmailVerificationRecipe.ts | 2 +- .../supertokens/recipes/initSessionRecipe.ts | 2 +- .../initThirdPartyEmailPasswordRecipe.ts | 2 +- .../recipes/initUserRolesRecipe.ts | 3 +- packages/user/src/types/config.ts | 8 +- packages/user/src/types/index.ts | 5 ++ packages/user/src/userContext.ts | 18 +--- 46 files changed, 229 insertions(+), 227 deletions(-) diff --git a/packages/user/src/auth/adapter.ts b/packages/user/src/auth/adapter.ts index 752b6e4bf..7b9889059 100644 --- a/packages/user/src/auth/adapter.ts +++ b/packages/user/src/auth/adapter.ts @@ -207,6 +207,12 @@ export interface UpdateEmailOrPasswordResult { export type { ClaimValidationError, RefreshableClaim } from "./types"; +declare module "fastify" { + interface FastifyRequest { + session?: AuthSession; + } +} + let authInstance: AuthAdapter | undefined; export function getAuth(): AuthAdapter { diff --git a/packages/user/src/index.ts b/packages/user/src/index.ts index d0719842c..6d314dc05 100644 --- a/packages/user/src/index.ts +++ b/packages/user/src/index.ts @@ -58,6 +58,11 @@ export { } from "./model/users/sql"; export { default as UserSqlFactory } from "./model/users/sqlFactory"; export { default } from "./plugin"; +/* + * @deprecated Import supertokens internals directly from "@prefabs.tech/fastify-user/supertokens" + * if you need them. These exports will be removed in a future release. + * The auth adapter is available via `import { auth } from "@prefabs.tech/fastify-user/auth"`. + */ export { errorHandler as supertokensErrorHandler } from "./supertokens/errorHandler"; export { default as areRolesExist } from "./supertokens/utils/areRolesExist"; export { default as createUserContext } from "./supertokens/utils/createUserContext"; diff --git a/packages/user/src/model/invitations/handlers/acceptInvitation.ts b/packages/user/src/model/invitations/handlers/acceptInvitation.ts index eb0fdfa4a..97962edf5 100644 --- a/packages/user/src/model/invitations/handlers/acceptInvitation.ts +++ b/packages/user/src/model/invitations/handlers/acceptInvitation.ts @@ -1,11 +1,10 @@ import type { FastifyReply, FastifyRequest } from "fastify"; import { formatDate } from "@prefabs.tech/fastify-slonik"; -import { createNewSession } from "supertokens-node/recipe/session"; -import { emailPasswordSignUp } from "supertokens-node/recipe/thirdpartyemailpassword"; import type { User } from "../../../types"; +import { auth } from "../../../auth/adapter"; import getInvitationService from "../../../lib/getInvitationService"; import isInvitationValid from "../../../lib/isInvitationValid"; import validateEmail from "../../../validator/email"; @@ -66,14 +65,18 @@ const acceptInvitation = async ( } // signup - const signUpResponse = await emailPasswordSignUp(email, password, { - autoVerifyEmail: true, - roles: [invitation.role], - }); + const signUpResponse = await auth.emailPassword.emailPasswordSignUp( + email, + password, + { + autoVerifyEmail: true, + roles: [invitation.role], + }, + ); - if (signUpResponse.status !== "OK") { + if (!signUpResponse.success) { throw request.server.httpErrors.unprocessableEntity( - "EMAIL_ALREADY_EXISTS_ERROR", + signUpResponse.error || "EMAIL_ALREADY_EXISTS_ERROR", ); } @@ -94,10 +97,10 @@ const acceptInvitation = async ( } // create new session so the user be logged in on signup - await createNewSession(request, reply, signUpResponse.user.id); + await auth.session.createNewSession(request, reply, signUpResponse.user.id); reply.send({ - ...signUpResponse, + status: "OK", user: { ...signUpResponse.user, roles: [invitation.role], diff --git a/packages/user/src/model/invitations/handlers/createInvitation.ts b/packages/user/src/model/invitations/handlers/createInvitation.ts index 08e31b378..d0f5fa20a 100644 --- a/packages/user/src/model/invitations/handlers/createInvitation.ts +++ b/packages/user/src/model/invitations/handlers/createInvitation.ts @@ -1,5 +1,4 @@ -import type { FastifyReply } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; +import type { FastifyReply, FastifyRequest } from "fastify"; import type { Invitation, @@ -10,7 +9,7 @@ import getInvitationService from "../../../lib/getInvitationService"; import sendInvitation from "../../../lib/sendInvitation"; const createInvitation = async ( - request: SessionRequest, + request: FastifyRequest, reply: FastifyReply, ) => { const { diff --git a/packages/user/src/model/invitations/handlers/deleteInvitation.ts b/packages/user/src/model/invitations/handlers/deleteInvitation.ts index c90133c1f..12778a38b 100644 --- a/packages/user/src/model/invitations/handlers/deleteInvitation.ts +++ b/packages/user/src/model/invitations/handlers/deleteInvitation.ts @@ -1,12 +1,11 @@ -import type { FastifyReply } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; +import type { FastifyReply, FastifyRequest } from "fastify"; import type { Invitation } from "../../../types/invitation"; import Service from "../service"; const deleteInvitation = async ( - request: SessionRequest, + request: FastifyRequest, reply: FastifyReply, ) => { const { config, dbSchema, params, slonik } = request; diff --git a/packages/user/src/model/invitations/handlers/listInvitation.ts b/packages/user/src/model/invitations/handlers/listInvitation.ts index 45ede5370..8518b4c61 100644 --- a/packages/user/src/model/invitations/handlers/listInvitation.ts +++ b/packages/user/src/model/invitations/handlers/listInvitation.ts @@ -1,12 +1,11 @@ import type { PaginatedList } from "@prefabs.tech/fastify-slonik"; -import type { FastifyReply } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; +import type { FastifyReply, FastifyRequest } from "fastify"; import type { Invitation } from "../../../types/invitation"; import getInvitationService from "../../../lib/getInvitationService"; -const listInvitation = async (request: SessionRequest, reply: FastifyReply) => { +const listInvitation = async (request: FastifyRequest, reply: FastifyReply) => { const { config, dbSchema, query, slonik } = request; const { filters, limit, offset, sort } = query as { diff --git a/packages/user/src/model/invitations/handlers/resendInvitation.ts b/packages/user/src/model/invitations/handlers/resendInvitation.ts index 09c1c1fb7..395b38e4a 100644 --- a/packages/user/src/model/invitations/handlers/resendInvitation.ts +++ b/packages/user/src/model/invitations/handlers/resendInvitation.ts @@ -1,5 +1,4 @@ -import type { FastifyReply } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; +import type { FastifyReply, FastifyRequest } from "fastify"; import type { Invitation } from "../../../types/invitation"; @@ -8,7 +7,7 @@ import isInvitationValid from "../../../lib/isInvitationValid"; import sendInvitation from "../../../lib/sendInvitation"; const resendInvitation = async ( - request: SessionRequest, + request: FastifyRequest, reply: FastifyReply, ) => { const { config, dbSchema, headers, hostname, log, params, server, slonik } = diff --git a/packages/user/src/model/invitations/handlers/revokeInvitation.ts b/packages/user/src/model/invitations/handlers/revokeInvitation.ts index b39fccf3f..f28ad52db 100644 --- a/packages/user/src/model/invitations/handlers/revokeInvitation.ts +++ b/packages/user/src/model/invitations/handlers/revokeInvitation.ts @@ -1,5 +1,4 @@ -import type { FastifyReply } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; +import type { FastifyReply, FastifyRequest } from "fastify"; import { formatDate } from "@prefabs.tech/fastify-slonik"; @@ -8,7 +7,7 @@ import type { Invitation } from "../../../types/invitation"; import getInvitationService from "../../../lib/getInvitationService"; const revokeInvitation = async ( - request: SessionRequest, + request: FastifyRequest, reply: FastifyReply, ) => { const { config, dbSchema, params, server, slonik } = request; diff --git a/packages/user/src/model/permissions/handlers/getPermissions.ts b/packages/user/src/model/permissions/handlers/getPermissions.ts index b52388db4..1f127a571 100644 --- a/packages/user/src/model/permissions/handlers/getPermissions.ts +++ b/packages/user/src/model/permissions/handlers/getPermissions.ts @@ -1,7 +1,6 @@ -import type { FastifyReply } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; +import type { FastifyReply, FastifyRequest } from "fastify"; -const getPermissions = async (request: SessionRequest, reply: FastifyReply) => { +const getPermissions = async (request: FastifyRequest, reply: FastifyReply) => { const { config } = request; const permissions: string[] = config.user.permissions || []; diff --git a/packages/user/src/model/roles/handlers/createRole.ts b/packages/user/src/model/roles/handlers/createRole.ts index 22cda0fbe..c40b25bf2 100644 --- a/packages/user/src/model/roles/handlers/createRole.ts +++ b/packages/user/src/model/roles/handlers/createRole.ts @@ -1,11 +1,10 @@ -import type { FastifyReply } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; +import type { FastifyReply, FastifyRequest } from "fastify"; import { CustomError } from "@prefabs.tech/fastify-error-handler"; import RoleService from "../service"; -const createRole = async (request: SessionRequest, reply: FastifyReply) => { +const createRole = async (request: FastifyRequest, reply: FastifyReply) => { const { body } = request; const { permissions, role } = body as { diff --git a/packages/user/src/model/roles/handlers/deleteRole.ts b/packages/user/src/model/roles/handlers/deleteRole.ts index dddd384e7..b7aa49ae0 100644 --- a/packages/user/src/model/roles/handlers/deleteRole.ts +++ b/packages/user/src/model/roles/handlers/deleteRole.ts @@ -1,12 +1,11 @@ -import type { FastifyReply } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; +import type { FastifyReply, FastifyRequest } from "fastify"; import { CustomError } from "@prefabs.tech/fastify-error-handler"; import { ERROR_CODES } from "../../../constants"; import RoleService from "../service"; -const deleteRole = async (request: SessionRequest, reply: FastifyReply) => { +const deleteRole = async (request: FastifyRequest, reply: FastifyReply) => { const { query } = request; try { diff --git a/packages/user/src/model/roles/handlers/getPermissions.ts b/packages/user/src/model/roles/handlers/getPermissions.ts index 2fe21f203..880ca210f 100644 --- a/packages/user/src/model/roles/handlers/getPermissions.ts +++ b/packages/user/src/model/roles/handlers/getPermissions.ts @@ -1,9 +1,8 @@ -import type { FastifyReply } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; +import type { FastifyReply, FastifyRequest } from "fastify"; import RoleService from "../service"; -const getPermissions = async (request: SessionRequest, reply: FastifyReply) => { +const getPermissions = async (request: FastifyRequest, reply: FastifyReply) => { const { query } = request; let permissions: string[] = []; diff --git a/packages/user/src/model/roles/handlers/getRoles.ts b/packages/user/src/model/roles/handlers/getRoles.ts index 40abe3db3..888f190e0 100644 --- a/packages/user/src/model/roles/handlers/getRoles.ts +++ b/packages/user/src/model/roles/handlers/getRoles.ts @@ -1,9 +1,8 @@ -import type { FastifyReply } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; +import type { FastifyReply, FastifyRequest } from "fastify"; import RoleService from "../service"; -const getRoles = async (request: SessionRequest, reply: FastifyReply) => { +const getRoles = async (request: FastifyRequest, reply: FastifyReply) => { const service = new RoleService(); const roles = await service.getRoles(); diff --git a/packages/user/src/model/roles/handlers/updatePermissions.ts b/packages/user/src/model/roles/handlers/updatePermissions.ts index 8dc30eae7..cd3a2e78a 100644 --- a/packages/user/src/model/roles/handlers/updatePermissions.ts +++ b/packages/user/src/model/roles/handlers/updatePermissions.ts @@ -1,12 +1,11 @@ -import type { FastifyReply } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; +import type { FastifyReply, FastifyRequest } from "fastify"; import { CustomError } from "@prefabs.tech/fastify-error-handler"; import RoleService from "../service"; const updatePermissions = async ( - request: SessionRequest, + request: FastifyRequest, reply: FastifyReply, ) => { const { body } = request; diff --git a/packages/user/src/model/users/handlers/canAdminSignUp.ts b/packages/user/src/model/users/handlers/canAdminSignUp.ts index dc9993a16..85e6fe8fa 100644 --- a/packages/user/src/model/users/handlers/canAdminSignUp.ts +++ b/packages/user/src/model/users/handlers/canAdminSignUp.ts @@ -1,25 +1,25 @@ import type { FastifyReply, FastifyRequest } from "fastify"; -import UserRoles from "supertokens-node/recipe/userroles"; - +import { auth } from "../../../auth/adapter"; import { ROLE_ADMIN, ROLE_SUPERADMIN } from "../../../constants"; const canAdminSignUp = async (request: FastifyRequest, reply: FastifyReply) => { const { server } = request; // check if already admin user exists - const adminUsers = await UserRoles.getUsersThatHaveRole(ROLE_ADMIN); - const superAdminUsers = await UserRoles.getUsersThatHaveRole(ROLE_SUPERADMIN); - - if ( - adminUsers.status === "UNKNOWN_ROLE_ERROR" && - superAdminUsers.status === "UNKNOWN_ROLE_ERROR" - ) { - throw server.httpErrors.unprocessableEntity(adminUsers.status); - } else if ( - (adminUsers.status === "OK" && adminUsers.users.length > 0) || - (superAdminUsers.status === "OK" && superAdminUsers.users.length > 0) - ) { + const adminUsers = await auth.roles.getUsersThatHaveRole(ROLE_ADMIN); + const superAdminUsers = + await auth.roles.getUsersThatHaveRole(ROLE_SUPERADMIN); + + if (adminUsers.length === 0 && superAdminUsers.length === 0) { + const allRoles = await auth.roles.getAllRoles(); + + if (!allRoles.includes(ROLE_ADMIN) && !allRoles.includes(ROLE_SUPERADMIN)) { + throw server.httpErrors.unprocessableEntity("UNKNOWN_ROLE_ERROR"); + } + } + + if (adminUsers.length > 0 || superAdminUsers.length > 0) { return reply.send({ signUp: false }); } diff --git a/packages/user/src/model/users/handlers/changeEmail.ts b/packages/user/src/model/users/handlers/changeEmail.ts index d3926fb11..e50d42c37 100644 --- a/packages/user/src/model/users/handlers/changeEmail.ts +++ b/packages/user/src/model/users/handlers/changeEmail.ts @@ -1,21 +1,16 @@ -import type { SessionRequest } from "supertokens-node/framework/fastify"; - -import { FastifyReply } from "fastify"; -import EmailVerification, { - EmailVerificationClaim, - isEmailVerified, -} from "supertokens-node/recipe/emailverification"; -import { getUsersByEmail } from "supertokens-node/recipe/thirdpartyemailpassword"; +import type { FastifyReply, FastifyRequest } from "fastify"; +import type { AuthSession } from "../../../auth/adapter"; import type { ChangeEmailInput } from "../../../types"; +import { auth } from "../../../auth/adapter"; import getUserService from "../../../lib/getUserService"; -import createUserContext from "../../../supertokens/utils/createUserContext"; -import ProfileValidationClaim from "../../../supertokens/utils/profileValidationClaim"; import validateEmail from "../../../validator/email"; -const changeEmail = async (request: SessionRequest, reply: FastifyReply) => { - const { body, config, server, session, slonik, user } = request; +const changeEmail = async (request: FastifyRequest, reply: FastifyReply) => { + const { body, config, server, slonik, user } = request; + const session = (request as FastifyRequest & { session: AuthSession }) + .session; if (!user) { throw server.httpErrors.unauthorized("Unauthorised"); @@ -28,17 +23,23 @@ const changeEmail = async (request: SessionRequest, reply: FastifyReply) => { } try { + const userContext = auth.createUserContext(request); + if (config.user.features?.profileValidation?.enabled) { - await session?.fetchAndSetClaim( - new ProfileValidationClaim(), - createUserContext(undefined, request), + await auth.claims.refreshSessionClaims( + session, + request, + ["profileValidation"], + userContext, ); } if (config.user.features?.signUp?.emailVerification) { - await session?.fetchAndSetClaim( - EmailVerificationClaim, - createUserContext(undefined, request), + await auth.claims.refreshSessionClaims( + session, + request, + ["emailVerification"], + userContext, ); } @@ -59,14 +60,20 @@ const changeEmail = async (request: SessionRequest, reply: FastifyReply) => { }); } - if (config.user.features?.signUp?.emailVerification) { - const isVerified = await isEmailVerified(user.id, email); + if ( + config.user.features?.signUp?.emailVerification && + auth.emailVerification + ) { + const isVerified = await auth.emailVerification.isEmailVerified( + user.id, + email, + ); if (!isVerified) { - const users = await getUsersByEmail(email); + const users = (await auth.emailPassword.getUsersByEmail?.(email)) || []; const emailPasswordRecipeUsers = users.filter( - (user) => !user.thirdParty, + (user) => !(user as Record).thirdParty, ); if (emailPasswordRecipeUsers.length > 0) { @@ -75,24 +82,19 @@ const changeEmail = async (request: SessionRequest, reply: FastifyReply) => { }); } - const tokenResponse = - await EmailVerification.createEmailVerificationToken(user.id, email); - - if (tokenResponse.status === "OK") { - await EmailVerification.sendEmail({ - emailVerifyLink: `${config.appOrigin[0]}/auth/verify-email?token=${tokenResponse.token}&rid=emailverification`, - type: "EMAIL_VERIFICATION", - user: { - email: email, - id: user.id, - }, - userContext: { - _default: { - request: { - request: request, - }, - }, - }, + const token = await auth.emailVerification.createEmailVerificationToken( + user.id, + email, + userContext, + ); + + if (token) { + await auth.emailVerification.sendVerificationEmail?.({ + appOrigin: config.appOrigin[0] as string, + email, + token, + userContext, + userId: user.id, }); return reply.send({ @@ -101,7 +103,7 @@ const changeEmail = async (request: SessionRequest, reply: FastifyReply) => { }); } - return reply.send(tokenResponse.status); + return reply.send({ status: "EMAIL_VERIFICATION_TOKEN_FAILED" }); } } diff --git a/packages/user/src/model/users/handlers/changePassword.ts b/packages/user/src/model/users/handlers/changePassword.ts index 98bff47b0..b3b398b89 100644 --- a/packages/user/src/model/users/handlers/changePassword.ts +++ b/packages/user/src/model/users/handlers/changePassword.ts @@ -1,14 +1,11 @@ -import type { FastifyReply } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; - -import { createNewSession } from "supertokens-node/recipe/session"; +import type { FastifyReply, FastifyRequest } from "fastify"; import type { ChangePasswordInput } from "../../../types"; +import { auth } from "../../../auth/adapter"; import getUserService from "../../../lib/getUserService"; -import createUserContext from "../../../supertokens/utils/createUserContext"; -const changePassword = async (request: SessionRequest, reply: FastifyReply) => { +const changePassword = async (request: FastifyRequest, reply: FastifyReply) => { const { body, config, dbSchema, server, slonik, user } = request; if (!user) { @@ -27,13 +24,13 @@ const changePassword = async (request: SessionRequest, reply: FastifyReply) => { ); if (response.status === "OK") { - await createNewSession( + await auth.session.createNewSession( request, reply, user.id, undefined, undefined, - createUserContext(undefined, request), + auth.createUserContext(request), ); } diff --git a/packages/user/src/model/users/handlers/deleteMe.ts b/packages/user/src/model/users/handlers/deleteMe.ts index f6915fdfe..45a080921 100644 --- a/packages/user/src/model/users/handlers/deleteMe.ts +++ b/packages/user/src/model/users/handlers/deleteMe.ts @@ -1,11 +1,10 @@ import type { FastifyReply, FastifyRequest } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; import { CustomError } from "@prefabs.tech/fastify-error-handler"; import getUserService from "../../../lib/getUserService"; -const deleteMe = async (request: SessionRequest, reply: FastifyReply) => { +const deleteMe = async (request: FastifyRequest, reply: FastifyReply) => { const { body, config, dbSchema, server, slonik, user } = request as FastifyRequest<{ Body: { diff --git a/packages/user/src/model/users/handlers/disable.ts b/packages/user/src/model/users/handlers/disable.ts index 910780ef7..03ac852df 100644 --- a/packages/user/src/model/users/handlers/disable.ts +++ b/packages/user/src/model/users/handlers/disable.ts @@ -1,9 +1,8 @@ -import type { FastifyReply } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; +import type { FastifyReply, FastifyRequest } from "fastify"; import getUserService from "../../../lib/getUserService"; -const disable = async (request: SessionRequest, reply: FastifyReply) => { +const disable = async (request: FastifyRequest, reply: FastifyReply) => { const { config, dbSchema, server, slonik, user } = request; if (!user) { diff --git a/packages/user/src/model/users/handlers/enable.ts b/packages/user/src/model/users/handlers/enable.ts index e3cbc4587..20bd38491 100644 --- a/packages/user/src/model/users/handlers/enable.ts +++ b/packages/user/src/model/users/handlers/enable.ts @@ -1,9 +1,8 @@ -import type { FastifyReply } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; +import type { FastifyReply, FastifyRequest } from "fastify"; import getUserService from "../../../lib/getUserService"; -const enable = async (request: SessionRequest, reply: FastifyReply) => { +const enable = async (request: FastifyRequest, reply: FastifyReply) => { const { config, dbSchema, server, slonik, user } = request; if (!user) { diff --git a/packages/user/src/model/users/handlers/me.ts b/packages/user/src/model/users/handlers/me.ts index 0c131af9f..2cd976c8e 100644 --- a/packages/user/src/model/users/handlers/me.ts +++ b/packages/user/src/model/users/handlers/me.ts @@ -1,38 +1,42 @@ -import type { FastifyReply } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; +import type { FastifyReply, FastifyRequest } from "fastify"; -import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; -import { getUserById } from "supertokens-node/recipe/thirdpartyemailpassword"; +import type { AuthSession } from "../../../auth/adapter"; -import createUserContext from "../../../supertokens/utils/createUserContext"; -import ProfileValidationClaim from "../../../supertokens/utils/profileValidationClaim"; +import { auth } from "../../../auth/adapter"; -const me = async (request: SessionRequest, reply: FastifyReply) => { - const { config, server, session, user } = request; +const me = async (request: FastifyRequest, reply: FastifyReply) => { + const { config, server, session, user } = request as FastifyRequest & { + session: AuthSession; + }; if (!user) { throw server.httpErrors.unauthorized("Unauthorised"); } - const authUser = await getUserById(user.id); + const authUser = await auth.emailPassword.getUserById(user.id); + const userContext = auth.createUserContext(request); if (config.user.features?.profileValidation?.enabled) { - await session?.fetchAndSetClaim( - new ProfileValidationClaim(), - createUserContext(undefined, request), + await auth.claims.refreshSessionClaims( + session, + request, + ["profileValidation"], + userContext, ); } if (config.user.features?.signUp?.emailVerification) { - await session?.fetchAndSetClaim( - EmailVerificationClaim, - createUserContext(undefined, request), + await auth.claims.refreshSessionClaims( + session, + request, + ["emailVerification"], + userContext, ); } const response = { ...user, - thirdParty: authUser?.thirdParty, + thirdParty: (authUser as Record)?.thirdParty, }; reply.send(response); diff --git a/packages/user/src/model/users/handlers/removePhoto.ts b/packages/user/src/model/users/handlers/removePhoto.ts index c00496aba..914a664e1 100644 --- a/packages/user/src/model/users/handlers/removePhoto.ts +++ b/packages/user/src/model/users/handlers/removePhoto.ts @@ -1,14 +1,11 @@ -import type { FastifyReply } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; +import type { FastifyReply, FastifyRequest } from "fastify"; -import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; -import { getUserById } from "supertokens-node/recipe/thirdpartyemailpassword"; +import type { AuthSession } from "../../../auth/adapter"; +import { auth } from "../../../auth/adapter"; import getUserService from "../../../lib/getUserService"; -import createUserContext from "../../../supertokens/utils/createUserContext"; -import ProfileValidationClaim from "../../../supertokens/utils/profileValidationClaim"; -const removePhoto = async (request: SessionRequest, reply: FastifyReply) => { +const removePhoto = async (request: FastifyRequest, reply: FastifyReply) => { const { config, dbSchema, server, slonik, user } = request; if (!user) { @@ -26,25 +23,33 @@ const removePhoto = async (request: SessionRequest, reply: FastifyReply) => { request.user = updatedUser; - const authUser = await getUserById(user.id); + const authUser = await auth.emailPassword.getUserById(user.id); + const userContext = auth.createUserContext(request); + + const session = (request as FastifyRequest & { session: AuthSession }) + .session; if (request.config.user.features?.profileValidation?.enabled) { - await request.session?.fetchAndSetClaim( - new ProfileValidationClaim(), - createUserContext(undefined, request), + await auth.claims.refreshSessionClaims( + session, + request, + ["profileValidation"], + userContext, ); } if (request.config.user.features?.signUp?.emailVerification) { - await request.session?.fetchAndSetClaim( - EmailVerificationClaim, - createUserContext(undefined, request), + await auth.claims.refreshSessionClaims( + session, + request, + ["emailVerification"], + userContext, ); } const response = { ...updatedUser, - thirdParty: authUser?.thirdParty, + thirdParty: (authUser as Record)?.thirdParty, }; reply.send(response); diff --git a/packages/user/src/model/users/handlers/updateMe.ts b/packages/user/src/model/users/handlers/updateMe.ts index f1fb4f227..4a2ef0429 100644 --- a/packages/user/src/model/users/handlers/updateMe.ts +++ b/packages/user/src/model/users/handlers/updateMe.ts @@ -1,20 +1,17 @@ import type { File } from "@prefabs.tech/fastify-s3"; import type { FastifyReply, FastifyRequest } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; import { CustomError } from "@prefabs.tech/fastify-error-handler"; -import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; -import { getUserById } from "supertokens-node/recipe/thirdpartyemailpassword"; +import type { AuthSession } from "../../../auth/adapter"; import type { UserUpdateInput } from "../../../types"; +import { auth } from "../../../auth/adapter"; import { ERROR_CODES } from "../../../constants"; import getUserService from "../../../lib/getUserService"; -import createUserContext from "../../../supertokens/utils/createUserContext"; -import ProfileValidationClaim from "../../../supertokens/utils/profileValidationClaim"; import filterUserUpdateInput from "../filterUserUpdateInput"; -const updateMe = async (request: SessionRequest, reply: FastifyReply) => { +const updateMe = async (request: FastifyRequest, reply: FastifyReply) => { const { body, config, dbSchema, server, slonik, user } = request as FastifyRequest<{ Body: UserUpdateInput; @@ -49,25 +46,33 @@ const updateMe = async (request: SessionRequest, reply: FastifyReply) => { request.user = updatedUser; - const authUser = await getUserById(user.id); + const authUser = await auth.emailPassword.getUserById(user.id); + const userContext = auth.createUserContext(request); + + const session = (request as FastifyRequest & { session: AuthSession }) + .session; if (request.config.user.features?.profileValidation?.enabled) { - await request.session?.fetchAndSetClaim( - new ProfileValidationClaim(), - createUserContext(undefined, request), + await auth.claims.refreshSessionClaims( + session, + request, + ["profileValidation"], + userContext, ); } if (request.config.user.features?.signUp?.emailVerification) { - await request.session?.fetchAndSetClaim( - EmailVerificationClaim, - createUserContext(undefined, request), + await auth.claims.refreshSessionClaims( + session, + request, + ["emailVerification"], + userContext, ); } const response = { ...updatedUser, - thirdParty: authUser?.thirdParty, + thirdParty: (authUser as Record)?.thirdParty, }; reply.send(response); diff --git a/packages/user/src/model/users/handlers/uploadPhoto.ts b/packages/user/src/model/users/handlers/uploadPhoto.ts index fbfd2384e..206260576 100644 --- a/packages/user/src/model/users/handlers/uploadPhoto.ts +++ b/packages/user/src/model/users/handlers/uploadPhoto.ts @@ -1,19 +1,16 @@ import type { Multipart } from "@prefabs.tech/fastify-s3"; import type { FastifyReply, FastifyRequest } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; import { CustomError } from "@prefabs.tech/fastify-error-handler"; -import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; -import { getUserById } from "supertokens-node/recipe/thirdpartyemailpassword"; +import type { AuthSession } from "../../../auth/adapter"; import type { UserUpdateInput } from "../../../types"; +import { auth } from "../../../auth/adapter"; import { ERROR_CODES } from "../../../constants"; import getUserService from "../../../lib/getUserService"; -import createUserContext from "../../../supertokens/utils/createUserContext"; -import ProfileValidationClaim from "../../../supertokens/utils/profileValidationClaim"; -const uploadPhoto = async (request: SessionRequest, reply: FastifyReply) => { +const uploadPhoto = async (request: FastifyRequest, reply: FastifyReply) => { const { body, config, dbSchema, server, slonik, user } = request as FastifyRequest<{ Body: UserUpdateInput; @@ -51,25 +48,33 @@ const uploadPhoto = async (request: SessionRequest, reply: FastifyReply) => { request.user = updatedUser; - const authUser = await getUserById(user.id); + const authUser = await auth.emailPassword.getUserById(user.id); + const userContext = auth.createUserContext(request); + + const session = (request as FastifyRequest & { session: AuthSession }) + .session; if (request.config.user.features?.profileValidation?.enabled) { - await request.session?.fetchAndSetClaim( - new ProfileValidationClaim(), - createUserContext(undefined, request), + await auth.claims.refreshSessionClaims( + session, + request, + ["profileValidation"], + userContext, ); } if (request.config.user.features?.signUp?.emailVerification) { - await request.session?.fetchAndSetClaim( - EmailVerificationClaim, - createUserContext(undefined, request), + await auth.claims.refreshSessionClaims( + session, + request, + ["emailVerification"], + userContext, ); } const response = { ...updatedUser, - thirdParty: authUser?.thirdParty, + thirdParty: (authUser as Record)?.thirdParty, }; reply.send(response); diff --git a/packages/user/src/model/users/handlers/user.ts b/packages/user/src/model/users/handlers/user.ts index 0de2764e4..e6d2faef3 100644 --- a/packages/user/src/model/users/handlers/user.ts +++ b/packages/user/src/model/users/handlers/user.ts @@ -1,9 +1,8 @@ -import type { FastifyReply } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; +import type { FastifyReply, FastifyRequest } from "fastify"; import getUserService from "../../../lib/getUserService"; -const user = async (request: SessionRequest, reply: FastifyReply) => { +const user = async (request: FastifyRequest, reply: FastifyReply) => { const service = getUserService( request.config, request.slonik, diff --git a/packages/user/src/model/users/handlers/users.ts b/packages/user/src/model/users/handlers/users.ts index 0a28a042b..12a84a0bc 100644 --- a/packages/user/src/model/users/handlers/users.ts +++ b/packages/user/src/model/users/handlers/users.ts @@ -1,9 +1,8 @@ -import type { FastifyReply } from "fastify"; -import type { SessionRequest } from "supertokens-node/framework/fastify"; +import type { FastifyReply, FastifyRequest } from "fastify"; import getUserService from "../../../lib/getUserService"; -const users = async (request: SessionRequest, reply: FastifyReply) => { +const users = async (request: FastifyRequest, reply: FastifyReply) => { const service = getUserService( request.config, request.slonik, diff --git a/packages/user/src/supertokens/index.ts b/packages/user/src/supertokens/index.ts index 22045a56f..1c2e4f662 100644 --- a/packages/user/src/supertokens/index.ts +++ b/packages/user/src/supertokens/index.ts @@ -1,14 +1,9 @@ -import Session from "supertokens-node/lib/build/recipe/session/sessionClass"; import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; declare module "fastify" { interface FastifyInstance { verifySession: typeof verifySession; } - - interface FastifyRequest { - session?: Session; - } } export { default } from "./plugin"; diff --git a/packages/user/src/supertokens/init.ts b/packages/user/src/supertokens/init.ts index 05fb5b751..2d3454b72 100644 --- a/packages/user/src/supertokens/init.ts +++ b/packages/user/src/supertokens/init.ts @@ -9,7 +9,7 @@ const init = (fastify: FastifyInstance) => { supertokens.init({ appInfo: { - apiBasePath: config.user.supertokens.apiBasePath, + apiBasePath: config.user.supertokens!.apiBasePath, apiDomain: config.baseUrl as string, appName: config.appName as string, websiteDomain: config.appOrigin[0] as string, @@ -17,7 +17,7 @@ const init = (fastify: FastifyInstance) => { framework: "fastify", recipeList: getRecipeList(fastify), supertokens: { - connectionURI: config.user.supertokens.connectionUri as string, + connectionURI: config.user.supertokens!.connectionUri as string, }, }); }; diff --git a/packages/user/src/supertokens/plugin.ts b/packages/user/src/supertokens/plugin.ts index 291068e05..36c4c058c 100644 --- a/packages/user/src/supertokens/plugin.ts +++ b/packages/user/src/supertokens/plugin.ts @@ -14,7 +14,7 @@ const plugin = async (fastify: FastifyInstance) => { init(fastify); - if (config.user.supertokens.setErrorHandler !== false) { + if (config.user.supertokens!.setErrorHandler !== false) { fastify.setErrorHandler(errorHandler); } @@ -27,7 +27,7 @@ const plugin = async (fastify: FastifyInstance) => { // [RL 2024-06-11] change sRefreshToken cookie path from config fastify.addHook("onSend", async (request, reply) => { const refreshTokenCookiePath = - request.server.config.user.supertokens.refreshTokenCookiePath; + request.server.config.user.supertokens!.refreshTokenCookiePath; const setCookieHeader = reply.getHeader("set-cookie"); diff --git a/packages/user/src/supertokens/recipes/config/email-verification/sendEmailVerificationEmail.ts b/packages/user/src/supertokens/recipes/config/email-verification/sendEmailVerificationEmail.ts index b500b9d0e..6e69b1463 100644 --- a/packages/user/src/supertokens/recipes/config/email-verification/sendEmailVerificationEmail.ts +++ b/packages/user/src/supertokens/recipes/config/email-verification/sendEmailVerificationEmail.ts @@ -32,7 +32,7 @@ const sendEmailVerificationEmail = ( const emailVerifyLink = input.emailVerifyLink.replace( websiteDomain + "/auth/verify-email", origin + - (fastify.config.user.supertokens.emailVerificationPath || + (fastify.config.user.supertokens!.emailVerificationPath || EMAIL_VERIFICATION_PATH), ); diff --git a/packages/user/src/supertokens/recipes/config/emailVerificationRecipeConfig.ts b/packages/user/src/supertokens/recipes/config/emailVerificationRecipeConfig.ts index e2cee9212..8b72ae3c2 100644 --- a/packages/user/src/supertokens/recipes/config/emailVerificationRecipeConfig.ts +++ b/packages/user/src/supertokens/recipes/config/emailVerificationRecipeConfig.ts @@ -21,8 +21,8 @@ const getEmailVerificationRecipeConfig = ( let emailVerification: EmailVerificationRecipe = {}; - if (typeof config.user.supertokens.recipes?.emailVerification === "object") { - emailVerification = config.user.supertokens.recipes.emailVerification; + if (typeof config.user.supertokens!.recipes?.emailVerification === "object") { + emailVerification = config.user.supertokens!.recipes.emailVerification; } return { diff --git a/packages/user/src/supertokens/recipes/config/session/getSession.ts b/packages/user/src/supertokens/recipes/config/session/getSession.ts index 874a0869b..1087f5f4e 100644 --- a/packages/user/src/supertokens/recipes/config/session/getSession.ts +++ b/packages/user/src/supertokens/recipes/config/session/getSession.ts @@ -17,7 +17,7 @@ const getSession = ( .request as FastifyRequest; input.options = { - checkDatabase: config.user.supertokens.checkSessionInDatabase ?? true, + checkDatabase: config.user.supertokens!.checkSessionInDatabase ?? true, ...input.options, }; diff --git a/packages/user/src/supertokens/recipes/config/session/verifySession.ts b/packages/user/src/supertokens/recipes/config/session/verifySession.ts index dc036428e..25486781c 100644 --- a/packages/user/src/supertokens/recipes/config/session/verifySession.ts +++ b/packages/user/src/supertokens/recipes/config/session/verifySession.ts @@ -13,7 +13,7 @@ const verifySession = ( input.verifySessionOptions = { checkDatabase: - fastify.config.user.supertokens.checkSessionInDatabase ?? true, + fastify.config.user.supertokens!.checkSessionInDatabase ?? true, ...input.verifySessionOptions, }; diff --git a/packages/user/src/supertokens/recipes/config/sessionRecipeConfig.ts b/packages/user/src/supertokens/recipes/config/sessionRecipeConfig.ts index be9245a9c..958748731 100644 --- a/packages/user/src/supertokens/recipes/config/sessionRecipeConfig.ts +++ b/packages/user/src/supertokens/recipes/config/sessionRecipeConfig.ts @@ -19,8 +19,8 @@ const getSessionRecipeConfig = ( let session: SessionRecipe = {}; - if (typeof config.user.supertokens.recipes?.session === "object") { - session = config.user.supertokens.recipes.session; + if (typeof config.user.supertokens!.recipes?.session === "object") { + session = config.user.supertokens!.recipes.session; } return { diff --git a/packages/user/src/supertokens/recipes/config/third-party-email-password/emailPasswordSignIn.ts b/packages/user/src/supertokens/recipes/config/third-party-email-password/emailPasswordSignIn.ts index 68d5eb6c3..267ee9593 100644 --- a/packages/user/src/supertokens/recipes/config/third-party-email-password/emailPasswordSignIn.ts +++ b/packages/user/src/supertokens/recipes/config/third-party-email-password/emailPasswordSignIn.ts @@ -3,8 +3,6 @@ import type { RecipeInterface } from "supertokens-node/recipe/thirdpartyemailpas import { formatDate } from "@prefabs.tech/fastify-slonik"; -import type { AuthUser } from "../../../../types"; - import getUserService from "../../../../lib/getUserService"; const emailPasswordSignIn = ( @@ -45,7 +43,7 @@ const emailPasswordSignIn = ( log.error(error); }); - const authUser: AuthUser = { + const authUser = { ...originalResponse.user, ...user, }; diff --git a/packages/user/src/supertokens/recipes/config/third-party-email-password/emailPasswordSignUp.ts b/packages/user/src/supertokens/recipes/config/third-party-email-password/emailPasswordSignUp.ts index 77f33cfc1..e87dac20d 100644 --- a/packages/user/src/supertokens/recipes/config/third-party-email-password/emailPasswordSignUp.ts +++ b/packages/user/src/supertokens/recipes/config/third-party-email-password/emailPasswordSignUp.ts @@ -100,7 +100,7 @@ const emailPasswordSignUp = ( } if ( - config.user.supertokens.sendUserAlreadyExistsWarning && + config.user.supertokens!.sendUserAlreadyExistsWarning && originalResponse.status === "EMAIL_ALREADY_EXISTS_ERROR" ) { try { diff --git a/packages/user/src/supertokens/recipes/config/third-party-email-password/sendPasswordResetEmail.ts b/packages/user/src/supertokens/recipes/config/third-party-email-password/sendPasswordResetEmail.ts index 892c6506f..9664ca9b0 100644 --- a/packages/user/src/supertokens/recipes/config/third-party-email-password/sendPasswordResetEmail.ts +++ b/packages/user/src/supertokens/recipes/config/third-party-email-password/sendPasswordResetEmail.ts @@ -39,7 +39,7 @@ const sendPasswordResetEmail = ( const passwordResetLink = input.passwordResetLink.replace( websiteDomain + "/auth/reset-password", origin + - (fastify.config.user.supertokens.resetPasswordPath || + (fastify.config.user.supertokens!.resetPasswordPath || RESET_PASSWORD_PATH), ); diff --git a/packages/user/src/supertokens/recipes/config/thirdPartyEmailPasswordRecipeConfig.ts b/packages/user/src/supertokens/recipes/config/thirdPartyEmailPasswordRecipeConfig.ts index d504068a3..822e5fb04 100644 --- a/packages/user/src/supertokens/recipes/config/thirdPartyEmailPasswordRecipeConfig.ts +++ b/packages/user/src/supertokens/recipes/config/thirdPartyEmailPasswordRecipeConfig.ts @@ -29,10 +29,11 @@ const getThirdPartyEmailPasswordRecipeConfig = ( let thirdPartyEmailPassword: ThirdPartyEmailPasswordRecipe = {}; if ( - typeof config.user.supertokens.recipes?.thirdPartyEmailPassword === "object" + typeof config.user.supertokens!.recipes?.thirdPartyEmailPassword === + "object" ) { thirdPartyEmailPassword = - config.user.supertokens.recipes.thirdPartyEmailPassword; + config.user.supertokens!.recipes.thirdPartyEmailPassword; } return { diff --git a/packages/user/src/supertokens/recipes/config/thirdPartyProviders.ts b/packages/user/src/supertokens/recipes/config/thirdPartyProviders.ts index 609c9c2a2..e6e53cd3a 100644 --- a/packages/user/src/supertokens/recipes/config/thirdPartyProviders.ts +++ b/packages/user/src/supertokens/recipes/config/thirdPartyProviders.ts @@ -5,7 +5,7 @@ import ThirdPartyEmailPassword from "supertokens-node/recipe/thirdpartyemailpass const getThirdPartyProviders = (config: ApiConfig) => { const { Apple, Facebook, Github, Google } = ThirdPartyEmailPassword; - const providersConfig = config.user.supertokens.providers; + const providersConfig = config.user.supertokens!.providers; const providers: TypeProvider[] = []; const providerFunctions = [ diff --git a/packages/user/src/supertokens/recipes/initEmailVerificationRecipe.ts b/packages/user/src/supertokens/recipes/initEmailVerificationRecipe.ts index 2568a2bb7..fb4a476d2 100644 --- a/packages/user/src/supertokens/recipes/initEmailVerificationRecipe.ts +++ b/packages/user/src/supertokens/recipes/initEmailVerificationRecipe.ts @@ -8,7 +8,7 @@ import getEmailVerificationRecipeConfig from "./config/emailVerificationRecipeCo const init = (fastify: FastifyInstance) => { const emailVerification: SupertokensRecipes["emailVerification"] = - fastify.config.user.supertokens.recipes?.emailVerification; + fastify.config.user.supertokens!.recipes?.emailVerification; if (typeof emailVerification === "function") { return EmailVerification.init(emailVerification(fastify)); diff --git a/packages/user/src/supertokens/recipes/initSessionRecipe.ts b/packages/user/src/supertokens/recipes/initSessionRecipe.ts index 3adf21646..19210690c 100644 --- a/packages/user/src/supertokens/recipes/initSessionRecipe.ts +++ b/packages/user/src/supertokens/recipes/initSessionRecipe.ts @@ -8,7 +8,7 @@ import getSessionRecipeConfig from "./config/sessionRecipeConfig"; const init = (fastify: FastifyInstance) => { const session: SupertokensRecipes["session"] = - fastify.config.user.supertokens.recipes?.session; + fastify.config.user.supertokens!.recipes?.session; if (typeof session === "function") { return Session.init(session(fastify)); diff --git a/packages/user/src/supertokens/recipes/initThirdPartyEmailPasswordRecipe.ts b/packages/user/src/supertokens/recipes/initThirdPartyEmailPasswordRecipe.ts index 4484d89d7..06cf03ad6 100644 --- a/packages/user/src/supertokens/recipes/initThirdPartyEmailPasswordRecipe.ts +++ b/packages/user/src/supertokens/recipes/initThirdPartyEmailPasswordRecipe.ts @@ -8,7 +8,7 @@ import getThirdPartyEmailPasswordRecipeConfig from "./config/thirdPartyEmailPass const init = (fastify: FastifyInstance) => { const thirdPartyEmailPassword: SupertokensRecipes["thirdPartyEmailPassword"] = - fastify.config.user.supertokens.recipes?.thirdPartyEmailPassword; + fastify.config.user.supertokens!.recipes?.thirdPartyEmailPassword; if (typeof thirdPartyEmailPassword === "function") { return ThirdPartyEmailPassword.init(thirdPartyEmailPassword(fastify)); diff --git a/packages/user/src/supertokens/recipes/initUserRolesRecipe.ts b/packages/user/src/supertokens/recipes/initUserRolesRecipe.ts index c5b651ac1..a93f1a909 100644 --- a/packages/user/src/supertokens/recipes/initUserRolesRecipe.ts +++ b/packages/user/src/supertokens/recipes/initUserRolesRecipe.ts @@ -7,7 +7,8 @@ import type { SupertokensRecipes } from "../types"; import getUserRolesRecipeConfig from "./config/userRolesRecipeConfig"; const init = (fastify: FastifyInstance) => { - const recipes = fastify.config.user.supertokens.recipes as SupertokensRecipes; + const recipes = fastify.config.user.supertokens! + .recipes as SupertokensRecipes; if (recipes && recipes.userRoles) { return UserRoles.init(recipes.userRoles(fastify)); diff --git a/packages/user/src/types/config.ts b/packages/user/src/types/config.ts index b2699d565..87d8fdcf6 100644 --- a/packages/user/src/types/config.ts +++ b/packages/user/src/types/config.ts @@ -17,11 +17,7 @@ interface EmailOptions { } interface UserConfig { - authConfig?: { - [provider: string]: unknown; - supertokens?: SupertokensConfig; - }; - authProvider?: "supertokens" | string; + authProvider?: string; email?: IsEmailOptions; emailOverrides?: { duplicateEmail?: EmailOptions; @@ -120,7 +116,7 @@ interface UserConfig { invitation?: typeof InvitationService; user?: typeof UserService; }; - supertokens: SupertokensConfig; + supertokens?: SupertokensConfig; tables?: { invitations?: { name?: string; diff --git a/packages/user/src/types/index.ts b/packages/user/src/types/index.ts index ba9be6735..121eec4cc 100644 --- a/packages/user/src/types/index.ts +++ b/packages/user/src/types/index.ts @@ -42,6 +42,11 @@ export type { export type { EmailVerificationRecipe } from "../supertokens/types/emailVerificationRecipe"; export type { SessionRecipe } from "../supertokens/types/sessionRecipe"; export type { ThirdPartyEmailPasswordRecipe } from "../supertokens/types/thirdPartyEmailPasswordRecipe"; + +/* + * @deprecated Import auth types from "@prefabs.tech/fastify-user/auth" instead. + * These supertokens-specific types will be removed in a future release. + */ export * from "./config"; export * from "./invitation"; diff --git a/packages/user/src/userContext.ts b/packages/user/src/userContext.ts index ef482fd68..0e80053ea 100644 --- a/packages/user/src/userContext.ts +++ b/packages/user/src/userContext.ts @@ -1,11 +1,7 @@ import type { FastifyReply, FastifyRequest } from "fastify"; import type { MercuriusContext } from "mercurius"; -import { wrapResponse } from "supertokens-node/framework/fastify"; -import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; -import Session from "supertokens-node/recipe/session"; - -import ProfileValidationClaim from "./supertokens/utils/profileValidationClaim"; +import { auth } from "./auth/adapter"; const userContext = async ( context: MercuriusContext, @@ -13,18 +9,12 @@ const userContext = async ( reply: FastifyReply, ) => { try { - request.session = (await Session.getSession(request, wrapResponse(reply), { - overrideGlobalClaimValidators: async (globalValidators) => - globalValidators.filter( - (sessionClaimValidator) => - ![EmailVerificationClaim.key, ProfileValidationClaim.key].includes( - sessionClaimValidator.id, - ), - ), + request.session = (await auth.session.getSession(request, reply, { sessionRequired: false, + skipClaims: ["emailVerification", "profileValidation"], })) as (typeof request)["session"]; } catch (error) { - if (!Session.Error.isErrorFromSuperTokens(error)) { + if (!auth.errors.isAuthError(error)) { throw error; } } From d2bb9ee7d30f773b1cd246e063044d2cc160f1cd Mon Sep 17 00:00:00 2001 From: kabin thakuri Date: Fri, 29 May 2026 18:44:09 +0545 Subject: [PATCH 05/11] chore(auth): decouple supertokens dependencies and implement auth adapter --- .../user/src/mercurius-auth/authPlugin.ts | 55 +++-- .../__test__/hasPermission.spec.ts | 37 ++- .../user/src/middlewares/hasPermission.ts | 32 +-- .../user/src/model/invitations/controller.ts | 31 ++- .../src/model/invitations/graphql/resolver.ts | 27 ++- .../user/src/model/invitations/service.ts | 4 +- .../user/src/model/permissions/controller.ts | 10 +- packages/user/src/model/roles/controller.ts | 26 +- packages/user/src/model/users/controller.ts | 145 +++++------ .../user/src/model/users/graphql/resolver.ts | 226 ++++++++++-------- 10 files changed, 325 insertions(+), 268 deletions(-) diff --git a/packages/user/src/mercurius-auth/authPlugin.ts b/packages/user/src/mercurius-auth/authPlugin.ts index 5b91a5662..9b01f59fb 100644 --- a/packages/user/src/mercurius-auth/authPlugin.ts +++ b/packages/user/src/mercurius-auth/authPlugin.ts @@ -1,13 +1,12 @@ -import type { FastifyInstance } from "fastify"; +import type { FastifyInstance, FastifyRequest } from "fastify"; import FastifyPlugin from "fastify-plugin"; import { mercurius } from "mercurius"; import mercuriusAuth from "mercurius-auth"; -import emailVerificationRecipe from "supertokens-node/recipe/emailverification"; -import { Error } from "supertokens-node/recipe/session"; -import createUserContext from "../supertokens/utils/createUserContext"; -import ProfileValidationClaim from "../supertokens/utils/profileValidationClaim"; +import type { AuthSession } from "../auth/adapter"; + +import { auth } from "../auth/adapter"; const plugin = FastifyPlugin(async (fastify: FastifyInstance) => { await fastify.register(mercuriusAuth, { @@ -20,7 +19,10 @@ const plugin = FastifyPlugin(async (fastify: FastifyInstance) => { return new mercurius.ErrorWithProps("user is disabled", {}, 401); } - if (fastify.config.user.features?.signUp?.emailVerification) { + if ( + fastify.config.user.features?.signUp?.emailVerification && + auth.emailVerification + ) { const emailVerification = authDirectiveAST.arguments.find( (argument: { name: { value: string } }) => argument?.name?.value === "emailVerification", @@ -28,16 +30,14 @@ const plugin = FastifyPlugin(async (fastify: FastifyInstance) => { if ( emailVerification?.value?.value !== false && - !(await emailVerificationRecipe.isEmailVerified(context.user.id)) + !(await auth.emailVerification.isEmailVerified(context.user.id)) ) { - // Added the claim validation errors to match with rest endpoint - // response for email verification return new mercurius.ErrorWithProps( "invalid claim", { claimValidationErrors: [ { - id: "st-ev", + id: auth.claims.keys.emailVerification, reason: { actualValue: false, expectedValue: true, @@ -59,30 +59,41 @@ const plugin = FastifyPlugin(async (fastify: FastifyInstance) => { if (profileValidation?.value?.value != false) { const request = context.reply.request; + const session = (request as FastifyRequest & { session: AuthSession }) + .session; - const profileValidationClaim = new ProfileValidationClaim(); - - const userContext = createUserContext( - undefined, - context.reply.request, - ); + const userContext = auth.createUserContext(request); - await request.session?.fetchAndSetClaim( - profileValidationClaim, + await auth.claims.refreshSessionClaims( + session, + request, + ["profileValidation"], userContext, ); try { - await request.session?.assertClaims( - [profileValidationClaim.validators.isVerified()], + const errors = await auth.claims.assertProfileValid( + session, + request, userContext, ); + + if (errors && errors.length > 0) { + return new mercurius.ErrorWithProps( + "invalid claim", + { + claimValidationErrors: errors, + }, + 403, + ); + } } catch (error) { - if (error instanceof Error) { + if (auth.errors.isAuthError(error)) { return new mercurius.ErrorWithProps( "invalid claim", { - claimValidationErrors: error.payload, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + claimValidationErrors: (error as any).payload, }, 403, ); diff --git a/packages/user/src/middlewares/__test__/hasPermission.spec.ts b/packages/user/src/middlewares/__test__/hasPermission.spec.ts index 515fe209d..0fbca374f 100644 --- a/packages/user/src/middlewares/__test__/hasPermission.spec.ts +++ b/packages/user/src/middlewares/__test__/hasPermission.spec.ts @@ -1,12 +1,24 @@ -import type { SessionRequest } from "supertokens-node/framework/fastify"; - -import Fastify, { type FastifyInstance } from "fastify"; -import { Error as STError } from "supertokens-node/recipe/session"; +import Fastify, { type FastifyInstance, type FastifyRequest } from "fastify"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import hasPermission from "../hasPermission"; -const { mockHasUserPermission } = vi.hoisted(() => ({ +const { + mockCreateInvalidClaimsError, + mockCreateUnauthorizedError, + mockHasUserPermission, +} = vi.hoisted(() => ({ + mockCreateInvalidClaimsError: vi.fn((errors: unknown[]) => { + const err = new Error("Not have enough permission"); + (err as Record).type = "INVALID_CLAIMS"; + (err as Record).payload = errors; + return err; + }), + mockCreateUnauthorizedError: vi.fn((message?: string) => { + const err = new Error(message); + (err as Record).type = "UNAUTHORISED"; + return err; + }), mockHasUserPermission: vi.fn(), })); @@ -16,6 +28,10 @@ vi.mock("../../lib/hasUserPermission", () => ({ vi.mock("../../auth/adapter", () => ({ auth: { + errors: { + createInvalidClaimsError: mockCreateInvalidClaimsError, + createUnauthorizedError: mockCreateUnauthorizedError, + }, roles: { PermissionClaim: { key: "st-role.permissions", @@ -27,11 +43,11 @@ vi.mock("../../auth/adapter", () => ({ const buildRequest = ( fastify: FastifyInstance, user?: { id: string }, -): SessionRequest => { +): FastifyRequest => { return { server: fastify, user, - } as unknown as SessionRequest; + } as FastifyRequest; }; describe("hasPermission middleware", () => { @@ -91,12 +107,15 @@ describe("hasPermission middleware", () => { expect(mockHasUserPermission.mock.calls[0]?.[2]).toBe("users:read"); }); - it("throws SuperTokens errors for unauthorized outcomes", async () => { + it("throws auth errors for unauthorized outcomes", async () => { mockHasUserPermission.mockResolvedValue(false); const preHandler = hasPermission("roles:update"); await expect( preHandler(buildRequest(fastify, { id: "user-7" })), - ).rejects.toBeInstanceOf(STError); + ).rejects.toMatchObject({ + message: "Not have enough permission", + type: "INVALID_CLAIMS", + }); }); }); diff --git a/packages/user/src/middlewares/hasPermission.ts b/packages/user/src/middlewares/hasPermission.ts index 534289a77..7be31e823 100644 --- a/packages/user/src/middlewares/hasPermission.ts +++ b/packages/user/src/middlewares/hasPermission.ts @@ -1,37 +1,27 @@ -import type { SessionRequest } from "supertokens-node/framework/fastify"; - -import { Error as STError } from "supertokens-node/recipe/session"; +import type { FastifyRequest } from "fastify"; import { auth } from "../auth/adapter"; import hasUserPermission from "../lib/hasUserPermission"; const hasPermission = (permission: string) => - async (request: SessionRequest): Promise => { + async (request: FastifyRequest): Promise => { const user = request.user; if (!user) { - throw new STError({ - message: "unauthorised", - type: "UNAUTHORISED", - }); + throw auth.errors.createUnauthorizedError("unauthorised"); } if (!(await hasUserPermission(request.server, user.id, permission))) { - // this error tells SuperTokens to return a 403 http response. - throw new STError({ - message: "Not have enough permission", - payload: [ - { - id: auth.roles.PermissionClaim?.key || "st-role.permissions", - reason: { - expectedToInclude: permission, - message: "Not have enough permission", - }, + throw auth.errors.createInvalidClaimsError([ + { + id: auth.roles.PermissionClaim?.key || "st-role.permissions", + reason: { + expectedToInclude: permission, + message: "Not have enough permission", }, - ], - type: "INVALID_CLAIMS", - }); + }, + ]); } }; diff --git a/packages/user/src/model/invitations/controller.ts b/packages/user/src/model/invitations/controller.ts index 14795e16c..9b133f1f4 100644 --- a/packages/user/src/model/invitations/controller.ts +++ b/packages/user/src/model/invitations/controller.ts @@ -1,4 +1,8 @@ -import type { FastifyInstance } from "fastify"; +import type { + FastifyInstance, + RouteHandler, + RouteShorthandOptions, +} from "fastify"; import { PERMISSIONS_INVITATIONS_CREATE, @@ -36,8 +40,9 @@ const plugin = async (fastify: FastifyInstance) => { fastify.hasPermission(PERMISSIONS_INVITATIONS_LIST), ], schema: getInvitationsListSchema, - }, - handlersConfig?.list || handlers.listInvitation, + } as unknown as RouteShorthandOptions, + (handlersConfig?.list || + handlers.listInvitation) as unknown as RouteHandler, ); fastify.post( @@ -48,8 +53,9 @@ const plugin = async (fastify: FastifyInstance) => { fastify.hasPermission(PERMISSIONS_INVITATIONS_CREATE), ], schema: createInvitationSchema, - }, - handlersConfig?.create || handlers.createInvitation, + } as unknown as RouteShorthandOptions, + (handlersConfig?.create || + handlers.createInvitation) as unknown as RouteHandler, ); fastify.get( @@ -76,8 +82,9 @@ const plugin = async (fastify: FastifyInstance) => { fastify.hasPermission(PERMISSIONS_INVITATIONS_REVOKE), ], schema: revokeInvitationSchema, - }, - handlersConfig?.revoke || handlers.revokeInvitation, + } as unknown as RouteShorthandOptions, + (handlersConfig?.revoke || + handlers.revokeInvitation) as unknown as RouteHandler, ); fastify.post( @@ -88,8 +95,9 @@ const plugin = async (fastify: FastifyInstance) => { fastify.hasPermission(PERMISSIONS_INVITATIONS_RESEND), ], schema: resendInvitationSchema, - }, - handlersConfig?.resend || handlers.resendInvitation, + } as unknown as RouteShorthandOptions, + (handlersConfig?.resend || + handlers.resendInvitation) as unknown as RouteHandler, ); fastify.delete( @@ -100,8 +108,9 @@ const plugin = async (fastify: FastifyInstance) => { fastify.hasPermission(PERMISSIONS_INVITATIONS_DELETE), ], schema: deleteInvitationSchema, - }, - handlersConfig?.delete || handlers.deleteInvitation, + } as unknown as RouteShorthandOptions, + (handlersConfig?.delete || + handlers.deleteInvitation) as unknown as RouteHandler, ); }; diff --git a/packages/user/src/model/invitations/graphql/resolver.ts b/packages/user/src/model/invitations/graphql/resolver.ts index 876bd49ed..5ff279ac8 100644 --- a/packages/user/src/model/invitations/graphql/resolver.ts +++ b/packages/user/src/model/invitations/graphql/resolver.ts @@ -3,8 +3,6 @@ import type { MercuriusContext } from "mercurius"; import { formatDate } from "@prefabs.tech/fastify-slonik"; import { mercurius } from "mercurius"; -import { createNewSession } from "supertokens-node/recipe/session"; -import { emailPasswordSignUp } from "supertokens-node/recipe/thirdpartyemailpassword"; import type { User } from "../../../types"; import type { @@ -12,6 +10,7 @@ import type { InvitationCreateInput, } from "../../../types/invitation"; +import { auth } from "../../../auth/adapter"; import getInvitationService from "../../../lib/getInvitationService"; import isInvitationValid from "../../../lib/isInvitationValid"; import sendInvitation from "../../../lib/sendInvitation"; @@ -82,13 +81,17 @@ const Mutation = { } // signup - const signUpResponse = await emailPasswordSignUp(email, password, { - autoVerifyEmail: true, - roles: [invitation.role], - }); + const signUpResponse = await auth.emailPassword.emailPasswordSignUp( + email, + password, + { + autoVerifyEmail: true, + roles: [invitation.role], + }, + ); - if (signUpResponse.status !== "OK") { - return signUpResponse; + if (!signUpResponse.success) { + return { status: signUpResponse.error, user: undefined }; } // update invitation's acceptedAt value with current time @@ -108,10 +111,14 @@ const Mutation = { } // create new session so the user be logged in on signup - await createNewSession(reply.request, reply, signUpResponse.user.id); + await auth.session.createNewSession( + reply.request, + reply, + signUpResponse.user.id, + ); return { - ...signUpResponse, + status: "OK", user: { ...signUpResponse.user, roles: [invitation.role], diff --git a/packages/user/src/model/invitations/service.ts b/packages/user/src/model/invitations/service.ts index 8f07ee624..d5bb8208f 100644 --- a/packages/user/src/model/invitations/service.ts +++ b/packages/user/src/model/invitations/service.ts @@ -9,10 +9,10 @@ import type { InvitationUpdateInput, } from "../../types"; +import { auth } from "../../auth/adapter"; import { ERROR_CODES } from "../../constants"; import computeInvitationExpiresAt from "../../lib/computeInvitationExpiresAt"; import getUserService from "../../lib/getUserService"; -import areRolesExist from "../../supertokens/utils/areRolesExist"; import validateEmail from "../../validator/email"; import InvitationSqlFactory from "./sqlFactory"; @@ -78,7 +78,7 @@ class InvitationService extends BaseService< ); } - if (!(await areRolesExist([role]))) { + if (!(await auth.roles.rolesExist([role]))) { throw new CustomError( `Role "${role}" does not exist`, ERROR_CODES.ROLE_NOT_FOUND, diff --git a/packages/user/src/model/permissions/controller.ts b/packages/user/src/model/permissions/controller.ts index f7bd416fd..ef1f82102 100644 --- a/packages/user/src/model/permissions/controller.ts +++ b/packages/user/src/model/permissions/controller.ts @@ -1,4 +1,8 @@ -import type { FastifyInstance } from "fastify"; +import type { + FastifyInstance, + RouteHandler, + RouteShorthandOptions, +} from "fastify"; import { ROUTE_PERMISSIONS } from "../../constants"; import handlers from "./handlers"; @@ -10,8 +14,8 @@ const plugin = async (fastify: FastifyInstance) => { { preHandler: [fastify.verifySession()], schema: getPermissionsSchema, - }, - handlers.getPermissions, + } as unknown as RouteShorthandOptions, + handlers.getPermissions as unknown as RouteHandler, ); }; diff --git a/packages/user/src/model/roles/controller.ts b/packages/user/src/model/roles/controller.ts index b41971f49..ab1400369 100644 --- a/packages/user/src/model/roles/controller.ts +++ b/packages/user/src/model/roles/controller.ts @@ -1,4 +1,8 @@ -import type { FastifyInstance } from "fastify"; +import type { + FastifyInstance, + RouteHandler, + RouteShorthandOptions, +} from "fastify"; import { ROUTE_ROLES, ROUTE_ROLES_PERMISSIONS } from "../../constants"; import handlers from "./handlers"; @@ -16,8 +20,8 @@ const plugin = async (fastify: FastifyInstance) => { { preHandler: [fastify.verifySession()], schema: deleteRoleSchema, - }, - handlers.deleteRole, + } as unknown as RouteShorthandOptions, + handlers.deleteRole as unknown as RouteHandler, ); fastify.get( @@ -25,8 +29,8 @@ const plugin = async (fastify: FastifyInstance) => { { preHandler: [fastify.verifySession()], schema: getRolesSchema, - }, - handlers.getRoles, + } as unknown as RouteShorthandOptions, + handlers.getRoles as unknown as RouteHandler, ); fastify.get( @@ -34,8 +38,8 @@ const plugin = async (fastify: FastifyInstance) => { { preHandler: [fastify.verifySession()], schema: getRolePermissionsSchema, - }, - handlers.getPermissions, + } as unknown as RouteShorthandOptions, + handlers.getPermissions as unknown as RouteHandler, ); fastify.post( @@ -43,8 +47,8 @@ const plugin = async (fastify: FastifyInstance) => { { preHandler: [fastify.verifySession()], schema: createRoleSchema, - }, - handlers.createRole, + } as unknown as RouteShorthandOptions, + handlers.createRole as unknown as RouteHandler, ); fastify.put( @@ -52,8 +56,8 @@ const plugin = async (fastify: FastifyInstance) => { { preHandler: [fastify.verifySession()], schema: updateRoleSchema, - }, - handlers.updatePermissions, + } as unknown as RouteShorthandOptions, + handlers.updatePermissions as unknown as RouteHandler, ); }; diff --git a/packages/user/src/model/users/controller.ts b/packages/user/src/model/users/controller.ts index 48f81e186..4be51a7ec 100644 --- a/packages/user/src/model/users/controller.ts +++ b/packages/user/src/model/users/controller.ts @@ -1,7 +1,10 @@ -import type { FastifyInstance } from "fastify"; - -import { EmailVerificationClaim } from "supertokens-node/recipe/emailverification"; +import type { + FastifyInstance, + RouteHandler, + RouteShorthandOptions, +} from "fastify"; +import { auth } from "../../auth/adapter"; import { PERMISSIONS_USERS_DISABLE, PERMISSIONS_USERS_ENABLE, @@ -17,7 +20,6 @@ import { ROUTE_USERS_ENABLE, ROUTE_USERS_FIND_BY_ID, } from "../../constants"; -import ProfileValidationClaim from "../../supertokens/utils/profileValidationClaim"; import handlers from "./handlers"; import { adminSignUpSchema, @@ -46,8 +48,8 @@ const plugin = async (fastify: FastifyInstance) => { fastify.hasPermission(PERMISSIONS_USERS_LIST), ], schema: getUsersSchema, - }, - handlersConfig?.users || handlers.users, + } as unknown as RouteShorthandOptions, + (handlersConfig?.users || handlers.users) as unknown as RouteHandler, ); fastify.get( @@ -58,8 +60,8 @@ const plugin = async (fastify: FastifyInstance) => { fastify.hasPermission(PERMISSIONS_USERS_READ), ], schema: getUserSchema, - }, - handlersConfig?.user || handlers.user, + } as unknown as RouteShorthandOptions, + (handlersConfig?.user || handlers.user) as unknown as RouteHandler, ); fastify.post( @@ -67,113 +69,90 @@ const plugin = async (fastify: FastifyInstance) => { { preHandler: fastify.verifySession(), schema: changePasswordSchema, - }, - handlersConfig?.changePassword || handlers.changePassword, + } as unknown as RouteShorthandOptions, + (handlersConfig?.changePassword || + handlers.changePassword) as unknown as RouteHandler, ); fastify.post( ROUTE_CHANGE_EMAIL, { - preHandler: fastify.verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => - globalValidators.filter( - (sessionClaimValidator) => - ![ - EmailVerificationClaim.key, - ProfileValidationClaim.key, - ].includes(sessionClaimValidator.id), - ), - }), + preHandler: fastify.verifySession( + auth.claims.verifySessionOptions([ + "emailVerification", + "profileValidation", + ]), + ), schema: changeEmailSchema, - }, - handlers.changeEmail, + } as unknown as RouteShorthandOptions, + handlers.changeEmail as unknown as RouteHandler, ); fastify.get( ROUTE_ME, { - preHandler: fastify.verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => - globalValidators.filter( - (sessionClaimValidator) => - ![ - EmailVerificationClaim.key, - ProfileValidationClaim.key, - ].includes(sessionClaimValidator.id), - ), - }), + preHandler: fastify.verifySession( + auth.claims.verifySessionOptions([ + "emailVerification", + "profileValidation", + ]), + ), schema: getMeSchema, - }, - handlersConfig?.me || handlers.me, + } as unknown as RouteShorthandOptions, + (handlersConfig?.me || handlers.me) as unknown as RouteHandler, ); fastify.put( ROUTE_ME, { - preHandler: fastify.verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => - globalValidators.filter( - (sessionClaimValidator) => - ![ - EmailVerificationClaim.key, - ProfileValidationClaim.key, - ].includes(sessionClaimValidator.id), - ), - }), + preHandler: fastify.verifySession( + auth.claims.verifySessionOptions([ + "emailVerification", + "profileValidation", + ]), + ), schema: updateMeSchema, - }, - handlersConfig?.updateMe || handlers.updateMe, + } as unknown as RouteShorthandOptions, + (handlersConfig?.updateMe || handlers.updateMe) as unknown as RouteHandler, ); fastify.delete( ROUTE_ME, { - preHandler: fastify.verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => - globalValidators.filter( - (sessionClaimValidator) => - sessionClaimValidator.id !== ProfileValidationClaim.key, - ), - }), + preHandler: fastify.verifySession( + auth.claims.verifySessionOptions(["profileValidation"]), + ), schema: deleteMeSchema, - }, - handlersConfig?.deleteMe || handlers.deleteMe, + } as unknown as RouteShorthandOptions, + (handlersConfig?.deleteMe || handlers.deleteMe) as unknown as RouteHandler, ); fastify.put( ROUTE_ME_PHOTO, { - preHandler: fastify.verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => - globalValidators.filter( - (sessionClaimValidator) => - ![ - EmailVerificationClaim.key, - ProfileValidationClaim.key, - ].includes(sessionClaimValidator.id), - ), - }), + preHandler: fastify.verifySession( + auth.claims.verifySessionOptions([ + "emailVerification", + "profileValidation", + ]), + ), schema: uploadPhotoSchema, - }, - handlers.uploadPhoto, + } as unknown as RouteShorthandOptions, + handlers.uploadPhoto as unknown as RouteHandler, ); fastify.delete( ROUTE_ME_PHOTO, { - preHandler: fastify.verifySession({ - overrideGlobalClaimValidators: async (globalValidators) => - globalValidators.filter( - (sessionClaimValidator) => - ![ - EmailVerificationClaim.key, - ProfileValidationClaim.key, - ].includes(sessionClaimValidator.id), - ), - }), + preHandler: fastify.verifySession( + auth.claims.verifySessionOptions([ + "emailVerification", + "profileValidation", + ]), + ), schema: removePhotoSchema, - }, - handlers.removePhoto, + } as unknown as RouteShorthandOptions, + handlers.removePhoto as unknown as RouteHandler, ); fastify.put( @@ -184,8 +163,8 @@ const plugin = async (fastify: FastifyInstance) => { fastify.hasPermission(PERMISSIONS_USERS_DISABLE), ], schema: disableUserSchema, - }, - handlersConfig?.disable || handlers.disable, + } as unknown as RouteShorthandOptions, + (handlersConfig?.disable || handlers.disable) as unknown as RouteHandler, ); fastify.put( @@ -196,8 +175,8 @@ const plugin = async (fastify: FastifyInstance) => { fastify.hasPermission(PERMISSIONS_USERS_ENABLE), ], schema: enableUserSchema, - }, - handlersConfig?.enable || handlers.enable, + } as unknown as RouteShorthandOptions, + (handlersConfig?.enable || handlers.enable) as unknown as RouteHandler, ); fastify.post( diff --git a/packages/user/src/model/users/graphql/resolver.ts b/packages/user/src/model/users/graphql/resolver.ts index 452d19ff8..189c39227 100644 --- a/packages/user/src/model/users/graphql/resolver.ts +++ b/packages/user/src/model/users/graphql/resolver.ts @@ -1,26 +1,17 @@ import type { FilterInput, SortInput } from "@prefabs.tech/fastify-slonik"; +import type { FastifyRequest } from "fastify"; import type { MercuriusContext } from "mercurius"; import { GraphQLUpload, Multipart } from "@prefabs.tech/fastify-s3"; import { mercurius } from "mercurius"; -import EmailVerification, { - EmailVerificationClaim, - isEmailVerified, -} from "supertokens-node/recipe/emailverification"; -import { createNewSession } from "supertokens-node/recipe/session"; -import { - emailPasswordSignUp, - getUsersByEmail, -} from "supertokens-node/recipe/thirdpartyemailpassword"; -import UserRoles from "supertokens-node/recipe/userroles"; +import type { AuthSession } from "../../../auth/adapter"; import type { UserUpdateInput } from "../../../types"; +import { auth } from "../../../auth/adapter"; import { ROLE_ADMIN, ROLE_SUPERADMIN } from "../../../constants"; import CustomApiError from "../../../customApiError"; import getUserService from "../../../lib/getUserService"; -import createUserContext from "../../../supertokens/utils/createUserContext"; -import ProfileValidationClaim from "../../../supertokens/utils/profileValidationClaim"; import validateEmail from "../../../validator/email"; import validatePassword from "../../../validator/password"; import filterUserUpdateInput from "../filterUserUpdateInput"; @@ -42,21 +33,22 @@ const Mutation = { const { email, password } = arguments_.data; // check if already admin user exists - const adminUsers = await UserRoles.getUsersThatHaveRole(ROLE_ADMIN); + const adminUsers = await auth.roles.getUsersThatHaveRole(ROLE_ADMIN); const superAdminUsers = - await UserRoles.getUsersThatHaveRole(ROLE_SUPERADMIN); + await auth.roles.getUsersThatHaveRole(ROLE_SUPERADMIN); let errorMessage: string | undefined; - if ( - adminUsers.status === "UNKNOWN_ROLE_ERROR" && - superAdminUsers.status === "UNKNOWN_ROLE_ERROR" - ) { - errorMessage = adminUsers.status; - } else if ( - (adminUsers.status === "OK" && adminUsers.users.length > 0) || - (superAdminUsers.status === "OK" && superAdminUsers.users.length > 0) - ) { + if (adminUsers.length === 0 && superAdminUsers.length === 0) { + const allRoles = await auth.roles.getAllRoles(); + + if ( + !allRoles.includes(ROLE_ADMIN) && + !allRoles.includes(ROLE_SUPERADMIN) + ) { + errorMessage = "UNKNOWN_ROLE_ERROR"; + } + } else if (adminUsers.length > 0 || superAdminUsers.length > 0) { errorMessage = "First admin user already exists"; } @@ -89,29 +81,32 @@ const Mutation = { } // signup - const signUpResponse = await emailPasswordSignUp(email, password, { - _default: { - request: { - request: reply.request, - }, + const signUpResponse = await auth.emailPassword.emailPasswordSignUp( + email, + password, + { + autoVerifyEmail: true, + roles: [ + ROLE_ADMIN, + ...(superAdminUsers.length > 0 ? [ROLE_SUPERADMIN] : []), + ], }, - autoVerifyEmail: true, - roles: [ - ROLE_ADMIN, - ...(superAdminUsers.status === "OK" ? [ROLE_SUPERADMIN] : []), - ], - }); + ); - if (signUpResponse.status !== "OK") { + if (!signUpResponse.success) { const mercuriusError = new mercurius.ErrorWithProps( - signUpResponse.status, + signUpResponse.error || "UNKNOWN_ERROR", ); return mercuriusError; } // create new session so the user be logged in on signup - await createNewSession(reply.request, reply, signUpResponse.user.id); + await auth.session.createNewSession( + reply.request, + reply, + signUpResponse.user.id, + ); return signUpResponse; } catch (error) { @@ -143,18 +138,25 @@ const Mutation = { } const request = reply.request; + const session = (request as FastifyRequest & { session: AuthSession }) + .session; + const userContext = auth.createUserContext(request); if (config.user.features?.profileValidation?.enabled) { - await request.session?.fetchAndSetClaim( - new ProfileValidationClaim(), - createUserContext(undefined, request), + await auth.claims.refreshSessionClaims( + session, + request, + ["profileValidation"], + userContext, ); } if (config.user.features?.signUp?.emailVerification) { - await request.session?.fetchAndSetClaim( - EmailVerificationClaim, - createUserContext(undefined, request), + await auth.claims.refreshSessionClaims( + session, + request, + ["emailVerification"], + userContext, ); } @@ -168,41 +170,42 @@ const Mutation = { return new mercurius.ErrorWithProps("EMAIL_SAME_AS_CURRENT_ERROR"); } - if (config.user.features?.signUp?.emailVerification) { - const isVerified = await isEmailVerified(user.id, arguments_.email); + if ( + config.user.features?.signUp?.emailVerification && + auth.emailVerification + ) { + const isVerified = await auth.emailVerification.isEmailVerified( + user.id, + arguments_.email, + ); if (!isVerified) { - const users = await getUsersByEmail(arguments_.email); + const users = + (await auth.emailPassword.getUsersByEmail?.(arguments_.email)) || + []; const emailPasswordRecipeUsers = users.filter( - (user) => !user.thirdParty, + (user) => !(user as Record).thirdParty, ); if (emailPasswordRecipeUsers.length > 0) { return new mercurius.ErrorWithProps("EMAIL_ALREADY_EXISTS_ERROR"); } - const tokenResponse = - await EmailVerification.createEmailVerificationToken( + const token = + await auth.emailVerification.createEmailVerificationToken( user.id, arguments_.email, + userContext, ); - if (tokenResponse.status === "OK") { - await EmailVerification.sendEmail({ - emailVerifyLink: `${config.appOrigin[0]}/auth/verify-email?token=${tokenResponse.token}&rid=emailverification`, - type: "EMAIL_VERIFICATION", - user: { - email: arguments_.email, - id: user.id, - }, - userContext: { - _default: { - request: { - request: request, - }, - }, - }, + if (token) { + await auth.emailVerification.sendVerificationEmail?.({ + appOrigin: config.appOrigin[0] as string, + email: arguments_.email, + token, + userContext, + userId: user.id, }); return { @@ -211,7 +214,9 @@ const Mutation = { }; } - return new mercurius.ErrorWithProps(tokenResponse.status); + return new mercurius.ErrorWithProps( + "EMAIL_VERIFICATION_TOKEN_FAILED", + ); } } @@ -267,7 +272,7 @@ const Mutation = { ); if (response.status === "OK") { - await createNewSession(reply.request, reply, user.id); + await auth.session.createNewSession(reply.request, reply, user.id); } return response; @@ -405,17 +410,25 @@ const Mutation = { request.user = updatedUser; + const userContext = auth.createUserContext(request); + const session = (request as FastifyRequest & { session: AuthSession }) + .session; + if (request.config.user.features?.profileValidation?.enabled) { - await request.session?.fetchAndSetClaim( - new ProfileValidationClaim(), - createUserContext(undefined, request), + await auth.claims.refreshSessionClaims( + session, + request, + ["profileValidation"], + userContext, ); } if (request.config.user.features?.signUp?.emailVerification) { - await request.session?.fetchAndSetClaim( - EmailVerificationClaim, - createUserContext(undefined, request), + await auth.claims.refreshSessionClaims( + session, + request, + ["emailVerification"], + userContext, ); } @@ -453,13 +466,17 @@ const Mutation = { const updatedUser = await service.update(user.id, data); const request = reply.request; + const session = (request as FastifyRequest & { session: AuthSession }) + .session; request.user = updatedUser; if (config.user.features?.profileValidation?.enabled) { - await request.session?.fetchAndSetClaim( - new ProfileValidationClaim(), - createUserContext(undefined, request), + await auth.claims.refreshSessionClaims( + session, + request, + ["profileValidation"], + auth.createUserContext(request), ); } @@ -525,17 +542,25 @@ const Mutation = { request.user = updatedUser; + const userContext = auth.createUserContext(request); + const session = (request as FastifyRequest & { session: AuthSession }) + .session; + if (request.config.user.features?.profileValidation?.enabled) { - await request.session?.fetchAndSetClaim( - new ProfileValidationClaim(), - createUserContext(undefined, request), + await auth.claims.refreshSessionClaims( + session, + request, + ["profileValidation"], + userContext, ); } if (request.config.user.features?.signUp?.emailVerification) { - await request.session?.fetchAndSetClaim( - EmailVerificationClaim, - createUserContext(undefined, request), + await auth.claims.refreshSessionClaims( + session, + request, + ["emailVerification"], + userContext, ); } @@ -571,21 +596,26 @@ const Query = { try { // check if already admin user exists - const adminUsers = await UserRoles.getUsersThatHaveRole(ROLE_ADMIN); + const adminUsers = await auth.roles.getUsersThatHaveRole(ROLE_ADMIN); const superAdminUsers = - await UserRoles.getUsersThatHaveRole(ROLE_SUPERADMIN); + await auth.roles.getUsersThatHaveRole(ROLE_SUPERADMIN); - if ( - adminUsers.status === "UNKNOWN_ROLE_ERROR" && - superAdminUsers.status === "UNKNOWN_ROLE_ERROR" - ) { - const mercuriusError = new mercurius.ErrorWithProps(adminUsers.status); + if (adminUsers.length === 0 && superAdminUsers.length === 0) { + const allRoles = await auth.roles.getAllRoles(); - return mercuriusError; - } else if ( - (adminUsers.status === "OK" && adminUsers.users.length > 0) || - (superAdminUsers.status === "OK" && superAdminUsers.users.length > 0) - ) { + if ( + !allRoles.includes(ROLE_ADMIN) && + !allRoles.includes(ROLE_SUPERADMIN) + ) { + const mercuriusError = new mercurius.ErrorWithProps( + "UNKNOWN_ROLE_ERROR", + ); + + return mercuriusError; + } + } + + if (adminUsers.length > 0 || superAdminUsers.length > 0) { return { signUp: false }; } @@ -639,9 +669,13 @@ const Query = { if (context.config.user.features?.profileValidation?.enabled) { const request = context.reply.request; - await request.session?.fetchAndSetClaim( - new ProfileValidationClaim(), - createUserContext(undefined, request), + const session = (request as FastifyRequest & { session: AuthSession }) + .session; + await auth.claims.refreshSessionClaims( + session, + request, + ["profileValidation"], + auth.createUserContext(request), ); } From 3fb9e6b97e4e1affa7d37df47daf401f8e04abb7 Mon Sep 17 00:00:00 2001 From: kabin thakuri Date: Mon, 1 Jun 2026 13:43:48 +0545 Subject: [PATCH 06/11] refactor(user/auth): abstract verifySession into SessionProvider interface --- packages/user/src/auth/adapter.ts | 15 ++++-- packages/user/src/auth/supertokens.ts | 35 +++++++------- packages/user/src/model/users/controller.ts | 51 ++++++++------------- packages/user/src/supertokens/index.ts | 11 ++++- packages/user/src/supertokens/plugin.ts | 6 ++- 5 files changed, 59 insertions(+), 59 deletions(-) diff --git a/packages/user/src/auth/adapter.ts b/packages/user/src/auth/adapter.ts index 7b9889059..017b2bf98 100644 --- a/packages/user/src/auth/adapter.ts +++ b/packages/user/src/auth/adapter.ts @@ -80,9 +80,6 @@ export interface ClaimsProvider { claims: RefreshableClaim[], userContext?: AuthUserContext, ): Promise; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - verifySessionOptions(skip: RefreshableClaim[]): any; } export interface EmailPasswordProvider { @@ -194,6 +191,18 @@ export interface SessionProvider { options?: GetSessionOptions, ): Promise; + /** + * Returns a Fastify preHandler middleware that validates the session + * and attaches it to the request. Abstracts provider-specific session + * verification (e.g. SuperTokens claims validation). + */ + getVerifySession( + options?: GetSessionOptions, + ): ( + request: FastifyRequest, + reply: FastifyReply, + ) => Promise; + revokeAllSessionsForUser( userId: string, userContext?: AuthUserContext, diff --git a/packages/user/src/auth/supertokens.ts b/packages/user/src/auth/supertokens.ts index 4ad58a305..965de38ac 100644 --- a/packages/user/src/auth/supertokens.ts +++ b/packages/user/src/auth/supertokens.ts @@ -79,14 +79,6 @@ const supertokensClaimsAdapter: ClaimsProvider = { } } }, - - verifySessionOptions(skip: RefreshableClaim[]) { - return { - overrideGlobalClaimValidators: async ( - globalValidators: T[], - ) => supertokensClaimsAdapter.excludeValidatorIds(globalValidators, skip), - }; - }, }; const supertokensErrorsAdapter: AuthErrorsProvider = { @@ -393,19 +385,24 @@ const supertokensSessionAdapter: SessionProvider = { }, async getSession(request, reply, options): Promise { + return supertokensSessionAdapter.getVerifySession(options)(request, reply); + }, + + getVerifySession(options) { const skipClaims = options?.skipClaims; - return Session.getSession(request, wrapResponse(reply), { - checkDatabase: options?.checkDatabase, - overrideGlobalClaimValidators: skipClaims?.length - ? async (globalValidators) => - supertokensClaimsAdapter.excludeValidatorIds( - globalValidators, - skipClaims, - ) - : undefined, - sessionRequired: options?.sessionRequired, - }) as unknown as AuthSession | undefined; + return (request, reply) => + Session.getSession(request, wrapResponse(reply), { + checkDatabase: options?.checkDatabase, + overrideGlobalClaimValidators: skipClaims?.length + ? async (globalValidators) => + supertokensClaimsAdapter.excludeValidatorIds( + globalValidators, + skipClaims, + ) + : undefined, + sessionRequired: options?.sessionRequired, + }) as unknown as Promise; }, async revokeAllSessionsForUser(userId: string): Promise { diff --git a/packages/user/src/model/users/controller.ts b/packages/user/src/model/users/controller.ts index 4be51a7ec..c26a2d36f 100644 --- a/packages/user/src/model/users/controller.ts +++ b/packages/user/src/model/users/controller.ts @@ -77,12 +77,9 @@ const plugin = async (fastify: FastifyInstance) => { fastify.post( ROUTE_CHANGE_EMAIL, { - preHandler: fastify.verifySession( - auth.claims.verifySessionOptions([ - "emailVerification", - "profileValidation", - ]), - ), + preHandler: auth.session.getVerifySession({ + skipClaims: ["emailVerification", "profileValidation"], + }), schema: changeEmailSchema, } as unknown as RouteShorthandOptions, handlers.changeEmail as unknown as RouteHandler, @@ -91,12 +88,9 @@ const plugin = async (fastify: FastifyInstance) => { fastify.get( ROUTE_ME, { - preHandler: fastify.verifySession( - auth.claims.verifySessionOptions([ - "emailVerification", - "profileValidation", - ]), - ), + preHandler: auth.session.getVerifySession({ + skipClaims: ["emailVerification", "profileValidation"], + }), schema: getMeSchema, } as unknown as RouteShorthandOptions, (handlersConfig?.me || handlers.me) as unknown as RouteHandler, @@ -105,12 +99,9 @@ const plugin = async (fastify: FastifyInstance) => { fastify.put( ROUTE_ME, { - preHandler: fastify.verifySession( - auth.claims.verifySessionOptions([ - "emailVerification", - "profileValidation", - ]), - ), + preHandler: auth.session.getVerifySession({ + skipClaims: ["emailVerification", "profileValidation"], + }), schema: updateMeSchema, } as unknown as RouteShorthandOptions, (handlersConfig?.updateMe || handlers.updateMe) as unknown as RouteHandler, @@ -119,9 +110,9 @@ const plugin = async (fastify: FastifyInstance) => { fastify.delete( ROUTE_ME, { - preHandler: fastify.verifySession( - auth.claims.verifySessionOptions(["profileValidation"]), - ), + preHandler: auth.session.getVerifySession({ + skipClaims: ["profileValidation"], + }), schema: deleteMeSchema, } as unknown as RouteShorthandOptions, (handlersConfig?.deleteMe || handlers.deleteMe) as unknown as RouteHandler, @@ -130,12 +121,9 @@ const plugin = async (fastify: FastifyInstance) => { fastify.put( ROUTE_ME_PHOTO, { - preHandler: fastify.verifySession( - auth.claims.verifySessionOptions([ - "emailVerification", - "profileValidation", - ]), - ), + preHandler: auth.session.getVerifySession({ + skipClaims: ["emailVerification", "profileValidation"], + }), schema: uploadPhotoSchema, } as unknown as RouteShorthandOptions, handlers.uploadPhoto as unknown as RouteHandler, @@ -144,12 +132,9 @@ const plugin = async (fastify: FastifyInstance) => { fastify.delete( ROUTE_ME_PHOTO, { - preHandler: fastify.verifySession( - auth.claims.verifySessionOptions([ - "emailVerification", - "profileValidation", - ]), - ), + preHandler: auth.session.getVerifySession({ + skipClaims: ["emailVerification", "profileValidation"], + }), schema: removePhotoSchema, } as unknown as RouteShorthandOptions, handlers.removePhoto as unknown as RouteHandler, diff --git a/packages/user/src/supertokens/index.ts b/packages/user/src/supertokens/index.ts index 1c2e4f662..99ef9b720 100644 --- a/packages/user/src/supertokens/index.ts +++ b/packages/user/src/supertokens/index.ts @@ -1,8 +1,15 @@ -import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; +import type { FastifyReply, FastifyRequest } from "fastify"; + +import type { AuthSession, GetSessionOptions } from "../auth/adapter"; declare module "fastify" { interface FastifyInstance { - verifySession: typeof verifySession; + verifySession( + options?: GetSessionOptions, + ): ( + request: FastifyRequest, + reply: FastifyReply, + ) => Promise; } } diff --git a/packages/user/src/supertokens/plugin.ts b/packages/user/src/supertokens/plugin.ts index 36c4c058c..a773a9f08 100644 --- a/packages/user/src/supertokens/plugin.ts +++ b/packages/user/src/supertokens/plugin.ts @@ -2,8 +2,8 @@ import type { FastifyInstance } from "fastify"; import FastifyPlugin from "fastify-plugin"; import { plugin as supertokensPlugin } from "supertokens-node/framework/fastify"; -import { verifySession } from "supertokens-node/recipe/session/framework/fastify"; +import { auth } from "../auth/adapter"; import { errorHandler } from "./errorHandler"; import init from "./init"; @@ -22,7 +22,9 @@ const plugin = async (fastify: FastifyInstance) => { log.info("Registering supertokens plugin complete"); - fastify.decorate("verifySession", verifySession); + fastify.decorate("verifySession", (options) => + auth.session.getVerifySession(options), + ); // [RL 2024-06-11] change sRefreshToken cookie path from config fastify.addHook("onSend", async (request, reply) => { From 799c42cc3653e5e94f7c8ef0cfd7ff3f2435120a Mon Sep 17 00:00:00 2001 From: kabin thakuri Date: Mon, 1 Jun 2026 14:23:47 +0545 Subject: [PATCH 07/11] refactor(auth): decouple ProfileValidationClaim from SuperTokens SessionClaim --- packages/user/src/auth/adapter.ts | 13 ------- .../user/src/auth/claims/profileValidation.ts | 32 +++++++++++++++++ packages/user/src/auth/index.ts | 6 ++++ packages/user/src/auth/supertokens.ts | 36 ++++++++++++------- .../utils/profileValidationClaim.ts | 24 ++++--------- 5 files changed, 68 insertions(+), 43 deletions(-) create mode 100644 packages/user/src/auth/claims/profileValidation.ts diff --git a/packages/user/src/auth/adapter.ts b/packages/user/src/auth/adapter.ts index 017b2bf98..2d88456e1 100644 --- a/packages/user/src/auth/adapter.ts +++ b/packages/user/src/auth/adapter.ts @@ -33,14 +33,6 @@ export type AuthResult = | { success: true; user: T }; export interface AuthSession { - assertClaims?( - validators: unknown[], - userContext?: AuthUserContext, - ): Promise; - fetchAndSetClaim?( - claim: unknown, - userContext?: AuthUserContext, - ): Promise; getAccessTokenPayload(userContext?: AuthUserContext): unknown; getUserId(userContext?: AuthUserContext): string; revokeSession(userContext?: AuthUserContext): Promise; @@ -64,11 +56,6 @@ export interface ClaimsProvider { userContext?: AuthUserContext, ): Promise; - excludeValidatorIds( - validators: T[], - skip: RefreshableClaim[], - ): T[]; - readonly keys: { emailVerification: string; profileValidation: string; diff --git a/packages/user/src/auth/claims/profileValidation.ts b/packages/user/src/auth/claims/profileValidation.ts new file mode 100644 index 000000000..b2bd993e4 --- /dev/null +++ b/packages/user/src/auth/claims/profileValidation.ts @@ -0,0 +1,32 @@ +import type { UserUpdateInput } from "../../types"; + +export interface ProfileValidationConfig { + enabled?: boolean; + fields?: Array; + gracePeriodInDays?: number; +} + +export interface ProfileValidationResult { + gracePeriodEndsAt?: number; + isVerified: boolean; +} + +export function checkProfileValidation( + user: T, + config: ProfileValidationConfig, +): ProfileValidationResult { + const fields = config.fields ?? []; + + const isVerified = !fields.some( + (field) => + (user as Record)[field as string] === null || + (user as Record)[field as string] === undefined, + ); + + const gracePeriodEndsAt = + !isVerified && config.gracePeriodInDays + ? user.signedUpAt + config.gracePeriodInDays * 24 * 60 * 60 * 1000 + : undefined; + + return { gracePeriodEndsAt, isVerified }; +} diff --git a/packages/user/src/auth/index.ts b/packages/user/src/auth/index.ts index a65decc9c..a12264be2 100644 --- a/packages/user/src/auth/index.ts +++ b/packages/user/src/auth/index.ts @@ -15,6 +15,12 @@ export type { RolesProvider, SessionProvider, } from "./adapter"; +export { checkProfileValidation } from "./claims/profileValidation"; + +export type { + ProfileValidationConfig, + ProfileValidationResult, +} from "./claims/profileValidation"; export { getAuthProvider, registerAuthProvider, diff --git a/packages/user/src/auth/supertokens.ts b/packages/user/src/auth/supertokens.ts index 965de38ac..e832d9e2d 100644 --- a/packages/user/src/auth/supertokens.ts +++ b/packages/user/src/auth/supertokens.ts @@ -36,13 +36,25 @@ const claimKeyByType: Record = { profileValidation: ProfileValidationClaim.key, }; +function excludeValidatorIds( + validators: T[], + skip: RefreshableClaim[], +): T[] { + const skipKeys = new Set(skip.map((claim) => claimKeyByType[claim])); + return validators.filter((validator) => !skipKeys.has(validator.id)); +} + const supertokensClaimsAdapter: ClaimsProvider = { async assertProfileValid(session, request, userContext) { const profileValidationClaim = new ProfileValidationClaim(); const context = createUserContextImpl(userContext, request); try { - await session.assertClaims?.( + await ( + session as unknown as { + assertClaims?: (...arguments_: unknown[]) => Promise; + } + ).assertClaims?.( [profileValidationClaim.validators.isVerified()], context, ); @@ -57,12 +69,6 @@ const supertokensClaimsAdapter: ClaimsProvider = { } }, - excludeValidatorIds(validators, skip) { - const skipKeys = new Set(skip.map((claim) => claimKeyByType[claim])); - - return validators.filter((validator) => !skipKeys.has(validator.id)); - }, - keys: { emailVerification: EmailVerificationClaim.key, profileValidation: ProfileValidationClaim.key, @@ -72,10 +78,17 @@ const supertokensClaimsAdapter: ClaimsProvider = { const context = createUserContextImpl(userContext, request); for (const claim of claims) { + const stSession = session as unknown as { + fetchAndSetClaim?: (...arguments_: unknown[]) => Promise; + }; + if (claim === "emailVerification") { - await session.fetchAndSetClaim?.(EmailVerificationClaim, context); + await stSession.fetchAndSetClaim?.(EmailVerificationClaim, context); } else if (claim === "profileValidation") { - await session.fetchAndSetClaim?.(new ProfileValidationClaim(), context); + await stSession.fetchAndSetClaim?.( + new ProfileValidationClaim(), + context, + ); } } }, @@ -396,10 +409,7 @@ const supertokensSessionAdapter: SessionProvider = { checkDatabase: options?.checkDatabase, overrideGlobalClaimValidators: skipClaims?.length ? async (globalValidators) => - supertokensClaimsAdapter.excludeValidatorIds( - globalValidators, - skipClaims, - ) + excludeValidatorIds(globalValidators, skipClaims) : undefined, sessionRequired: options?.sessionRequired, }) as unknown as Promise; diff --git a/packages/user/src/supertokens/utils/profileValidationClaim.ts b/packages/user/src/supertokens/utils/profileValidationClaim.ts index be4fd61dc..a81279203 100644 --- a/packages/user/src/supertokens/utils/profileValidationClaim.ts +++ b/packages/user/src/supertokens/utils/profileValidationClaim.ts @@ -10,6 +10,10 @@ import type { SessionClaimValidator } from "supertokens-node/recipe/session"; import { getRequestFromUserContext } from "supertokens-node"; import { SessionClaim } from "supertokens-node/lib/build/recipe/session/claims"; +import type { ProfileValidationConfig } from "../../auth/claims/profileValidation"; + +import { checkProfileValidation } from "../../auth/claims/profileValidation"; + interface Response { gracePeriodEndsAt?: number; isVerified: boolean; @@ -91,7 +95,8 @@ class ProfileValidationClaim extends SessionClaim { throw new Error("Request not set in userContext"); } - const profileValidation = request.config.user?.features?.profileValidation; + const profileValidation = request.config.user?.features + ?.profileValidation as ProfileValidationConfig | undefined; if (!profileValidation?.enabled) { throw new Error("Profile validation is not enabled"); @@ -103,22 +108,7 @@ class ProfileValidationClaim extends SessionClaim { throw new Error("User not found"); } - const fields = profileValidation.fields || []; - - // Verify that none of the specified fields in the user are null - const isVerified = !fields.some((field) => user[field] === null); - - // Calculate the grace period expiry date if the user is not verified - const gracePeriodEndsAt = - !isVerified && profileValidation.gracePeriodInDays - ? user.signedUpAt + - profileValidation.gracePeriodInDays * (24 * 60 * 60 * 1000) - : undefined; - - return { - gracePeriodEndsAt, - isVerified, - }; + return checkProfileValidation(user, profileValidation); }; getLastRefetchTime(payload: any, _userContext: any): number | undefined { From 7d5ed6d56d3f36735e7453e50275bf1abc1d067f Mon Sep 17 00:00:00 2001 From: kabin thakuri Date: Mon, 1 Jun 2026 18:04:34 +0545 Subject: [PATCH 08/11] chore(user/auth): make claim provider optional --- packages/user/src/auth/adapter.ts | 2 +- .../user/src/mercurius-auth/authPlugin.ts | 8 +++-- .../user/src/model/users/graphql/resolver.ts | 31 ++++++++++++++----- .../src/model/users/handlers/changeEmail.ts | 4 +-- packages/user/src/model/users/handlers/me.ts | 4 +-- .../src/model/users/handlers/removePhoto.ts | 4 +-- .../user/src/model/users/handlers/updateMe.ts | 10 ++++-- .../src/model/users/handlers/uploadPhoto.ts | 10 ++++-- 8 files changed, 52 insertions(+), 21 deletions(-) diff --git a/packages/user/src/auth/adapter.ts b/packages/user/src/auth/adapter.ts index 2d88456e1..e29b0044b 100644 --- a/packages/user/src/auth/adapter.ts +++ b/packages/user/src/auth/adapter.ts @@ -3,7 +3,7 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import type { ClaimValidationError, RefreshableClaim } from "./types"; export interface AuthAdapter { - claims: ClaimsProvider; + claims?: ClaimsProvider; createUserContext( request: FastifyRequest, existing?: AuthUserContext, diff --git a/packages/user/src/mercurius-auth/authPlugin.ts b/packages/user/src/mercurius-auth/authPlugin.ts index 9b01f59fb..05f53e010 100644 --- a/packages/user/src/mercurius-auth/authPlugin.ts +++ b/packages/user/src/mercurius-auth/authPlugin.ts @@ -37,7 +37,8 @@ const plugin = FastifyPlugin(async (fastify: FastifyInstance) => { { claimValidationErrors: [ { - id: auth.claims.keys.emailVerification, + id: + auth.claims?.keys.emailVerification ?? "emailVerification", reason: { actualValue: false, expectedValue: true, @@ -51,7 +52,10 @@ const plugin = FastifyPlugin(async (fastify: FastifyInstance) => { } } - if (fastify.config.user.features?.profileValidation?.enabled) { + if ( + fastify.config.user.features?.profileValidation?.enabled && + auth.claims + ) { const profileValidation = authDirectiveAST.arguments.find( (argument: { name: { value: string } }) => argument?.name?.value === "profileValidation", diff --git a/packages/user/src/model/users/graphql/resolver.ts b/packages/user/src/model/users/graphql/resolver.ts index 189c39227..d7f95c567 100644 --- a/packages/user/src/model/users/graphql/resolver.ts +++ b/packages/user/src/model/users/graphql/resolver.ts @@ -142,7 +142,7 @@ const Mutation = { .session; const userContext = auth.createUserContext(request); - if (config.user.features?.profileValidation?.enabled) { + if (config.user.features?.profileValidation?.enabled && auth.claims) { await auth.claims.refreshSessionClaims( session, request, @@ -151,7 +151,7 @@ const Mutation = { ); } - if (config.user.features?.signUp?.emailVerification) { + if (config.user.features?.signUp?.emailVerification && auth.claims) { await auth.claims.refreshSessionClaims( session, request, @@ -414,7 +414,10 @@ const Mutation = { const session = (request as FastifyRequest & { session: AuthSession }) .session; - if (request.config.user.features?.profileValidation?.enabled) { + if ( + request.config.user.features?.profileValidation?.enabled && + auth.claims + ) { await auth.claims.refreshSessionClaims( session, request, @@ -423,7 +426,10 @@ const Mutation = { ); } - if (request.config.user.features?.signUp?.emailVerification) { + if ( + request.config.user.features?.signUp?.emailVerification && + auth.claims + ) { await auth.claims.refreshSessionClaims( session, request, @@ -471,7 +477,7 @@ const Mutation = { request.user = updatedUser; - if (config.user.features?.profileValidation?.enabled) { + if (config.user.features?.profileValidation?.enabled && auth.claims) { await auth.claims.refreshSessionClaims( session, request, @@ -546,7 +552,10 @@ const Mutation = { const session = (request as FastifyRequest & { session: AuthSession }) .session; - if (request.config.user.features?.profileValidation?.enabled) { + if ( + request.config.user.features?.profileValidation?.enabled && + auth.claims + ) { await auth.claims.refreshSessionClaims( session, request, @@ -555,7 +564,10 @@ const Mutation = { ); } - if (request.config.user.features?.signUp?.emailVerification) { + if ( + request.config.user.features?.signUp?.emailVerification && + auth.claims + ) { await auth.claims.refreshSessionClaims( session, request, @@ -666,7 +678,10 @@ const Query = { const user = await service.findById(arguments_.id); - if (context.config.user.features?.profileValidation?.enabled) { + if ( + context.config.user.features?.profileValidation?.enabled && + auth.claims + ) { const request = context.reply.request; const session = (request as FastifyRequest & { session: AuthSession }) diff --git a/packages/user/src/model/users/handlers/changeEmail.ts b/packages/user/src/model/users/handlers/changeEmail.ts index e50d42c37..97d72c017 100644 --- a/packages/user/src/model/users/handlers/changeEmail.ts +++ b/packages/user/src/model/users/handlers/changeEmail.ts @@ -25,7 +25,7 @@ const changeEmail = async (request: FastifyRequest, reply: FastifyReply) => { try { const userContext = auth.createUserContext(request); - if (config.user.features?.profileValidation?.enabled) { + if (config.user.features?.profileValidation?.enabled && auth.claims) { await auth.claims.refreshSessionClaims( session, request, @@ -34,7 +34,7 @@ const changeEmail = async (request: FastifyRequest, reply: FastifyReply) => { ); } - if (config.user.features?.signUp?.emailVerification) { + if (config.user.features?.signUp?.emailVerification && auth.claims) { await auth.claims.refreshSessionClaims( session, request, diff --git a/packages/user/src/model/users/handlers/me.ts b/packages/user/src/model/users/handlers/me.ts index 2cd976c8e..8abe46d51 100644 --- a/packages/user/src/model/users/handlers/me.ts +++ b/packages/user/src/model/users/handlers/me.ts @@ -16,7 +16,7 @@ const me = async (request: FastifyRequest, reply: FastifyReply) => { const authUser = await auth.emailPassword.getUserById(user.id); const userContext = auth.createUserContext(request); - if (config.user.features?.profileValidation?.enabled) { + if (config.user.features?.profileValidation?.enabled && auth.claims) { await auth.claims.refreshSessionClaims( session, request, @@ -25,7 +25,7 @@ const me = async (request: FastifyRequest, reply: FastifyReply) => { ); } - if (config.user.features?.signUp?.emailVerification) { + if (config.user.features?.signUp?.emailVerification && auth.claims) { await auth.claims.refreshSessionClaims( session, request, diff --git a/packages/user/src/model/users/handlers/removePhoto.ts b/packages/user/src/model/users/handlers/removePhoto.ts index 914a664e1..59a279e27 100644 --- a/packages/user/src/model/users/handlers/removePhoto.ts +++ b/packages/user/src/model/users/handlers/removePhoto.ts @@ -29,7 +29,7 @@ const removePhoto = async (request: FastifyRequest, reply: FastifyReply) => { const session = (request as FastifyRequest & { session: AuthSession }) .session; - if (request.config.user.features?.profileValidation?.enabled) { + if (request.config.user.features?.profileValidation?.enabled && auth.claims) { await auth.claims.refreshSessionClaims( session, request, @@ -38,7 +38,7 @@ const removePhoto = async (request: FastifyRequest, reply: FastifyReply) => { ); } - if (request.config.user.features?.signUp?.emailVerification) { + if (request.config.user.features?.signUp?.emailVerification && auth.claims) { await auth.claims.refreshSessionClaims( session, request, diff --git a/packages/user/src/model/users/handlers/updateMe.ts b/packages/user/src/model/users/handlers/updateMe.ts index 4a2ef0429..d91b741e1 100644 --- a/packages/user/src/model/users/handlers/updateMe.ts +++ b/packages/user/src/model/users/handlers/updateMe.ts @@ -52,7 +52,10 @@ const updateMe = async (request: FastifyRequest, reply: FastifyReply) => { const session = (request as FastifyRequest & { session: AuthSession }) .session; - if (request.config.user.features?.profileValidation?.enabled) { + if ( + request.config.user.features?.profileValidation?.enabled && + auth.claims + ) { await auth.claims.refreshSessionClaims( session, request, @@ -61,7 +64,10 @@ const updateMe = async (request: FastifyRequest, reply: FastifyReply) => { ); } - if (request.config.user.features?.signUp?.emailVerification) { + if ( + request.config.user.features?.signUp?.emailVerification && + auth.claims + ) { await auth.claims.refreshSessionClaims( session, request, diff --git a/packages/user/src/model/users/handlers/uploadPhoto.ts b/packages/user/src/model/users/handlers/uploadPhoto.ts index 206260576..4ad8bb3f3 100644 --- a/packages/user/src/model/users/handlers/uploadPhoto.ts +++ b/packages/user/src/model/users/handlers/uploadPhoto.ts @@ -54,7 +54,10 @@ const uploadPhoto = async (request: FastifyRequest, reply: FastifyReply) => { const session = (request as FastifyRequest & { session: AuthSession }) .session; - if (request.config.user.features?.profileValidation?.enabled) { + if ( + request.config.user.features?.profileValidation?.enabled && + auth.claims + ) { await auth.claims.refreshSessionClaims( session, request, @@ -63,7 +66,10 @@ const uploadPhoto = async (request: FastifyRequest, reply: FastifyReply) => { ); } - if (request.config.user.features?.signUp?.emailVerification) { + if ( + request.config.user.features?.signUp?.emailVerification && + auth.claims + ) { await auth.claims.refreshSessionClaims( session, request, From 23034829b31f509b00b485fe4601db69739c7497 Mon Sep 17 00:00:00 2001 From: kabin thakuri Date: Tue, 2 Jun 2026 19:12:16 +0545 Subject: [PATCH 09/11] refactor(user/auth): update session handling and clean up unused types --- packages/user/package.json | 9 ++++- packages/user/src/auth/adapter.ts | 6 ---- packages/user/src/auth/index.ts | 2 +- packages/user/src/auth/supertokens.ts | 36 +++++++------------ packages/user/src/index.ts | 2 ++ .../user/src/model/users/graphql/resolver.ts | 2 +- packages/user/src/types/index.ts | 4 --- packages/user/src/userContext.ts | 5 +-- 8 files changed, 28 insertions(+), 38 deletions(-) diff --git a/packages/user/package.json b/packages/user/package.json index 426528a47..039dc410d 100644 --- a/packages/user/package.json +++ b/packages/user/package.json @@ -19,7 +19,9 @@ "main": "./dist/prefabs-tech-fastify-user.cjs", "module": "./dist/prefabs-tech-fastify-user.js", "types": "./dist/types/index.d.ts", - "files": ["dist"], + "files": [ + "dist" + ], "scripts": { "build": "vite build && tsc --emitDeclarationOnly && mv dist/src dist/types", "lint": "eslint .", @@ -78,5 +80,10 @@ }, "engines": { "node": ">=20" + }, + "peerDependenciesMeta": { + "supertokens-node": { + "optional": true + } } } diff --git a/packages/user/src/auth/adapter.ts b/packages/user/src/auth/adapter.ts index e29b0044b..b7e27e7b4 100644 --- a/packages/user/src/auth/adapter.ts +++ b/packages/user/src/auth/adapter.ts @@ -203,12 +203,6 @@ export interface UpdateEmailOrPasswordResult { export type { ClaimValidationError, RefreshableClaim } from "./types"; -declare module "fastify" { - interface FastifyRequest { - session?: AuthSession; - } -} - let authInstance: AuthAdapter | undefined; export function getAuth(): AuthAdapter { diff --git a/packages/user/src/auth/index.ts b/packages/user/src/auth/index.ts index a12264be2..4755bc073 100644 --- a/packages/user/src/auth/index.ts +++ b/packages/user/src/auth/index.ts @@ -4,13 +4,13 @@ export type { AuthErrorsProvider, AuthProvider, AuthSession, + AuthUser, AuthUserContext, ClaimsProvider, ClaimValidationError, EmailPasswordProvider, EmailVerificationProvider, GetSessionOptions, - AuthUser as ProviderAuthUser, RefreshableClaim, RolesProvider, SessionProvider, diff --git a/packages/user/src/auth/supertokens.ts b/packages/user/src/auth/supertokens.ts index e832d9e2d..f48beb62a 100644 --- a/packages/user/src/auth/supertokens.ts +++ b/packages/user/src/auth/supertokens.ts @@ -150,11 +150,7 @@ const supertokensEmailPasswordAdapter: EmailPasswordProvider = { if (response.status === "OK" && response.user) { return { success: true, - user: { - email: response.user.email, - id: response.user.id, - timeJoined: response.user.timeJoined, - }, + user: response.user as AuthUser, }; } @@ -178,11 +174,7 @@ const supertokensEmailPasswordAdapter: EmailPasswordProvider = { if (response.status === "OK" && response.user) { return { success: true, - user: { - email: response.user.email, - id: response.user.id, - timeJoined: response.user.timeJoined, - }, + user: response.user as AuthUser, }; } @@ -197,21 +189,13 @@ const supertokensEmailPasswordAdapter: EmailPasswordProvider = { if (!user) return undefined; - return { - email: user.email, - id: user.id, - timeJoined: user.timeJoined, - }; + return user as AuthUser; }, async getUsersByEmail(email: string): Promise { const users = await ThirdPartyEmailPassword.getUsersByEmail(email); - return users.map((user) => ({ - email: user.email, - id: user.id, - timeJoined: user.timeJoined, - })); + return users.map((user) => user as AuthUser); }, async resetPasswordUsingToken( @@ -404,15 +388,21 @@ const supertokensSessionAdapter: SessionProvider = { getVerifySession(options) { const skipClaims = options?.skipClaims; - return (request, reply) => - Session.getSession(request, wrapResponse(reply), { + return async (request, reply) => { + const session = await Session.getSession(request, wrapResponse(reply), { checkDatabase: options?.checkDatabase, overrideGlobalClaimValidators: skipClaims?.length ? async (globalValidators) => excludeValidatorIds(globalValidators, skipClaims) : undefined, sessionRequired: options?.sessionRequired, - }) as unknown as Promise; + }); + + // Attach the session to the request so handlers can access it + request.session = session; + + return session as unknown as AuthSession | undefined; + }; }, async revokeAllSessionsForUser(userId: string): Promise { diff --git a/packages/user/src/index.ts b/packages/user/src/index.ts index 6d314dc05..ac31b9af0 100644 --- a/packages/user/src/index.ts +++ b/packages/user/src/index.ts @@ -1,3 +1,4 @@ +import type { AuthSession } from "./auth/adapter"; import type { User, UserConfig } from "./types"; import hasPermission from "./middlewares/hasPermission"; @@ -8,6 +9,7 @@ declare module "fastify" { } interface FastifyRequest { + session?: AuthSession; user?: User; } } diff --git a/packages/user/src/model/users/graphql/resolver.ts b/packages/user/src/model/users/graphql/resolver.ts index d7f95c567..4ef4af8e1 100644 --- a/packages/user/src/model/users/graphql/resolver.ts +++ b/packages/user/src/model/users/graphql/resolver.ts @@ -108,7 +108,7 @@ const Mutation = { signUpResponse.user.id, ); - return signUpResponse; + return { status: "OK", user: signUpResponse.user }; } catch (error) { // FIXME [OP 28 SEP 2022] app.log.error(error); diff --git a/packages/user/src/types/index.ts b/packages/user/src/types/index.ts index 121eec4cc..d27650c2e 100644 --- a/packages/user/src/types/index.ts +++ b/packages/user/src/types/index.ts @@ -39,10 +39,6 @@ export type { Resolver, }; -export type { EmailVerificationRecipe } from "../supertokens/types/emailVerificationRecipe"; -export type { SessionRecipe } from "../supertokens/types/sessionRecipe"; -export type { ThirdPartyEmailPasswordRecipe } from "../supertokens/types/thirdPartyEmailPasswordRecipe"; - /* * @deprecated Import auth types from "@prefabs.tech/fastify-user/auth" instead. * These supertokens-specific types will be removed in a future release. diff --git a/packages/user/src/userContext.ts b/packages/user/src/userContext.ts index 0e80053ea..0c56daa3e 100644 --- a/packages/user/src/userContext.ts +++ b/packages/user/src/userContext.ts @@ -9,10 +9,11 @@ const userContext = async ( reply: FastifyReply, ) => { try { - request.session = (await auth.session.getSession(request, reply, { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (request as any).session = await auth.session.getSession(request, reply, { sessionRequired: false, skipClaims: ["emailVerification", "profileValidation"], - })) as (typeof request)["session"]; + }); } catch (error) { if (!auth.errors.isAuthError(error)) { throw error; From 784f777cf28a82e2691960c39181c7fdbc40059b Mon Sep 17 00:00:00 2001 From: kabin thakuri Date: Wed, 3 Jun 2026 14:26:47 +0545 Subject: [PATCH 10/11] chore(user): add new line before return --- packages/user/src/auth/adapter.ts | 2 ++ packages/user/src/auth/supertokens.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/packages/user/src/auth/adapter.ts b/packages/user/src/auth/adapter.ts index b7e27e7b4..b776e9a62 100644 --- a/packages/user/src/auth/adapter.ts +++ b/packages/user/src/auth/adapter.ts @@ -209,6 +209,7 @@ export function getAuth(): AuthAdapter { if (!authInstance) { throw new Error("Auth adapter not initialized. Call initAuth() first."); } + return authInstance; } @@ -223,6 +224,7 @@ export async function initAuth( await provider.init(fastify); } } + return authInstance; } diff --git a/packages/user/src/auth/supertokens.ts b/packages/user/src/auth/supertokens.ts index f48beb62a..78c7aa107 100644 --- a/packages/user/src/auth/supertokens.ts +++ b/packages/user/src/auth/supertokens.ts @@ -334,6 +334,7 @@ const supertokensRolesAdapter: RolesProvider = { async getRolesForUser(userId: string): Promise { const response = await UserRoles.getRolesForUser(userId); + return response.roles; }, @@ -343,6 +344,7 @@ const supertokensRolesAdapter: RolesProvider = { if (response.status === "OK") { return response.users; } + return []; }, From 38fd66d05b1ac4cadfc15513d8a6f6aacea6d412 Mon Sep 17 00:00:00 2001 From: kabin thakuri Date: Wed, 3 Jun 2026 18:00:43 +0545 Subject: [PATCH 11/11] chore(user): remove unnecessary type assertions in route handlers --- .../user/src/model/invitations/controller.ts | 31 ++++------- .../user/src/model/permissions/controller.ts | 10 ++-- packages/user/src/model/roles/controller.ts | 26 ++++------ packages/user/src/model/users/controller.ts | 51 +++++++++---------- 4 files changed, 48 insertions(+), 70 deletions(-) diff --git a/packages/user/src/model/invitations/controller.ts b/packages/user/src/model/invitations/controller.ts index 9b133f1f4..14795e16c 100644 --- a/packages/user/src/model/invitations/controller.ts +++ b/packages/user/src/model/invitations/controller.ts @@ -1,8 +1,4 @@ -import type { - FastifyInstance, - RouteHandler, - RouteShorthandOptions, -} from "fastify"; +import type { FastifyInstance } from "fastify"; import { PERMISSIONS_INVITATIONS_CREATE, @@ -40,9 +36,8 @@ const plugin = async (fastify: FastifyInstance) => { fastify.hasPermission(PERMISSIONS_INVITATIONS_LIST), ], schema: getInvitationsListSchema, - } as unknown as RouteShorthandOptions, - (handlersConfig?.list || - handlers.listInvitation) as unknown as RouteHandler, + }, + handlersConfig?.list || handlers.listInvitation, ); fastify.post( @@ -53,9 +48,8 @@ const plugin = async (fastify: FastifyInstance) => { fastify.hasPermission(PERMISSIONS_INVITATIONS_CREATE), ], schema: createInvitationSchema, - } as unknown as RouteShorthandOptions, - (handlersConfig?.create || - handlers.createInvitation) as unknown as RouteHandler, + }, + handlersConfig?.create || handlers.createInvitation, ); fastify.get( @@ -82,9 +76,8 @@ const plugin = async (fastify: FastifyInstance) => { fastify.hasPermission(PERMISSIONS_INVITATIONS_REVOKE), ], schema: revokeInvitationSchema, - } as unknown as RouteShorthandOptions, - (handlersConfig?.revoke || - handlers.revokeInvitation) as unknown as RouteHandler, + }, + handlersConfig?.revoke || handlers.revokeInvitation, ); fastify.post( @@ -95,9 +88,8 @@ const plugin = async (fastify: FastifyInstance) => { fastify.hasPermission(PERMISSIONS_INVITATIONS_RESEND), ], schema: resendInvitationSchema, - } as unknown as RouteShorthandOptions, - (handlersConfig?.resend || - handlers.resendInvitation) as unknown as RouteHandler, + }, + handlersConfig?.resend || handlers.resendInvitation, ); fastify.delete( @@ -108,9 +100,8 @@ const plugin = async (fastify: FastifyInstance) => { fastify.hasPermission(PERMISSIONS_INVITATIONS_DELETE), ], schema: deleteInvitationSchema, - } as unknown as RouteShorthandOptions, - (handlersConfig?.delete || - handlers.deleteInvitation) as unknown as RouteHandler, + }, + handlersConfig?.delete || handlers.deleteInvitation, ); }; diff --git a/packages/user/src/model/permissions/controller.ts b/packages/user/src/model/permissions/controller.ts index ef1f82102..f7bd416fd 100644 --- a/packages/user/src/model/permissions/controller.ts +++ b/packages/user/src/model/permissions/controller.ts @@ -1,8 +1,4 @@ -import type { - FastifyInstance, - RouteHandler, - RouteShorthandOptions, -} from "fastify"; +import type { FastifyInstance } from "fastify"; import { ROUTE_PERMISSIONS } from "../../constants"; import handlers from "./handlers"; @@ -14,8 +10,8 @@ const plugin = async (fastify: FastifyInstance) => { { preHandler: [fastify.verifySession()], schema: getPermissionsSchema, - } as unknown as RouteShorthandOptions, - handlers.getPermissions as unknown as RouteHandler, + }, + handlers.getPermissions, ); }; diff --git a/packages/user/src/model/roles/controller.ts b/packages/user/src/model/roles/controller.ts index ab1400369..b41971f49 100644 --- a/packages/user/src/model/roles/controller.ts +++ b/packages/user/src/model/roles/controller.ts @@ -1,8 +1,4 @@ -import type { - FastifyInstance, - RouteHandler, - RouteShorthandOptions, -} from "fastify"; +import type { FastifyInstance } from "fastify"; import { ROUTE_ROLES, ROUTE_ROLES_PERMISSIONS } from "../../constants"; import handlers from "./handlers"; @@ -20,8 +16,8 @@ const plugin = async (fastify: FastifyInstance) => { { preHandler: [fastify.verifySession()], schema: deleteRoleSchema, - } as unknown as RouteShorthandOptions, - handlers.deleteRole as unknown as RouteHandler, + }, + handlers.deleteRole, ); fastify.get( @@ -29,8 +25,8 @@ const plugin = async (fastify: FastifyInstance) => { { preHandler: [fastify.verifySession()], schema: getRolesSchema, - } as unknown as RouteShorthandOptions, - handlers.getRoles as unknown as RouteHandler, + }, + handlers.getRoles, ); fastify.get( @@ -38,8 +34,8 @@ const plugin = async (fastify: FastifyInstance) => { { preHandler: [fastify.verifySession()], schema: getRolePermissionsSchema, - } as unknown as RouteShorthandOptions, - handlers.getPermissions as unknown as RouteHandler, + }, + handlers.getPermissions, ); fastify.post( @@ -47,8 +43,8 @@ const plugin = async (fastify: FastifyInstance) => { { preHandler: [fastify.verifySession()], schema: createRoleSchema, - } as unknown as RouteShorthandOptions, - handlers.createRole as unknown as RouteHandler, + }, + handlers.createRole, ); fastify.put( @@ -56,8 +52,8 @@ const plugin = async (fastify: FastifyInstance) => { { preHandler: [fastify.verifySession()], schema: updateRoleSchema, - } as unknown as RouteShorthandOptions, - handlers.updatePermissions as unknown as RouteHandler, + }, + handlers.updatePermissions, ); }; diff --git a/packages/user/src/model/users/controller.ts b/packages/user/src/model/users/controller.ts index c26a2d36f..15efd96f6 100644 --- a/packages/user/src/model/users/controller.ts +++ b/packages/user/src/model/users/controller.ts @@ -1,8 +1,4 @@ -import type { - FastifyInstance, - RouteHandler, - RouteShorthandOptions, -} from "fastify"; +import type { FastifyInstance } from "fastify"; import { auth } from "../../auth/adapter"; import { @@ -48,8 +44,8 @@ const plugin = async (fastify: FastifyInstance) => { fastify.hasPermission(PERMISSIONS_USERS_LIST), ], schema: getUsersSchema, - } as unknown as RouteShorthandOptions, - (handlersConfig?.users || handlers.users) as unknown as RouteHandler, + }, + handlersConfig?.users || handlers.users, ); fastify.get( @@ -60,8 +56,8 @@ const plugin = async (fastify: FastifyInstance) => { fastify.hasPermission(PERMISSIONS_USERS_READ), ], schema: getUserSchema, - } as unknown as RouteShorthandOptions, - (handlersConfig?.user || handlers.user) as unknown as RouteHandler, + }, + handlersConfig?.user || handlers.user, ); fastify.post( @@ -69,9 +65,8 @@ const plugin = async (fastify: FastifyInstance) => { { preHandler: fastify.verifySession(), schema: changePasswordSchema, - } as unknown as RouteShorthandOptions, - (handlersConfig?.changePassword || - handlers.changePassword) as unknown as RouteHandler, + }, + handlersConfig?.changePassword || handlers.changePassword, ); fastify.post( @@ -81,8 +76,8 @@ const plugin = async (fastify: FastifyInstance) => { skipClaims: ["emailVerification", "profileValidation"], }), schema: changeEmailSchema, - } as unknown as RouteShorthandOptions, - handlers.changeEmail as unknown as RouteHandler, + }, + handlers.changeEmail, ); fastify.get( @@ -92,8 +87,8 @@ const plugin = async (fastify: FastifyInstance) => { skipClaims: ["emailVerification", "profileValidation"], }), schema: getMeSchema, - } as unknown as RouteShorthandOptions, - (handlersConfig?.me || handlers.me) as unknown as RouteHandler, + }, + handlersConfig?.me || handlers.me, ); fastify.put( @@ -103,8 +98,8 @@ const plugin = async (fastify: FastifyInstance) => { skipClaims: ["emailVerification", "profileValidation"], }), schema: updateMeSchema, - } as unknown as RouteShorthandOptions, - (handlersConfig?.updateMe || handlers.updateMe) as unknown as RouteHandler, + }, + handlersConfig?.updateMe || handlers.updateMe, ); fastify.delete( @@ -114,8 +109,8 @@ const plugin = async (fastify: FastifyInstance) => { skipClaims: ["profileValidation"], }), schema: deleteMeSchema, - } as unknown as RouteShorthandOptions, - (handlersConfig?.deleteMe || handlers.deleteMe) as unknown as RouteHandler, + }, + handlersConfig?.deleteMe || handlers.deleteMe, ); fastify.put( @@ -125,8 +120,8 @@ const plugin = async (fastify: FastifyInstance) => { skipClaims: ["emailVerification", "profileValidation"], }), schema: uploadPhotoSchema, - } as unknown as RouteShorthandOptions, - handlers.uploadPhoto as unknown as RouteHandler, + }, + handlers.uploadPhoto, ); fastify.delete( @@ -136,8 +131,8 @@ const plugin = async (fastify: FastifyInstance) => { skipClaims: ["emailVerification", "profileValidation"], }), schema: removePhotoSchema, - } as unknown as RouteShorthandOptions, - handlers.removePhoto as unknown as RouteHandler, + }, + handlers.removePhoto, ); fastify.put( @@ -148,8 +143,8 @@ const plugin = async (fastify: FastifyInstance) => { fastify.hasPermission(PERMISSIONS_USERS_DISABLE), ], schema: disableUserSchema, - } as unknown as RouteShorthandOptions, - (handlersConfig?.disable || handlers.disable) as unknown as RouteHandler, + }, + handlersConfig?.disable || handlers.disable, ); fastify.put( @@ -160,8 +155,8 @@ const plugin = async (fastify: FastifyInstance) => { fastify.hasPermission(PERMISSIONS_USERS_ENABLE), ], schema: enableUserSchema, - } as unknown as RouteShorthandOptions, - (handlersConfig?.enable || handlers.enable) as unknown as RouteHandler, + }, + handlersConfig?.enable || handlers.enable, ); fastify.post(