import "server-only";
import { cache } from "react";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { ClientResponseError, getTokenPayload, isTokenExpired } from "pocketbase";
import { client, db } from "./db";
import { SECURE_COOKIES } from "./request-origin";
import { authenticateUser, toUser } from "./users";
// Sessions are PocketBase auth tokens, kept in an httpOnly cookie. They expire after 30 days (the
// users collection's token duration) and stop working when the password or email changes.
const SESSION_COOKIE = "session";
const SESSION_SECONDS = 30 * 86400;
async function setSessionCookie(token: string) {
(await cookies()).set(SESSION_COOKIE, token, {
httpOnly: true,
sameSite: "lax",
secure: SECURE_COOKIES,
path: "/",
expires: new Date(getTokenPayload(token).exp * 1000),
});
}
/** Checks a username or email and password and starts a session. Returns the user, or null. */
export async function signIn(login: string, password: string) {
const auth = await authenticateUser(login, password);
if (!auth) return null;
await setSessionCookie(auth.token);
return auth.user;
}
/** A new session for a signed-in user whose old token stopped working (after an email change). */
export async function renewSession(userId: string) {
const impersonated = await (await db()).collection("users").impersonate(userId, SESSION_SECONDS);
await setSessionCookie(impersonated.authStore.token);
}
export async function endSession() {
(await cookies()).delete(SESSION_COOKIE);
}
/** The signed-in user for this request, or null. */
export const getViewer = cache(async () => userForSession((await cookies()).get(SESSION_COOKIE)?.value));
/** The user a session token belongs to, or null if it's expired or no longer valid. The mobile app sends these as bearer tokens (lib/app-api.ts). */
export async function userForSession(token: string | undefined) {
if (!token || isTokenExpired(token)) return null;
const { id, type } = getTokenPayload(token);
if (type !== "auth" || typeof id !== "string") return null;
await db(); // makes sure PocketBase is running
// Reading the record with the user's own token is what checks the token.
const pb = client();
pb.authStore.save(token);
try {
return toUser(await pb.collection("users").getOne(id));
} catch (e) {
if (e instanceof ClientResponseError && [401, 403, 404].includes(e.status)) return null;
throw e;
}
}
/** Returns the signed-in user, or redirects to the login page. */
export async function requireViewer(returnTo?: string) {
const viewer = await getViewer();
if (!viewer) redirect(returnTo ? `/login?next=${encodeURIComponent(returnTo)}` : "/login");
return viewer;
}
/** Only allow same-site relative redirects after login. */
export function safeNext(next: unknown) {
return typeof next === "string" && next.startsWith("/") && !next.startsWith("//") ? next : "/";
}