Compare commits

..

9 Commits

Author SHA1 Message Date
Ephraim Atta-Duncan 5cb03a9448 Merge branch 'main' into feat/handle-redirectto-param 2025-06-16 22:02:57 +00:00
Timur Ercan 4fd8a767b2 chore: Update README.md (#1840) 2025-06-13 22:42:38 +10:00
David Nguyen b8e08e88ac fix: api keys not showing (#1839) 2025-06-13 17:20:03 +10:00
David Nguyen 031a7b9e36 fix: visibility 2025-06-13 01:02:40 +10:00
David Nguyen 12fe045195 fix: visiblity 2025-06-13 00:05:08 +10:00
David Nguyen 614106a5e4 fix: rework documents limits logic (#1836) 2025-06-12 13:42:31 +10:00
Ephraim Duncan 266f2eab71 Merge branch 'main' into feat/handle-redirectto-param 2025-05-23 03:12:45 +00:00
Ephraim Atta-Duncan e1fc49fa49 chore: use cookie in production 2025-05-21 11:36:30 +00:00
Ephraim Atta-Duncan d213b378b8 feat: handle redirectTo query parameter in middleware 2025-05-21 11:31:00 +00:00
10 changed files with 108 additions and 123 deletions
-2
View File
@@ -49,8 +49,6 @@ Join us in creating the next generation of open trust infrastructure.
## Community and Next Steps 🎯
We're currently working on a redesign of the application, including a revamp of the codebase, so Documenso can be more intuitive to use and robust to develop upon.
- Check out the first source code release in this repository and test it.
- Tell us what you think in the [Discussions](https://github.com/documenso/documenso/discussions).
- Join the [Discord server](https://documen.so/discord) for any questions and getting to know to other community members.
@@ -83,7 +83,9 @@ export const OrganisationCreateDialog = ({ trigger, ...props }: OrganisationCrea
const { mutateAsync: createOrganisation } = trpc.organisation.create.useMutation();
const { data: plansData } = trpc.billing.plans.get.useQuery();
const { data: plansData } = trpc.billing.plans.get.useQuery(undefined, {
enabled: IS_BILLING_ENABLED(),
});
const onFormSubmit = async ({ name }: TCreateOrganisationFormSchema) => {
try {
@@ -1,6 +1,7 @@
import { Trans } from '@lingui/react/macro';
import { Link, redirect } from 'react-router';
import { extractCookieFromHeaders } from '@documenso/auth/server/lib/utils/cookies';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import {
IS_GOOGLE_SSO_ENABLED,
@@ -20,13 +21,18 @@ export function meta() {
export async function loader({ request }: Route.LoaderArgs) {
const { isAuthenticated } = await getOptionalSession(request);
const redirectToCookie = extractCookieFromHeaders('redirectTo', request.headers);
const redirectToAfterLogin = redirectToCookie ? decodeURIComponent(redirectToCookie) : '';
// SSR env variables.
const isGoogleSSOEnabled = IS_GOOGLE_SSO_ENABLED;
const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED;
const oidcProviderLabel = OIDC_PROVIDER_LABEL;
if (isAuthenticated) {
if (redirectToAfterLogin) {
throw redirect(redirectToAfterLogin);
}
throw redirect('/');
}
@@ -34,11 +40,13 @@ export async function loader({ request }: Route.LoaderArgs) {
isGoogleSSOEnabled,
isOIDCSSOEnabled,
oidcProviderLabel,
redirectToAfterLogin,
};
}
export default function SignIn({ loaderData }: Route.ComponentProps) {
const { isGoogleSSOEnabled, isOIDCSSOEnabled, oidcProviderLabel } = loaderData;
const { isGoogleSSOEnabled, isOIDCSSOEnabled, oidcProviderLabel, redirectToAfterLogin } =
loaderData;
return (
<div className="w-screen max-w-lg px-4">
@@ -56,6 +64,7 @@ export default function SignIn({ loaderData }: Route.ComponentProps) {
isGoogleSSOEnabled={isGoogleSSOEnabled}
isOIDCSSOEnabled={isOIDCSSOEnabled}
oidcProviderLabel={oidcProviderLabel}
returnTo={redirectToAfterLogin ?? undefined}
/>
{env('NEXT_PUBLIC_DISABLE_SIGNUP') !== 'true' && (
+22 -2
View File
@@ -20,12 +20,33 @@ export const appMiddleware = async (c: Context, next: Next) => {
const { req } = c;
const { path } = req;
// PRE-HANDLER CODE: Place code here to execute BEFORE the route handler runs.
const redirectTo = req.query('redirectTo');
if (redirectTo) {
if (redirectTo.startsWith('/') && !redirectTo.startsWith('//') && !redirectTo.includes('..')) {
debug.log('Setting redirectTo cookie to:', redirectTo);
setCookie(c, 'redirectTo', redirectTo, {
path: '/',
httpOnly: true,
sameSite: 'Lax',
maxAge: 150,
secure: process.env.NODE_ENV === 'production',
});
debug.log('Redirecting to (from param):', redirectTo);
return c.redirect(redirectTo, 307);
} else {
debug.log('Invalid redirectTo parameter encountered:', redirectTo);
}
}
// Paths to ignore.
if (nonPagePathRegex.test(path)) {
return next();
}
// PRE-HANDLER CODE: Place code here to execute BEFORE the route handler runs.
// Handle team-based routing redirects (documents/templates to team URLs)
const redirectPath = await handleRedirects(c);
if (redirectPath) {
@@ -34,7 +55,6 @@ export const appMiddleware = async (c: Context, next: Next) => {
return c.redirect(redirectPath);
}
await next();
// POST-HANDLER CODE: Place code here to execute AFTER the route handler completes.
+31 -37
View File
@@ -66,7 +66,7 @@ export const getServerLimits = async ({
};
}
// If plan expired.
// Early return for users with an expired subscription.
if (subscription && subscription.status !== SubscriptionStatus.ACTIVE) {
return {
quota: INACTIVE_PLAN_LIMITS,
@@ -74,52 +74,46 @@ export const getServerLimits = async ({
};
}
if (subscription && organisation.organisationClaim.flags.unlimitedDocuments) {
// Allow unlimited documents for users with an unlimited documents claim.
// This also allows "free" claim users without subscriptions if they have this flag.
if (organisation.organisationClaim.flags.unlimitedDocuments) {
return {
quota: PAID_PLAN_LIMITS,
remaining: PAID_PLAN_LIMITS,
};
}
// If free tier or plan does not have unlimited documents.
if (!subscription || !organisation.organisationClaim.flags.unlimitedDocuments) {
const [documents, directTemplates] = await Promise.all([
prisma.document.count({
where: {
team: {
organisationId: organisation.id,
},
createdAt: {
gte: DateTime.utc().startOf('month').toJSDate(),
},
source: {
not: DocumentSource.TEMPLATE_DIRECT_LINK,
},
const [documents, directTemplates] = await Promise.all([
prisma.document.count({
where: {
team: {
organisationId: organisation.id,
},
}),
prisma.template.count({
where: {
team: {
organisationId: organisation.id,
},
directLink: {
isNot: null,
},
createdAt: {
gte: DateTime.utc().startOf('month').toJSDate(),
},
}),
]);
source: {
not: DocumentSource.TEMPLATE_DIRECT_LINK,
},
},
}),
prisma.template.count({
where: {
team: {
organisationId: organisation.id,
},
directLink: {
isNot: null,
},
},
}),
]);
remaining.documents = Math.max(remaining.documents - documents, 0);
remaining.directTemplates = Math.max(remaining.directTemplates - directTemplates, 0);
return {
quota,
remaining,
};
}
remaining.documents = Math.max(remaining.documents - documents, 0);
remaining.directTemplates = Math.max(remaining.directTemplates - directTemplates, 0);
return {
quota: PAID_PLAN_LIMITS,
remaining: PAID_PLAN_LIMITS,
quota,
remaining,
};
};
@@ -1,5 +1,5 @@
import type { Prisma } from '@prisma/client';
import { TeamMemberRole } from '@prisma/client';
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
import { match } from 'ts-pattern';
import { prisma } from '@documenso/prisma';
@@ -83,9 +83,46 @@ export const getDocumentWhereInput = async ({
}: GetDocumentWhereInputOptions) => {
const team = await getTeamById({ teamId, userId });
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
});
const teamVisibilityFilters = match(team.currentTeamRole)
.with(TeamMemberRole.ADMIN, () => [
DocumentVisibility.EVERYONE,
DocumentVisibility.MANAGER_AND_ABOVE,
DocumentVisibility.ADMIN,
])
.with(TeamMemberRole.MANAGER, () => [
DocumentVisibility.EVERYONE,
DocumentVisibility.MANAGER_AND_ABOVE,
])
.otherwise(() => [DocumentVisibility.EVERYONE]);
const documentOrInput: Prisma.DocumentWhereInput[] = [
// Allow access if they own the document.
{
teamId: team.id,
userId,
},
// Or, if they belong to the team that the document is associated with.
{
visibility: {
in: teamVisibilityFilters,
},
teamId,
},
// Or, if they are a recipient of the document.
{
status: {
not: DocumentStatus.DRAFT,
},
recipients: {
some: {
email: user.email,
},
},
},
];
@@ -112,45 +149,8 @@ export const getDocumentWhereInput = async ({
OR: documentOrInput,
};
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
},
});
const visibilityFilters = [
...match(team.currentTeamRole)
.with(TeamMemberRole.ADMIN, () => [
{ visibility: DocumentVisibility.EVERYONE },
{ visibility: DocumentVisibility.MANAGER_AND_ABOVE },
{ visibility: DocumentVisibility.ADMIN },
])
.with(TeamMemberRole.MANAGER, () => [
{ visibility: DocumentVisibility.EVERYONE },
{ visibility: DocumentVisibility.MANAGER_AND_ABOVE },
])
.otherwise(() => [{ visibility: DocumentVisibility.EVERYONE }]),
{
OR: [
{
recipients: {
some: {
email: user.email,
},
},
},
{
userId: user.id,
},
],
},
];
return {
documentWhereInput: {
...documentWhereInput,
OR: [...visibilityFilters],
},
documentWhereInput,
team,
};
};
@@ -1,15 +0,0 @@
import { prisma } from '@documenso/prisma';
export type GetApiTokenByIdOptions = {
id: number;
userId: number;
};
export const getApiTokenById = async ({ id, userId }: GetApiTokenByIdOptions) => {
return await prisma.apiToken.findFirstOrThrow({
where: {
id,
userId,
},
});
};
@@ -11,7 +11,6 @@ export type GetApiTokensOptions = {
export const getApiTokens = async ({ userId, teamId }: GetApiTokensOptions) => {
return await prisma.apiToken.findMany({
where: {
userId,
team: buildTeamWhereQuery({
teamId,
userId,
@@ -1,31 +1,15 @@
import { createApiToken } from '@documenso/lib/server-only/public-api/create-api-token';
import { deleteTokenById } from '@documenso/lib/server-only/public-api/delete-api-token-by-id';
import { getApiTokenById } from '@documenso/lib/server-only/public-api/get-api-token-by-id';
import { getApiTokens } from '@documenso/lib/server-only/public-api/get-api-tokens';
import { authenticatedProcedure, router } from '../trpc';
import {
ZCreateTokenMutationSchema,
ZDeleteTokenByIdMutationSchema,
ZGetApiTokenByIdQuerySchema,
} from './schema';
import { ZCreateTokenMutationSchema, ZDeleteTokenByIdMutationSchema } from './schema';
export const apiTokenRouter = router({
getTokens: authenticatedProcedure.query(async ({ ctx }) => {
return await getApiTokens({ userId: ctx.user.id, teamId: ctx.teamId });
}),
getTokenById: authenticatedProcedure
.input(ZGetApiTokenByIdQuerySchema)
.query(async ({ input, ctx }) => {
const { id } = input;
return await getApiTokenById({
id,
userId: ctx.user.id,
});
}),
createToken: authenticatedProcedure
.input(ZCreateTokenMutationSchema)
.mutation(async ({ input, ctx }) => {
@@ -1,11 +1,5 @@
import { z } from 'zod';
export const ZGetApiTokenByIdQuerySchema = z.object({
id: z.number().min(1),
});
export type TGetApiTokenByIdQuerySchema = z.infer<typeof ZGetApiTokenByIdQuerySchema>;
export const ZCreateTokenMutationSchema = z.object({
teamId: z.number(),
tokenName: z.string().min(3, { message: 'The token name should be 3 characters or longer' }),