Skip to content

Commit f3fb173

Browse files
authored
feat: set password via mutation (#3745)
1 parent cbbf873 commit f3fb173

3 files changed

Lines changed: 87 additions & 48 deletions

File tree

__tests__/users.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,16 @@ jest.mock('../src/cio', () => ({
215215
syncNotificationFlagsToCio: jest.fn(),
216216
}));
217217

218+
const mockSetPassword = jest.fn();
219+
jest.mock('../src/betterAuth', () => ({
220+
...(jest.requireActual('../src/betterAuth') as Record<string, unknown>),
221+
getBetterAuth: () => ({
222+
api: {
223+
setPassword: mockSetPassword,
224+
},
225+
}),
226+
}));
227+
218228
beforeAll(async () => {
219229
con = await createOrGetConnection();
220230
state = await initializeGraphQLTesting(
@@ -8150,3 +8160,50 @@ describe('query userPostsAnalyticsHistory', () => {
81508160
});
81518161
});
81528162
});
8163+
8164+
describe('mutation setPassword', () => {
8165+
const MUTATION = `
8166+
mutation SetPassword($newPassword: String!) {
8167+
setPassword(newPassword: $newPassword) {
8168+
_
8169+
}
8170+
}
8171+
`;
8172+
8173+
it('should not authorize when not logged in', () =>
8174+
testMutationErrorCode(
8175+
client,
8176+
{
8177+
mutation: MUTATION,
8178+
variables: { newPassword: 'newPassword123!' },
8179+
},
8180+
'UNAUTHENTICATED',
8181+
));
8182+
8183+
it('should set password via better auth api', async () => {
8184+
loggedUser = '1';
8185+
mockSetPassword.mockResolvedValueOnce({ status: true });
8186+
8187+
const res = await client.mutate(MUTATION, {
8188+
variables: { newPassword: 'newPassword123!' },
8189+
});
8190+
8191+
expect(res.errors).toBeFalsy();
8192+
expect(mockSetPassword).toHaveBeenCalledWith({
8193+
body: { newPassword: 'newPassword123!' },
8194+
headers: expect.any(Headers),
8195+
});
8196+
});
8197+
8198+
it('should propagate error when better auth api fails', async () => {
8199+
loggedUser = '1';
8200+
mockSetPassword.mockRejectedValueOnce(new Error('Password too weak'));
8201+
8202+
const res = await client.mutate(MUTATION, {
8203+
variables: { newPassword: 'weak' },
8204+
});
8205+
8206+
expect(res.errors).toBeTruthy();
8207+
expect(res.errors[0].message).toBe('Password too weak');
8208+
});
8209+
});

src/routes/betterAuth.ts

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -92,54 +92,6 @@ export const callBetterAuth = async ({
9292
};
9393

9494
const betterAuthRoute = async (fastify: FastifyInstance): Promise<void> => {
95-
// BetterAuth's setPassword is a server-only API (no HTTP route registered),
96-
// so we expose it as a custom route that delegates to auth.api.setPassword().
97-
fastify.route({
98-
method: 'POST',
99-
url: '/auth/set-password',
100-
handler: async (request, reply) => {
101-
try {
102-
const { newPassword } =
103-
(request.body as { newPassword?: string }) ?? {};
104-
if (!newPassword) {
105-
return reply.status(400).send({ error: 'newPassword is required' });
106-
}
107-
108-
const headers = fromNodeHeaders(
109-
request.headers as Record<string, string | string[] | undefined>,
110-
);
111-
const result = await getBetterAuth().api.setPassword({
112-
body: { newPassword },
113-
headers,
114-
});
115-
116-
return reply.send(result);
117-
} catch (error) {
118-
request.log.error(
119-
{ err: formatError(error) },
120-
'BetterAuth set-password failed',
121-
);
122-
if (!reply.sent) {
123-
const status =
124-
error &&
125-
typeof error === 'object' &&
126-
'statusCode' in error &&
127-
typeof error.statusCode === 'number'
128-
? error.statusCode
129-
: 500;
130-
const message =
131-
error &&
132-
typeof error === 'object' &&
133-
'message' in error &&
134-
typeof error.message === 'string'
135-
? error.message
136-
: 'Failed to set password';
137-
return reply.status(status).send({ error: message });
138-
}
139-
}
140-
},
141-
});
142-
14395
fastify.route({
14496
method: ['GET', 'POST'],
14597
url: '/auth/*',

src/schema/users.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ import {
107107
getUserCoresRole,
108108
hasUserProfileAnalyticsPermissions,
109109
} from '../common/user';
110+
import { getBetterAuth } from '../betterAuth';
111+
import { fromNodeHeaders } from 'better-auth/node';
110112
import { randomInt, randomUUID } from 'crypto';
111113
import { ArrayContains, DataSource, In, IsNull, QueryRunner } from 'typeorm';
112114
import { DisallowHandle } from '../entity/DisallowHandle';
@@ -1689,6 +1691,11 @@ export const typeDefs = /* GraphQL */ `
16891691
Update user's notification preferences
16901692
"""
16911693
updateNotificationSettings(notificationFlags: JSON!): EmptyResponse @auth
1694+
1695+
"""
1696+
Set a password for the authenticated user (Better Auth)
1697+
"""
1698+
setPassword(newPassword: String!): EmptyResponse @auth
16921699
}
16931700
`;
16941701

@@ -3858,6 +3865,29 @@ export const resolvers: IResolvers<unknown, BaseContext> = {
38583865

38593866
return { _: true };
38603867
},
3868+
setPassword: async (
3869+
_,
3870+
{ newPassword }: { newPassword: string },
3871+
ctx: AuthContext,
3872+
): Promise<GQLEmptyResponse> => {
3873+
const headers = fromNodeHeaders(
3874+
(ctx.req.raw?.headers as Record<
3875+
string,
3876+
string | string[] | undefined
3877+
>) ?? {},
3878+
);
3879+
try {
3880+
await getBetterAuth().api.setPassword({
3881+
body: { newPassword },
3882+
headers,
3883+
});
3884+
} catch (error) {
3885+
const message =
3886+
error instanceof Error ? error.message : 'Failed to set password';
3887+
throw new ValidationError(message);
3888+
}
3889+
return { _: true };
3890+
},
38613891
},
38623892
User: {
38633893
image: (user: GQLUser): GQLUser['image'] => mapCloudinaryUrl(user.image),

0 commit comments

Comments
 (0)