Skip to content

Commit f4f011f

Browse files
Merge branch 'stack-auth:dev' into dev
2 parents 0d05673 + 91dd23c commit f4f011f

File tree

113 files changed

+4901
-2556
lines changed

Some content is hidden

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

113 files changed

+4901
-2556
lines changed

apps/backend/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# @stackframe/stack-backend
22

3+
## 2.6.22
4+
5+
### Patch Changes
6+
7+
- Bugfixes
8+
- Updated dependencies
9+
- @stackframe/stack-emails@2.6.22
10+
- @stackframe/stack-shared@2.6.22
11+
312
## 2.6.21
413

514
### Patch Changes

apps/backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@stackframe/stack-backend",
3-
"version": "2.6.21",
3+
"version": "2.6.22",
44
"private": true,
55
"scripts": {
66
"clean": "rimraf .next && rimraf node_modules",

apps/dashboard/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# @stackframe/stack-dashboard
22

3+
## 2.6.22
4+
5+
### Patch Changes
6+
7+
- Bugfixes
8+
- Updated dependencies
9+
- @stackframe/stack-emails@2.6.22
10+
- @stackframe/stack-shared@2.6.22
11+
- @stackframe/stack-ui@2.6.22
12+
- @stackframe/stack@2.6.22
13+
314
## 2.6.21
415

516
### Patch Changes

apps/dashboard/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@stackframe/stack-dashboard",
3-
"version": "2.6.21",
3+
"version": "2.6.22",
44
"private": true,
55
"scripts": {
66
"clean": "rimraf .next && rimraf node_modules",

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx

Lines changed: 66 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"use client";
2-
import { SmartFormDialog } from "@/components/form-dialog";
2+
import { FormDialog } from "@/components/form-dialog";
3+
import { InputField, SwitchField } from "@/components/form-fields";
34
import { SettingCard, SettingSwitch } from "@/components/settings";
45
import { AdminDomainConfig, AdminProject } from "@stackframe/stack";
56
import { urlSchema } from "@stackframe/stack-shared/dist/schema-fields";
6-
import { ActionCell, ActionDialog, Alert, Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Typography } from "@stackframe/stack-ui";
7+
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, ActionCell, ActionDialog, Alert, Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Typography } from "@stackframe/stack-ui";
78
import React from "react";
89
import * as yup from "yup";
910
import { PageLayout } from "../page-layout";
@@ -28,48 +29,29 @@ function EditDialog(props: {
2829
}
2930
)) {
3031
const domainFormSchema = yup.object({
31-
makeSureAlert: yup.mixed().meta({
32-
stackFormFieldRender: () => (
33-
<Alert>
34-
Make sure this is a trusted domain or a URL that you control.
35-
</Alert>
36-
),
37-
}),
3832
domain: urlSchema
39-
.matches(/^https:\/\//, "Origin must start with https://")
40-
.url("Domain must be a valid URL")
33+
.url("Invalid URL")
34+
.transform((value) => 'https://' + value)
4135
.notOneOf(
4236
props.domains
4337
.filter((_, i) => (props.type === 'update' && i !== props.editIndex) || props.type === 'create')
4438
.map(({ domain }) => domain),
4539
"Domain already exists"
4640
)
47-
.required()
48-
.label("Origin (starts with https://)")
49-
.meta({
50-
stackFormFieldPlaceholder: "https://example.com",
51-
}).default(props.type === 'update' ? props.defaultDomain : ""),
41+
.required(),
5242
handlerPath: yup.string()
5343
.matches(/^\//, "Handler path must start with /")
54-
.required()
55-
.label("Handler path (default: /handler)")
56-
.default(props.type === 'update' ? props.defaultHandlerPath : "/handler"),
57-
description: yup.mixed().meta({
58-
stackFormFieldRender: () => (
59-
<>
60-
<Typography variant="secondary" type="footnote">
61-
Note that sub-domains are not automatically added. Create two domains like www.example.com and example.com if you want to allow both.
62-
</Typography>
63-
<Typography variant="secondary" type="footnote">
64-
{"You don't need to change the handler path unless you updated the path to the StackHandler."}
65-
</Typography>
66-
</>
67-
),
68-
}),
44+
.required(),
45+
addWww: yup.boolean(),
6946
});
7047

71-
return <SmartFormDialog
48+
return <FormDialog
7249
open={props.open}
50+
defaultValues={{
51+
addWww: props.type === 'create',
52+
domain: props.type === 'update' ? props.defaultDomain : undefined,
53+
handlerPath: props.type === 'update' ? props.defaultHandlerPath : "/handler",
54+
}}
7355
onOpenChange={props.onOpenChange}
7456
trigger={props.trigger}
7557
title={(props.type === 'create' ? "Create" : "Update") + " domain and handler"}
@@ -79,10 +61,17 @@ function EditDialog(props: {
7961
if (props.type === 'create') {
8062
await props.project.update({
8163
config: {
82-
domains: [...props.domains, {
83-
domain: values.domain,
84-
handlerPath: values.handlerPath,
85-
}],
64+
domains: [
65+
...props.domains,
66+
{
67+
domain: values.domain,
68+
handlerPath: values.handlerPath,
69+
},
70+
...(values.addWww ? [{
71+
domain: 'https://www.' + values.domain.slice(8),
72+
handlerPath: values.handlerPath,
73+
}] : []),
74+
],
8675
},
8776
});
8877
} else {
@@ -101,6 +90,47 @@ function EditDialog(props: {
10190
});
10291
}
10392
}}
93+
render={(form) => (
94+
<>
95+
<Alert>
96+
Please ensure you own or have control over this domain. Note that each subdomain (e.g. blog.example.com, app.example.com) is treated as a distinct domain.
97+
</Alert>
98+
<InputField
99+
label="Domain"
100+
name="domain"
101+
control={form.control}
102+
prefixItem='https://'
103+
placeholder='example.com'
104+
/>
105+
106+
{props.type === 'create' &&
107+
urlSchema.url().required().isValidSync('https://' + form.watch('domain')) &&
108+
!((form.watch('domain') as any)?.startsWith('www.')) && (
109+
<SwitchField
110+
label={`Also add www.${form.watch('domain') as any ?? ''} to the trusted domains`}
111+
name="addWww"
112+
control={form.control}
113+
/>
114+
)}
115+
116+
<Accordion type="single" collapsible className="w-full">
117+
<AccordionItem value="item-1">
118+
<AccordionTrigger>Advanced</AccordionTrigger>
119+
<AccordionContent>
120+
<InputField
121+
label="Handler path"
122+
name="handlerPath"
123+
control={form.control}
124+
placeholder='/handler'
125+
/>
126+
<Typography variant="secondary" type="footnote">
127+
only modify this if you changed the default handler path in your app
128+
</Typography>
129+
</AccordionContent>
130+
</AccordionItem>
131+
</Accordion>
132+
</>
133+
)}
104134
/>;
105135
}
106136

@@ -200,15 +230,13 @@ export default function PageClient() {
200230
<TableHeader>
201231
<TableRow>
202232
<TableHead className="w-[200px]">Domain</TableHead>
203-
<TableHead className="w-[100px]">Handler</TableHead>
204233
<TableHead></TableHead>
205234
</TableRow>
206235
</TableHeader>
207236
<TableBody>
208237
{domains.map(({ domain, handlerPath }, i) => (
209238
<TableRow key={domain}>
210239
<TableCell>{domain}</TableCell>
211-
<TableCell>{handlerPath}</TableCell>
212240
<TableCell className="flex justify-end gap-4">
213241
<ActionMenu
214242
domains={domains}

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/teams/[teamId]/page-client.tsx

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,41 @@
11
"use client";
2-
import { useAdminApp } from '../../use-admin-app';
2+
import { TeamMemberSearchTable } from '@/components/data-table/team-member-search-table';
3+
import { TeamMemberTable } from '@/components/data-table/team-member-table';
4+
import { ServerTeam } from '@stackframe/stack';
5+
import { ActionDialog, Button } from '@stackframe/stack-ui';
36
import { notFound } from 'next/navigation';
47
import { PageLayout } from '../../page-layout';
5-
import { TeamMemberTable } from '@/components/data-table/team-member-table';
8+
import { useAdminApp } from '../../use-admin-app';
9+
10+
11+
export function AddUserDialog(props: {
12+
open?: boolean,
13+
onOpenChange?: (open: boolean) => void,
14+
trigger?: React.ReactNode,
15+
team: ServerTeam,
16+
}) {
17+
const teamUsers = props.team.useUsers();
18+
19+
return <ActionDialog
20+
title="Add a user"
21+
trigger={props.trigger}
22+
>
23+
<TeamMemberSearchTable
24+
action={(user) =>
25+
<div className="w-[100px] flex justify-center">
26+
<Button
27+
variant="outline"
28+
disabled={teamUsers.find(u => u.id === user.id) !== undefined}
29+
onClick={async () => {
30+
await props.team.addUser(user.id);
31+
}}
32+
>
33+
{teamUsers.find(u => u.id === user.id) ? 'Added' : 'Add'}
34+
</Button>
35+
</div>}
36+
/>
37+
</ActionDialog>;
38+
}
639

740

841
export default function PageClient(props: { teamId: string }) {
@@ -15,7 +48,13 @@ export default function PageClient(props: { teamId: string }) {
1548
}
1649

1750
return (
18-
<PageLayout title="Team Members" description={`Manage team members of "${team.displayName}"`}>
51+
<PageLayout
52+
title="Team Members"
53+
description={`Manage team members of "${team.displayName}"`}
54+
actions={
55+
<AddUserDialog trigger={<Button>Add a user</Button>} team={team} />
56+
}
57+
>
1958
<TeamMemberTable users={users || []} team={team} />
2059
</PageLayout>
2160
);
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
'use client';
2+
import { useAdminApp } from '@/app/(main)/(protected)/projects/[projectId]/use-admin-app';
3+
import { ServerUser } from '@stackframe/stack';
4+
import { AvatarCell, DataTableColumnHeader, DataTableManualPagination, SearchToolbarItem, TextCell } from "@stackframe/stack-ui";
5+
import { ColumnDef, ColumnFiltersState, SortingState } from "@tanstack/react-table";
6+
import { useState } from "react";
7+
import { extendUsers } from './user-table';
8+
9+
export function TeamMemberSearchTable(props: {
10+
action: (user: ServerUser) => React.ReactNode,
11+
}) {
12+
const stackAdminApp = useAdminApp();
13+
const [filters, setFilters] = useState<Parameters<typeof stackAdminApp.listUsers>[0]>({ limit: 10 });
14+
const users = extendUsers(stackAdminApp.useUsers(filters));
15+
16+
const columns: ColumnDef<ServerUser>[] = [
17+
{
18+
accessorKey: "profileImageUrl",
19+
header: ({ column }) => <DataTableColumnHeader column={column} columnTitle="Avatar" />,
20+
cell: ({ row }) => <AvatarCell src={row.original.profileImageUrl || undefined} />,
21+
enableSorting: false,
22+
},
23+
{
24+
accessorKey: "displayName",
25+
header: ({ column }) => <DataTableColumnHeader column={column} columnTitle="Display Name" />,
26+
cell: ({ row }) => <TextCell size={100}><span className={row.original.displayName === null ? 'text-slate-400' : ''}>{row.original.displayName ?? '–'}</span></TextCell>,
27+
enableSorting: false,
28+
},
29+
{
30+
accessorKey: "primaryEmail",
31+
header: ({ column }) => <DataTableColumnHeader column={column} columnTitle="Primary Email" />,
32+
cell: ({ row }) => <TextCell size={150}>{row.original.primaryEmail}</TextCell>,
33+
enableSorting: false,
34+
},
35+
{
36+
id: "actions",
37+
cell: ({ row }) => props.action(row.original),
38+
},
39+
];
40+
41+
const onUpdate = async (options: {
42+
cursor: string,
43+
limit: number,
44+
sorting: SortingState,
45+
columnFilters: ColumnFiltersState,
46+
globalFilters: any,
47+
}) => {
48+
let filters: Parameters<typeof stackAdminApp.listUsers>[0] = {
49+
cursor: options.cursor,
50+
limit: options.limit,
51+
query: options.globalFilters,
52+
};
53+
54+
setFilters(filters);
55+
const users = await stackAdminApp.listUsers(filters);
56+
return { nextCursor: users.nextCursor };
57+
};
58+
59+
return <DataTableManualPagination
60+
showDefaultToolbar={false}
61+
columns={columns}
62+
data={users}
63+
onUpdate={onUpdate}
64+
toolbarRender={table => <SearchToolbarItem table={table} placeholder="Search table" className="w-full" />}
65+
/>;
66+
}

apps/dashboard/src/components/form-fields.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export function InputField<F extends FieldValues>(props: {
6161
required?: boolean,
6262
type?: string,
6363
disabled?: boolean,
64+
prefixItem?: React.ReactNode,
6465
}) {
6566
return (
6667
<FormField
@@ -78,6 +79,7 @@ export function InputField<F extends FieldValues>(props: {
7879
className="max-w-lg"
7980
disabled={props.disabled}
8081
type={props.type}
82+
prefixItem={props.prefixItem}
8183
/>
8284
</FormControl>
8385
<FormMessage />

apps/e2e/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# @stackframe/e2e-tests
22

3+
## 2.6.22
4+
5+
### Patch Changes
6+
7+
- Bugfixes
8+
- Updated dependencies
9+
- @stackframe/stack-shared@2.6.22
10+
311
## 2.6.21
412

513
### Patch Changes

apps/e2e/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@stackframe/e2e-tests",
3-
"version": "2.6.21",
3+
"version": "2.6.22",
44
"private": true,
55
"type": "module",
66
"scripts": {

0 commit comments

Comments
 (0)