Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
c54ad17
Updates react-email to @latest version
samejr Aug 3, 2025
793ceee
Remove unncessary styles from <hr>
samejr Aug 3, 2025
e6376f6
Email Footer component uses tailwind
samejr Aug 3, 2025
7f83b93
Adds triangle logo
samejr Aug 3, 2025
33d3ac1
Updates the magic link email
samejr Aug 3, 2025
96eaf89
Merge branch 'main' into nicer-app-emails
samejr Sep 7, 2025
bd964a4
Fixed broken links in the welcome email and improved the copy
samejr Sep 7, 2025
b6a2a66
Improve copy in magic link email
samejr Sep 7, 2025
4439bf8
Updates invite email with newer components and tailwind
samejr Sep 7, 2025
a42c799
Style improvements
samejr Sep 7, 2025
8751001
Merge branch 'main' into nicer-app-emails
samejr Sep 7, 2025
78dbf36
import tailwind from other package (coderabbit suggestion)
samejr Sep 7, 2025
d3a7fcc
Updatea email preview package version
samejr Sep 7, 2025
6677844
Fixes typescript errors
samejr Sep 7, 2025
0d29cef
Merge branch 'main' into nicer-app-emails
nicktrn Sep 24, 2025
a645850
chore: update lockfile
nicktrn Sep 24, 2025
ff5037d
fix(engine): limit the number of snapshots returned when getting late…
ericallam Sep 25, 2025
5f08990
chore(engine): add additional logging when we fail to get snapshots s…
ericallam Sep 25, 2025
543d9cd
fix(engine) truncate errors before storing them on a run and waitpoin…
ericallam Sep 25, 2025
9ef745d
fix(webapp): project scoping for runs (#2553)
myftija Sep 25, 2025
56c19b9
chore(run-engine): improve concurrency sweeper logging to get better …
ericallam Sep 25, 2025
88aa95b
fix(webapp): org invite scoping (#2554)
myftija Sep 25, 2025
11d1c4a
fix(webapp): org scoping issues in plan selection, alerts, pats and u…
myftija Sep 25, 2025
82d158d
fix: use higher entropy invite tokens (#2558)
myftija Sep 25, 2025
8dfeb0a
feat(server): add two admin endpoints for queue and environment concu…
ericallam Sep 25, 2025
5c06ba1
chore(run-engine): add additional logging around dequeueing and worke…
ericallam Sep 26, 2025
f23c3c1
fix(run-engine): carryover batchId after PENDING_EXECUTING stalls (#2…
nicktrn Sep 26, 2025
94d96b8
feat(run-engine): ability to repair runs in QUEUED, SUSPENDED, and FI…
ericallam Sep 26, 2025
0b7ea8a
fix(run-engine): pass through engine fair dequeue selection strategy …
ericallam Sep 26, 2025
3c6ee30
fix(run-engine): waitpoint update misleading error logs (#2566)
myftija Sep 26, 2025
b4278cb
fix(webapp): add recommended security headers (#2569)
myftija Sep 29, 2025
1d3b50d
feat(webapp): rate limit magic-link login attempts (#2568)
myftija Sep 29, 2025
d1152c3
feat(helm): support topology spread constraints for webapp (#2560)
nicktrn Sep 30, 2025
959f04c
chore(helm): migrate to bitnami legacy registry and add configurable …
nicktrn Sep 30, 2025
fe29988
Update docs/idempotency.mdx (#2575)
mintlify[bot] Sep 30, 2025
03e6bce
Adds 200 and 500 % billing alert options (#2571)
samejr Sep 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/webapp/app/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const EnvironmentSchema = z
ADMIN_EMAILS: z.string().refine(isValidRegex, "ADMIN_EMAILS must be a valid regex.").optional(),
REMIX_APP_PORT: z.string().optional(),
LOGIN_ORIGIN: z.string().default("http://localhost:3030"),
LOGIN_RATE_LIMITS_ENABLED: BoolEnv.default(true),
APP_ORIGIN: z.string().default("http://localhost:3030"),
API_ORIGIN: z.string().optional(),
STREAM_ORIGIN: z.string().optional(),
Expand Down
91 changes: 58 additions & 33 deletions apps/webapp/app/models/member.server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { prisma } from "~/db.server";
import { type Prisma, prisma } from "~/db.server";
import { createEnvironment } from "./organization.server";
import { customAlphabet } from "nanoid";

const tokenValueLength = 40;
const tokenGenerator = customAlphabet("123456789abcdefghijkmnopqrstuvwxyz", tokenValueLength);

export async function getTeamMembersAndInvites({
userId,
Expand Down Expand Up @@ -95,14 +99,19 @@ export async function inviteMembers({
throw new Error("User does not have access to this organization");
}

const created = await prisma.orgMemberInvite.createMany({
data: emails.map((email) => ({
email,
organizationId: org.id,
inviterId: userId,
role: "MEMBER",
})),
skipDuplicates: true,
const invites = [...new Set(emails)].map(
(email) =>
({
email,
token: tokenGenerator(),
organizationId: org.id,
inviterId: userId,
role: "MEMBER",
} satisfies Prisma.OrgMemberInviteCreateManyInput)
);

await prisma.orgMemberInvite.createMany({
data: invites,
});

return await prisma.orgMemberInvite.findMany({
Expand Down Expand Up @@ -147,12 +156,19 @@ export async function getUsersInvites({ email }: { email: string }) {
});
}

export async function acceptInvite({ userId, inviteId }: { userId: string; inviteId: string }) {
export async function acceptInvite({
user,
inviteId,
}: {
user: { id: string; email: string };
inviteId: string;
}) {
return await prisma.$transaction(async (tx) => {
// 1. Delete the invite and get the invite details
const invite = await tx.orgMemberInvite.delete({
where: {
id: inviteId,
email: user.email,
},
include: {
organization: {
Expand All @@ -167,7 +183,7 @@ export async function acceptInvite({ userId, inviteId }: { userId: string; invit
const member = await tx.orgMember.create({
data: {
organizationId: invite.organizationId,
userId,
userId: user.id,
role: invite.role,
},
});
Expand All @@ -187,47 +203,49 @@ export async function acceptInvite({ userId, inviteId }: { userId: string; invit
// 4. Check for other invites
const remainingInvites = await tx.orgMemberInvite.findMany({
where: {
email: invite.email,
email: user.email,
},
});

return { remainingInvites, organization: invite.organization };
});
}

export async function declineInvite({ userId, inviteId }: { userId: string; inviteId: string }) {
export async function declineInvite({
user,
inviteId,
}: {
user: { id: string; email: string };
inviteId: string;
}) {
return await prisma.$transaction(async (tx) => {
//1. delete invite
const declinedInvite = await prisma.orgMemberInvite.delete({
where: {
id: inviteId,
email: user.email,
},
include: {
organization: true,
},
});

//2. get email
const user = await prisma.user.findUnique({
where: { id: userId },
select: { email: true },
});

//3. check for other invites
//2. check for other invites
const remainingInvites = await prisma.orgMemberInvite.findMany({
where: {
email: user!.email,
email: user.email,
},
});

return { remainingInvites, organization: declinedInvite.organization };
});
}

export async function resendInvite({ inviteId }: { inviteId: string }) {
export async function resendInvite({ inviteId, userId }: { inviteId: string; userId: string }) {
return await prisma.orgMemberInvite.update({
where: {
id: inviteId,
inviterId: userId,
},
data: {
updatedAt: new Date(),
Expand All @@ -241,26 +259,27 @@ export async function resendInvite({ inviteId }: { inviteId: string }) {

export async function revokeInvite({
userId,
slug,
orgSlug,
inviteId,
}: {
userId: string;
slug: string;
orgSlug: string;
inviteId: string;
}) {
const org = await prisma.organization.findFirst({
where: { slug, members: { some: { userId } } },
});

if (!org) {
throw new Error("User does not have access to this organization");
}
const invite = await prisma.orgMemberInvite.delete({
const invite = await prisma.orgMemberInvite.findFirst({
where: {
id: inviteId,
organizationId: org.id,
organization: {
slug: orgSlug,
members: {
some: {
userId,
},
},
},
},
select: {
id: true,
email: true,
organization: true,
},
Expand All @@ -270,5 +289,11 @@ export async function revokeInvite({
throw new Error("Invite not found");
}

await prisma.orgMemberInvite.delete({
where: {
id: invite.id,
},
});

return { email: invite.email, organization: invite.organization };
}
9 changes: 7 additions & 2 deletions apps/webapp/app/presenters/v3/RunPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,13 @@ export class RunPresenter {
public async call({
userId,
projectSlug,
organizationSlug,
environmentSlug,
runFriendlyId,
showDeletedLogs,
showDebug,
}: {
userId: string;
projectSlug: string;
organizationSlug: string;
environmentSlug: string;
runFriendlyId: string;
showDeletedLogs: boolean;
Expand Down Expand Up @@ -93,6 +91,13 @@ export class RunPresenter {
friendlyId: runFriendlyId,
project: {
slug: projectSlug,
organization: {
members: {
some: {
userId,
},
},
},
},
},
});
Expand Down
49 changes: 27 additions & 22 deletions apps/webapp/app/presenters/v3/SpanPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,26 @@ type GetSpanResult = NonNullable<Awaited<ReturnType<(typeof eventRepository)["ge

export class SpanPresenter extends BasePresenter {
public async call({
userId,
projectSlug,
spanId,
runFriendlyId,
}: {
userId: string;
projectSlug: string;
spanId: string;
runFriendlyId: string;
}) {
const project = await this._replica.project.findFirst({
where: {
slug: projectSlug,
organization: {
members: {
some: {
userId,
},
},
},
},
});

Expand All @@ -57,20 +66,19 @@ export class SpanPresenter extends BasePresenter {
},
where: {
friendlyId: runFriendlyId,
projectId: project.id,
},
});

if (!parentRun) {
return;
}

const { traceId } = parentRun;

const eventStore = getTaskEventStoreTableForRun(parentRun);

const run = await this.getRun({
eventStore,
traceId,
environmentId: parentRun.runtimeEnvironmentId,
spanId,
createdAt: parentRun.createdAt,
completedAt: parentRun.completedAt,
Expand All @@ -82,10 +90,8 @@ export class SpanPresenter extends BasePresenter {
};
}

//get the run
const span = await this.#getSpan({
eventStore,
traceId,
spanId,
environmentId: parentRun.runtimeEnvironmentId,
projectId: parentRun.projectId,
Expand All @@ -105,24 +111,24 @@ export class SpanPresenter extends BasePresenter {

async getRun({
eventStore,
traceId,
environmentId,
spanId,
createdAt,
completedAt,
}: {
eventStore: TaskEventStoreTable;
traceId: string;
environmentId: string;
spanId: string;
createdAt: Date;
completedAt: Date | null;
}) {
const span = await eventRepository.getSpan(
eventStore,
const span = await eventRepository.getSpan({
storeTable: eventStore,
spanId,
traceId,
createdAt,
completedAt ?? undefined
);
environmentId,
startCreatedAt: createdAt,
endCreatedAt: completedAt ?? undefined,
});

if (!span) {
return;
Expand Down Expand Up @@ -412,29 +418,28 @@ export class SpanPresenter extends BasePresenter {

async #getSpan({
eventStore,
traceId,
spanId,
environmentId,
projectId,
createdAt,
completedAt,
}: {
traceId: string;
spanId: string;
environmentId: string;
projectId: string;
eventStore: TaskEventStoreTable;
createdAt: Date;
completedAt: Date | null;
}) {
const span = await eventRepository.getSpan(
eventStore,
const span = await eventRepository.getSpan({
storeTable: eventStore,
spanId,
traceId,
createdAt,
completedAt ?? undefined,
{ includeDebugLogs: true }
);
environmentId,
startCreatedAt: createdAt,
endCreatedAt: completedAt ?? undefined,
options: { includeDebugLogs: true },
});

if (!span) {
return;
}
Expand Down
7 changes: 7 additions & 0 deletions apps/webapp/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ export const links: LinksFunction = () => {
return [{ rel: "stylesheet", href: tailwindStylesheetUrl }];
};

export const headers = () => ({
"Referrer-Policy": "strict-origin-when-cross-origin",
"X-Content-Type-Options": "nosniff",
"Permissions-Policy":
"geolocation=(), microphone=(), camera=(), accelerometer=(), gyroscope=(), magnetometer=(), payment=(), usb=()",
});

export const meta: MetaFunction = ({ data }) => {
const typedData = data as UseDataFunctionReturn<typeof loader>;
return [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const [error, result] = await tryCatch(
presenter.call({
userId,
organizationSlug,
showDeletedLogs: !!impersonationId,
projectSlug: projectParam,
runFriendlyId: runParam,
Expand Down
Loading