mirror of
https://github.com/documenso/documenso.git
synced 2025-11-20 03:32:14 +10:00
fix: wip
This commit is contained in:
@ -13,6 +13,7 @@ import {
|
||||
import { seedTestEmail, seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin, apiSignout } from '../fixtures/authentication';
|
||||
import { signSignaturePad } from '../fixtures/signature';
|
||||
|
||||
test.describe.configure({ mode: 'parallel', timeout: 60000 });
|
||||
|
||||
@ -35,15 +36,7 @@ test('[DOCUMENT_AUTH]: should allow signing when no auth setup', async ({ page }
|
||||
await page.goto(signUrl);
|
||||
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
||||
|
||||
// Add signature.
|
||||
const canvas = page.locator('canvas').first();
|
||||
const box = await canvas.boundingBox();
|
||||
if (box) {
|
||||
await page.mouse.move(box.x + 40, box.y + 40);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + box.width - 2, box.y + box.height - 2);
|
||||
await page.mouse.up();
|
||||
}
|
||||
await signSignaturePad(page);
|
||||
|
||||
for (const field of fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
@ -92,15 +85,7 @@ test('[DOCUMENT_AUTH]: should allow signing with valid global auth', async ({ pa
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
||||
|
||||
// Add signature.
|
||||
const canvas = page.locator('canvas').first();
|
||||
const box = await canvas.boundingBox();
|
||||
if (box) {
|
||||
await page.mouse.move(box.x + 40, box.y + 40);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + box.width - 2, box.y + box.height - 2);
|
||||
await page.mouse.up();
|
||||
}
|
||||
await signSignaturePad(page);
|
||||
|
||||
for (const field of fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
@ -261,15 +246,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient au
|
||||
});
|
||||
}
|
||||
|
||||
// Add signature.
|
||||
const canvas = page.locator('canvas').first();
|
||||
const box = await canvas.boundingBox();
|
||||
if (box) {
|
||||
await page.mouse.move(box.x + 40, box.y + 40);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + box.width - 2, box.y + box.height - 2);
|
||||
await page.mouse.up();
|
||||
}
|
||||
await signSignaturePad(page);
|
||||
|
||||
for (const field of fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
@ -372,15 +349,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient an
|
||||
});
|
||||
}
|
||||
|
||||
// Add signature.
|
||||
const canvas = page.locator('canvas').first();
|
||||
const box = await canvas.boundingBox();
|
||||
if (box) {
|
||||
await page.mouse.move(box.x + 40, box.y + 40);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + box.width - 2, box.y + box.height - 2);
|
||||
await page.mouse.up();
|
||||
}
|
||||
await signSignaturePad(page);
|
||||
|
||||
for (const field of fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
import { signSignaturePad } from '../fixtures/signature';
|
||||
|
||||
// Can't use the function in server-only/document due to it indirectly using
|
||||
// require imports.
|
||||
@ -368,15 +369,7 @@ test('[DOCUMENT_FLOW]: should be able to approve a document', async ({ page }) =
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
// Add signature.
|
||||
const canvas = page.locator('canvas');
|
||||
const box = await canvas.boundingBox();
|
||||
if (box) {
|
||||
await page.mouse.move(box.x + 40, box.y + 40);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + box.width - 2, box.y + box.height - 2);
|
||||
await page.mouse.up();
|
||||
}
|
||||
await signSignaturePad(page);
|
||||
|
||||
for (const field of fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
@ -607,19 +600,10 @@ test('[DOCUMENT_FLOW]: should be able to create and sign a document with 3 recip
|
||||
|
||||
await page.goto(`/sign/${recipient?.token}`);
|
||||
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
||||
await signSignaturePad(page);
|
||||
|
||||
await page.locator(`#field-${recipientField.id}`).getByRole('button').click();
|
||||
|
||||
const canvas = page.locator('canvas#signature');
|
||||
const box = await canvas.boundingBox();
|
||||
if (box) {
|
||||
await page.mouse.move(box.x + 40, box.y + 40);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + box.width - 2, box.y + box.height - 2);
|
||||
await page.mouse.up();
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Sign', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
await page.getByRole('button', { name: 'Sign' }).click();
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import { seedTeam } from '@documenso/prisma/seed/teams';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { apiSignin } from '../fixtures/authentication';
|
||||
import { signSignaturePad } from '../fixtures/signature';
|
||||
|
||||
test.describe('Signing Certificate Tests', () => {
|
||||
test('individual document should always include signing certificate', async ({ page }) => {
|
||||
@ -36,14 +37,7 @@ test.describe('Signing Certificate Tests', () => {
|
||||
// Sign the document
|
||||
await page.goto(`/sign/${recipient.token}`);
|
||||
|
||||
const canvas = page.locator('canvas');
|
||||
const box = await canvas.boundingBox();
|
||||
if (box) {
|
||||
await page.mouse.move(box.x + 40, box.y + 40);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + box.width - 2, box.y + box.height - 2);
|
||||
await page.mouse.up();
|
||||
}
|
||||
await signSignaturePad(page);
|
||||
|
||||
for (const field of recipient.fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
@ -113,14 +107,7 @@ test.describe('Signing Certificate Tests', () => {
|
||||
// Sign the document
|
||||
await page.goto(`/sign/${recipient.token}`);
|
||||
|
||||
const canvas = page.locator('canvas');
|
||||
const box = await canvas.boundingBox();
|
||||
if (box) {
|
||||
await page.mouse.move(box.x + 40, box.y + 40);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + box.width - 2, box.y + box.height - 2);
|
||||
await page.mouse.up();
|
||||
}
|
||||
await signSignaturePad(page);
|
||||
|
||||
for (const field of recipient.fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
@ -190,14 +177,7 @@ test.describe('Signing Certificate Tests', () => {
|
||||
// Sign the document
|
||||
await page.goto(`/sign/${recipient.token}`);
|
||||
|
||||
const canvas = page.locator('canvas');
|
||||
const box = await canvas.boundingBox();
|
||||
if (box) {
|
||||
await page.mouse.move(box.x + 40, box.y + 40);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + box.width - 2, box.y + box.height - 2);
|
||||
await page.mouse.up();
|
||||
}
|
||||
await signSignaturePad(page);
|
||||
|
||||
for (const field of recipient.fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
|
||||
@ -21,31 +21,23 @@ export const apiSignin = async ({
|
||||
}: LoginOptions) => {
|
||||
const { request } = page.context();
|
||||
|
||||
const csrfToken = await getCsrfToken(page);
|
||||
// const csrfToken = await getCsrfToken(page);
|
||||
|
||||
await request.post(`${NEXT_PUBLIC_WEBAPP_URL()}/api/auth/callback/credentials`, {
|
||||
form: {
|
||||
await request.post(`${NEXT_PUBLIC_WEBAPP_URL()}/api/auth/email-password/authorize`, {
|
||||
data: {
|
||||
email,
|
||||
password,
|
||||
json: true,
|
||||
csrfToken,
|
||||
},
|
||||
});
|
||||
|
||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}${redirectPath}`);
|
||||
await page.waitForTimeout(500);
|
||||
};
|
||||
|
||||
export const apiSignout = async ({ page }: { page: Page }) => {
|
||||
const { request } = page.context();
|
||||
|
||||
const csrfToken = await getCsrfToken(page);
|
||||
|
||||
await request.post(`${NEXT_PUBLIC_WEBAPP_URL()}/api/auth/signout`, {
|
||||
form: {
|
||||
csrfToken,
|
||||
json: true,
|
||||
},
|
||||
});
|
||||
await request.post(`${NEXT_PUBLIC_WEBAPP_URL()}/api/auth/signout`);
|
||||
|
||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/signin`);
|
||||
};
|
||||
|
||||
40
packages/app-tests/e2e/fixtures/signature.ts
Normal file
40
packages/app-tests/e2e/fixtures/signature.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
export const signSignaturePad = async (page: Page) => {
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
const canvas = page.getByTestId('signature-pad');
|
||||
|
||||
const box = await canvas.boundingBox();
|
||||
|
||||
if (!box) {
|
||||
throw new Error('Signature pad not found');
|
||||
}
|
||||
|
||||
// Calculate center point
|
||||
const centerX = box.x + box.width / 2;
|
||||
const centerY = box.y + box.height / 2;
|
||||
|
||||
// Calculate square size (making it slightly smaller than the canvas)
|
||||
const squareSize = Math.min(box.width, box.height) * 0.4; // 40% of the smallest dimension
|
||||
|
||||
// Move to center
|
||||
await page.mouse.move(centerX, centerY);
|
||||
await page.mouse.down();
|
||||
|
||||
// Draw square clockwise from center
|
||||
// Move right
|
||||
await page.mouse.move(centerX + squareSize, centerY, { steps: 10 });
|
||||
// Move down
|
||||
await page.mouse.move(centerX + squareSize, centerY + squareSize, { steps: 10 });
|
||||
// Move left
|
||||
await page.mouse.move(centerX - squareSize, centerY + squareSize, { steps: 10 });
|
||||
// Move up
|
||||
await page.mouse.move(centerX - squareSize, centerY - squareSize, { steps: 10 });
|
||||
// Move right
|
||||
await page.mouse.move(centerX + squareSize, centerY - squareSize, { steps: 10 });
|
||||
// Move down to close the square
|
||||
await page.mouse.move(centerX + squareSize, centerY, { steps: 10 });
|
||||
|
||||
await page.mouse.up();
|
||||
};
|
||||
@ -61,7 +61,7 @@ test('[TEAMS]: search respects team document visibility', async ({ page }) => {
|
||||
});
|
||||
|
||||
await page.getByPlaceholder('Search documents...').fill('Searchable');
|
||||
await page.waitForURL(/search=Searchable/);
|
||||
await page.waitForURL(/query=Searchable/);
|
||||
|
||||
await checkDocumentTabCount(page, 'All', visibleDocs);
|
||||
|
||||
@ -103,7 +103,7 @@ test('[TEAMS]: search does not reveal documents from other teams', async ({ page
|
||||
});
|
||||
|
||||
await page.getByPlaceholder('Search documents...').fill('Unique');
|
||||
await page.waitForURL(/search=Unique/);
|
||||
await page.waitForURL(/query=Unique/);
|
||||
|
||||
await checkDocumentTabCount(page, 'All', 1);
|
||||
await expect(page.getByRole('link', { name: 'Unique Team A Document' })).toBeVisible();
|
||||
@ -144,7 +144,7 @@ test('[PERSONAL]: search does not reveal team documents in personal account', as
|
||||
});
|
||||
|
||||
await page.getByPlaceholder('Search documents...').fill('Unique');
|
||||
await page.waitForURL(/search=Unique/);
|
||||
await page.waitForURL(/query=Unique/);
|
||||
|
||||
await checkDocumentTabCount(page, 'All', 1);
|
||||
await expect(page.getByRole('link', { name: 'Personal Unique Document' })).toBeVisible();
|
||||
@ -179,7 +179,7 @@ test('[TEAMS]: search respects recipient visibility regardless of team visibilit
|
||||
});
|
||||
|
||||
await page.getByPlaceholder('Search documents...').fill('Admin Document');
|
||||
await page.waitForURL(/search=Admin(%20|\+|\s)Document/);
|
||||
await page.waitForURL(/query=Admin(%20|\+|\s)Document/);
|
||||
|
||||
await checkDocumentTabCount(page, 'All', 1);
|
||||
await expect(
|
||||
@ -221,7 +221,7 @@ test('[TEAMS]: search by recipient name respects visibility', async ({ page }) =
|
||||
});
|
||||
|
||||
await page.getByPlaceholder('Search documents...').fill('Unique Recipient');
|
||||
await page.waitForURL(/search=Unique(%20|\+|\s)Recipient/);
|
||||
await page.waitForURL(/query=Unique(%20|\+|\s)Recipient/);
|
||||
|
||||
await checkDocumentTabCount(page, 'All', 1);
|
||||
await expect(
|
||||
@ -238,7 +238,7 @@ test('[TEAMS]: search by recipient name respects visibility', async ({ page }) =
|
||||
});
|
||||
|
||||
await page.getByPlaceholder('Search documents...').fill('Unique Recipient');
|
||||
await page.waitForURL(/search=Unique(%20|\+|\s)Recipient/);
|
||||
await page.waitForURL(/query=Unique(%20|\+|\s)Recipient/);
|
||||
|
||||
await checkDocumentTabCount(page, 'All', 0);
|
||||
await expect(
|
||||
|
||||
@ -113,7 +113,7 @@ test('[TEAMS]: check team documents count with internal team email', async ({ pa
|
||||
await apiSignin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/t/${team.url}/documents`,
|
||||
redirectPath: `/t/${team.url}/documents?perPage=20`,
|
||||
});
|
||||
|
||||
// Check document counts.
|
||||
|
||||
@ -33,7 +33,6 @@ test('[TEAMS]: update team member role', async ({ page }) => {
|
||||
await page.getByLabel('Manager').click();
|
||||
await page.getByRole('button', { name: 'Update' }).click();
|
||||
|
||||
// TODO: Remove me, but i don't care for now
|
||||
await page.reload();
|
||||
|
||||
await expect(
|
||||
|
||||
@ -117,7 +117,7 @@ test('[DIRECT_TEMPLATES]: toggle direct template link', async ({ page }) => {
|
||||
|
||||
// Check that the direct template link is no longer accessible.
|
||||
await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));
|
||||
await expect(page.getByText('Template not found')).toBeVisible();
|
||||
await expect(page.getByText('404 not found')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
@ -162,7 +162,7 @@ test('[DIRECT_TEMPLATES]: delete direct template link', async ({ page }) => {
|
||||
|
||||
// Check that the direct template link is no longer accessible.
|
||||
await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));
|
||||
await expect(page.getByText('Template not found')).toBeVisible();
|
||||
await expect(page.getByText('404 not found')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -42,13 +42,12 @@ test('[TEMPLATES]: view templates', async ({ page }) => {
|
||||
redirectPath: '/templates',
|
||||
});
|
||||
|
||||
// Only should only see their personal template.
|
||||
await expect(page.getByTestId('data-table-count')).toContainText('Showing 1 result');
|
||||
|
||||
// Owner should see both team templates.
|
||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/templates`);
|
||||
await expect(page.getByRole('main')).toContainText('Showing 2 results');
|
||||
|
||||
// Only should only see their personal template.
|
||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/templates`);
|
||||
await expect(page.getByRole('main')).toContainText('Showing 1 result');
|
||||
await expect(page.getByTestId('data-table-count')).toContainText('Showing 2 results');
|
||||
});
|
||||
|
||||
test('[TEMPLATES]: delete template', async ({ page }) => {
|
||||
@ -142,7 +141,7 @@ test('[TEMPLATES]: duplicate template', async ({ page }) => {
|
||||
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
|
||||
await page.getByRole('button', { name: 'Duplicate' }).click();
|
||||
await expect(page.getByText('Template duplicated').first()).toBeVisible();
|
||||
await expect(page.getByRole('main')).toContainText('Showing 2 results');
|
||||
await expect(page.getByTestId('data-table-count')).toContainText('Showing 2 results');
|
||||
|
||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/templates`);
|
||||
|
||||
@ -151,7 +150,7 @@ test('[TEMPLATES]: duplicate template', async ({ page }) => {
|
||||
await page.getByRole('menuitem', { name: 'Duplicate' }).click();
|
||||
await page.getByRole('button', { name: 'Duplicate' }).click();
|
||||
await expect(page.getByText('Template duplicated').first()).toBeVisible();
|
||||
await expect(page.getByRole('main')).toContainText('Showing 2 results');
|
||||
await expect(page.getByTestId('data-table-count')).toContainText('Showing 2 results');
|
||||
});
|
||||
|
||||
test('[TEMPLATES]: use template', async ({ page }) => {
|
||||
@ -194,7 +193,7 @@ test('[TEMPLATES]: use template', async ({ page }) => {
|
||||
await page.waitForURL(/documents/);
|
||||
await page.getByRole('main').getByRole('link', { name: 'Documents' }).click();
|
||||
await page.waitForURL('/documents');
|
||||
await expect(page.getByRole('main')).toContainText('Showing 1 result');
|
||||
await expect(page.getByTestId('data-table-count')).toContainText('Showing 1 result');
|
||||
|
||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/templates`);
|
||||
await page.waitForTimeout(1000);
|
||||
@ -212,5 +211,5 @@ test('[TEMPLATES]: use template', async ({ page }) => {
|
||||
await page.waitForURL(/\/t\/.+\/documents/);
|
||||
await page.getByRole('main').getByRole('link', { name: 'Documents' }).click();
|
||||
await page.waitForURL(`/t/${team.url}/documents`);
|
||||
await expect(page.getByRole('main')).toContainText('Showing 1 result');
|
||||
await expect(page.getByTestId('data-table-count')).toContainText('Showing 1 result');
|
||||
});
|
||||
|
||||
@ -6,6 +6,8 @@ import {
|
||||
seedUser,
|
||||
} from '@documenso/prisma/seed/users';
|
||||
|
||||
import { signSignaturePad } from '../fixtures/signature';
|
||||
|
||||
test.use({ storageState: { cookies: [], origins: [] } });
|
||||
|
||||
test('[USER] can sign up with email and password', async ({ page }: { page: Page }) => {
|
||||
@ -18,14 +20,7 @@ test('[USER] can sign up with email and password', async ({ page }: { page: Page
|
||||
await page.getByLabel('Email').fill(email);
|
||||
await page.getByLabel('Password', { exact: true }).fill(password);
|
||||
|
||||
const canvas = page.locator('canvas').first();
|
||||
const box = await canvas.boundingBox();
|
||||
if (box) {
|
||||
await page.mouse.move(box.x + 40, box.y + 40);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + box.width - 2, box.y + box.height - 2);
|
||||
await page.mouse.up();
|
||||
}
|
||||
await signSignaturePad(page);
|
||||
|
||||
await page.getByRole('button', { name: 'Next', exact: true }).click();
|
||||
await page.getByLabel('Public profile username').fill(Date.now().toString());
|
||||
@ -41,7 +36,7 @@ test('[USER] can sign up with email and password', async ({ page }: { page: Page
|
||||
await expect(page.getByRole('heading')).toContainText('Email Confirmed!');
|
||||
|
||||
// We now automatically redirect to the home page
|
||||
// await page.getByRole('link', { name: 'Go back home' }).click();
|
||||
await page.getByRole('link', { name: 'Continue' }).click();
|
||||
|
||||
await page.waitForURL('/documents');
|
||||
|
||||
|
||||
@ -28,9 +28,7 @@ export const auth = new Hono<HonoAuthContext>()
|
||||
* Handle errors.
|
||||
*/
|
||||
auth.onError((err, c) => {
|
||||
console.error(`-----------`);
|
||||
console.error(`-----------`);
|
||||
console.error(`-----------`);
|
||||
// Todo Remove
|
||||
console.error(`${err}`);
|
||||
|
||||
if (err instanceof HTTPException) {
|
||||
|
||||
@ -2,6 +2,7 @@ import type { Context } from 'hono';
|
||||
import { deleteCookie, getSignedCookie, setSignedCookie } from 'hono/cookie';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { useSecureCookies } from '@documenso/lib/constants/auth';
|
||||
import { appLog } from '@documenso/lib/utils/debugger';
|
||||
import { env } from '@documenso/lib/utils/env';
|
||||
|
||||
@ -23,6 +24,18 @@ const getAuthDomain = () => {
|
||||
return url.hostname;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic auth session cookie options.
|
||||
*/
|
||||
export const sessionCookieOptions = {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: useSecureCookies ? 'none' : 'lax',
|
||||
secure: useSecureCookies,
|
||||
domain: getAuthDomain(),
|
||||
// Todo: Max age for specific auth cookies.
|
||||
} as const;
|
||||
|
||||
export const extractSessionCookieFromHeaders = (headers: Headers): string | null => {
|
||||
const cookieHeader = headers.get('cookie') || '';
|
||||
const cookiePairs = cookieHeader.split(';');
|
||||
@ -54,12 +67,13 @@ export const getSessionCookie = async (c: Context): Promise<string | null> => {
|
||||
* @param sessionToken - The session token to set.
|
||||
*/
|
||||
export const setSessionCookie = async (c: Context, sessionToken: string) => {
|
||||
await setSignedCookie(c, sessionCookieName, sessionToken, getAuthSecret(), {
|
||||
path: '/',
|
||||
// sameSite: '', // whats the default? we need to change this for embed right?
|
||||
// secure: true,
|
||||
domain: getAuthDomain(),
|
||||
}).catch((err) => {
|
||||
await setSignedCookie(
|
||||
c,
|
||||
sessionCookieName,
|
||||
sessionToken,
|
||||
getAuthSecret(),
|
||||
sessionCookieOptions,
|
||||
).catch((err) => {
|
||||
appLog('SetSessionCookie', `Error setting signed cookie: ${err}`);
|
||||
|
||||
throw err;
|
||||
@ -73,9 +87,5 @@ export const setSessionCookie = async (c: Context, sessionToken: string) => {
|
||||
* @param sessionToken - The session token to set.
|
||||
*/
|
||||
export const deleteSessionCookie = (c: Context) => {
|
||||
deleteCookie(c, sessionCookieName, {
|
||||
path: '/',
|
||||
secure: true,
|
||||
domain: getAuthDomain(),
|
||||
});
|
||||
deleteCookie(c, sessionCookieName, sessionCookieOptions);
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { sha256 } from '@oslojs/crypto/sha2';
|
||||
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding';
|
||||
import type { Session, User } from '@prisma/client';
|
||||
import { type Session, type User, UserSecurityAuditLogType } from '@prisma/client';
|
||||
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
@ -49,6 +49,15 @@ export const createSession = async (
|
||||
data: session,
|
||||
});
|
||||
|
||||
await prisma.userSecurityAuditLog.create({
|
||||
data: {
|
||||
userId,
|
||||
ipAddress: metadata.ipAddress,
|
||||
userAgent: metadata.userAgent,
|
||||
type: UserSecurityAuditLogType.SIGN_IN,
|
||||
},
|
||||
});
|
||||
|
||||
return session;
|
||||
};
|
||||
|
||||
@ -103,6 +112,18 @@ export const validateSessionToken = async (token: string): Promise<SessionValida
|
||||
return { session, user, isAuthenticated: true };
|
||||
};
|
||||
|
||||
export const invalidateSession = async (sessionId: string): Promise<void> => {
|
||||
await prisma.session.delete({ where: { id: sessionId } });
|
||||
export const invalidateSession = async (
|
||||
sessionId: string,
|
||||
metadata: RequestMetadata,
|
||||
): Promise<void> => {
|
||||
const session = await prisma.session.delete({ where: { id: sessionId } });
|
||||
|
||||
await prisma.userSecurityAuditLog.create({
|
||||
data: {
|
||||
userId: session.userId,
|
||||
ipAddress: metadata.ipAddress,
|
||||
userAgent: metadata.userAgent,
|
||||
type: UserSecurityAuditLogType.SIGN_OUT,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -19,4 +19,12 @@ export const onAuthorize = async (user: AuthorizeUser, c: Context<HonoAuthContex
|
||||
await createSession(sessionToken, user.userId, metadata);
|
||||
|
||||
await setSessionCookie(c, sessionToken);
|
||||
|
||||
// Todo.
|
||||
// Create the Stripe customer and attach it to the user if it doesn't exist.
|
||||
// if (user.customerId === null && IS_BILLING_ENABLED()) {
|
||||
// await getStripeCustomerByUser(user).catch((err) => {
|
||||
// console.error(err);
|
||||
// });
|
||||
// }
|
||||
};
|
||||
|
||||
@ -5,6 +5,7 @@ import { DateTime } from 'luxon';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { EMAIL_VERIFICATION_STATE } from '@documenso/lib/constants/email';
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { jobsClient } from '@documenso/lib/jobs/client';
|
||||
import { disableTwoFactorAuthentication } from '@documenso/lib/server-only/2fa/disable-2fa';
|
||||
@ -18,10 +19,7 @@ import { forgotPassword } from '@documenso/lib/server-only/user/forgot-password'
|
||||
import { getMostRecentVerificationTokenByUserId } from '@documenso/lib/server-only/user/get-most-recent-verification-token-by-user-id';
|
||||
import { resetPassword } from '@documenso/lib/server-only/user/reset-password';
|
||||
import { updatePassword } from '@documenso/lib/server-only/user/update-password';
|
||||
import {
|
||||
EMAIL_VERIFICATION_STATE,
|
||||
verifyEmail,
|
||||
} from '@documenso/lib/server-only/user/verify-email';
|
||||
import { verifyEmail } from '@documenso/lib/server-only/user/verify-email';
|
||||
import { env } from '@documenso/lib/utils/env';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
|
||||
|
||||
@ -10,6 +10,7 @@ import { env } from '@documenso/lib/utils/env';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AuthenticationErrorCode } from '../lib/errors/error-codes';
|
||||
import { sessionCookieOptions } from '../lib/session/session-cookies';
|
||||
import { onAuthorize } from '../lib/utils/authorizer';
|
||||
import type { HonoAuthContext } from '../types/context';
|
||||
|
||||
@ -43,28 +44,22 @@ export const googleRoute = new Hono<HonoAuthContext>()
|
||||
const { redirectPath } = c.req.valid('json');
|
||||
|
||||
setCookie(c, 'google_oauth_state', state, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: env('NODE_ENV') === 'production', // Todo: Check.
|
||||
maxAge: 60 * 10, // 10 minutes
|
||||
sameSite: 'lax', // Todo??
|
||||
...sessionCookieOptions,
|
||||
sameSite: 'lax', // Todo
|
||||
maxAge: 60 * 10, // 10 minutes.
|
||||
});
|
||||
|
||||
setCookie(c, 'google_code_verifier', codeVerifier, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: env('NODE_ENV') === 'production', // Todo: Check.
|
||||
maxAge: 60 * 10, // 10 minutes
|
||||
sameSite: 'lax', // Todo??
|
||||
...sessionCookieOptions,
|
||||
sameSite: 'lax', // Todo
|
||||
maxAge: 60 * 10, // 10 minutes.
|
||||
});
|
||||
|
||||
if (redirectPath) {
|
||||
setCookie(c, 'google_redirect_path', `${state}:${redirectPath}`, {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: env('NODE_ENV') === 'production', // Todo: Check.
|
||||
maxAge: 60 * 10, // 10 minutes
|
||||
sameSite: 'lax', // Todo??
|
||||
...sessionCookieOptions,
|
||||
sameSite: 'lax', // Todo
|
||||
maxAge: 60 * 10, // 10 minutes.
|
||||
});
|
||||
}
|
||||
|
||||
@ -81,6 +76,7 @@ export const googleRoute = new Hono<HonoAuthContext>()
|
||||
|
||||
const storedState = deleteCookie(c, 'google_oauth_state');
|
||||
const storedCodeVerifier = deleteCookie(c, 'google_code_verifier');
|
||||
const storedredirectPath = deleteCookie(c, 'google_redirect_path') ?? '';
|
||||
|
||||
if (!code || !storedState || state !== storedState || !storedCodeVerifier) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
@ -88,8 +84,6 @@ export const googleRoute = new Hono<HonoAuthContext>()
|
||||
});
|
||||
}
|
||||
|
||||
const storedredirectPath = deleteCookie(c, 'google_redirect_path') ?? '';
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
let [redirectState, redirectPath] = storedredirectPath.split(':');
|
||||
|
||||
|
||||
@ -2,8 +2,11 @@ import { Hono } from 'hono';
|
||||
|
||||
import { invalidateSession, validateSessionToken } from '../lib/session/session';
|
||||
import { deleteSessionCookie, getSessionCookie } from '../lib/session/session-cookies';
|
||||
import type { HonoAuthContext } from '../types/context';
|
||||
|
||||
export const signOutRoute = new Hono<HonoAuthContext>().post('/signout', async (c) => {
|
||||
const metadata = c.get('requestMetadata');
|
||||
|
||||
export const signOutRoute = new Hono().post('/signout', async (c) => {
|
||||
const sessionId = await getSessionCookie(c);
|
||||
|
||||
if (!sessionId) {
|
||||
@ -16,7 +19,7 @@ export const signOutRoute = new Hono().post('/signout', async (c) => {
|
||||
return new Response('No session found', { status: 401 });
|
||||
}
|
||||
|
||||
await invalidateSession(session.id);
|
||||
await invalidateSession(session.id, metadata);
|
||||
|
||||
deleteSessionCookie(c);
|
||||
|
||||
|
||||
@ -47,9 +47,11 @@ export const PASSKEY_TIMEOUT = 60000;
|
||||
*/
|
||||
export const MAXIMUM_PASSKEYS = 50;
|
||||
|
||||
// Todo: nextuauth_url ??
|
||||
export const useSecureCookies =
|
||||
env('NODE_ENV') === 'production' && String(env('NEXTAUTH_URL')).startsWith('https://');
|
||||
|
||||
// Todo: Test secure cookies prefix in remix.
|
||||
const secureCookiePrefix = useSecureCookies ? '__Secure-' : '';
|
||||
|
||||
export const formatSecureCookieName = (name: string) => `${secureCookiePrefix}${name}`;
|
||||
|
||||
@ -4,3 +4,10 @@ export const FROM_ADDRESS = env('NEXT_PRIVATE_SMTP_FROM_ADDRESS') || 'noreply@do
|
||||
export const FROM_NAME = env('NEXT_PRIVATE_SMTP_FROM_NAME') || 'Documenso';
|
||||
|
||||
export const SERVICE_USER_EMAIL = 'serviceaccount@documenso.com';
|
||||
|
||||
export const EMAIL_VERIFICATION_STATE = {
|
||||
NOT_FOUND: 'NOT_FOUND',
|
||||
VERIFIED: 'VERIFIED',
|
||||
EXPIRED: 'EXPIRED',
|
||||
ALREADY_VERIFIED: 'ALREADY_VERIFIED',
|
||||
} as const;
|
||||
|
||||
@ -68,13 +68,12 @@ export class LocalJobProvider extends BaseJobProvider {
|
||||
);
|
||||
}
|
||||
|
||||
public getApiHandler(): (context: HonoContext) => Promise<Response | void> {
|
||||
return async (context: HonoContext) => {
|
||||
const req = context.req;
|
||||
public getApiHandler(): (c: HonoContext) => Promise<Response | void> {
|
||||
return async (c: HonoContext) => {
|
||||
const req = c.req;
|
||||
|
||||
if (req.method !== 'POST') {
|
||||
context.text('Method not allowed', 405);
|
||||
return;
|
||||
return c.text('Method not allowed', 405);
|
||||
}
|
||||
|
||||
const jobId = req.header('x-job-id');
|
||||
@ -89,8 +88,7 @@ export class LocalJobProvider extends BaseJobProvider {
|
||||
.catch(() => null);
|
||||
|
||||
if (!options) {
|
||||
context.text('Bad request', 400);
|
||||
return;
|
||||
return c.text('Bad request', 400);
|
||||
}
|
||||
|
||||
const definition = this._jobDefinitions[options.name];
|
||||
@ -100,33 +98,28 @@ export class LocalJobProvider extends BaseJobProvider {
|
||||
typeof signature !== 'string' ||
|
||||
typeof options !== 'object'
|
||||
) {
|
||||
context.text('Bad request', 400);
|
||||
return;
|
||||
return c.text('Bad request', 400);
|
||||
}
|
||||
|
||||
if (!definition) {
|
||||
context.text('Job not found', 404);
|
||||
return;
|
||||
return c.text('Job not found', 404);
|
||||
}
|
||||
|
||||
if (definition && !definition.enabled) {
|
||||
console.log('Attempted to trigger a disabled job', options.name);
|
||||
|
||||
context.text('Job not found', 404);
|
||||
return;
|
||||
return c.text('Job not found', 404);
|
||||
}
|
||||
|
||||
if (!signature || !verify(options, signature)) {
|
||||
context.text('Unauthorized', 401);
|
||||
return;
|
||||
return c.text('Unauthorized', 401);
|
||||
}
|
||||
|
||||
if (definition.trigger.schema) {
|
||||
const result = definition.trigger.schema.safeParse(options.payload);
|
||||
|
||||
if (!result.success) {
|
||||
context.text('Bad request', 400);
|
||||
return;
|
||||
return c.text('Bad request', 400);
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,8 +142,7 @@ export class LocalJobProvider extends BaseJobProvider {
|
||||
.catch(() => null);
|
||||
|
||||
if (!backgroundJob) {
|
||||
context.text('Job not found', 404);
|
||||
return;
|
||||
return c.text('Job not found', 404);
|
||||
}
|
||||
|
||||
try {
|
||||
@ -189,8 +181,7 @@ export class LocalJobProvider extends BaseJobProvider {
|
||||
},
|
||||
});
|
||||
|
||||
context.text('Task exceeded retries', 500);
|
||||
return;
|
||||
return c.text('Task exceeded retries', 500);
|
||||
}
|
||||
|
||||
backgroundJob = await prisma.backgroundJob.update({
|
||||
@ -210,7 +201,7 @@ export class LocalJobProvider extends BaseJobProvider {
|
||||
});
|
||||
}
|
||||
|
||||
context.text('OK', 200);
|
||||
return c.text('OK', 200);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -134,7 +134,7 @@ export const resendDocument = async ({
|
||||
emailMessage =
|
||||
customEmail?.message ||
|
||||
i18n._(
|
||||
msg`${user.name} on behalf of "${document.team.name}" has invited you to ${recipientActionVerb} the document "${document.title}".`,
|
||||
msg`${user.name || user.email} on behalf of "${document.team.name}" has invited you to ${recipientActionVerb} the document "${document.title}".`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -164,20 +164,20 @@ export const resendDocument = async ({
|
||||
? teamGlobalSettingsToBranding(document.team.teamGlobalSettings)
|
||||
: undefined;
|
||||
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, {
|
||||
lang: document.documentMeta?.language,
|
||||
branding,
|
||||
}),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: document.documentMeta?.language,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const [html, text] = await Promise.all([
|
||||
renderEmailWithI18N(template, {
|
||||
lang: document.documentMeta?.language,
|
||||
branding,
|
||||
}),
|
||||
renderEmailWithI18N(template, {
|
||||
lang: document.documentMeta?.language,
|
||||
branding,
|
||||
plainText: true,
|
||||
}),
|
||||
]);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: email,
|
||||
|
||||
@ -2,15 +2,9 @@ import { DateTime } from 'luxon';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { EMAIL_VERIFICATION_STATE } from '../../constants/email';
|
||||
import { jobsClient } from '../../jobs/client';
|
||||
|
||||
export const EMAIL_VERIFICATION_STATE = {
|
||||
NOT_FOUND: 'NOT_FOUND',
|
||||
VERIFIED: 'VERIFIED',
|
||||
EXPIRED: 'EXPIRED',
|
||||
ALREADY_VERIFIED: 'ALREADY_VERIFIED',
|
||||
} as const;
|
||||
|
||||
export type VerifyEmailProps = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
@ -477,6 +477,7 @@ export const SignaturePad = ({
|
||||
})}
|
||||
>
|
||||
<canvas
|
||||
data-testid="signature-pad"
|
||||
ref={$el}
|
||||
className={cn(
|
||||
'relative block',
|
||||
|
||||
Reference in New Issue
Block a user