Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e3080a9

Browse files
committed
Merge dev into dashboard/ui/user-teams and resolve conflicts
2 parents d89b904 + a03774f commit e3080a9

File tree

109 files changed

+5488
-1418
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

109 files changed

+5488
-1418
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
name: Sync Reviewers and Assignees
2+
3+
on:
4+
pull_request_target:
5+
types: [review_requested]
6+
pull_request_review:
7+
types: [submitted]
8+
9+
permissions:
10+
contents: read
11+
pull-requests: write
12+
issues: write
13+
14+
jobs:
15+
sync:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- name: Add reviewer as assignee
19+
if: ${{ github.event_name == 'pull_request' && github.event.action == 'review_requested' }}
20+
uses: actions/github-script@v7
21+
with:
22+
script: |
23+
const pr = context.payload.pull_request;
24+
const reviewer = context.payload.requested_reviewer;
25+
if (!reviewer || !reviewer.login) {
26+
core.info('No individual reviewer in payload (team review or missing). Skipping.');
27+
return;
28+
}
29+
await github.rest.issues.addAssignees({
30+
owner: context.repo.owner,
31+
repo: context.repo.repo,
32+
issue_number: pr.number,
33+
assignees: [reviewer.login],
34+
});
35+
core.info(`Assigned ${reviewer.login} to PR #${pr.number}`);
36+
37+
- name: Remove reviewer from assignees on review submission
38+
if: ${{ github.event_name == 'pull_request_review' && github.event.action == 'submitted' }}
39+
uses: actions/github-script@v7
40+
with:
41+
script: |
42+
const pr = context.payload.pull_request;
43+
const reviewer = context.payload.review?.user;
44+
const state = context.payload.review?.state?.toLowerCase();
45+
const author = pr.user;
46+
if (!reviewer || !reviewer.login) {
47+
core.info('No reviewer user found in payload. Skipping.');
48+
return;
49+
}
50+
if (!state) {
51+
core.info('No review state provided. Skipping.');
52+
return;
53+
}
54+
if (!['approved', 'changes_requested'].includes(state)) {
55+
core.info(`Review state is '${state}' (likely a comment). Not removing assignee.`);
56+
return;
57+
}
58+
if (author && reviewer.login === author.login) {
59+
core.info('Reviewer is the PR author. Not removing assignee.');
60+
return;
61+
}
62+
await github.rest.issues.removeAssignees({
63+
owner: context.repo.owner,
64+
repo: context.repo.repo,
65+
issue_number: pr.number,
66+
assignees: [reviewer.login],
67+
});
68+
core.info(`Removed ${reviewer.login} from assignees on PR #${pr.number}`);
69+
70+
if (author?.login) {
71+
await github.rest.issues.addAssignees({
72+
owner: context.repo.owner,
73+
repo: context.repo.repo,
74+
issue_number: pr.number,
75+
assignees: [author.login],
76+
});
77+
core.info(`Re-assigned PR author ${author.login} to PR #${pr.number}`);
78+
} else {
79+
core.info('No PR author found to re-assign.');
80+
}

apps/backend/.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ NEXT_PUBLIC_STACK_API_URL=# the base URL of Stack's backend/API. For local devel
33
NEXT_PUBLIC_STACK_DASHBOARD_URL=# the URL of Stack's dashboard. For local development, this is `http://localhost:8101`; for the managed service, this is `https://app.stack-auth.com`.
44
STACK_SECRET_SERVER_KEY=# a random, unguessable secret key generated by `pnpm generate-keys`
55

6+
67
# seed script settings
78
STACK_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED=# true to enable user sign up to the dashboard when seeding
89
STACK_SEED_INTERNAL_PROJECT_OTP_ENABLED=# true to add OTP auth to the dashboard when seeding
@@ -82,3 +83,4 @@ STACK_OPENAI_API_KEY=# enter your openai api key
8283
STACK_FEATUREBASE_API_KEY=# enter your featurebase api key
8384
STACK_STRIPE_SECRET_KEY=# enter your stripe api key
8485
STACK_STRIPE_WEBHOOK_SECRET=# enter your stripe webhook secret
86+

apps/backend/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,9 @@
6161
"@prisma/adapter-pg": "^6.12.0",
6262
"@prisma/client": "^6.12.0",
6363
"@prisma/instrumentation": "^6.12.0",
64-
"@sentry/nextjs": "^8.40.0",
64+
"@sentry/nextjs": "^10.11.0",
6565
"@simplewebauthn/server": "^11.0.0",
66+
"@stackframe/stack": "workspace:*",
6667
"@stackframe/stack-shared": "workspace:*",
6768
"@upstash/qstash": "^2.8.2",
6869
"@vercel/functions": "^2.0.0",
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
-- CreateEnum
2+
CREATE TYPE "DraftThemeMode" AS ENUM ('PROJECT_DEFAULT', 'NONE', 'CUSTOM');
3+
4+
-- CreateTable
5+
CREATE TABLE "EmailDraft" (
6+
"tenancyId" UUID NOT NULL,
7+
"id" UUID NOT NULL,
8+
"displayName" TEXT NOT NULL,
9+
"themeMode" "DraftThemeMode" NOT NULL DEFAULT 'PROJECT_DEFAULT',
10+
"themeId" TEXT,
11+
"tsxSource" TEXT NOT NULL,
12+
"sentAt" TIMESTAMP(3),
13+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
14+
"updatedAt" TIMESTAMP(3) NOT NULL,
15+
16+
CONSTRAINT "EmailDraft_pkey" PRIMARY KEY ("tenancyId","id")
17+
);

apps/backend/prisma/schema.prisma

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,29 @@ model SentEmail {
673673
@@id([tenancyId, id])
674674
}
675675

676+
model EmailDraft {
677+
tenancyId String @db.Uuid
678+
679+
id String @default(uuid()) @db.Uuid
680+
681+
displayName String
682+
themeMode DraftThemeMode @default(PROJECT_DEFAULT)
683+
themeId String?
684+
tsxSource String
685+
sentAt DateTime?
686+
687+
createdAt DateTime @default(now())
688+
updatedAt DateTime @updatedAt
689+
690+
@@id([tenancyId, id])
691+
}
692+
693+
enum DraftThemeMode {
694+
PROJECT_DEFAULT
695+
NONE
696+
CUSTOM
697+
}
698+
676699
model CliAuthAttempt {
677700
tenancyId String @db.Uuid
678701

apps/backend/prisma/seed.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,13 @@ async function seed() {
9090
projectId: 'internal',
9191
branchId: DEFAULT_BRANCH_ID,
9292
environmentConfigOverrideOverride: {
93+
dataVault: {
94+
stores: {
95+
'neon-connection-strings': {
96+
displayName: 'Neon Connection Strings',
97+
}
98+
}
99+
},
93100
payments: {
94101
groups: {
95102
plans: {

apps/backend/src/app/api/latest/auth/sessions/crud.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const sessionsCrudHandlers = createLazyProxy(() => createCrudHandlers(ses
1818
}).defined(),
1919
onList: async ({ auth, query }) => {
2020
const prisma = await getPrismaClientForTenancy(auth.tenancy);
21-
const schema = getPrismaSchemaForTenancy(auth.tenancy);
21+
const schema = await getPrismaSchemaForTenancy(auth.tenancy);
2222
const listImpersonations = auth.type === 'admin';
2323

2424
if (auth.type === 'client') {

apps/backend/src/app/api/latest/connected-accounts/[user_id]/[provider_id]/access-token/crud.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,18 +141,18 @@ export const connectedAccountAccessTokenCrudHandlers = createLazyProxy(() => cre
141141
});
142142

143143
if (tokenSet.refreshToken) {
144-
// remove the old token, add the new token to the DB
145-
await prisma.oAuthToken.deleteMany({
146-
where: {
147-
refreshToken: token.refreshToken,
148-
},
144+
// mark the old token as invalid, add the new token to the DB
145+
const oldToken = token;
146+
await prisma.oAuthToken.update({
147+
where: { id: oldToken.id },
148+
data: { isValid: false },
149149
});
150150
await prisma.oAuthToken.create({
151151
data: {
152152
tenancyId: auth.tenancy.id,
153153
refreshToken: tokenSet.refreshToken,
154-
oauthAccountId: token.projectUserOAuthAccount.id,
155-
scopes: token.scopes,
154+
oauthAccountId: oldToken.projectUserOAuthAccount.id,
155+
scopes: oldToken.scopes,
156156
}
157157
});
158158
}

apps/backend/src/app/api/latest/data-vault/stores/[id]/set/route.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ export const POST = createSmartRouteHandler({
3939
// note that encryptedValue is encrypted by client-side encryption, while encrypted is encrypted by both client-side
4040
// and server-side encryption.
4141
const encrypted = await encryptWithKms(encryptedValue);
42-
4342
// Store or update the entry
4443
await prisma.dataVaultEntry.upsert({
4544
where: {
@@ -59,7 +58,6 @@ export const POST = createSmartRouteHandler({
5958
encrypted,
6059
},
6160
});
62-
6361
return {
6462
statusCode: 200,
6563
bodyType: "success",

apps/backend/src/app/api/latest/emails/render-email/route.tsx

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { getEmailThemeForTemplate, renderEmailWithTemplate } from "@/lib/email-rendering";
22
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
33
import { KnownErrors } from "@stackframe/stack-shared/dist/known-errors";
4-
import { adaptSchema, templateThemeIdSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
4+
import { adaptSchema, templateThemeIdSchema, yupNumber, yupObject, yupString, yupUnion } from "@stackframe/stack-shared/dist/schema-fields";
55
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";
66

77
export const POST = createSmartRouteHandler({
@@ -15,12 +15,24 @@ export const POST = createSmartRouteHandler({
1515
type: yupString().oneOf(["admin"]).defined(),
1616
tenancy: adaptSchema.defined(),
1717
}).defined(),
18-
body: yupObject({
19-
theme_id: templateThemeIdSchema.nullable(),
20-
theme_tsx_source: yupString(),
21-
template_id: yupString(),
22-
template_tsx_source: yupString(),
23-
}),
18+
body: yupUnion(
19+
yupObject({
20+
template_id: yupString().uuid().defined(),
21+
theme_id: templateThemeIdSchema,
22+
}),
23+
yupObject({
24+
template_id: yupString().uuid().defined(),
25+
theme_tsx_source: yupString().defined(),
26+
}),
27+
yupObject({
28+
template_tsx_source: yupString().defined(),
29+
theme_id: templateThemeIdSchema,
30+
}),
31+
yupObject({
32+
template_tsx_source: yupString().defined(),
33+
theme_tsx_source: yupString().defined(),
34+
}),
35+
).defined(),
2436
}),
2537
response: yupObject({
2638
statusCode: yupNumber().oneOf([200]).defined(),
@@ -32,32 +44,40 @@ export const POST = createSmartRouteHandler({
3244
}).defined(),
3345
}),
3446
async handler({ body, auth: { tenancy } }) {
35-
if ((body.theme_id === undefined && !body.theme_tsx_source) || (body.theme_id && body.theme_tsx_source)) {
36-
throw new StatusError(400, "Exactly one of theme_id or theme_tsx_source must be provided");
37-
}
38-
if ((!body.template_id && !body.template_tsx_source) || (body.template_id && body.template_tsx_source)) {
39-
throw new StatusError(400, "Exactly one of template_id or template_tsx_source must be provided");
47+
const templateList = new Map(Object.entries(tenancy.config.emails.templates));
48+
const themeList = new Map(Object.entries(tenancy.config.emails.themes));
49+
let themeSource: string;
50+
if ("theme_tsx_source" in body) {
51+
themeSource = body.theme_tsx_source;
52+
} else {
53+
if (typeof body.theme_id === "string" && !themeList.has(body.theme_id)) {
54+
throw new StatusError(400, "No theme found with given id");
55+
}
56+
themeSource = getEmailThemeForTemplate(tenancy, body.theme_id);
4057
}
4158

42-
if (body.theme_id && !(body.theme_id in tenancy.config.emails.themes)) {
43-
throw new StatusError(400, "No theme found with given id");
59+
let contentSource: string;
60+
if ("template_tsx_source" in body) {
61+
contentSource = body.template_tsx_source;
62+
} else if ("template_id" in body) {
63+
const template = templateList.get(body.template_id);
64+
if (!template) {
65+
throw new StatusError(400, "No template found with given id");
66+
}
67+
contentSource = template.tsxSource;
68+
} else {
69+
throw new KnownErrors.SchemaError("Either template_id or template_tsx_source must be provided");
4470
}
45-
const templateList = new Map(Object.entries(tenancy.config.emails.templates));
46-
const themeSource = body.theme_id === undefined ? body.theme_tsx_source! : getEmailThemeForTemplate(tenancy, body.theme_id);
47-
const templateSource = body.template_id ? templateList.get(body.template_id)?.tsxSource : body.template_tsx_source;
4871

49-
if (!templateSource) {
50-
throw new StatusError(400, "No template found with given id");
51-
}
5272
const result = await renderEmailWithTemplate(
53-
templateSource,
73+
contentSource,
5474
themeSource,
5575
{
5676
project: { displayName: tenancy.project.display_name },
5777
previewMode: true,
5878
},
5979
);
60-
if ("error" in result) {
80+
if (result.status === "error") {
6181
throw new KnownErrors.EmailRenderingError(result.error);
6282
}
6383
return {

0 commit comments

Comments
 (0)