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(
token: string,
isSecure: boolean
isSecure: boolean,
expiresAt: Date
): string {
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 {
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 {
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 {
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/;`;
}

View file

@ -167,12 +167,19 @@ export function serializeResourceSessionCookie(
cookieName: string,
domain: string,
token: string,
isHttp: boolean = false
isHttp: boolean = false,
expiresAt?: Date
): string {
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 {
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();
await createSession(token, existingUser.userId);
const sess = await createSession(token, existingUser.userId);
const isSecure = req.protocol === "https";
const cookie = serializeSessionCookie(token, isSecure);
const cookie = serializeSessionCookie(
token,
isSecure,
new Date(sess.expiresAt)
);
res.appendHeader("Set-Cookie", cookie);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -33,6 +33,7 @@ import {
import {
Table,
TableBody,
TableCaption,
TableCell,
TableContainer,
TableHead,
@ -721,7 +722,6 @@ export default function ResourceRules(props: {
</div>
</form>
</Form>
<TableContainer>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
@ -731,8 +731,8 @@ export default function ResourceRules(props: {
{header.isPlaceholder
? null
: flexRender(
header.column
.columnDef.header,
header.column.columnDef
.header,
header.getContext()
)}
</TableHead>
@ -744,13 +744,10 @@ export default function ResourceRules(props: {
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row
.getVisibleCells()
.map((cell) => (
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column
.columnDef.cell,
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
@ -768,11 +765,10 @@ export default function ResourceRules(props: {
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
<p className="text-sm text-muted-foreground">
<TableCaption>
Rules are evaluated by priority in ascending order.
</p>
</TableCaption>
</Table>
</SettingsSectionBody>
<SettingsSectionFooter>
<Button

View file

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

View file

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

View file

@ -1,5 +1,5 @@
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 }) {

View file

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

View file

@ -38,7 +38,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
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
)}
{...props}

View file

@ -11,7 +11,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
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
)}
{...props}
@ -19,7 +19,7 @@ const Switch = React.forwardRef<
>
<SwitchPrimitives.Thumb
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>

View file

@ -25,7 +25,7 @@ const ToastViewport = React.forwardRef<
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
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: {
variant: {