more visual enhancements and use expires instead of max age in cookies

This commit is contained in:
miloschwartz 2025-03-02 15:23:11 -05:00
parent 759434e9f8
commit adef93623d
No known key found for this signature in database
17 changed files with 151 additions and 137 deletions

View file

@ -129,18 +129,19 @@ export async function invalidateAllSessions(userId: string): Promise<void> {
export function serializeSessionCookie( export function serializeSessionCookie(
token: string, token: string,
isSecure: boolean isSecure: boolean,
expiresAt: Date
): string { ): string {
if (isSecure) { if (isSecure) {
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
} else { } else {
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/;`; return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/;`;
} }
} }
export function createBlankSessionTokenCookie(isSecure: boolean): string { export function createBlankSessionTokenCookie(isSecure: boolean): string {
if (isSecure) { if (isSecure) {
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
} else { } else {
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/;`; return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/;`;
} }

View file

@ -167,12 +167,19 @@ export function serializeResourceSessionCookie(
cookieName: string, cookieName: string,
domain: string, domain: string,
token: string, token: string,
isHttp: boolean = false isHttp: boolean = false,
expiresAt?: Date
): string { ): string {
if (!isHttp) { if (!isHttp) {
return `${cookieName}_s=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Secure; Domain=${"." + domain}`; if (expiresAt === undefined) {
return `${cookieName}_s=${token}; HttpOnly; SameSite=Lax; Path=/; Secure; Domain=${"." + domain}`;
}
return `${cookieName}_s=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure; Domain=${"." + domain}`;
} else { } else {
return `${cookieName}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Domain=${"." + domain}`; if (expiresAt === undefined) {
return `${cookieName}=${token}; HttpOnly; SameSite=Lax; Path=/; Domain=${"." + domain}`;
}
return `${cookieName}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Domain=${"." + domain}`;
} }
} }

View file

@ -137,9 +137,13 @@ export async function login(
} }
const token = generateSessionToken(); const token = generateSessionToken();
await createSession(token, existingUser.userId); const sess = await createSession(token, existingUser.userId);
const isSecure = req.protocol === "https"; const isSecure = req.protocol === "https";
const cookie = serializeSessionCookie(token, isSecure); const cookie = serializeSessionCookie(
token,
isSecure,
new Date(sess.expiresAt)
);
res.appendHeader("Set-Cookie", cookie); res.appendHeader("Set-Cookie", cookie);

View file

@ -170,9 +170,13 @@ export async function signup(
// }); // });
const token = generateSessionToken(); const token = generateSessionToken();
await createSession(token, userId); const sess = await createSession(token, userId);
const isSecure = req.protocol === "https"; const isSecure = req.protocol === "https";
const cookie = serializeSessionCookie(token, isSecure); const cookie = serializeSessionCookie(
token,
isSecure,
new Date(sess.expiresAt)
);
res.appendHeader("Set-Cookie", cookie); res.appendHeader("Set-Cookie", cookie);
if (config.getRawConfig().flags?.require_email_verification) { if (config.getRawConfig().flags?.require_email_verification) {

View file

@ -102,6 +102,8 @@ export async function exchangeSession(
const token = generateSessionToken(); const token = generateSessionToken();
let expiresAt: number | null = null;
if (requestSession.userSessionId) { if (requestSession.userSessionId) {
const [res] = await db const [res] = await db
.select() .select()
@ -118,6 +120,7 @@ export async function exchangeSession(
expiresAt: res.expiresAt, expiresAt: res.expiresAt,
sessionLength: SESSION_COOKIE_EXPIRES sessionLength: SESSION_COOKIE_EXPIRES
}); });
expiresAt = res.expiresAt;
} }
} else if (requestSession.accessTokenId) { } else if (requestSession.accessTokenId) {
const [res] = await db const [res] = await db
@ -140,8 +143,12 @@ export async function exchangeSession(
expiresAt: res.expiresAt, expiresAt: res.expiresAt,
sessionLength: res.sessionLength sessionLength: res.sessionLength
}); });
expiresAt = res.expiresAt;
} }
} else { } else {
const expires = new Date(
Date.now() + SESSION_COOKIE_EXPIRES
).getTime();
await createResourceSession({ await createResourceSession({
token, token,
resourceId: resource.resourceId, resourceId: resource.resourceId,
@ -152,11 +159,10 @@ export async function exchangeSession(
whitelistId: requestSession.whitelistId, whitelistId: requestSession.whitelistId,
accessTokenId: requestSession.accessTokenId, accessTokenId: requestSession.accessTokenId,
doNotExtend: false, doNotExtend: false,
expiresAt: new Date( expiresAt: expires,
Date.now() + SESSION_COOKIE_EXPIRES
).getTime(),
sessionLength: RESOURCE_SESSION_COOKIE_EXPIRES sessionLength: RESOURCE_SESSION_COOKIE_EXPIRES
}); });
expiresAt = expires;
} }
const cookieName = `${config.getRawConfig().server.session_cookie_name}`; const cookieName = `${config.getRawConfig().server.session_cookie_name}`;
@ -164,7 +170,8 @@ export async function exchangeSession(
cookieName, cookieName,
resource.fullDomain!, resource.fullDomain!,
token, token,
!resource.ssl !resource.ssl,
expiresAt ? new Date(expiresAt) : undefined
); );
logger.debug(JSON.stringify("Exchange cookie: " + cookie)); logger.debug(JSON.stringify("Exchange cookie: " + cookie));

View file

@ -384,7 +384,7 @@ async function createAccessTokenSession(
tokenItem: ResourceAccessToken tokenItem: ResourceAccessToken
) { ) {
const token = generateSessionToken(); const token = generateSessionToken();
await createResourceSession({ const sess = await createResourceSession({
resourceId: resource.resourceId, resourceId: resource.resourceId,
token, token,
accessTokenId: tokenItem.accessTokenId, accessTokenId: tokenItem.accessTokenId,
@ -397,7 +397,8 @@ async function createAccessTokenSession(
cookieName, cookieName,
resource.fullDomain!, resource.fullDomain!,
token, token,
!resource.ssl !resource.ssl,
new Date(sess.expiresAt)
); );
res.appendHeader("Set-Cookie", cookie); res.appendHeader("Set-Cookie", cookie);
logger.debug("Access token is valid, creating new session"); logger.debug("Access token is valid, creating new session");

View file

@ -39,6 +39,7 @@ import {
import { import {
Table, Table,
TableBody, TableBody,
TableCaption,
TableCell, TableCell,
TableContainer, TableContainer,
TableHead, TableHead,
@ -562,67 +563,65 @@ export default function ReverseProxyTargets(props: {
</FormItem> </FormItem>
)} )}
/> />
<Button type="submit" variant="outlinePrimary" className="mt-8"> <Button
type="submit"
variant="outlinePrimary"
className="mt-8"
>
Add Target Add Target
</Button> </Button>
</div> </div>
</form> </form>
</Form> </Form>
<TableContainer> <Table>
<Table> <TableHeader>
<TableHeader> {table.getHeaderGroups().map((headerGroup) => (
{table.getHeaderGroups().map((headerGroup) => ( <TableRow key={headerGroup.id}>
<TableRow key={headerGroup.id}> {headerGroup.headers.map((header) => (
{headerGroup.headers.map((header) => ( <TableHead key={header.id}>
<TableHead key={header.id}> {header.isPlaceholder
{header.isPlaceholder ? null
? null : flexRender(
: flexRender( header.column.columnDef
header.column .header,
.columnDef.header, header.getContext()
header.getContext() )}
)} </TableHead>
</TableHead> ))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))} ))}
</TableRow> </TableRow>
))} ))
</TableHeader> ) : (
<TableBody> <TableRow>
{table.getRowModel().rows?.length ? ( <TableCell
table.getRowModel().rows.map((row) => ( colSpan={columns.length}
<TableRow key={row.id}> className="h-24 text-center"
{row >
.getVisibleCells() No targets. Add a target using the form.
.map((cell) => ( </TableCell>
<TableCell key={cell.id}> </TableRow>
{flexRender( )}
cell.column </TableBody>
.columnDef.cell, <TableCaption>
cell.getContext() Adding more than one target above will enable load
)} balancing.
</TableCell> </TableCaption>
))} </Table>
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No targets. Add a target using the
form.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<p className="text-sm text-muted-foreground">
Adding more than one target above will enable load
balancing.
</p>
</SettingsSectionBody> </SettingsSectionBody>
<SettingsSectionFooter> <SettingsSectionFooter>
<Button <Button

View file

@ -608,7 +608,6 @@ export default function GeneralForm() {
<Command> <Command>
<CommandInput <CommandInput
placeholder="Search sites" placeholder="Search sites"
className="h-9"
/> />
<CommandEmpty> <CommandEmpty>
No sites found. No sites found.

View file

@ -130,9 +130,7 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
<OrgProvider org={org}> <OrgProvider org={org}>
<ResourceProvider resource={resource} authInfo={authInfo}> <ResourceProvider resource={resource} authInfo={authInfo}>
<SidebarSettings sidebarNavItems={sidebarNavItems}> <SidebarSettings sidebarNavItems={sidebarNavItems}>
<div className="mb-4"> <ResourceInfoBox />
<ResourceInfoBox />
</div>
{children} {children}
</SidebarSettings> </SidebarSettings>
</ResourceProvider> </ResourceProvider>

View file

@ -33,6 +33,7 @@ import {
import { import {
Table, Table,
TableBody, TableBody,
TableCaption,
TableCell, TableCell,
TableContainer, TableContainer,
TableHead, TableHead,
@ -721,58 +722,53 @@ export default function ResourceRules(props: {
</div> </div>
</form> </form>
</Form> </Form>
<TableContainer> <Table>
<Table> <TableHeader>
<TableHeader> {table.getHeaderGroups().map((headerGroup) => (
{table.getHeaderGroups().map((headerGroup) => ( <TableRow key={headerGroup.id}>
<TableRow key={headerGroup.id}> {headerGroup.headers.map((header) => (
{headerGroup.headers.map((header) => ( <TableHead key={header.id}>
<TableHead key={header.id}> {header.isPlaceholder
{header.isPlaceholder ? null
? null : flexRender(
: flexRender( header.column.columnDef
header.column .header,
.columnDef.header, header.getContext()
header.getContext() )}
)} </TableHead>
</TableHead> ))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))} ))}
</TableRow> </TableRow>
))} ))
</TableHeader> ) : (
<TableBody> <TableRow>
{table.getRowModel().rows?.length ? ( <TableCell
table.getRowModel().rows.map((row) => ( colSpan={columns.length}
<TableRow key={row.id}> className="h-24 text-center"
{row >
.getVisibleCells() No rules. Add a rule using the form.
.map((cell) => ( </TableCell>
<TableCell key={cell.id}> </TableRow>
{flexRender( )}
cell.column </TableBody>
.columnDef.cell, <TableCaption>
cell.getContext() Rules are evaluated by priority in ascending order.
)} </TableCaption>
</TableCell> </Table>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No rules. Add a rule using the form.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<p className="text-sm text-muted-foreground">
Rules are evaluated by priority in ascending order.
</p>
</SettingsSectionBody> </SettingsSectionBody>
<SettingsSectionFooter> <SettingsSectionFooter>
<Button <Button

View file

@ -68,9 +68,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
<SiteProvider site={site}> <SiteProvider site={site}>
<SidebarSettings sidebarNavItems={sidebarNavItems}> <SidebarSettings sidebarNavItems={sidebarNavItems}>
<div className="mb-4"> <SiteInfoCard />
<SiteInfoCard />
</div>
{children} {children}
</SidebarSettings> </SidebarSettings>
</SiteProvider> </SiteProvider>

View file

@ -21,8 +21,8 @@
--accent-foreground: 24 9.8% 10%; --accent-foreground: 24 9.8% 10%;
--destructive: 0 84.2% 60.2%; --destructive: 0 84.2% 60.2%;
--destructive-foreground: 60 9.1% 97.8%; --destructive-foreground: 60 9.1% 97.8%;
--border: 20 5.9% 85%; --border: 20 5.9% 80%;
--input: 20 5.9% 80%; --input: 20 5.9% 75%;
--ring: 24.6 95% 53.1%; --ring: 24.6 95% 53.1%;
--radius: 0.75rem; --radius: 0.75rem;
--chart-1: 12 76% 61%; --chart-1: 12 76% 61%;
@ -49,8 +49,8 @@
--accent-foreground: 60 9.1% 97.8%; --accent-foreground: 60 9.1% 97.8%;
--destructive: 0 72.2% 50.6%; --destructive: 0 72.2% 50.6%;
--destructive-foreground: 60 9.1% 97.8%; --destructive-foreground: 60 9.1% 97.8%;
--border: 12 6.5% 25.0%; --border: 12 6.5% 30.0%;
--input: 12 6.5% 30.0%; --input: 12 6.5% 35.0%;
--ring: 20.5 90.2% 48.2%; --ring: 20.5 90.2% 48.2%;
--chart-1: 220 70% 50%; --chart-1: 220 70% 50%;
--chart-2: 160 60% 45%; --chart-2: 160 60% 45%;

View file

@ -1,5 +1,5 @@
export function SettingsContainer({ children }: { children: React.ReactNode }) { export function SettingsContainer({ children }: { children: React.ReactNode }) {
return <div className="space-y-4">{children}</div> return <div className="space-y-6">{children}</div>
} }
export function SettingsSection({ children }: { children: React.ReactNode }) { export function SettingsSection({ children }: { children: React.ReactNode }) {

View file

@ -26,7 +26,7 @@ export function SidebarSettings({
<aside className="lg:w-1/5"> <aside className="lg:w-1/5">
<SidebarNav items={sidebarNavItems} disabled={disabled} /> <SidebarNav items={sidebarNavItems} disabled={disabled} />
</aside> </aside>
<div className={`flex-1 ${limitWidth ? "lg:max-w-2xl" : ""}`}> <div className={`flex-1 ${limitWidth ? "lg:max-w-2xl" : ""} space-y-6`}>
{children} {children}
</div> </div>
</div> </div>

View file

@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content <DialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(
"fixed left-[50%] top-[35%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-card p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className className
)} )}
{...props} {...props}

View file

@ -11,7 +11,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SwitchPrimitives.Root <SwitchPrimitives.Root
className={cn( className={cn(
"peer inline-flex h-4 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input", "peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className className
)} )}
{...props} {...props}
@ -19,7 +19,7 @@ const Switch = React.forwardRef<
> >
<SwitchPrimitives.Thumb <SwitchPrimitives.Thumb
className={cn( className={cn(
"pointer-events-none block h-3 w-3 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0" "pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)} )}
/> />
</SwitchPrimitives.Root> </SwitchPrimitives.Root>

View file

@ -25,7 +25,7 @@ const ToastViewport = React.forwardRef<
ToastViewport.displayName = ToastPrimitives.Viewport.displayName ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva( const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-3 pr-8 transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-3 pr-8 transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=open]:fade-in data-[state=closed]:animate-out data-[state=closed]:fade-out data-[swipe=end]:animate-out",
{ {
variants: { variants: {
variant: { variant: {