mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
Compare commits
2 Commits
f93d34c38e
...
fix/sessio
| Author | SHA1 | Date | |
|---|---|---|---|
| 28865c2f71 | |||
| d70ea9c6a7 |
@ -1,4 +1,4 @@
|
|||||||
import { type Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
|
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
|
|
||||||
@ -58,3 +58,15 @@ const getCsrfToken = async (page: Page) => {
|
|||||||
|
|
||||||
return csrfToken;
|
return csrfToken;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const checkSessionValid = async (page: Page): Promise<boolean> => {
|
||||||
|
const { request } = page.context();
|
||||||
|
|
||||||
|
const response = await request.fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/api/auth/session`, {
|
||||||
|
method: 'get',
|
||||||
|
});
|
||||||
|
|
||||||
|
const session = await response.json();
|
||||||
|
|
||||||
|
return session.isAuthenticated === true;
|
||||||
|
};
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { type Page, expect, test } from '@playwright/test';
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { seedUser } from '@documenso/prisma/seed/users';
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
|
||||||
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
import { apiSignin, apiSignout, checkSessionValid } from '../fixtures/authentication';
|
||||||
|
|
||||||
test.use({ storageState: { cookies: [], origins: [] } });
|
test.use({ storageState: { cookies: [], origins: [] } });
|
||||||
|
|
||||||
@ -17,6 +17,7 @@ test('[USER] can reset password via forgot password', async ({ page }: { page: P
|
|||||||
|
|
||||||
await page.goto('http://localhost:3000/signin');
|
await page.goto('http://localhost:3000/signin');
|
||||||
await page.getByRole('link', { name: 'Forgot your password?' }).click();
|
await page.getByRole('link', { name: 'Forgot your password?' }).click();
|
||||||
|
await expect(page).toHaveURL('http://localhost:3000/forgot-password');
|
||||||
|
|
||||||
await page.getByRole('textbox', { name: 'Email' }).click();
|
await page.getByRole('textbox', { name: 'Email' }).click();
|
||||||
await page.getByRole('textbox', { name: 'Email' }).fill(user.email);
|
await page.getByRole('textbox', { name: 'Email' }).fill(user.email);
|
||||||
@ -24,7 +25,9 @@ test('[USER] can reset password via forgot password', async ({ page }: { page: P
|
|||||||
await expect(page.getByRole('button', { name: 'Reset Password' })).toBeEnabled();
|
await expect(page.getByRole('button', { name: 'Reset Password' })).toBeEnabled();
|
||||||
await page.getByRole('button', { name: 'Reset Password' }).click();
|
await page.getByRole('button', { name: 'Reset Password' }).click();
|
||||||
|
|
||||||
await expect(page.locator('body')).toContainText('Reset email sent', { timeout: 10000 });
|
await expect(page.locator('body')).toContainText('Reset email sent', {
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
const foundToken = await prisma.passwordResetToken.findFirstOrThrow({
|
const foundToken = await prisma.passwordResetToken.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
@ -109,3 +112,116 @@ test('[USER] can reset password via user settings', async ({ page }: { page: Pag
|
|||||||
await page.waitForURL('/settings/profile');
|
await page.waitForURL('/settings/profile');
|
||||||
await expect(page).toHaveURL('/settings/profile');
|
await expect(page).toHaveURL('/settings/profile');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('[USER] password reset invalidates all sessions', async ({ page }: { page: Page }) => {
|
||||||
|
const oldPassword = 'Test123!';
|
||||||
|
const newPassword = 'Test124!';
|
||||||
|
|
||||||
|
const { user } = await seedUser({
|
||||||
|
password: oldPassword,
|
||||||
|
});
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
password: oldPassword,
|
||||||
|
redirectPath: '/settings/profile',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await checkSessionValid(page)).toBe(true);
|
||||||
|
|
||||||
|
const initialCookies = await page.context().cookies();
|
||||||
|
|
||||||
|
await page.context().clearCookies();
|
||||||
|
|
||||||
|
await page.goto('http://localhost:3000/signin');
|
||||||
|
await page.getByRole('link', { name: 'Forgot your password?' }).click();
|
||||||
|
await expect(page).toHaveURL('http://localhost:3000/forgot-password');
|
||||||
|
await page.getByRole('textbox', { name: 'Email' }).fill(user.email);
|
||||||
|
await page.getByRole('button', { name: 'Reset Password' }).click();
|
||||||
|
await expect(page.locator('body')).toContainText('Reset email sent', {
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const foundToken = await prisma.passwordResetToken.findFirstOrThrow({
|
||||||
|
where: { userId: user.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`http://localhost:3000/reset-password/${foundToken.token}`);
|
||||||
|
await page.getByLabel('Password', { exact: true }).fill(newPassword);
|
||||||
|
await page.getByLabel('Repeat Password').fill(newPassword);
|
||||||
|
await page.getByRole('button', { name: 'Reset Password' }).click();
|
||||||
|
await expect(page.locator('body')).toContainText('Your password has been updated successfully.');
|
||||||
|
|
||||||
|
await page.context().addCookies(initialCookies);
|
||||||
|
|
||||||
|
await page.goto('http://localhost:3000/settings/profile');
|
||||||
|
await expect(page).toHaveURL('http://localhost:3000/signin');
|
||||||
|
|
||||||
|
expect(await checkSessionValid(page)).toBe(false);
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
password: newPassword,
|
||||||
|
redirectPath: '/settings/profile',
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.waitForURL('/settings/profile');
|
||||||
|
expect(await checkSessionValid(page)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('[USER] password update invalidates other sessions but keeps current', async ({
|
||||||
|
page,
|
||||||
|
}: {
|
||||||
|
page: Page;
|
||||||
|
}) => {
|
||||||
|
const oldPassword = 'Test123!';
|
||||||
|
const newPassword = 'Test124!';
|
||||||
|
|
||||||
|
const { user } = await seedUser({
|
||||||
|
password: oldPassword,
|
||||||
|
});
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
password: oldPassword,
|
||||||
|
redirectPath: '/settings/profile',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await checkSessionValid(page)).toBe(true);
|
||||||
|
|
||||||
|
const initialCookies = await page.context().cookies();
|
||||||
|
|
||||||
|
await page.context().clearCookies();
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
password: oldPassword,
|
||||||
|
redirectPath: '/settings/profile',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await checkSessionValid(page)).toBe(true);
|
||||||
|
|
||||||
|
await page.goto('http://localhost:3000/settings/security');
|
||||||
|
await page.getByLabel('Current password').fill(oldPassword);
|
||||||
|
await page.getByLabel('New password').fill(newPassword);
|
||||||
|
await page.getByLabel('Repeat password').fill(newPassword);
|
||||||
|
await page.getByRole('button', { name: 'Update password' }).click();
|
||||||
|
await expect(page.locator('body')).toContainText('Password updated');
|
||||||
|
|
||||||
|
const finalCookies = await page.context().cookies();
|
||||||
|
|
||||||
|
await page.context().clearCookies();
|
||||||
|
await page.context().addCookies(initialCookies);
|
||||||
|
await page.goto('http://localhost:3000/settings/profile');
|
||||||
|
await expect(page).toHaveURL('http://localhost:3000/signin');
|
||||||
|
expect(await checkSessionValid(page)).toBe(false);
|
||||||
|
|
||||||
|
await page.context().clearCookies();
|
||||||
|
await page.context().addCookies(finalCookies);
|
||||||
|
await page.goto('http://localhost:3000/settings/security');
|
||||||
|
await expect(page).toHaveURL('http://localhost:3000/settings/security');
|
||||||
|
expect(await checkSessionValid(page)).toBe(true);
|
||||||
|
});
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import { env } from '@documenso/lib/utils/env';
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { AuthenticationErrorCode } from '../lib/errors/error-codes';
|
import { AuthenticationErrorCode } from '../lib/errors/error-codes';
|
||||||
|
import { invalidateSessions } from '../lib/session/session';
|
||||||
import { getCsrfCookie } from '../lib/session/session-cookies';
|
import { getCsrfCookie } from '../lib/session/session-cookies';
|
||||||
import { onAuthorize } from '../lib/utils/authorizer';
|
import { onAuthorize } from '../lib/utils/authorizer';
|
||||||
import { getSession } from '../lib/utils/get-session';
|
import { getSession } from '../lib/utils/get-session';
|
||||||
@ -170,15 +171,38 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
|
|||||||
const { password, currentPassword } = c.req.valid('json');
|
const { password, currentPassword } = c.req.valid('json');
|
||||||
const requestMetadata = c.get('requestMetadata');
|
const requestMetadata = c.get('requestMetadata');
|
||||||
|
|
||||||
const session = await getSession(c);
|
const { session, user } = await getSession(c);
|
||||||
|
|
||||||
await updatePassword({
|
await updatePassword({
|
||||||
userId: session.user.id,
|
userId: user.id,
|
||||||
password,
|
password,
|
||||||
currentPassword,
|
currentPassword,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const userSessionIds = await prisma.session
|
||||||
|
.findMany({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
id: {
|
||||||
|
not: session.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((sessions) => sessions.map((s) => s.id));
|
||||||
|
|
||||||
|
if (userSessionIds.length > 0) {
|
||||||
|
await invalidateSessions({
|
||||||
|
userId: user.id,
|
||||||
|
sessionIds: userSessionIds,
|
||||||
|
metadata: requestMetadata,
|
||||||
|
isRevoke: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return c.text('OK', 201);
|
return c.text('OK', 201);
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
@ -231,12 +255,41 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
|
|||||||
|
|
||||||
const requestMetadata = c.get('requestMetadata');
|
const requestMetadata = c.get('requestMetadata');
|
||||||
|
|
||||||
|
// Look up user ID before password reset for session invalidation
|
||||||
|
const passwordResetToken = await prisma.passwordResetToken.findFirst({
|
||||||
|
where: { token },
|
||||||
|
select: { userId: true },
|
||||||
|
});
|
||||||
|
|
||||||
await resetPassword({
|
await resetPassword({
|
||||||
token,
|
token,
|
||||||
password,
|
password,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Invalidate all sessions after successful password reset
|
||||||
|
if (passwordResetToken) {
|
||||||
|
const userSessionIds = await prisma.session
|
||||||
|
.findMany({
|
||||||
|
where: {
|
||||||
|
userId: passwordResetToken.userId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((sessions) => sessions.map((session) => session.id));
|
||||||
|
|
||||||
|
if (userSessionIds.length > 0) {
|
||||||
|
await invalidateSessions({
|
||||||
|
userId: passwordResetToken.userId,
|
||||||
|
sessionIds: userSessionIds,
|
||||||
|
metadata: requestMetadata,
|
||||||
|
isRevoke: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return c.text('OK', 201);
|
return c.text('OK', 201);
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user