This commit is contained in:
David Nguyen
2025-02-12 18:39:00 +11:00
parent 15922d447b
commit 4c57095ee1
11 changed files with 72 additions and 231 deletions

View File

@ -5,8 +5,9 @@ import { formatDocumentsPath } from '@documenso/lib/utils/teams';
export function loader() {
const { currentTeam } = getLoaderSession();
if (!currentTeam) {
throw redirect('/documents');
throw redirect('/settings/teams');
}
throw redirect(formatDocumentsPath(currentTeam.url));

View File

@ -19,7 +19,7 @@ export const loader = () => {
const { currentTeam } = getLoaderSession();
if (!currentTeam) {
throw redirect('/documents');
throw redirect('/settings/teams');
}
const trpcHeaders = {

View File

@ -97,7 +97,6 @@
"typescript": "5.6.2",
"vite": "^6.1.0",
"vite-plugin-babel-macros": "^1.0.6",
"vite-plugin-checker": "^0.8.0",
"vite-tsconfig-paths": "^5.1.4"
}
}

View File

@ -1,7 +1,6 @@
import type { Context, Next } from 'hono';
import { getCookie } from 'hono/cookie';
import { setCsrfCookie } from '@documenso/auth/server/lib/session/session-cookies';
import { AppLogger } from '@documenso/lib/utils/debugger';
const logger = new AppLogger('Middleware');
@ -32,14 +31,6 @@ export const appMiddleware = async (c: Context, next: Next) => {
const referrerUrl = referrer ? new URL(referrer) : null;
const referrerPathname = referrerUrl ? referrerUrl.pathname : null;
// Set csrf token if not set.
const csrfToken = getCookie(c, 'csrfToken');
// Todo: Currently not working.
if (!csrfToken) {
await setCsrfCookie(c);
}
// // Whether to reset the preferred team url cookie if the user accesses a non team page from a team page.
// const resetPreferredTeamUrl =
// referrerPathname &&
@ -59,24 +50,6 @@ export const appMiddleware = async (c: Context, next: Next) => {
// return c.redirect(redirectUrl);
// }
// // Redirect `/t` to `/settings/teams`.
// if (path === '/t' || path === '/t/') {
// logger.log('Redirecting to /settings/teams');
// const redirectUrl = new URL('/settings/teams', req.url);
// return c.redirect(redirectUrl);
// }
// // Redirect `/t/<team_url>` to `/t/<team_url>/documents`.
// if (TEAM_URL_ROOT_REGEX.test(path)) {
// logger.log('Redirecting team documents');
// const redirectUrl = new URL(`${path}/documents`, req.url);
// setCookie(c, 'preferred-team-url', path.replace('/t/', ''));
// return c.redirect(redirectUrl);
// }
// // Set the preferred team url cookie if user accesses a team page.
// if (path.startsWith('/t/')) {
// setCookie(c, 'preferred-team-url', path.split('/')[2]);
@ -90,6 +63,4 @@ export const appMiddleware = async (c: Context, next: Next) => {
// deleteCookie(c, 'preferred-team-url');
// return next();
// }
return next();
};

View File

@ -9,7 +9,6 @@ import { openApiDocument } from '@documenso/trpc/server/open-api';
import { filesRoute } from './api/files';
import { type AppContext, appContext } from './context';
import { appMiddleware } from './middleware';
import { openApiTrpcServerHandler } from './trpc/hono-trpc-open-api';
import { reactRouterTrpcServer } from './trpc/hono-trpc-remix';
@ -30,7 +29,7 @@ app.use(appContext);
/**
* Middleware for initial page loads.
*/
app.use('*', appMiddleware);
// app.use('*', appMiddleware);
// Auth server.
app.route('/api/auth', auth);

192
package-lock.json generated
View File

@ -179,7 +179,6 @@
"typescript": "5.6.2",
"vite": "^6.1.0",
"vite-plugin-babel-macros": "^1.0.6",
"vite-plugin-checker": "^0.8.0",
"vite-tsconfig-paths": "^5.1.4"
}
},
@ -849,16 +848,6 @@
"win32"
]
},
"apps/remix/node_modules/commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"apps/remix/node_modules/esbuild": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
@ -900,35 +889,6 @@
"@esbuild/win32-x64": "0.24.2"
}
},
"apps/remix/node_modules/meow": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz",
"integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@types/minimist": "^1.2.0",
"camelcase-keys": "^6.2.2",
"decamelize": "^1.2.0",
"decamelize-keys": "^1.1.0",
"hard-rejection": "^2.1.0",
"minimist-options": "4.1.0",
"normalize-package-data": "^3.0.0",
"read-pkg-up": "^7.0.1",
"redent": "^3.0.0",
"trim-newlines": "^3.0.0",
"type-fest": "^0.18.0",
"yargs-parser": "^20.2.3"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"apps/remix/node_modules/rollup": {
"version": "4.34.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.5.tgz",
@ -968,88 +928,6 @@
"fsevents": "~2.3.2"
}
},
"apps/remix/node_modules/type-fest": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz",
"integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"optional": true,
"peer": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"apps/remix/node_modules/vite-plugin-checker": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.8.0.tgz",
"integrity": "sha512-UA5uzOGm97UvZRTdZHiQVYFnd86AVn8EVaD4L3PoVzxH+IZSfaAw14WGFwX9QS23UW3lV/5bVKZn6l0w+q9P0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.12.13",
"ansi-escapes": "^4.3.0",
"chalk": "^4.1.1",
"chokidar": "^3.5.1",
"commander": "^8.0.0",
"fast-glob": "^3.2.7",
"fs-extra": "^11.1.0",
"npm-run-path": "^4.0.1",
"strip-ansi": "^6.0.0",
"tiny-invariant": "^1.1.0",
"vscode-languageclient": "^7.0.0",
"vscode-languageserver": "^7.0.0",
"vscode-languageserver-textdocument": "^1.0.1",
"vscode-uri": "^3.0.2"
},
"engines": {
"node": ">=14.16"
},
"peerDependencies": {
"@biomejs/biome": ">=1.7",
"eslint": ">=7",
"meow": "^9.0.0",
"optionator": "^0.9.1",
"stylelint": ">=13",
"typescript": "*",
"vite": ">=2.0.0",
"vls": "*",
"vti": "*",
"vue-tsc": "~2.1.6"
},
"peerDependenciesMeta": {
"@biomejs/biome": {
"optional": true
},
"eslint": {
"optional": true
},
"meow": {
"optional": true
},
"optionator": {
"optional": true
},
"stylelint": {
"optional": true
},
"typescript": {
"optional": true
},
"vls": {
"optional": true
},
"vti": {
"optional": true
},
"vue-tsc": {
"optional": true
}
}
},
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@ -40093,69 +39971,6 @@
"@esbuild/win32-x64": "0.24.2"
}
},
"node_modules/vscode-jsonrpc": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz",
"integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.0.0 || >=10.0.0"
}
},
"node_modules/vscode-languageclient": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz",
"integrity": "sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg==",
"dev": true,
"license": "MIT",
"dependencies": {
"minimatch": "^3.0.4",
"semver": "^7.3.4",
"vscode-languageserver-protocol": "3.16.0"
},
"engines": {
"vscode": "^1.52.0"
}
},
"node_modules/vscode-languageserver": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz",
"integrity": "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==",
"dev": true,
"license": "MIT",
"dependencies": {
"vscode-languageserver-protocol": "3.16.0"
},
"bin": {
"installServerIntoExtension": "bin/installServerIntoExtension"
}
},
"node_modules/vscode-languageserver-protocol": {
"version": "3.16.0",
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz",
"integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==",
"dev": true,
"license": "MIT",
"dependencies": {
"vscode-jsonrpc": "6.0.0",
"vscode-languageserver-types": "3.16.0"
}
},
"node_modules/vscode-languageserver-textdocument": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz",
"integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==",
"dev": true,
"license": "MIT"
},
"node_modules/vscode-languageserver-types": {
"version": "3.16.0",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz",
"integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==",
"dev": true,
"license": "MIT"
},
"node_modules/vscode-oniguruma": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz",
@ -40168,13 +39983,6 @@
"integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==",
"license": "MIT"
},
"node_modules/vscode-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
"integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
"dev": true,
"license": "MIT"
},
"node_modules/wait-on": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.2.tgz",

View File

@ -53,8 +53,16 @@ export class AuthClient {
}
public emailPassword = {
signIn: async (data: TEmailPasswordSignin) => {
const response = await this.client['email-password'].authorize.$post({ json: data });
signIn: async (data: Omit<TEmailPasswordSignin, 'csrfToken'>) => {
const { csrfToken } = await this.client.csrf.$get().then(async (res) => res.json());
const response = await this.client['email-password'].authorize.$post({
json: {
...data,
csrfToken,
},
});
await this.handleError(response);
handleSignInRedirect(data.redirectPath);

View File

@ -2,9 +2,11 @@ import { Hono } from 'hono';
import { HTTPException } from 'hono/http-exception';
import type { ContentfulStatusCode } from 'hono/utils/http-status';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { extractRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
import { setCsrfCookie } from './lib/session/session-cookies';
import { emailPasswordRoute } from './routes/email-password';
import { googleRoute } from './routes/google';
import { passkeyRoute } from './routes/passkey';
@ -16,8 +18,28 @@ import type { HonoAuthContext } from './types/context';
export const auth = new Hono<HonoAuthContext>()
.use(async (c, next) => {
c.set('requestMetadata', extractRequestMetadata(c.req.raw));
// Todo: Maybe use auth URL.
const validOrigin = new URL(NEXT_PUBLIC_WEBAPP_URL()).origin;
const headerOrigin = c.req.header('Origin');
if (headerOrigin && headerOrigin !== validOrigin) {
return c.json(
{
message: 'Forbidden',
statusCode: 403,
},
403,
);
}
await next();
})
.get('/csrf', async (c) => {
const csrfToken = await setCsrfCookie(c);
return c.json({ csrfToken });
})
.route('/', sessionRoute)
.route('/', signOutRoute)
.route('/email-password', emailPasswordRoute)

View File

@ -6,6 +6,8 @@ import { useSecureCookies } from '@documenso/lib/constants/auth';
import { appLog } from '@documenso/lib/utils/debugger';
import { env } from '@documenso/lib/utils/env';
import { generateSessionToken } from './session';
export const sessionCookieName = 'sessionId';
const getAuthSecret = () => {
@ -30,7 +32,7 @@ const getAuthDomain = () => {
export const sessionCookieOptions = {
httpOnly: true,
path: '/',
sameSite: useSecureCookies ? 'none' : 'lax',
sameSite: useSecureCookies ? 'none' : 'lax', // Todo: This feels wrong?
secure: useSecureCookies,
domain: getAuthDomain(),
// Todo: Max age for specific auth cookies.
@ -89,3 +91,23 @@ export const setSessionCookie = async (c: Context, sessionToken: string) => {
export const deleteSessionCookie = (c: Context) => {
deleteCookie(c, sessionCookieName, sessionCookieOptions);
};
export const getCsrfCookie = async (c: Context) => {
const csrfToken = await getSignedCookie(c, getAuthSecret(), 'csrfToken');
return csrfToken || null;
};
export const setCsrfCookie = async (c: Context) => {
const csrfToken = generateSessionToken();
await setSignedCookie(c, 'csrfToken', csrfToken, getAuthSecret(), {
...sessionCookieOptions,
// Explicity set to undefined for session lived cookie.
expires: undefined,
maxAge: undefined,
});
return csrfToken;
};

View File

@ -25,6 +25,7 @@ import { prisma } from '@documenso/prisma';
import { UserSecurityAuditLogType } from '@documenso/prisma/client';
import { AuthenticationErrorCode } from '../lib/errors/error-codes';
import { getCsrfCookie } from '../lib/session/session-cookies';
import { onAuthorize } from '../lib/utils/authorizer';
import { getRequiredSession, getSession } from '../lib/utils/get-session';
import type { HonoAuthContext } from '../types/context';
@ -45,7 +46,16 @@ export const emailPasswordRoute = new Hono<HonoAuthContext>()
.post('/authorize', sValidator('json', ZSignInSchema), async (c) => {
const requestMetadata = c.get('requestMetadata');
const { email, password, totpCode, backupCode } = c.req.valid('json');
const { email, password, totpCode, backupCode, csrfToken } = c.req.valid('json');
const csrfCookieToken = await getCsrfCookie(c);
// Todo: Add logging here.
if (csrfToken !== csrfCookieToken || !csrfCookieToken) {
throw new AppError(AuthenticationErrorCode.InvalidRequest, {
message: 'Invalid CSRF token',
});
}
const user = await prisma.user.findFirst({
where: {

View File

@ -10,6 +10,7 @@ export const ZSignInSchema = z.object({
password: ZCurrentPasswordSchema,
totpCode: z.string().trim().optional(),
backupCode: z.string().trim().optional(),
csrfToken: z.string().trim(),
});
export type TSignInSchema = z.infer<typeof ZSignInSchema>;