TypeScript Errors in new Lucia documentation - Basic API
Closed this issue · 17 comments
Hi,
I've started the process of migrating from v3 to the new, self-rolled way and going throught the implemenatation I've noticed two small type bugs under the Basic API section. The async function createSession explicitly returns type Session. In TS projects this throws an error since async function must return a Promise.
So it should be typed like this
export async function createSession(token: string, userId: number): Promise<Session> {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const session: Session = {
id: sessionId,
userId,
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30)
};
await db.insert(sessionTable).values(session);
return session;
}
as all other functions are.
The 2nd one occurs in validateSessionToken function right after this line of code
if (result.length < 1) {
return { session: null, user: null };
}
Even thought this confirms that result is not undefined, when doing object destructuring on the next line I am getting this error
Property 'session' does not exist on type '{ user: { id: string; username: string; email: string; isVerified: boolean | null; createdAt: Date | null; preferences: UserPreferences | null; }; session: { id: string; userId: string; expiresAt: Date; }; } | undefined'.ts(2339)
so it has to be either explicitly cast as
const { user, session } = result[0] as { user: User; session: Session };
or marked with non-null assertion operator like this
const { user, session } = result[0]!;
Thank you for the awesome auth experience - v3 was so good migration looks even better! Cheers!
The return type should be fixed.
Are you using Drizzle ORM?
The return type should be fixed.
Are you using Drizzle ORM?
Yes, I am using Drizzle ORM.
Currently on Next 14.2.15 I've spotted a one more bug.
Using React.cache with middleware.ts throw this error
Client side
TypeError: (0 , react__WEBPACK_IMPORTED_MODULE_0__.cache) is not a function.
Server side
GET / 404 in 22ms
⨯ src\util\session.ts (90:8) @ <unknown>
⨯ (0 , react__WEBPACK_IMPORTED_MODULE_0__.cache) is not a function
88 | }
89 |
> 90 | export const getAuthSession = cache(
| ^
91 | async (): Promise<SessionValidationResult> => {
92 | //When upgrading to Next15 await cookie() beofre modifying
93 | const sessionToken = cookies().get(SESSION_COOKIE_NAME)?.value ?? null;
When I disable middleware the error is gone, but I don't want to not use middleware since I do need it for CSRF protection and extending cookie session on GET requests.
Code needed:
middleware.ts
import { NextResponse } from "next/server";
import { SESSION_COOKIE_NAME } from "./util/session";
import type { NextRequest } from "next/server";
export async function middleware(request: NextRequest): Promise<NextResponse> {
if (request.method === "GET") {
const response = NextResponse.next();
const token = request.cookies.get(SESSION_COOKIE_NAME)?.value ?? null;
if (token !== null) {
// Only extend cookie expiration on GET requests since we can be sure
// a new session wasn't set when handling the request.
response.cookies.set(SESSION_COOKIE_NAME, token, {
path: "/",
maxAge: 60 * 60 * 24 * 30,
sameSite: "lax",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
});
}
return response;
}
// CSRF protection
const originHeader = request.headers.get("Origin");
// NOTE: You may need to use `X-Forwarded-Host` instead
const hostHeader = request.headers.get("Host");
if (originHeader === null || hostHeader === null) {
return new NextResponse(null, {
status: 403,
});
}
let origin: URL;
try {
origin = new URL(originHeader);
} catch {
return new NextResponse(null, {
status: 403,
});
}
if (origin.host !== hostHeader) {
return new NextResponse(null, {
status: 403,
});
}
return NextResponse.next();
}
session.ts
export const getAuthSession = cache(
async (): Promise<SessionValidationResult> => {
//When upgrading to Next15 await cookie() beofre modifying
const sessionToken = cookies().get(SESSION_COOKIE_NAME)?.value ?? null;
if (sessionToken === null) {
return { session: null, user: null };
}
const result = await validateSessionToken(sessionToken);
return result;
},
);
Thanks for actively working on this. I thought migrating would be a breeze (TBH setting it up was easy), but so many things broke and now I am stuck :/
getAuthSession()
doesn't work with middleware since both React.cache()
and cookies()
are unavailable.
What driver are you using Drizzle ORM and can you share the relevant query? It shouldn't be returning undefined
.
getAuthSession()
doesn't work with middleware since bothReact.cache()
andcookies()
are unavailable.
Yeah, I figured. So basically there is no option to extend cookie session if you are using React.cache with cookies?
What driver are you using Drizzle ORM and can you share the relevant query? It shouldn't be returning
undefined
.
Driver/dialect: "postgresql",
Currently I am doing it like this
session.ts
export type SessionValidationResult =
| { session: Session; user: User }
| { session: null; user: null };
export async function validateSessionToken(
token: string,
): Promise<SessionValidationResult> {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const sessionInDb = await db.query.sessions.findFirst({
where: eq(sessions.id, sessionId),
});
if (!sessionInDb) {
return { session: null, user: null };
}
if (Date.now() >= sessionInDb.expiresAt.getTime()) {
await db.delete(sessions).where(eq(sessions.id, sessionInDb.id));
return { session: null, user: null };
}
const user = await db.query.users.findFirst({
where: eq(users.id, sessionInDb.userId),
columns: {
hashedPassword: false,
},
});
if (!user) {
await db.delete(sessions).where(eq(sessions.id, sessionInDb.id));
return { session: null, user: null };
}
if (
Date.now() >=
sessionInDb.expiresAt.getTime() - 1000 * 60 * 60 * 24 * 15
) {
sessionInDb.expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30);
await db
.update(sessions)
.set({
expiresAt: sessionInDb.expiresAt,
})
.where(eq(sessions.id, sessionInDb.id));
}
return { session: sessionInDb, user };
}
which solved my problem.
Originally I was using the example from the docs with small tweakings to fit my use case like this,
session.ts
export type User = Omit<typeof users.$inferSelect, "hashedPassword">;
export type Session = typeof sessions.$inferSelect;
export type SessionValidationResult =
| { session: Session; user: User }
| { session: null; user: null };
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { hashedPassword, ...rest } = getTableColumns(users);
export async function validateSessionToken(
token: string,
): Promise<SessionValidationResult> {
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
const result = await db
.select({
user: {
...rest
},
session: sessions,
})
.from(sessions)
.innerJoin(users, eq(sessions.userId, users.id))
.where(eq(sessions.id, sessionId));
if (result.length < 1) {
return { session: null, user: null };
}
const { user, session } = result[0];
if (Date.now() >= session.expiresAt.getTime()) {
await db.delete(sessions).where(eq(sessions.id, session.id));
return { session: null, user: null };
}
if (Date.now() >= session.expiresAt.getTime() - 1000 * 60 * 60 * 24 * 15) {
session.expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30);
await db
.update(sessions)
.set({
expiresAt: session.expiresAt,
})
.where(eq(sessions.id, session.id));
}
return { session, user };
}
For the purpose of this reply I've returned the original code and immediately got the error
Property 'user' does not exist on type '{ user: { id: string; username: string; email: string; isVerified: boolean; createdAt: Date; preferences: UserPreferences; }; session: { id: string; userId: string; expiresAt: Date; }; } | undefined'.ts(2339)
Property 'session' does not exist on type '{ user: { id: string; username: string; email: string; isVerified: boolean; createdAt: Date; preferences: UserPreferences; }; session: { id: string; userId: string; expiresAt: Date; }; } | undefined'.ts(2339)
even though when I hover over result it is properly typed as
const result: {
user: {
id: string;
username: string;
email: string;
isVerified: boolean;
createdAt: Date;
preferences: UserPreferences;
};
session: {
id: string;
userId: string;
expiresAt: Date;
};
}[]
so destructuring it should be posible.
So basically there is no option to extend cookie session if you are using React.cache with cookies?
You'd have to recreate the validate function but without cache and using NextRequest.cookies
I can't seem to recreate the type issue tho. Maybe it's something to do with your Drizzle ORM or TypeScript version?
So basically there is no option to extend cookie session if you are using React.cache with cookies?
You'd have to recreate the validate function but without cache and using NextRequest.cookies
Ahh that's a bummer. I really like the cache option since it doesn't do db query for every route or server action invocation as far as I am getting it correctly. So there is no point in extending db session if the cookie session is going to expire in 30 days, right?
I should just keep the same expiration date on both and let user login again after 30 days, creating both new db and cookie session.
I can't seem to recreate the type issue tho. Maybe it's something to do with your Drizzle ORM or TypeScript version?
"typescript": "5.6.3"
"drizzle-orm": "0.35.3"
Both up to date
I can't seem to find the solution, other than switcing to the way I did above so I will stick to it. It would be interesting to see if someone else encounters the same issue in the future.
I should just keep the same expiration date on both and let user login again after 30 days, creating both new db and cookie session.
Can't you just call getCurrentSession()
in page.tsx etc? Or is website static and just protected by middleware?
It is mostly dynamic and I am protecting routes by calling getAuthSession()
inside different layout.tsx - I got 3 different group routes each having different layout.tsx - excluding the root one.
Example:
layout.tsx
export default async function UserPagesLayout({
children,
}: {
children: React.ReactNode;
}) {
const { user } = await getAuthSession();
if (user === null) {
redirect("/login");
}
return (
<>
<UserPagesHeader>
<Suspense fallback={<LoadingProfileButtonSkeleton />}>
<ProfileButton />
</Suspense>
</UserPagesHeader>
{children}
</>
);
}
So instead of duplicating code across all the page.tsx files, I call it inside 3 layout.tsx files and on few pages that actually need user data instead of just being a safety mechanism.
I don't know what you mean by getCurrentSession()
but the way I am doing it, it just doesn't work since, like you said, both React.cache() and cookies() aren't available. I don't want to ditch caching since getAuthSession()
is called a bucnh of times across the whole project which could easily increase db compute.
If there is a way to actually extend cookie session while keeping cache functionality on getAuthSession()
that would be awesome. I am interested in hearing how Lucia v3 managed to do that, cuz I remember not using middleware yet I still go to extend cookie session and keep the cache on route protection function.
Sorry if I am confusing you, I am new to the rolling auth on your own and I mostly learned about it through your docs.
Sorry, I meant getAuthSession()
by getCurrentSession()
.
Anyway, the current docs just extends the cookie lifetime (not session) regardless of whether the session is valid or not in the middleware, if that's what you need.
Sorry, I meant
getAuthSession()
bygetCurrentSession()
.Anyway, the current docs just extends the cookie lifetime (not session) regardless of whether the session is valid or not in the middleware, if that's what you need.
Yeah I figured later on that I made the same function as the docs are suggesting but didn’t name it the same way so got me confused there 😅.
Basically it extends the db session - getAuthSession(), but not the cookie session, right? And I needed a way to keep users logged in infinitely like v3 was doing.
Is it possible to call setSessionTokenCookie() after getting a result from calling validateSessionToken() inside getAuthSession()/getCurrentSession(), and setting the new expiration date to result.session.expiresAt? From what I am understanding, the docs say it’s not but I would like to confirm.
But using getAuthSession()
inside page.tsx
and server actions already extends the session lifetime?
But using
getAuthSession()
insidepage.tsx
and server actions already extends the session lifetime?
Only in db right? But not the cookie in the browser, if I am getting it right?
By no means I am not as good at this as you and I would like to be correct if wrong, but to even initiate getAuthSession() you need a valid cookie session. If cookie session and db session expiration times are not in sync, users would have to log in again after cookie expires even though their db session has been extended few times after doing the initial setSessionTokenCookie() inside login() function.
At this moment, when users log in to my app their cookie’s sessions are set to expire in 30 days and it’s not being updated to sync with the new expiresAt from db session, only once on the initial log in. If it expires, it’s gone and users will be redirected to the login page even before reaching validateSessionToken(), right? At that point it doesn’t matter if db session is valid/extended since they would have to log in again. So I don’t quite understand what’s the point of extending the db session if it doesn’t match the cookie session?
Like I said, the current docs recommend extending the lifetime of the session cookie (not the session itself) in the middleware. It's not "in sync" but the cookie will persist beyond 30 days